mirror of
https://github.com/telekom-security/tpotce.git
synced 2025-04-20 06:02:24 +00:00
1289 lines
30 KiB
C
1289 lines
30 KiB
C
/*
|
|
p0f - main entry point and all the pcap / unix socket innards
|
|
-------------------------------------------------------------
|
|
|
|
Copyright (C) 2012 by Michal Zalewski <lcamtuf@coredump.cx>
|
|
|
|
Distributed under the terms and conditions of GNU LGPL.
|
|
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
#define _FROM_P0F
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <signal.h>
|
|
#include <getopt.h>
|
|
#include <errno.h>
|
|
#include <dirent.h>
|
|
#include <pwd.h>
|
|
#include <grp.h>
|
|
#include <poll.h>
|
|
#include <time.h>
|
|
#include <locale.h>
|
|
#include <jansson.h>
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <arpa/inet.h>
|
|
#include <sys/un.h>
|
|
#include <sys/fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/file.h>
|
|
#include <sys/wait.h>
|
|
#include <netinet/in.h>
|
|
|
|
#include <pcap.h>
|
|
|
|
#ifdef NET_BPF
|
|
# include <net/bpf.h>
|
|
#else
|
|
# include <pcap-bpf.h>
|
|
#endif /* !NET_BPF */
|
|
|
|
#include "types.h"
|
|
#include "debug.h"
|
|
#include "alloc-inl.h"
|
|
#include "process.h"
|
|
#include "readfp.h"
|
|
#include "api.h"
|
|
#include "tcp.h"
|
|
#include "fp_http.h"
|
|
#include "p0f.h"
|
|
|
|
#ifndef PF_INET6
|
|
# define PF_INET6 10
|
|
#endif /* !PF_INET6 */
|
|
|
|
#ifndef O_NOFOLLOW
|
|
# define O_NOFOLLOW 0
|
|
#endif /* !O_NOFOLLOW */
|
|
|
|
#ifndef O_LARGEFILE
|
|
# define O_LARGEFILE 0
|
|
#endif /* !O_LARGEFILE */
|
|
|
|
static json_t *json_record = NULL;
|
|
|
|
static u8 *use_iface, /* Interface to listen on */
|
|
*orig_rule, /* Original filter rule */
|
|
*switch_user, /* Target username */
|
|
*log_file, /* Binary log file name */
|
|
*api_sock, /* API socket file name */
|
|
*fp_file; /* Location of p0f.fp */
|
|
|
|
u8* read_file; /* File to read pcap data from */
|
|
|
|
static u32
|
|
api_max_conn = API_MAX_CONN; /* Maximum number of API connections */
|
|
|
|
u32
|
|
max_conn = MAX_CONN, /* Connection entry count limit */
|
|
max_hosts = MAX_HOSTS, /* Host cache entry count limit */
|
|
conn_max_age = CONN_MAX_AGE, /* Maximum age of a connection entry */
|
|
host_idle_limit = HOST_IDLE_LIMIT; /* Host cache idle timeout */
|
|
|
|
static struct api_client *api_cl; /* Array with API client state */
|
|
|
|
static s32 null_fd = -1, /* File descriptor of /dev/null */
|
|
api_fd = -1; /* API socket descriptor */
|
|
|
|
static FILE* lf; /* Log file stream */
|
|
|
|
static u8 stop_soon; /* Ctrl-C or so pressed? */
|
|
|
|
u8 daemon_mode; /* Running in daemon mode? */
|
|
u8 json_mode; /* Log in JSON? */
|
|
u8 line_buffered_mode; /* Line Buffered Mode? */
|
|
|
|
static u8 set_promisc; /* Use promiscuous mode? */
|
|
|
|
static pcap_t *pt; /* PCAP capture thingy */
|
|
|
|
s32 link_type; /* PCAP link type */
|
|
|
|
u32 hash_seed; /* Hash seed */
|
|
|
|
static u8 obs_fields; /* No of pending observation fields */
|
|
|
|
/* Memory allocator data: */
|
|
|
|
#ifdef DEBUG_BUILD
|
|
struct TRK_obj* TRK[ALLOC_BUCKETS];
|
|
u32 TRK_cnt[ALLOC_BUCKETS];
|
|
#endif /* DEBUG_BUILD */
|
|
|
|
#define LOGF(_x...) fprintf(lf, _x)
|
|
|
|
/* Display usage information */
|
|
|
|
static void usage(void) {
|
|
|
|
ERRORF(
|
|
|
|
"Usage: p0f [ ...options... ] [ 'filter rule' ]\n"
|
|
"\n"
|
|
"Network interface options:\n"
|
|
"\n"
|
|
" -i iface - listen on the specified network interface\n"
|
|
" -r file - read offline pcap data from a given file\n"
|
|
" -p - put the listening interface in promiscuous mode\n"
|
|
" -L - list all available interfaces\n"
|
|
"\n"
|
|
"Operating mode and output settings:\n"
|
|
"\n"
|
|
" -f file - read fingerprint database from 'file' (%s)\n"
|
|
" -o file - write information to the specified log file\n"
|
|
" -j - Log in JSON format.\n"
|
|
" -l - Line buffered mode for logging to output file.\n"
|
|
#ifndef __CYGWIN__
|
|
" -s name - answer to API queries at a named unix socket\n"
|
|
#endif /* !__CYGWIN__ */
|
|
" -u user - switch to the specified unprivileged account and chroot\n"
|
|
" -d - fork into background (requires -o or -s)\n"
|
|
"\n"
|
|
"Performance-related options:\n"
|
|
"\n"
|
|
#ifndef __CYGWIN__
|
|
" -S limit - limit number of parallel API connections (%u)\n"
|
|
#endif /* !__CYGWIN__ */
|
|
" -t c,h - set connection / host cache age limits (%us,%um)\n"
|
|
" -m c,h - cap the number of active connections / hosts (%u,%u)\n"
|
|
"\n"
|
|
"Optional filter expressions (man tcpdump) can be specified in the command\n"
|
|
"line to prevent p0f from looking at incidental network traffic.\n"
|
|
"\n"
|
|
"Problems? You can reach the author at <lcamtuf@coredump.cx>.\n",
|
|
|
|
FP_FILE,
|
|
#ifndef __CYGWIN__
|
|
API_MAX_CONN,
|
|
#endif /* !__CYGWIN__ */
|
|
CONN_MAX_AGE, HOST_IDLE_LIMIT, MAX_CONN, MAX_HOSTS);
|
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
|
|
/* Obtain hash seed: */
|
|
|
|
static void get_hash_seed(void) {
|
|
|
|
s32 f = open("/dev/urandom", O_RDONLY);
|
|
|
|
if (f < 0) PFATAL("Cannot open /dev/urandom for reading.");
|
|
|
|
#ifndef DEBUG_BUILD
|
|
|
|
/* In debug versions, use a constant seed. */
|
|
|
|
if (read(f, &hash_seed, sizeof(hash_seed)) != sizeof(hash_seed))
|
|
FATAL("Cannot read data from /dev/urandom.");
|
|
|
|
#endif /* !DEBUG_BUILD */
|
|
|
|
close(f);
|
|
|
|
}
|
|
|
|
|
|
/* Get rid of unnecessary file descriptors */
|
|
|
|
static void close_spare_fds(void) {
|
|
|
|
s32 i, closed = 0;
|
|
DIR* d;
|
|
struct dirent* de;
|
|
|
|
d = opendir("/proc/self/fd");
|
|
|
|
if (!d) {
|
|
/* Best we could do... */
|
|
for (i = 3; i < 256; i++)
|
|
if (!close(i)) closed++;
|
|
return;
|
|
}
|
|
|
|
while ((de = readdir(d))) {
|
|
i = atol(de->d_name);
|
|
if (i > 2 && !close(i)) closed++;
|
|
}
|
|
|
|
closedir(d);
|
|
|
|
if (closed)
|
|
SAYF("[+] Closed %u file descriptor%s.\n", closed, closed == 1 ? "" : "s" );
|
|
|
|
}
|
|
|
|
|
|
/* Create or open log file */
|
|
|
|
static void open_log(void) {
|
|
|
|
struct stat st;
|
|
s32 log_fd;
|
|
|
|
log_fd = open((char*)log_file, O_WRONLY | O_APPEND | O_NOFOLLOW | O_LARGEFILE);
|
|
|
|
if (log_fd >= 0) {
|
|
|
|
if (fstat(log_fd, &st)) PFATAL("fstat() on '%s' failed.", log_file);
|
|
|
|
if (!S_ISREG(st.st_mode)) FATAL("'%s' is not a regular file.", log_file);
|
|
|
|
} else {
|
|
|
|
if (errno != ENOENT) PFATAL("Cannot open '%s'.", log_file);
|
|
|
|
log_fd = open((char*)log_file, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW,
|
|
LOG_MODE);
|
|
|
|
if (log_fd < 0) PFATAL("Cannot open '%s'.", log_file);
|
|
|
|
}
|
|
|
|
if (flock(log_fd, LOCK_EX | LOCK_NB))
|
|
FATAL("'%s' is being used by another process.", log_file);
|
|
|
|
lf = fdopen(log_fd, "a");
|
|
|
|
if (!lf) FATAL("fdopen() on '%s' failed.", log_file);
|
|
|
|
SAYF("[+] Log file '%s' opened for writing.\n", log_file);
|
|
|
|
}
|
|
|
|
|
|
/* Create and start listening on API socket */
|
|
|
|
static void open_api(void) {
|
|
|
|
s32 old_umask;
|
|
u32 i;
|
|
|
|
struct sockaddr_un u;
|
|
struct stat st;
|
|
|
|
api_fd = socket(PF_UNIX, SOCK_STREAM, 0);
|
|
|
|
if (api_fd < 0) PFATAL("socket(PF_UNIX) failed.");
|
|
|
|
memset(&u, 0, sizeof(u));
|
|
u.sun_family = AF_UNIX;
|
|
|
|
if (strlen((char*)api_sock) >= sizeof(u.sun_path))
|
|
FATAL("API socket filename is too long for sockaddr_un (blame Unix).");
|
|
|
|
strcpy(u.sun_path, (char*)api_sock);
|
|
|
|
/* This is bad, but you can't do any better with standard unix socket
|
|
semantics today :-( */
|
|
|
|
if (!stat((char*)api_sock, &st) && !S_ISSOCK(st.st_mode))
|
|
FATAL("'%s' exists but is not a socket.", api_sock);
|
|
|
|
if (unlink((char*)api_sock) && errno != ENOENT)
|
|
PFATAL("unlink('%s') failed.", api_sock);
|
|
|
|
old_umask = umask(0777 ^ API_MODE);
|
|
|
|
if (bind(api_fd, (struct sockaddr*)&u, sizeof(u)))
|
|
PFATAL("bind() on '%s' failed.", api_sock);
|
|
|
|
umask(old_umask);
|
|
|
|
if (listen(api_fd, api_max_conn))
|
|
PFATAL("listen() on '%s' failed.", api_sock);
|
|
|
|
if (fcntl(api_fd, F_SETFL, O_NONBLOCK))
|
|
PFATAL("fcntl() to set O_NONBLOCK on API listen socket fails.");
|
|
|
|
api_cl = DFL_ck_alloc(api_max_conn * sizeof(struct api_client));
|
|
|
|
for (i = 0; i < api_max_conn; i++) api_cl[i].fd = -1;
|
|
|
|
SAYF("[+] Listening on API socket '%s' (max %u clients).\n",
|
|
api_sock, api_max_conn);
|
|
|
|
}
|
|
|
|
|
|
/* Open log entry. */
|
|
|
|
void start_observation(char* keyword, u8 field_cnt, u8 to_srv,
|
|
struct packet_flow* f) {
|
|
|
|
if (obs_fields) FATAL("Premature end of observation.");
|
|
|
|
if (!daemon_mode) {
|
|
|
|
SAYF(".-[ %s/%u -> ", addr_to_str(f->client->addr, f->client->ip_ver),
|
|
f->cli_port);
|
|
SAYF("%s/%u (%s) ]-\n|\n", addr_to_str(f->server->addr, f->client->ip_ver),
|
|
f->srv_port, keyword);
|
|
|
|
SAYF("| %-8s = %s/%u\n", to_srv ? "client" : "server",
|
|
addr_to_str(to_srv ? f->client->addr :
|
|
f->server->addr, f->client->ip_ver),
|
|
to_srv ? f->cli_port : f->srv_port);
|
|
|
|
}
|
|
|
|
if (log_file) {
|
|
|
|
u8 tmp[64];
|
|
|
|
time_t ut = get_unix_time();
|
|
struct tm* lt = localtime(&ut);
|
|
|
|
strftime((char*)tmp, 64, "%Y/%m/%d %H:%M:%S", lt);
|
|
|
|
if (json_mode) {
|
|
json_record = json_object();
|
|
json_object_set_new(json_record, "timestamp", json_string((char *)tmp));
|
|
json_object_set_new(json_record, "mod", json_string((char *)keyword));
|
|
json_object_set_new(json_record, "client_ip", json_string((char *)addr_to_str(f->client->addr, f->client->ip_ver)));
|
|
json_object_set_new(json_record, "server_ip", json_string((char *)addr_to_str(f->server->addr, f->server->ip_ver)));
|
|
json_object_set_new(json_record, "client_port", json_integer(f->cli_port));
|
|
json_object_set_new(json_record, "server_port", json_integer(f->srv_port));
|
|
json_object_set_new(json_record, "subject", json_string((char *)(to_srv ? "cli" : "srv")));
|
|
} else {
|
|
LOGF("[%s] mod=%s|cli=%s/%u|",tmp, keyword, addr_to_str(f->client->addr,
|
|
f->client->ip_ver), f->cli_port);
|
|
|
|
LOGF("srv=%s/%u|subj=%s", addr_to_str(f->server->addr, f->server->ip_ver),
|
|
f->srv_port, to_srv ? "cli" : "srv");
|
|
}
|
|
}
|
|
|
|
obs_fields = field_cnt;
|
|
}
|
|
|
|
|
|
/* Add log item. */
|
|
|
|
void add_observation_field(char* key, u8* value) {
|
|
|
|
if (!obs_fields) FATAL("Unexpected observation field ('%s').", key);
|
|
|
|
if (!daemon_mode)
|
|
SAYF("| %-8s = %s\n", key, value ? value : (u8*)"???");
|
|
|
|
if (log_file) {
|
|
if (json_mode) {
|
|
json_object_set_new(json_record, key, json_string( (char *)(value ? value : (u8*)"???") ));
|
|
} else {
|
|
LOGF("|%s=%s", key, value ? value : (u8*)"???");
|
|
}
|
|
}
|
|
|
|
obs_fields--;
|
|
|
|
if (!obs_fields) {
|
|
|
|
if (!daemon_mode) SAYF("|\n`----\n\n");
|
|
|
|
if (log_file){
|
|
|
|
if (json_mode) {
|
|
json_dumpf(json_record, lf, 0);
|
|
json_decref(json_record);
|
|
}
|
|
|
|
LOGF("\n");
|
|
if (line_buffered_mode) {
|
|
fflush(lf);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/* Show PCAP interface list */
|
|
|
|
static void list_interfaces(void) {
|
|
|
|
char pcap_err[PCAP_ERRBUF_SIZE];
|
|
pcap_if_t *dev;
|
|
u8 i = 0;
|
|
|
|
/* There is a bug in several years' worth of libpcap releases that causes it
|
|
to SEGV here if /sys/class/net is not readable. See http://goo.gl/nEnGx */
|
|
|
|
if (access("/sys/class/net", R_OK | X_OK) && errno != ENOENT)
|
|
FATAL("This operation requires access to /sys/class/net/, sorry.");
|
|
|
|
if (pcap_findalldevs(&dev, pcap_err) == -1)
|
|
FATAL("pcap_findalldevs: %s\n", pcap_err);
|
|
|
|
if (!dev) FATAL("Can't find any interfaces. Maybe you need to be root?");
|
|
|
|
SAYF("\n-- Available interfaces --\n");
|
|
|
|
do {
|
|
|
|
pcap_addr_t *a = dev->addresses;
|
|
|
|
SAYF("\n%3d: Name : %s\n", i++, dev->name);
|
|
SAYF(" Description : %s\n", dev->description ? dev->description : "-");
|
|
|
|
/* Let's try to find something we can actually display. */
|
|
|
|
while (a && a->addr->sa_family != PF_INET && a->addr->sa_family != PF_INET6)
|
|
a = a->next;
|
|
|
|
if (a) {
|
|
|
|
if (a->addr->sa_family == PF_INET)
|
|
SAYF(" IP address : %s\n", addr_to_str(((u8*)a->addr) + 4, IP_VER4));
|
|
else
|
|
SAYF(" IP address : %s\n", addr_to_str(((u8*)a->addr) + 8, IP_VER6));
|
|
|
|
} else SAYF(" IP address : (none)\n");
|
|
|
|
} while ((dev = dev->next));
|
|
|
|
SAYF("\n");
|
|
|
|
pcap_freealldevs(dev);
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef __CYGWIN__
|
|
|
|
/* List PCAP-recognized interfaces */
|
|
|
|
static u8* find_interface(int num) {
|
|
|
|
char pcap_err[PCAP_ERRBUF_SIZE];
|
|
pcap_if_t *dev;
|
|
|
|
if (pcap_findalldevs(&dev, pcap_err) == -1)
|
|
FATAL("pcap_findalldevs: %s\n", pcap_err);
|
|
|
|
do {
|
|
|
|
if (!num--) {
|
|
u8* ret = DFL_ck_strdup((char*)dev->name);
|
|
pcap_freealldevs(dev);
|
|
return ret;
|
|
}
|
|
|
|
} while ((dev = dev->next));
|
|
|
|
FATAL("Interface not found (use -L to list all).");
|
|
|
|
}
|
|
|
|
#endif /* __CYGWIN__ */
|
|
|
|
|
|
/* Initialize PCAP capture */
|
|
|
|
static void prepare_pcap(void) {
|
|
|
|
char pcap_err[PCAP_ERRBUF_SIZE];
|
|
u8* orig_iface = use_iface;
|
|
|
|
if (read_file) {
|
|
|
|
if (set_promisc)
|
|
FATAL("Dude, how am I supposed to make a file promiscuous?");
|
|
|
|
if (use_iface)
|
|
FATAL("Options -i and -r are mutually exclusive.");
|
|
|
|
if (access((char*)read_file, R_OK))
|
|
PFATAL("Can't access file '%s'.", read_file);
|
|
|
|
pt = pcap_open_offline((char*)read_file, pcap_err);
|
|
|
|
if (!pt) FATAL("pcap_open_offline: %s", pcap_err);
|
|
|
|
SAYF("[+] Will read pcap data from file '%s'.\n", read_file);
|
|
|
|
} else {
|
|
|
|
if (!use_iface) {
|
|
|
|
/* See the earlier note on libpcap SEGV - same problem here.
|
|
Also, this returns something stupid on Windows, but hey... */
|
|
|
|
if (!access("/sys/class/net", R_OK | X_OK) || errno == ENOENT)
|
|
use_iface = (u8*)pcap_lookupdev(pcap_err);
|
|
|
|
if (!use_iface)
|
|
FATAL("libpcap is out of ideas; use -i to specify interface.");
|
|
|
|
}
|
|
|
|
#ifdef __CYGWIN__
|
|
|
|
/* On Windows, interface names are unwieldy, and people prefer to use
|
|
numerical IDs. */
|
|
|
|
else {
|
|
|
|
int iface_id;
|
|
|
|
if (sscanf((char*)use_iface, "%u", &iface_id) == 1) {
|
|
use_iface = find_interface(iface_id);
|
|
}
|
|
|
|
}
|
|
|
|
pt = pcap_open_live((char*)use_iface, SNAPLEN, set_promisc, 250, pcap_err);
|
|
|
|
#else
|
|
|
|
/* PCAP timeouts tend to be broken, so we'll use a very small value
|
|
and rely on select() instead. */
|
|
|
|
pt = pcap_open_live((char*)use_iface, SNAPLEN, set_promisc, 5, pcap_err);
|
|
|
|
#endif /* ^__CYGWIN__ */
|
|
|
|
if (!orig_iface)
|
|
SAYF("[+] Intercepting traffic on default interface '%s'.\n", use_iface);
|
|
else
|
|
SAYF("[+] Intercepting traffic on interface '%s'.\n", use_iface);
|
|
|
|
if (!pt) FATAL("pcap_open_live: %s", pcap_err);
|
|
|
|
}
|
|
|
|
link_type = pcap_datalink(pt);
|
|
|
|
}
|
|
|
|
|
|
/* Initialize BPF filtering */
|
|
|
|
static void prepare_bpf(void) {
|
|
|
|
struct bpf_program flt;
|
|
|
|
u8* final_rule;
|
|
u8 vlan_support;
|
|
|
|
/* VLAN matching is somewhat brain-dead: you need to request it explicitly,
|
|
and it alters the semantics of the remainder of the expression. */
|
|
|
|
vlan_support = (pcap_datalink(pt) == DLT_EN10MB);
|
|
|
|
retry_no_vlan:
|
|
|
|
if (!orig_rule) {
|
|
|
|
if (vlan_support) {
|
|
final_rule = (u8*)"tcp or (vlan and tcp)";
|
|
} else {
|
|
final_rule = (u8*)"tcp";
|
|
}
|
|
|
|
} else {
|
|
|
|
if (vlan_support) {
|
|
|
|
final_rule = ck_alloc(strlen((char*)orig_rule) * 2 + 64);
|
|
|
|
sprintf((char*)final_rule, "(tcp and (%s)) or (vlan and tcp and (%s))",
|
|
orig_rule, orig_rule);
|
|
|
|
} else {
|
|
|
|
final_rule = ck_alloc(strlen((char*)orig_rule) + 16);
|
|
|
|
sprintf((char*)final_rule, "tcp and (%s)", orig_rule);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
DEBUG("[#] Computed rule: %s\n", final_rule);
|
|
|
|
if (pcap_compile(pt, &flt, (char*)final_rule, 1, 0)) {
|
|
|
|
if (vlan_support) {
|
|
|
|
if (orig_rule) ck_free(final_rule);
|
|
vlan_support = 0;
|
|
goto retry_no_vlan;
|
|
|
|
}
|
|
|
|
pcap_perror(pt, "[-] pcap_compile");
|
|
|
|
if (!orig_rule)
|
|
FATAL("pcap_compile() didn't work, strange");
|
|
else
|
|
FATAL("Syntax error! See 'man tcpdump' for help on filters.");
|
|
|
|
}
|
|
|
|
if (pcap_setfilter(pt, &flt))
|
|
FATAL("pcap_setfilter() didn't work, strange.");
|
|
|
|
pcap_freecode(&flt);
|
|
|
|
if (!orig_rule) {
|
|
|
|
SAYF("[+] Default packet filtering configured%s.\n",
|
|
vlan_support ? " [+VLAN]" : "");
|
|
|
|
} else {
|
|
|
|
SAYF("[+] Custom filtering rule enabled: %s%s\n",
|
|
orig_rule ? orig_rule : (u8*)"tcp",
|
|
vlan_support ? " [+VLAN]" : "");
|
|
|
|
ck_free(final_rule);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/* Drop privileges and chroot(), with some sanity checks */
|
|
|
|
static void drop_privs(void) {
|
|
|
|
struct passwd* pw;
|
|
|
|
pw = getpwnam((char*)switch_user);
|
|
|
|
if (!pw) FATAL("User '%s' not found.", switch_user);
|
|
|
|
if (!strcmp(pw->pw_dir, "/"))
|
|
FATAL("User '%s' must have a dedicated home directory.", switch_user);
|
|
|
|
if (!pw->pw_uid || !pw->pw_gid)
|
|
FATAL("User '%s' must be non-root.", switch_user);
|
|
|
|
if (initgroups(pw->pw_name, pw->pw_gid))
|
|
PFATAL("initgroups() for '%s' failed.", switch_user);
|
|
|
|
if (chdir(pw->pw_dir))
|
|
PFATAL("chdir('%s') failed.", pw->pw_dir);
|
|
|
|
if (chroot(pw->pw_dir))
|
|
PFATAL("chroot('%s') failed.", pw->pw_dir);
|
|
|
|
if (chdir("/"))
|
|
PFATAL("chdir('/') after chroot('%s') failed.", pw->pw_dir);
|
|
|
|
if (!access("/proc/", F_OK) || !access("/sys/", F_OK))
|
|
FATAL("User '%s' must have a dedicated home directory.", switch_user);
|
|
|
|
if (setgid(pw->pw_gid))
|
|
PFATAL("setgid(%u) failed.", pw->pw_gid);
|
|
|
|
if (setuid(pw->pw_uid))
|
|
PFATAL("setuid(%u) failed.", pw->pw_uid);
|
|
|
|
if (getegid() != pw->pw_gid || geteuid() != pw->pw_uid)
|
|
FATAL("Inconsistent euid / egid after dropping privs.");
|
|
|
|
SAYF("[+] Privileges dropped: uid %u, gid %u, root '%s'.\n",
|
|
pw->pw_uid, pw->pw_gid, pw->pw_dir);
|
|
|
|
}
|
|
|
|
|
|
/* Enter daemon mode. */
|
|
|
|
static void fork_off(void) {
|
|
|
|
s32 npid;
|
|
|
|
fflush(0);
|
|
|
|
npid = fork();
|
|
|
|
if (npid < 0) PFATAL("fork() failed.");
|
|
|
|
if (!npid) {
|
|
|
|
/* Let's assume all this is fairly unlikely to fail, so we can live
|
|
with the parent possibly proclaiming success prematurely. */
|
|
|
|
if (dup2(null_fd, 0) < 0) PFATAL("dup2() failed.");
|
|
|
|
/* If stderr is redirected to a file, keep that fd and use it for
|
|
normal output. */
|
|
|
|
if (isatty(2)) {
|
|
|
|
if (dup2(null_fd, 1) < 0 || dup2(null_fd, 2) < 0)
|
|
PFATAL("dup2() failed.");
|
|
|
|
} else {
|
|
|
|
if (dup2(2, 1) < 0) PFATAL("dup2() failed.");
|
|
|
|
}
|
|
|
|
close(null_fd);
|
|
null_fd = -1;
|
|
|
|
if (chdir("/")) PFATAL("chdir('/') failed.");
|
|
|
|
setsid();
|
|
|
|
} else {
|
|
|
|
SAYF("[+] Daemon process created, PID %u (stderr %s).\n", npid,
|
|
isatty(2) ? "not kept" : "kept as-is");
|
|
|
|
SAYF("\nGood luck, you're on your own now!\n");
|
|
|
|
exit(0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/* Handler for Ctrl-C and related signals */
|
|
|
|
static void abort_handler(int sig) {
|
|
if (stop_soon) exit(1);
|
|
stop_soon = 1;
|
|
}
|
|
|
|
|
|
#ifndef __CYGWIN__
|
|
|
|
/* Regenerate pollfd data for poll() */
|
|
|
|
static u32 regen_pfds(struct pollfd* pfds, struct api_client** ctable) {
|
|
u32 i, count = 2;
|
|
|
|
pfds[0].fd = pcap_fileno(pt);
|
|
pfds[0].events = (POLLIN | POLLERR | POLLHUP);
|
|
|
|
DEBUG("[#] Recomputing pollfd data, pcap_fd = %d.\n", pfds[0].fd);
|
|
|
|
if (!api_sock) return 1;
|
|
|
|
pfds[1].fd = api_fd;
|
|
pfds[1].events = (POLLIN | POLLERR | POLLHUP);
|
|
|
|
for (i = 0; i < api_max_conn; i++) {
|
|
|
|
if (api_cl[i].fd == -1) continue;
|
|
|
|
ctable[count] = api_cl + i;
|
|
|
|
/* If we haven't received a complete query yet, wait for POLLIN.
|
|
Otherwise, we want to write stuff. */
|
|
|
|
if (api_cl[i].in_off < sizeof(struct p0f_api_query))
|
|
pfds[count].events = (POLLIN | POLLERR | POLLHUP);
|
|
else
|
|
pfds[count].events = (POLLOUT | POLLERR | POLLHUP);
|
|
|
|
pfds[count++].fd = api_cl[i].fd;
|
|
|
|
}
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
#endif /* !__CYGWIN__ */
|
|
|
|
|
|
/* Event loop! Accepts and dispatches pcap data, API queries, etc. */
|
|
|
|
static void live_event_loop(void) {
|
|
|
|
#ifndef __CYGWIN__
|
|
|
|
/* The huge problem with winpcap on cygwin is that you can't get a file
|
|
descriptor suitable for poll() / select() out of it:
|
|
|
|
http://www.winpcap.org/pipermail/winpcap-users/2009-April/003179.html
|
|
|
|
The only alternatives seem to be additional processes / threads, a
|
|
nasty busy loop, or a ton of Windows-specific code. If you need APi
|
|
queries on Windows, you are welcome to fix this :-) */
|
|
|
|
struct pollfd *pfds;
|
|
struct api_client** ctable;
|
|
u32 pfd_count;
|
|
|
|
/* We need room for pcap, and possibly api_fd + api_clients. */
|
|
|
|
pfds = ck_alloc((1 + (api_sock ? (1 + api_max_conn) : 0)) *
|
|
sizeof(struct pollfd));
|
|
|
|
ctable = ck_alloc((1 + (api_sock ? (1 + api_max_conn) : 0)) *
|
|
sizeof(struct api_client*));
|
|
|
|
pfd_count = regen_pfds(pfds, ctable);
|
|
|
|
if (!daemon_mode)
|
|
SAYF("[+] Entered main event loop.\n\n");
|
|
|
|
while (!stop_soon) {
|
|
|
|
s32 pret, i;
|
|
u32 cur;
|
|
|
|
/* We had a 250 ms timeout to keep Ctrl-C responsive without resortng
|
|
to silly sigaction hackery or unsafe signal handler code. Unfortunately,
|
|
if poll() timeout is much longer than pcap timeout, we end up with
|
|
dropped packets on VMs. Seems like a kernel bug, but for now, this
|
|
loop is a bit busier than it needs to be... */
|
|
|
|
poll_again:
|
|
|
|
pret = poll(pfds, pfd_count, 10);
|
|
|
|
if (pret < 0) {
|
|
if (errno == EINTR) break;
|
|
PFATAL("poll() failed.");
|
|
}
|
|
|
|
if (!pret) { if (log_file) fflush(lf); continue; }
|
|
|
|
/* Examine pfds... */
|
|
|
|
for (cur = 0; cur < pfd_count; cur++) {
|
|
|
|
if (pfds[cur].revents & (POLLERR | POLLHUP)) switch (cur) {
|
|
|
|
case 0:
|
|
|
|
FATAL("Packet capture interface is down.");
|
|
|
|
case 1:
|
|
|
|
FATAL("API socket is down.");
|
|
|
|
default:
|
|
|
|
/* Shut down API connection and free its state. */
|
|
|
|
DEBUG("[#] API connection on fd %d closed.\n", pfds[cur].fd);
|
|
|
|
close(pfds[cur].fd);
|
|
ctable[cur]->fd = -1;
|
|
|
|
pfd_count = regen_pfds(pfds, ctable);
|
|
goto poll_again;
|
|
|
|
}
|
|
|
|
if (pfds[cur].revents & POLLOUT) switch (cur) {
|
|
|
|
case 0: case 1:
|
|
|
|
FATAL("Unexpected POLLOUT on fd %d.\n", cur);
|
|
|
|
default:
|
|
|
|
/* Write API response, restart state when complete. */
|
|
|
|
if (ctable[cur]->in_off < sizeof(struct p0f_api_query))
|
|
FATAL("Inconsistent p0f_api_response state.\n");
|
|
|
|
i = write(pfds[cur].fd,
|
|
((char*)&ctable[cur]->out_data) + ctable[cur]->out_off,
|
|
sizeof(struct p0f_api_response) - ctable[cur]->out_off);
|
|
|
|
if (i <= 0) PFATAL("write() on API socket fails despite POLLOUT.");
|
|
|
|
ctable[cur]->out_off += i;
|
|
|
|
/* All done? Back to square zero then! */
|
|
|
|
if (ctable[cur]->out_off == sizeof(struct p0f_api_response)) {
|
|
|
|
ctable[cur]->in_off = ctable[cur]->out_off = 0;
|
|
pfds[cur].events = (POLLIN | POLLERR | POLLHUP);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (pfds[cur].revents & POLLIN) switch (cur) {
|
|
|
|
case 0:
|
|
|
|
/* Process traffic on the capture interface. */
|
|
|
|
if (pcap_dispatch(pt, -1, (pcap_handler)parse_packet, 0) < 0)
|
|
FATAL("Packet capture interface is down.");
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
/* Accept new API connection, limits permitting. */
|
|
|
|
if (!api_sock) FATAL("Unexpected API connection.");
|
|
|
|
if (pfd_count - 2 < api_max_conn) {
|
|
|
|
for (i = 0; i < api_max_conn && api_cl[i].fd >= 0; i++);
|
|
|
|
if (i == api_max_conn) FATAL("Inconsistent API connection data.");
|
|
|
|
api_cl[i].fd = accept(api_fd, NULL, NULL);
|
|
|
|
if (api_cl[i].fd < 0) {
|
|
|
|
WARN("Unable to handle API connection: accept() fails.");
|
|
|
|
} else {
|
|
|
|
if (fcntl(api_cl[i].fd, F_SETFL, O_NONBLOCK))
|
|
PFATAL("fcntl() to set O_NONBLOCK on API connection fails.");
|
|
|
|
api_cl[i].in_off = api_cl[i].out_off = 0;
|
|
pfd_count = regen_pfds(pfds, ctable);
|
|
|
|
DEBUG("[#] Accepted new API connection, fd %d.\n", api_cl[i].fd);
|
|
|
|
goto poll_again;
|
|
|
|
}
|
|
|
|
} else WARN("Too many API connections (use -S to adjust).\n");
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
/* Receive API query, dispatch when complete. */
|
|
|
|
if (ctable[cur]->in_off >= sizeof(struct p0f_api_query))
|
|
FATAL("Inconsistent p0f_api_query state.\n");
|
|
|
|
i = read(pfds[cur].fd,
|
|
((char*)&ctable[cur]->in_data) + ctable[cur]->in_off,
|
|
sizeof(struct p0f_api_query) - ctable[cur]->in_off);
|
|
|
|
if (i < 0) PFATAL("read() on API socket fails despite POLLIN.");
|
|
|
|
ctable[cur]->in_off += i;
|
|
|
|
/* Query in place? Compute response and prepare to send it back. */
|
|
|
|
if (ctable[cur]->in_off == sizeof(struct p0f_api_query)) {
|
|
|
|
handle_query(&ctable[cur]->in_data, &ctable[cur]->out_data);
|
|
pfds[cur].events = (POLLOUT | POLLERR | POLLHUP);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/* Processed all reported updates already? If so, bail out early. */
|
|
|
|
if (pfds[cur].revents && !--pret) break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ck_free(ctable);
|
|
ck_free(pfds);
|
|
|
|
#else
|
|
|
|
if (!daemon_mode)
|
|
SAYF("[+] Entered main event loop.\n\n");
|
|
|
|
/* Ugh. The only way to keep SIGINT and other signals working is to have this
|
|
funny loop with dummy I/O every 250 ms. Signal handlers don't get called
|
|
in pcap_dispatch() or pcap_loop() unless there's I/O. */
|
|
|
|
while (!stop_soon) {
|
|
|
|
s32 ret = pcap_dispatch(pt, -1, (pcap_handler)parse_packet, 0);
|
|
|
|
if (ret < 0) return;
|
|
|
|
if (log_file && !ret) fflush(lf);
|
|
|
|
write(2, NULL, 0);
|
|
|
|
}
|
|
|
|
#endif /* ^!__CYGWIN__ */
|
|
|
|
WARN("User-initiated shutdown.");
|
|
|
|
}
|
|
|
|
|
|
/* Simple event loop for processing offline captures. */
|
|
|
|
static void offline_event_loop(void) {
|
|
|
|
if (!daemon_mode)
|
|
SAYF("[+] Processing capture data.\n\n");
|
|
|
|
while (!stop_soon) {
|
|
|
|
if (pcap_dispatch(pt, -1, (pcap_handler)parse_packet, 0) <= 0) return;
|
|
|
|
}
|
|
|
|
WARN("User-initiated shutdown.");
|
|
|
|
}
|
|
|
|
|
|
/* Main entry point */
|
|
|
|
int main(int argc, char** argv) {
|
|
|
|
s32 r;
|
|
|
|
setlinebuf(stdout);
|
|
|
|
SAYF("--- p0f " VERSION " by Michal Zalewski <lcamtuf@coredump.cx> ---\n\n");
|
|
|
|
if (getuid() != geteuid())
|
|
FATAL("Please don't make me setuid. See README for more.\n");
|
|
|
|
while ((r = getopt(argc, argv, "+LS:djlf:i:m:o:pr:s:t:u:")) != -1) switch (r) {
|
|
|
|
case 'L':
|
|
|
|
list_interfaces();
|
|
exit(0);
|
|
|
|
case 'S':
|
|
|
|
#ifdef __CYGWIN__
|
|
|
|
FATAL("API mode not supported on Windows (see README).");
|
|
|
|
#else
|
|
|
|
if (api_max_conn != API_MAX_CONN)
|
|
FATAL("Multiple -S options not supported.");
|
|
|
|
api_max_conn = atol(optarg);
|
|
|
|
if (!api_max_conn || api_max_conn > 100)
|
|
FATAL("Outlandish value specified for -S.");
|
|
|
|
break;
|
|
|
|
#endif /* ^__CYGWIN__ */
|
|
|
|
|
|
case 'd':
|
|
|
|
if (daemon_mode)
|
|
FATAL("Double werewolf mode not supported yet.");
|
|
|
|
daemon_mode = 1;
|
|
break;
|
|
|
|
case 'f':
|
|
|
|
if (fp_file)
|
|
FATAL("Multiple -f options not supported.");
|
|
|
|
fp_file = (u8*)optarg;
|
|
break;
|
|
|
|
case 'i':
|
|
|
|
if (use_iface)
|
|
FATAL("Multiple -i options not supported (try '-i any').");
|
|
|
|
use_iface = (u8*)optarg;
|
|
|
|
break;
|
|
|
|
case 'j':
|
|
|
|
if (json_mode)
|
|
FATAL("Double werewolf mode not supported yet.");
|
|
|
|
json_mode = 1;
|
|
break;
|
|
|
|
case 'l':
|
|
|
|
if (line_buffered_mode)
|
|
FATAL("Double werewolf mode not supported yet.");
|
|
|
|
line_buffered_mode = 1;
|
|
break;
|
|
|
|
case 'm':
|
|
|
|
if (max_conn != MAX_CONN || max_hosts != MAX_HOSTS)
|
|
FATAL("Multiple -m options not supported.");
|
|
|
|
if (sscanf(optarg, "%u,%u", &max_conn, &max_hosts) != 2 ||
|
|
!max_conn || max_conn > 100000 ||
|
|
!max_hosts || max_hosts > 500000)
|
|
FATAL("Outlandish value specified for -m.");
|
|
|
|
break;
|
|
|
|
case 'o':
|
|
|
|
if (log_file)
|
|
FATAL("Multiple -o options not supported.");
|
|
|
|
log_file = (u8*)optarg;
|
|
|
|
break;
|
|
|
|
case 'p':
|
|
|
|
if (set_promisc)
|
|
FATAL("Even more promiscuous? People will start talking!");
|
|
|
|
set_promisc = 1;
|
|
break;
|
|
|
|
case 'r':
|
|
|
|
if (read_file)
|
|
FATAL("Multiple -r options not supported.");
|
|
|
|
read_file = (u8*)optarg;
|
|
|
|
break;
|
|
|
|
case 's':
|
|
|
|
#ifdef __CYGWIN__
|
|
|
|
FATAL("API mode not supported on Windows (see README).");
|
|
|
|
#else
|
|
|
|
if (api_sock)
|
|
FATAL("Multiple -s options not supported.");
|
|
|
|
api_sock = (u8*)optarg;
|
|
|
|
break;
|
|
|
|
#endif /* ^__CYGWIN__ */
|
|
|
|
case 't':
|
|
|
|
if (conn_max_age != CONN_MAX_AGE || host_idle_limit != HOST_IDLE_LIMIT)
|
|
FATAL("Multiple -t options not supported.");
|
|
|
|
if (sscanf(optarg, "%u,%u", &conn_max_age, &host_idle_limit) != 2 ||
|
|
!conn_max_age || conn_max_age > 1000000 ||
|
|
!host_idle_limit || host_idle_limit > 1000000)
|
|
FATAL("Outlandish value specified for -t.");
|
|
|
|
break;
|
|
|
|
case 'u':
|
|
|
|
if (switch_user)
|
|
FATAL("Split personality mode not supported.");
|
|
|
|
switch_user = (u8*)optarg;
|
|
|
|
break;
|
|
|
|
default: usage();
|
|
|
|
}
|
|
|
|
if (optind < argc) {
|
|
|
|
if (optind + 1 == argc) orig_rule = (u8*)argv[optind];
|
|
else FATAL("Filter rule must be a single parameter (use quotes).");
|
|
|
|
}
|
|
|
|
if (read_file && api_sock)
|
|
FATAL("API mode looks down on ofline captures.");
|
|
|
|
if (!api_sock && api_max_conn != API_MAX_CONN)
|
|
FATAL("Option -S makes sense only with -s.");
|
|
|
|
if (daemon_mode) {
|
|
|
|
if (read_file)
|
|
FATAL("Daemon mode and offline captures don't mix.");
|
|
|
|
if (!log_file && !api_sock)
|
|
FATAL("Daemon mode requires -o or -s.");
|
|
|
|
#ifdef __CYGWIN__
|
|
|
|
if (switch_user)
|
|
SAYF("[!] Note: under cygwin, -u is largely useless.\n");
|
|
|
|
#else
|
|
|
|
if (!switch_user)
|
|
SAYF("[!] Consider specifying -u in daemon mode (see README).\n");
|
|
|
|
#endif /* ^__CYGWIN__ */
|
|
|
|
}
|
|
|
|
tzset();
|
|
setlocale(LC_TIME, "C");
|
|
|
|
close_spare_fds();
|
|
|
|
get_hash_seed();
|
|
|
|
http_init();
|
|
|
|
read_config(fp_file ? fp_file : (u8*)FP_FILE);
|
|
|
|
prepare_pcap();
|
|
prepare_bpf();
|
|
|
|
if (log_file) open_log();
|
|
if (api_sock) open_api();
|
|
|
|
if (daemon_mode) {
|
|
null_fd = open("/dev/null", O_RDONLY);
|
|
if (null_fd < 0) PFATAL("Cannot open '/dev/null'.");
|
|
}
|
|
|
|
if (switch_user) drop_privs();
|
|
|
|
if (daemon_mode) fork_off();
|
|
|
|
signal(SIGHUP, daemon_mode ? SIG_IGN : abort_handler);
|
|
signal(SIGINT, abort_handler);
|
|
signal(SIGTERM, abort_handler);
|
|
|
|
if (read_file) offline_event_loop(); else live_event_loop();
|
|
|
|
if (!daemon_mode)
|
|
SAYF("\nAll done. Processed %llu packets.\n", packet_cnt);
|
|
|
|
#ifdef DEBUG_BUILD
|
|
destroy_all_hosts();
|
|
TRK_report();
|
|
#endif /* DEBUG_BUILD */
|
|
|
|
return 0;
|
|
|
|
}
|