/* p0f - TCP/IP packet matching ---------------------------- Copyright (C) 2012 by Michal Zalewski Distributed under the terms and conditions of GNU LGPL. */ #include #include #include #include #include #include #include "types.h" #include "config.h" #include "debug.h" #include "alloc-inl.h" #include "process.h" #include "hash.h" #include "tcp.h" #include "readfp.h" #include "p0f.h" #include "fp_tcp.h" /* TCP signature buckets: */ static struct tcp_sig_record* sigs[2][SIG_BUCKETS]; static u32 sig_cnt[2][SIG_BUCKETS]; /* Figure out what the TTL distance might have been for an unknown sig. */ static u8 guess_dist(u8 ttl) { if (ttl <= 32) return 32 - ttl; if (ttl <= 64) return 64 - ttl; if (ttl <= 128) return 128 - ttl; return 255 - ttl; } /* Figure out if window size is a multiplier of MSS or MTU. We don't take window scaling into account, because neither do TCP stack developers. */ static s16 detect_win_multi(struct tcp_sig* ts, u8* use_mtu, u16 syn_mss) { u16 win = ts->win; s32 mss = ts->mss, mss12 = mss - 12; if (!win || mss < 100 || ts->win_type != WIN_TYPE_NORMAL) return -1; #define RET_IF_DIV(_div, _use_mtu, _desc) do { \ if ((_div) && !(win % (_div))) { \ *use_mtu = (_use_mtu); \ DEBUG("[#] Window size %u is a multiple of %s [%u].\n", win, _desc, _div); \ return win / (_div); \ } \ } while (0) RET_IF_DIV(mss, 0, "MSS"); /* Some systems will sometimes subtract 12 bytes when timestamps are in use. */ if (ts->ts1) RET_IF_DIV(mss12, 0, "MSS - 12"); /* Some systems use MTU on the wrong interface, so let's check for the most common case. */ RET_IF_DIV(1500 - MIN_TCP4, 0, "MSS (MTU = 1500, IPv4)"); RET_IF_DIV(1500 - MIN_TCP4 - 12, 0, "MSS (MTU = 1500, IPv4 - 12)"); if (ts->ip_ver == IP_VER6) { RET_IF_DIV(1500 - MIN_TCP6, 0, "MSS (MTU = 1500, IPv6)"); RET_IF_DIV(1500 - MIN_TCP6 - 12, 0, "MSS (MTU = 1500, IPv6 - 12)"); } /* Some systems use MTU instead of MSS: */ RET_IF_DIV(mss + MIN_TCP4, 1, "MTU (IPv4)"); RET_IF_DIV(mss + ts->tot_hdr, 1, "MTU (actual size)"); if (ts->ip_ver == IP_VER6) RET_IF_DIV(mss + MIN_TCP6, 1, "MTU (IPv6)"); RET_IF_DIV(1500, 1, "MTU (1500)"); /* On SYN+ACKs, some systems use of the peer: */ if (syn_mss) { RET_IF_DIV(syn_mss, 0, "peer MSS"); RET_IF_DIV(syn_mss - 12, 0, "peer MSS - 12"); } #undef RET_IF_DIV return -1; } /* See if any of the p0f.fp signatures matches the collected data. */ static void tcp_find_match(u8 to_srv, struct tcp_sig* ts, u8 dupe_det, u16 syn_mss) { struct tcp_sig_record* fmatch = NULL; struct tcp_sig_record* gmatch = NULL; u32 bucket = ts->opt_hash % SIG_BUCKETS; u32 i; u8 use_mtu = 0; s16 win_multi = detect_win_multi(ts, &use_mtu, syn_mss); CP(sigs[to_srv][bucket]); for (i = 0; i < sig_cnt[to_srv][bucket]; i++) { struct tcp_sig_record* ref = sigs[to_srv][bucket] + i; struct tcp_sig* refs = CP(ref->sig); u8 fuzzy = 0; u32 ref_quirks = refs->quirks; if (ref->sig->opt_hash != ts->opt_hash) continue; /* If the p0f.fp signature has no IP version specified, we need to remove IPv6-specific quirks from it when matching IPv4 packets, and vice versa. */ if (refs->ip_ver == -1) ref_quirks &= ((ts->ip_ver == IP_VER4) ? ~(QUIRK_FLOW) : ~(QUIRK_DF | QUIRK_NZ_ID | QUIRK_ZERO_ID)); if (ref_quirks != ts->quirks) { u32 deleted = (ref_quirks ^ ts->quirks) & ref_quirks, added = (ref_quirks ^ ts->quirks) & ts->quirks; /* If there is a difference in quirks, but it amounts to 'df' or 'id+' disappearing, or 'id-' or 'ecn' appearing, allow a fuzzy match. */ if (fmatch || (deleted & ~(QUIRK_DF | QUIRK_NZ_ID)) || (added & ~(QUIRK_ZERO_ID | QUIRK_ECN))) continue; fuzzy = 1; } /* Fixed parameters. */ if (refs->opt_eol_pad != ts->opt_eol_pad || refs->ip_opt_len != ts->ip_opt_len) continue; /* TTL matching, with a provision to allow fuzzy match. */ if (ref->bad_ttl) { if (refs->ttl < ts->ttl) continue; } else { if (refs->ttl < ts->ttl || refs->ttl - ts->ttl > MAX_DIST) fuzzy = 1; } /* Simple wildcards. */ if (refs->mss != -1 && refs->mss != ts->mss) continue; if (refs->wscale != -1 && refs->wscale != ts->wscale) continue; if (refs->pay_class != -1 && refs->pay_class != ts->pay_class) continue; /* Window size. */ if (ts->win_type != WIN_TYPE_NORMAL) { /* Comparing two p0f.fp signatures. */ if (refs->win_type != ts->win_type || refs->win != ts->win) continue; } else { /* Comparing real-world stuff. */ switch (refs->win_type) { case WIN_TYPE_NORMAL: if (refs->win != ts->win) continue; break; case WIN_TYPE_MOD: if (ts->win % refs->win) continue; break; case WIN_TYPE_MSS: if (use_mtu || refs->win != win_multi) continue; break; case WIN_TYPE_MTU: if (!use_mtu || refs->win != win_multi) continue; break; /* WIN_TYPE_ANY */ } } /* Got a match? If not fuzzy, return. If fuzzy, keep looking. */ if (!fuzzy) { if (!ref->generic) { ts->matched = ref; ts->fuzzy = 0; ts->dist = refs->ttl - ts->ttl; return; } else if (!gmatch) gmatch = ref; } else if (!fmatch) fmatch = ref; } /* OK, no definitive match so far... */ if (dupe_det) return; /* If we found a generic signature, and nothing better, let's just use that. */ if (gmatch) { ts->matched = gmatch; ts->fuzzy = 0; ts->dist = gmatch->sig->ttl - ts->ttl; return; } /* No fuzzy matching for userland tools. */ if (fmatch && fmatch->class_id == -1) return; /* Let's try to guess distance if no match; or if match TTL out of range. */ if (!fmatch || fmatch->sig->ttl < ts->ttl || (!fmatch->bad_ttl && fmatch->sig->ttl - ts->ttl > MAX_DIST)) ts->dist = guess_dist(ts->ttl); else ts->dist = fmatch->sig->ttl - ts->ttl; /* Record the outcome. */ ts->matched = fmatch; if (fmatch) ts->fuzzy = 1; } /* Parse TCP-specific bits and register a signature read from p0f.fp. This function is too long. */ void tcp_register_sig(u8 to_srv, u8 generic, s32 sig_class, u32 sig_name, u8* sig_flavor, u32 label_id, u32* sys, u32 sys_cnt, u8* val, u32 line_no) { s8 ver, win_type, pay_class; u8 opt_layout[MAX_TCP_OPT]; u8 opt_cnt = 0, bad_ttl = 0; s32 ittl, olen, mss, win, scale, opt_eol_pad = 0; u32 quirks = 0, bucket, opt_hash; u8* nxt; struct tcp_sig* tsig; struct tcp_sig_record* trec; /* IP version */ switch (*val) { case '4': ver = IP_VER4; break; case '6': ver = IP_VER6; break; case '*': ver = -1; break; default: FATAL("Unrecognized IP version in line %u.", line_no); } if (val[1] != ':') FATAL("Malformed signature in line %u.", line_no); val += 2; /* Initial TTL (possibly ttl+dist or ttl-) */ nxt = val; while (isdigit(*nxt)) nxt++; if (*nxt != ':' && *nxt != '+' && *nxt != '-') FATAL("Malformed signature in line %u.", line_no); ittl = atol((char*)val); if (ittl < 1 || ittl > 255) FATAL("Bogus initial TTL in line %u.", line_no); val = nxt + 1; if (*nxt == '-' && nxt[1] == ':') { bad_ttl = 1; val += 2; } else if (*nxt == '+') { s32 ittl_add; nxt++; while (isdigit(*nxt)) nxt++; if (*nxt != ':') FATAL("Malformed signature in line %u.", line_no); ittl_add = atol((char*)val); if (ittl_add < 0 || ittl + ittl_add > 255) FATAL("Bogus initial TTL in line %u.", line_no); ittl += ittl_add; val = nxt + 1; } /* Length of IP options */ nxt = val; while (isdigit(*nxt)) nxt++; if (*nxt != ':') FATAL("Malformed signature in line %u.", line_no); olen = atol((char*)val); if (olen < 0 || olen > 255) FATAL("Bogus IP option length in line %u.", line_no); val = nxt + 1; /* MSS */ if (*val == '*' && val[1] == ':') { mss = -1; val += 2; } else { nxt = val; while (isdigit(*nxt)) nxt++; if (*nxt != ':') FATAL("Malformed signature in line %u.", line_no); mss = atol((char*)val); if (mss < 0 || mss > 65535) FATAL("Bogus MSS in line %u.", line_no); val = nxt + 1; } /* window size, followed by comma */ if (*val == '*' && val[1] == ',') { win_type = WIN_TYPE_ANY; win = 0; val += 2; } else if (*val == '%') { win_type = WIN_TYPE_MOD; val++; nxt = val; while (isdigit(*nxt)) nxt++; if (*nxt != ',') FATAL("Malformed signature in line %u.", line_no); win = atol((char*)val); if (win < 2 || win > 65535) FATAL("Bogus '%%' value in line %u.", line_no); val = nxt + 1; } else if (!strncmp((char*)val, "mss*", 4) || !strncmp((char*)val, "mtu*", 4)) { win_type = (val[1] == 's') ? WIN_TYPE_MSS : WIN_TYPE_MTU; val += 4; nxt = val; while (isdigit(*nxt)) nxt++; if (*nxt != ',') FATAL("Malformed signature in line %u.", line_no); win = atol((char*)val); if (win < 1 || win > 1000) FATAL("Bogus MSS/MTU multiplier in line %u.", line_no); val = nxt + 1; } else { win_type = WIN_TYPE_NORMAL; nxt = val; while (isdigit(*nxt)) nxt++; if (*nxt != ',') FATAL("Malformed signature in line %u.", line_no); win = atol((char*)val); if (win < 0 || win > 65535) FATAL("Bogus window size in line %u.", line_no); val = nxt + 1; } /* Window scale */ if (*val == '*' && val[1] == ':') { scale = -1; val += 2; } else { nxt = val; while (isdigit(*nxt)) nxt++; if (*nxt != ':') FATAL("Malformed signature in line %u.", line_no); scale = atol((char*)val); if (scale < 0 || scale > 255) FATAL("Bogus window scale in line %u.", line_no); val = nxt + 1; } /* Option layout */ memset(opt_layout, 0, sizeof(opt_layout)); while (*val != ':') { if (opt_cnt >= MAX_TCP_OPT) FATAL("Too many TCP options in line %u.", line_no); if (!strncmp((char*)val, "eol", 3)) { opt_layout[opt_cnt++] = TCPOPT_EOL; val += 3; if (*val != '+') FATAL("Malformed EOL option in line %u.", line_no); val++; nxt = val; while (isdigit(*nxt)) nxt++; if (!*nxt) FATAL("Truncated options in line %u.", line_no); if (*nxt != ':') FATAL("EOL must be the last option in line %u.", line_no); opt_eol_pad = atol((char*)val); if (opt_eol_pad < 0 || opt_eol_pad > 255) FATAL("Bogus EOL padding in line %u.", line_no); val = nxt; } else if (!strncmp((char*)val, "nop", 3)) { opt_layout[opt_cnt++] = TCPOPT_NOP; val += 3; } else if (!strncmp((char*)val, "mss", 3)) { opt_layout[opt_cnt++] = TCPOPT_MAXSEG; val += 3; } else if (!strncmp((char*)val, "ws", 2)) { opt_layout[opt_cnt++] = TCPOPT_WSCALE; val += 2; } else if (!strncmp((char*)val, "sok", 3)) { opt_layout[opt_cnt++] = TCPOPT_SACKOK; val += 3; } else if (!strncmp((char*)val, "sack", 4)) { opt_layout[opt_cnt++] = TCPOPT_SACK; val += 4; } else if (!strncmp((char*)val, "ts", 2)) { opt_layout[opt_cnt++] = TCPOPT_TSTAMP; val += 2; } else if (*val == '?') { s32 optno; val++; nxt = val; while (isdigit(*nxt)) nxt++; if (*nxt != ':' && *nxt != ',') FATAL("Malformed '?' option in line %u.", line_no); optno = atol((char*)val); if (optno < 0 || optno > 255) FATAL("Bogus '?' option in line %u.", line_no); opt_layout[opt_cnt++] = optno; val = nxt; } else { FATAL("Unrecognized TCP option in line %u.", line_no); } if (*val == ':') break; if (*val != ',') FATAL("Malformed TCP options in line %u.", line_no); val++; } val++; opt_hash = hash32(opt_layout, opt_cnt, hash_seed); /* Quirks */ while (*val != ':') { if (!strncmp((char*)val, "df", 2)) { if (ver == IP_VER6) FATAL("'df' is not valid for IPv6 in line %d.", line_no); quirks |= QUIRK_DF; val += 2; } else if (!strncmp((char*)val, "id+", 3)) { if (ver == IP_VER6) FATAL("'id+' is not valid for IPv6 in line %d.", line_no); quirks |= QUIRK_NZ_ID; val += 3; } else if (!strncmp((char*)val, "id-", 3)) { if (ver == IP_VER6) FATAL("'id-' is not valid for IPv6 in line %d.", line_no); quirks |= QUIRK_ZERO_ID; val += 3; } else if (!strncmp((char*)val, "ecn", 3)) { quirks |= QUIRK_ECN; val += 3; } else if (!strncmp((char*)val, "0+", 2)) { if (ver == IP_VER6) FATAL("'0+' is not valid for IPv6 in line %d.", line_no); quirks |= QUIRK_NZ_MBZ; val += 2; } else if (!strncmp((char*)val, "flow", 4)) { if (ver == IP_VER4) FATAL("'flow' is not valid for IPv4 in line %d.", line_no); quirks |= QUIRK_FLOW; val += 4; } else if (!strncmp((char*)val, "seq-", 4)) { quirks |= QUIRK_ZERO_SEQ; val += 4; } else if (!strncmp((char*)val, "ack+", 4)) { quirks |= QUIRK_NZ_ACK; val += 4; } else if (!strncmp((char*)val, "ack-", 4)) { quirks |= QUIRK_ZERO_ACK; val += 4; } else if (!strncmp((char*)val, "uptr+", 5)) { quirks |= QUIRK_NZ_URG; val += 5; } else if (!strncmp((char*)val, "urgf+", 5)) { quirks |= QUIRK_URG; val += 5; } else if (!strncmp((char*)val, "pushf+", 6)) { quirks |= QUIRK_PUSH; val += 6; } else if (!strncmp((char*)val, "ts1-", 4)) { quirks |= QUIRK_OPT_ZERO_TS1; val += 4; } else if (!strncmp((char*)val, "ts2+", 4)) { quirks |= QUIRK_OPT_NZ_TS2; val += 4; } else if (!strncmp((char*)val, "opt+", 4)) { quirks |= QUIRK_OPT_EOL_NZ; val += 4; } else if (!strncmp((char*)val, "exws", 4)) { quirks |= QUIRK_OPT_EXWS; val += 4; } else if (!strncmp((char*)val, "bad", 3)) { quirks |= QUIRK_OPT_BAD; val += 3; } else { FATAL("Unrecognized quirk in line %u.", line_no); } if (*val == ':') break; if (*val != ',') FATAL("Malformed quirks in line %u.", line_no); val++; } val++; /* Payload class */ if (!strcmp((char*)val, "*")) pay_class = -1; else if (!strcmp((char*)val, "0")) pay_class = 0; else if (!strcmp((char*)val, "+")) pay_class = 1; else FATAL("Malformed payload class in line %u.", line_no); /* Phew, okay, we're done. Now, create tcp_sig... */ tsig = DFL_ck_alloc(sizeof(struct tcp_sig)); tsig->opt_hash = opt_hash; tsig->opt_eol_pad = opt_eol_pad; tsig->quirks = quirks; tsig->ip_opt_len = olen; tsig->ip_ver = ver; tsig->ttl = ittl; tsig->mss = mss; tsig->win = win; tsig->win_type = win_type; tsig->wscale = scale; tsig->pay_class = pay_class; /* No need to set ts1, recv_ms, match, fuzzy, dist */ tcp_find_match(to_srv, tsig, 1, 0); if (tsig->matched) FATAL("Signature in line %u is already covered by line %u.", line_no, tsig->matched->line_no); /* Everything checks out, so let's register it. */ bucket = opt_hash % SIG_BUCKETS; sigs[to_srv][bucket] = DFL_ck_realloc(sigs[to_srv][bucket], (sig_cnt[to_srv][bucket] + 1) * sizeof(struct tcp_sig_record)); trec = sigs[to_srv][bucket] + sig_cnt[to_srv][bucket]; sig_cnt[to_srv][bucket]++; trec->generic = generic; trec->class_id = sig_class; trec->name_id = sig_name; trec->flavor = sig_flavor; trec->label_id = label_id; trec->sys = sys; trec->sys_cnt = sys_cnt; trec->line_no = line_no; trec->sig = tsig; trec->bad_ttl = bad_ttl; /* All done, phew. */ } /* Convert struct packet_data to a simplified struct tcp_sig representation suitable for signature matching. Compute hashes. */ static void packet_to_sig(struct packet_data* pk, struct tcp_sig* ts) { ts->opt_hash = hash32(pk->opt_layout, pk->opt_cnt, hash_seed); ts->quirks = pk->quirks; ts->opt_eol_pad = pk->opt_eol_pad; ts->ip_opt_len = pk->ip_opt_len; ts->ip_ver = pk->ip_ver; ts->ttl = pk->ttl; ts->mss = pk->mss; ts->win = pk->win; ts->win_type = WIN_TYPE_NORMAL; /* Keep as-is. */ ts->wscale = pk->wscale; ts->pay_class = !!pk->pay_len; ts->tot_hdr = pk->tot_hdr; ts->ts1 = pk->ts1; ts->recv_ms = get_unix_time_ms(); ts->matched = NULL; ts->fuzzy = 0; ts->dist = 0; }; /* Dump unknown signature. */ static u8* dump_sig(struct packet_data* pk, struct tcp_sig* ts, u16 syn_mss) { static u8* ret; u32 rlen = 0; u8 win_mtu; s16 win_m; u32 i; u8 dist = guess_dist(pk->ttl); #define RETF(_par...) do { \ s32 _len = snprintf(NULL, 0, _par); \ if (_len < 0) FATAL("Whoa, snprintf() fails?!"); \ ret = DFL_ck_realloc_kb(ret, rlen + _len + 1); \ snprintf((char*)ret + rlen, _len + 1, _par); \ rlen += _len; \ } while (0) if (dist > MAX_DIST) { RETF("%u:%u+?:%u:", pk->ip_ver, pk->ttl, pk->ip_opt_len); } else { RETF("%u:%u+%u:%u:", pk->ip_ver, pk->ttl, dist, pk->ip_opt_len); } /* Detect a system echoing back MSS from p0f-sendsyn queries, suggest using a wildcard in such a case. */ if (pk->mss == SPECIAL_MSS && pk->tcp_type == (TCP_SYN|TCP_ACK)) RETF("*:"); else RETF("%u:", pk->mss); win_m = detect_win_multi(ts, &win_mtu, syn_mss); if (win_m > 0) RETF("%s*%u", win_mtu ? "mtu" : "mss", win_m); else RETF("%u", pk->win); RETF(",%u:", pk->wscale); for (i = 0; i < pk->opt_cnt; i++) { switch (pk->opt_layout[i]) { case TCPOPT_EOL: RETF("%seol+%u", i ? "," : "", pk->opt_eol_pad); break; case TCPOPT_NOP: RETF("%snop", i ? "," : ""); break; case TCPOPT_MAXSEG: RETF("%smss", i ? "," : ""); break; case TCPOPT_WSCALE: RETF("%sws", i ? "," : ""); break; case TCPOPT_SACKOK: RETF("%ssok", i ? "," : ""); break; case TCPOPT_SACK: RETF("%ssack", i ? "," : ""); break; case TCPOPT_TSTAMP: RETF("%sts", i ? "," : ""); break; default: RETF("%s?%u", i ? "," : "", pk->opt_layout[i]); } } RETF(":"); if (pk->quirks) { u8 sp = 0; #define MAYBE_CM(_str) do { \ if (sp) RETF("," _str); else RETF(_str); \ sp = 1; \ } while (0) if (pk->quirks & QUIRK_DF) MAYBE_CM("df"); if (pk->quirks & QUIRK_NZ_ID) MAYBE_CM("id+"); if (pk->quirks & QUIRK_ZERO_ID) MAYBE_CM("id-"); if (pk->quirks & QUIRK_ECN) MAYBE_CM("ecn"); if (pk->quirks & QUIRK_NZ_MBZ) MAYBE_CM("0+"); if (pk->quirks & QUIRK_FLOW) MAYBE_CM("flow"); if (pk->quirks & QUIRK_ZERO_SEQ) MAYBE_CM("seq-"); if (pk->quirks & QUIRK_NZ_ACK) MAYBE_CM("ack+"); if (pk->quirks & QUIRK_ZERO_ACK) MAYBE_CM("ack-"); if (pk->quirks & QUIRK_NZ_URG) MAYBE_CM("uptr+"); if (pk->quirks & QUIRK_URG) MAYBE_CM("urgf+"); if (pk->quirks & QUIRK_PUSH) MAYBE_CM("pushf+"); if (pk->quirks & QUIRK_OPT_ZERO_TS1) MAYBE_CM("ts1-"); if (pk->quirks & QUIRK_OPT_NZ_TS2) MAYBE_CM("ts2+"); if (pk->quirks & QUIRK_OPT_EOL_NZ) MAYBE_CM("opt+"); if (pk->quirks & QUIRK_OPT_EXWS) MAYBE_CM("exws"); if (pk->quirks & QUIRK_OPT_BAD) MAYBE_CM("bad"); #undef MAYBE_CM } if (pk->pay_len) RETF(":+"); else RETF(":0"); return ret; } /* Dump signature-related flags. */ static u8* dump_flags(struct packet_data* pk, struct tcp_sig* ts) { static u8* ret; u32 rlen = 0; RETF(""); if (ts->matched) { if (ts->matched->generic) RETF(" generic"); if (ts->fuzzy) RETF(" fuzzy"); if (ts->matched->bad_ttl) RETF(" random_ttl"); } if (ts->dist > MAX_DIST) RETF(" excess_dist"); if (pk->tos) RETF(" tos:0x%02x", pk->tos); if (*ret) return ret + 1; else return (u8*)"none"; #undef RETF } /* Compare current signature with historical data, draw conclusions. This is called only for OS sigs. */ static void score_nat(u8 to_srv, struct tcp_sig* sig, struct packet_flow* f) { struct host_data* hd; struct tcp_sig* ref; u8 score = 0, diff_already = 0; u16 reason = 0; s32 ttl_diff; if (to_srv) { hd = f->client; ref = hd->last_syn; } else { hd = f->server; ref = hd->last_synack; } if (!ref) { /* No previous signature of matching type at all. We can perhaps still check if class / name is the same as on file, as that data might have been obtained from other types of sigs. */ if (sig->matched && hd->last_class_id != -1) { if (hd->last_name_id != sig->matched->name_id) { DEBUG("[#] New TCP signature different OS type than host data.\n"); reason |= NAT_OS_SIG; score += 8; } } goto log_and_update; } /* We have some previous data. */ if (!sig->matched || !ref->matched) { /* One or both of the signatures are unknown. Let's see if they differ. The scoring here isn't too strong, because we don't know if the unrecognized signature isn't originating from userland tools. */ if ((sig->quirks ^ ref->quirks) & ~(QUIRK_ECN|QUIRK_DF|QUIRK_NZ_ID| QUIRK_ZERO_ID)) { DEBUG("[#] Non-fuzzy quirks changed compared to previous sig.\n"); reason |= NAT_UNK_DIFF; score += 2; } else if (to_srv && sig->opt_hash != ref->opt_hash) { /* We only match option layout for SYNs; it may change on SYN+ACK, and the user may have gaps in SYN+ACK sigs if he ignored our advice on using p0f-sendsyn. */ DEBUG("[#] SYN option layout changed compared to previous sig.\n"); reason |= NAT_UNK_DIFF; score += 1; } /* Progression from known to unknown is also of interest for SYNs. */ if (to_srv && sig->matched != ref->matched) { DEBUG("[#] SYN signature changed from %s.\n", sig->matched ? "unknown to known" : "known to unknown"); score += 1; reason |= NAT_TO_UNK; } } else { /* Both signatures known! */ if (ref->matched->name_id != sig->matched->name_id) { DEBUG("[#] TCP signature different OS type on previous sig.\n"); score += 8; reason |= NAT_OS_SIG; diff_already = 1; } else if (to_srv) { /* SYN signatures match superficially, but... */ if (ref->matched->label_id != sig->matched->label_id) { /* SYN label changes are a weak but useful signal. SYN+ACK signatures may need less intuitive groupings, so we don't check that. */ DEBUG("[#] SYN signature label different on previous sig.\n"); score += 2; reason |= NAT_OS_SIG; } else if (ref->matched->line_no != sig->matched->line_no) { /* Change in line number is an extremely weak but still noteworthy signal. */ DEBUG("[#] SYN signature changes within the same label.\n"); score += 1; reason |= NAT_OS_SIG; } else if (sig->fuzzy != ref->fuzzy) { /* Fuzziness change on a perfectly matched signature? */ DEBUG("[#] SYN signature fuzziness changes.\n"); score += 1; reason |= NAT_FUZZY; } } } /* Unless the signatures are already known to differ radically, mismatch between host data and current sig is of additional note. */ if (!diff_already && sig->matched && hd->last_class_id != -1 && hd->last_name_id != sig->matched->name_id) { DEBUG("[#] New OS signature different OS type than host data.\n"); score += 8; reason |= NAT_OS_SIG; diff_already = 1; } /* TTL differences in absence of major signature mismatches is also interesting, unless the signatures are tagged as "bad TTL", or unless the difference is barely 1 and the host is distant. */ #define ABS(_x) ((_x) < 0 ? -(_x) : (_x)) ttl_diff = ((s16)sig->ttl) - ref->ttl; if (!diff_already && ttl_diff && (!sig->matched || !sig->matched->bad_ttl) && (!ref->matched || !ref->matched->bad_ttl) && (sig->dist <= NEAR_TTL_LIMIT || ttl_diff > 1)) { DEBUG("[#] Signature TTL differs by %d (dist = %u).\n", ttl_diff, sig->dist); if (sig->dist > LOCAL_TTL_LIMIT && ABS(ttl_diff) <= SMALL_TTL_CHG) score += 1; else score += 4; reason |= NAT_TTL; } /* Source port going back frequently is of some note, although it will happen spontaneously every now and then. Require the drop to be by at least few dozen, to account for simple case of several simultaneously opened connections arriving in odd order. */ if (to_srv && hd->last_port && f->cli_port < hd->last_port && hd->last_port - f->cli_port >= MIN_PORT_DROP) { DEBUG("[#] Source port drops from %u to %u.\n", hd->last_port, f->cli_port); score += 1; reason |= NAT_PORT; } /* Change of MTU is always sketchy. */ if (sig->mss != ref->mss) { DEBUG("[#] MSS for signature changed from %u to %u.\n", ref->mss, sig->mss); score += 1; reason |= NAT_MSS; } /* Check timestamp progression to possibly adjust current score. Don't rate on TS alone, because some systems may be just randomizing that. */ if (score && sig->ts1 && ref->ts1) { u64 ms_diff = sig->recv_ms - ref->recv_ms; /* Require a timestamp within the last day; if the apparent TS progression is much higher than 1 kHz, complain. */ if (ms_diff < MAX_NAT_TS) { u64 use_ms = (ms_diff < TSTAMP_GRACE) ? TSTAMP_GRACE : ms_diff; u64 max_ts = use_ms * MAX_TSCALE / 1000; u32 ts_diff = sig->ts1 - ref->ts1; if (ts_diff > max_ts && (ms_diff >= TSTAMP_GRACE || ~ts_diff > max_ts)) { DEBUG("[#] Dodgy timestamp progression across signatures (%d " "in %llu ms).\n", ts_diff, ms_diff); score += 4; reason |= NAT_TS; } else { DEBUG("[#] Timestamp consistent across signatures (%d in %llu ms), " "reducing score.\n", ts_diff, ms_diff); score /= 2; } } else DEBUG("[#] Timestamps available, but with bad interval (%llu ms).\n", ms_diff); } log_and_update: add_nat_score(to_srv, f, reason, score); /* Update some of the essential records. */ if (sig->matched) { hd->last_class_id = sig->matched->class_id; hd->last_name_id = sig->matched->name_id; hd->last_flavor = sig->matched->flavor; hd->last_quality = (sig->fuzzy * P0F_MATCH_FUZZY) | (sig->matched->generic * P0F_MATCH_GENERIC); } hd->last_port = f->cli_port; } /* Fingerprint SYN or SYN+ACK. */ struct tcp_sig* fingerprint_tcp(u8 to_srv, struct packet_data* pk, struct packet_flow* f) { struct tcp_sig* sig; struct tcp_sig_record* m; sig = ck_alloc(sizeof(struct tcp_sig)); packet_to_sig(pk, sig); /* Detect packets generated by p0f-sendsyn; they require special handling to provide the user with response fingerprints, but not interfere with NAT scores and such. */ if (pk->tcp_type == TCP_SYN && pk->win == SPECIAL_WIN && pk->mss == SPECIAL_MSS) f->sendsyn = 1; if (to_srv) start_observation(f->sendsyn ? "sendsyn probe" : "syn", 4, 1, f); else start_observation(f->sendsyn ? "sendsyn response" : "syn+ack", 4, 0, f); tcp_find_match(to_srv, sig, 0, f->syn_mss); if ((m = sig->matched)) { OBSERVF((m->class_id == -1 || f->sendsyn) ? "app" : "os", "%s%s%s", fp_os_names[m->name_id], m->flavor ? " " : "", m->flavor ? m->flavor : (u8*)""); } else { add_observation_field("os", NULL); } if (m && m->bad_ttl) { OBSERVF("dist", "<= %u", sig->dist); } else { if (to_srv) f->client->distance = sig->dist; else f->server->distance = sig->dist; OBSERVF("dist", "%u", sig->dist); } add_observation_field("params", dump_flags(pk, sig)); add_observation_field("raw_sig", dump_sig(pk, sig, f->syn_mss)); if (pk->tcp_type == TCP_SYN) f->syn_mss = pk->mss; /* That's about as far as we go with non-OS signatures. */ if (m && m->class_id == -1) { verify_tool_class(to_srv, f, m->sys, m->sys_cnt); ck_free(sig); return NULL; } if (f->sendsyn) { ck_free(sig); return NULL; } score_nat(to_srv, sig, f); return sig; } /* Perform uptime detection. This is the only FP function that gets called not only on SYN or SYN+ACK, but also on ACK traffic. */ void check_ts_tcp(u8 to_srv, struct packet_data* pk, struct packet_flow* f) { u32 ts_diff; u64 ms_diff; u32 freq; u32 up_min, up_mod_days; double ffreq; if (!pk->ts1 || f->sendsyn) return; /* If we're getting SYNs very rapidly, last_syn may be changing too quickly to be of any use. Perhaps lock into an older value? */ if (to_srv) { if (f->cli_tps || !f->client->last_syn || !f->client->last_syn->ts1) return; ms_diff = get_unix_time_ms() - f->client->last_syn->recv_ms; ts_diff = pk->ts1 - f->client->last_syn->ts1; } else { if (f->srv_tps || !f->server->last_synack || !f->server->last_synack->ts1) return; ms_diff = get_unix_time_ms() - f->server->last_synack->recv_ms; ts_diff = pk->ts1 - f->server->last_synack->ts1; } /* Wait at least 25 ms, and not more than 10 minutes, for at least 5 timestamp ticks. Allow the timestamp to go back slightly within a short window, too - we may be receiving packets a bit out of order. */ if (ms_diff < MIN_TWAIT || ms_diff > MAX_TWAIT) return; if (ts_diff < 5 || (ms_diff < TSTAMP_GRACE && (~ts_diff) / 1000 < MAX_TSCALE / TSTAMP_GRACE)) return; if (ts_diff > ~ts_diff) ffreq = ~ts_diff * -1000.0 / ms_diff; else ffreq = ts_diff * 1000.0 / ms_diff; if (ffreq < MIN_TSCALE || ffreq > MAX_TSCALE) { /* Allow bad reading on SYN, as this may be just an artifact of IP sharing or OS change. */ if (pk->tcp_type != TCP_SYN) { if (to_srv) f->cli_tps = -1; else f->srv_tps = -1; } DEBUG("[#] Bad %s TS frequency: %.02f Hz (%d ticks in %llu ms).\n", to_srv ? "client" : "server", ffreq, ts_diff, ms_diff); return; } freq = ffreq; /* Round the frequency neatly. */ switch (freq) { case 0: freq = 1; break; case 1 ... 10: break; case 11 ... 50: freq = (freq + 3) / 5 * 5; break; case 51 ... 100: freq = (freq + 7) / 10 * 10; break; case 101 ... 500: freq = (freq + 33) / 50 * 50; break; default: freq = (freq + 67) / 100 * 100; break; } if (to_srv) f->cli_tps = freq; else f->srv_tps = freq; up_min = pk->ts1 / freq / 60; up_mod_days = 0xFFFFFFFF / (freq * 60 * 60 * 24); start_observation("uptime", 2, to_srv, f); if (to_srv) { f->client->last_up_min = up_min; f->client->up_mod_days = up_mod_days; } else { f->server->last_up_min = up_min; f->server->up_mod_days = up_mod_days; } OBSERVF("uptime", "%u days %u hrs %u min (modulo %u days)", (up_min / 60 / 24), (up_min / 60) % 24, up_min % 60, up_mod_days); OBSERVF("raw_freq", "%.02f Hz", ffreq); }