id = "p0f signature" description = "Guesses target os per each opened port, instead of standard per host tests." author = "Marek Majkowski " license = "See nmaps COPYING for licence" --[[ it's just an implementation of lcamtuf's p0f SYN+ACK scan ]]-- categories = {"safe"} -- intrusive? ---------------------------------------------------------------------------------------------------------------- Tcp = {} function Tcp:new(packet, packet_len) local o = setmetatable({}, {__index = Tcp}) o.buf = packet o.packet_len = packet_len if not o:parse_ip() then return nil end if not o:parse_tcp() then return nil end return o end function u8(b, i) return string.byte(b, i+1) end function u16(b, i) local b1,b2 b1, b2 = string.byte(b, i+1), string.byte(b, i+2) -- 2^8 2^0 return b1*256 + b2 end function u32(b,i) local b1,b2,b3,b4 b1, b2 = string.byte(b, i+1), string.byte(b, i+2) b3, b4 = string.byte(b, i+3), string.byte(b, i+4) -- 2^24 2^16 2^8 2^0 return b1*16777216 + b2*65536 + b3*256 + b4 end function Tcp:u8(index) return u8(self.buf, index) end function Tcp:u16(index) return u16(self.buf, index) end function Tcp:u32(index) return u32(self.buf, index) end function Tcp:raw(index, length) return string.char(string.byte(self.buf, index+1, index+1+length-1)) end function Tcp:parse_options(offset, length) -- parse ip/tcp options to dict structure local options = {} local op = 1 local opt_ptr = 0 while opt_ptr < length do local t, l, d options[op] = {} t = self:u8(offset + opt_ptr) options[op].type = t if t==0 or t==1 then l = 1 d = nil else l = self:u8(offset + opt_ptr + 1) if l > 2 then d = self:raw(offset + opt_ptr + 2, l-2) end end options[op].len = l options[op].data = d opt_ptr = opt_ptr + l op = op + 1 end return options end function Tcp:toip(raw_ip_addr) return string.format("%i.%i.%i.%i", string.byte(raw_ip_addr,1,4)) end function Tcp:parse_ip() self.ip_offset = 0 if string.len(self.buf) < 20 then -- too short return false end self.ip_v = bit.rshift(bit.band(self:u8(self.ip_offset + 0), 0xF0), 4) self.ip_hl = bit.band(self:u8(self.ip_offset + 0), 0x0F) -- header_length or data_offset if self.ip_v ~= 4 then -- not ip return false end self.ip_tos = self:u8(self.ip_offset + 1) self.ip_len = self:u16(self.ip_offset + 2) self.ip_id = self:u16(self.ip_offset + 4) self.ip_off = self:u16(self.ip_offset + 6) self.ip_rf = bit.band(self.ip_off, 0x8000)~=0 -- true/false self.ip_df = bit.band(self.ip_off, 0x4000)~=0 self.ip_mf = bit.band(self.ip_off, 0x2000)~=0 self.ip_off = bit.band(self.ip_off, 0x1FFF) -- fragment offset self.ip_ttl = self:u8(self.ip_offset + 8) self.ip_p = self:u8(self.ip_offset + 9) self.ip_sum = self:u16(self.ip_offset + 10) self.ip_bin_src = self:raw(self.ip_offset + 12,4) -- raw 4-bytes string self.ip_bin_dst = self:raw(self.ip_offset + 16,4) self.ip_src = self:toip(self.ip_bin_src) -- formatted string self.ip_dst = self:toip(self.ip_bin_dst) self.ip_opt_offset = self.ip_offset + 20 self.ip_options = self:parse_options(self.ip_opt_offset, ((self.ip_hl*4)-20)) self.ip_data_offset = self.ip_offset + self.ip_hl*4 return true end function Tcp:tostring() return string.format( "%s:%i -> %s:%i", self.ip_src, self.tcp_sport, self.ip_dst, self.tcp_dport ) end function Tcp:to_p0f() return string.format( "%s:%i:%i:%i:%s:%s:?:?|link: %s, %s%s%s", self:to_p0f_window_size(), self.ip_ttl, (self.ip_df and "1" or "0"), self:to_p0f_packet_size(), self:to_p0f_tcp_options(), self:to_p0f_quirks(), self:lookup_link(), self:to_p0f_uptime(), self:to_fill(), self:to_ipid() ) end function tohex(str) local b = "" for c in string.gmatch(str, ".") do b = string.format('%s%02x',b, string.byte(c)) end return b end function Tcp:to_fill() -- ethernet fill, if packet is smaller than 46 bytes local l = self.tcp_data_offset + self.tcp_data_length local s = self.packet_len if l < s then return string.format(", fill:%s", tohex( string.sub(self.buf, l+1) )) end return "" end function Tcp:to_ipid() -- ipid number if self.ip_id > 0 then return string.format(", ipid:%i", self.ip_id) end return "" end function Tcp:to_p0f_uptime() if self.tcp_opt_t1 == nil then return "up: disabled" end return string.format("up: %i hrs", self.tcp_opt_t1/360000) end function Tcp:to_p0f_window_size() local win = self.tcp_win if self.tcp_opt_mss ~= nil and win % self.tcp_opt_mss == 0 then return string.format("S%i", win/self.tcp_opt_mss) -- not floating -> no reminder end if self.tcp_opt_mtu ~= nil and win % self.tcp_opt_mtu == 0 then return string.format("T%i", win/self.tcp_opt_mtu) -- not floating -> no reminder end return string.format("%i", win) end function Tcp:to_p0f_packet_size() if self.ip_len < 100 then return string.format("%i", self.ip_len) end -- bigger than 99 return "*" end function Tcp:to_p0f_tcp_options() local opt_chr = { [0] = "E", [1] = "N", [2] = "M", [3] = "W", [4] = "S", [8] = "T" } local str = "" for k,opt in ipairs(self.tcp_options) do if opt_chr[opt.type] ~= nil then str = str .. opt_chr[opt.type] else str = str .. string.format("?%i", opt.type) end if opt.type == 2 then str = str .. string.format("%i", u16(opt.data, 0)) elseif opt.type == 3 then str = str .. string.format("%i", u8(opt.data, 0)) elseif opt.type == 8 then local t = u32(opt.data, 0) if t == 0 then str = str .. "0" end end str = str .. "," end if string.len(str) == 0 then return "." end return string.sub(str, 0, string.len(str)-1) end function Tcp:tcp_parse_options() local eoo = false for _,opt in ipairs(self.tcp_options) do if eoo then self.tcp_opt_after_eol = true end if opt.type == 0 then -- end of options eoo = true elseif opt.type == 2 then -- MSS self.tcp_opt_mss = u16(opt.data, 0) self.tcp_opt_mtu = self.tcp_opt_mss + 40 elseif opt.type == 3 then -- widow scaling self.tcp_opt_ws = u8(opt.data, 0) elseif opt.type == 8 then -- timestamp self.tcp_opt_t1 = u32(opt.data, 0) self.tcp_opt_t2 = u32(opt.data, 4) end end end function Tcp:to_p0f_quirks() local str = "" -- P if self.tcp_opt_after_eol then str = str .. "P" end -- Z if self.ip_id == 0 then str = str .. "Z" end -- I if self.ip_options[1] ~= nil then str = str .. "I" end -- U if self.tcp_urp ~= 0 then str = str .. "U" end -- X if self.tcp_x2 ~= 0 then str = str .. "X" end -- A if self.tcp_ack ~= 0 then str = str .. "A" end -- T if self.tcp_opt_t2 ~= nil and self.tcp_opt_t2 > 0 then --[[ io.write(string.format("%s:%i->%i t1=%i t2=%i\n", self.ip_src, self.tcp_sport, self.tcp_dport, self.tcp_opt_t1, self.tcp_opt_t2)) ]]-- str = str .. "T" end -- D if self.tcp_data_length > 0 then str = str .. "D" end -- F if self.tcp_th_urg or self.tcp_th_fin then -- no push? str = str .. "F" end -- not implemented option -> "!" if string.len(str) == 0 then return "." end return str end function Tcp:parse_tcp() self.tcp_offset = self.ip_data_offset if string.len(self.buf) < self.tcp_offset + 20 then return false end self.tcp_sport = self:u16(self.tcp_offset + 0) self.tcp_dport = self:u16(self.tcp_offset + 2) self.tcp_seq = self:u32(self.tcp_offset + 4) self.tcp_ack = self:u32(self.tcp_offset + 8) self.tcp_hl = bit.rshift(bit.band(self:u8(self.tcp_offset+12), 0xF0), 4) -- header_length or data_offset self.tcp_x2 = bit.band(self:u8(self.tcp_offset+12), 0x0F) self.tcp_flags = self:u8(self.tcp_offset + 13) self.tcp_th_fin = bit.band(self.tcp_flags, 0x01)~=0 -- true/false self.tcp_th_syn = bit.band(self.tcp_flags, 0x02)~=0 self.tcp_th_rst = bit.band(self.tcp_flags, 0x04)~=0 self.tcp_th_push = bit.band(self.tcp_flags, 0x08)~=0 self.tcp_th_ack = bit.band(self.tcp_flags, 0x10)~=0 self.tcp_th_urg = bit.band(self.tcp_flags, 0x20)~=0 self.tcp_th_ece = bit.band(self.tcp_flags, 0x40)~=0 self.tcp_th_cwr = bit.band(self.tcp_flags, 0x80)~=0 self.tcp_win = self:u16(self.tcp_offset + 14) self.tcp_sum = self:u16(self.tcp_offset + 16) self.tcp_urp = self:u16(self.tcp_offset + 18) self.tcp_opt_offset = self.tcp_offset + 20 self.tcp_options = self:parse_options(self.tcp_opt_offset, ((self.tcp_hl*4)-20)) self.tcp_data_offset = self.tcp_offset + self.tcp_hl*4 self.tcp_data_length = self.ip_len - self.tcp_offset - self.tcp_hl*4 self:tcp_parse_options() return true end function Tcp:lookup_link() local mtu_def = { {["mtu"]=256, ["txt"]= "radio modem"}, {["mtu"]=386, ["txt"]= "ethernut"}, {["mtu"]=552, ["txt"]= "SLIP line / encap ppp"}, {["mtu"]=576, ["txt"]= "sometimes modem"}, {["mtu"]=1280, ["txt"]= "gif tunnel"}, {["mtu"]=1300, ["txt"]= "PIX, SMC, sometimes wireless"}, {["mtu"]=1362, ["txt"]= "sometimes DSL (1)"}, {["mtu"]=1372, ["txt"]= "cable modem"}, {["mtu"]=1400, ["txt"]= "(Google/AOL)"}, {["mtu"]=1415, ["txt"]= "sometimes wireless"}, {["mtu"]=1420, ["txt"]= "GPRS, T1, FreeS/WAN"}, {["mtu"]=1423, ["txt"]= "sometimes cable"}, {["mtu"]=1440, ["txt"]= "sometimes DSL (2)"}, {["mtu"]=1442, ["txt"]= "IPIP tunnel"}, {["mtu"]=1450, ["txt"]= "vtun"}, {["mtu"]=1452, ["txt"]= "sometimes DSL (3)"}, {["mtu"]=1454, ["txt"]= "sometimes DSL (4)"}, {["mtu"]=1456, ["txt"]= "ISDN ppp"}, {["mtu"]=1458, ["txt"]= "BT DSL (?)"}, {["mtu"]=1462, ["txt"]= "sometimes DSL (5)"}, {["mtu"]=1470, ["txt"]= "(Google 2)"}, {["mtu"]=1476, ["txt"]= "IPSec/GRE"}, {["mtu"]=1480, ["txt"]= "IPv6/IPIP"}, {["mtu"]=1492, ["txt"]= "pppoe (DSL)"}, {["mtu"]=1496, ["txt"]= "vLAN"}, {["mtu"]=1500, ["txt"]= "ethernet/modem"}, {["mtu"]=1656, ["txt"]= "Ericsson HIS"}, {["mtu"]=2024, ["txt"]= "wireless/IrDA"}, {["mtu"]=2048, ["txt"]= "Cyclom X.25 WAN"}, {["mtu"]=2250, ["txt"]= "AiroNet wireless"}, {["mtu"]=3924, ["txt"]= "loopback"}, {["mtu"]=4056, ["txt"]= "token ring (1)"}, {["mtu"]=4096, ["txt"]= "Sangoma X.25 WAN"}, {["mtu"]=4352, ["txt"]= "FDDI"}, {["mtu"]=4500, ["txt"]= "token ring (2)"}, {["mtu"]=9180, ["txt"]= "FORE ATM"}, {["mtu"]=16384, ["txt"]= "sometimes loopback (1)"}, {["mtu"]=16436, ["txt"]= "sometimes loopback (2)"}, {["mtu"]=18000, ["txt"]= "token ring x4"}, } if not self.tcp_opt_mss or self.tcp_opt_mss==0 then return "unspecified" end for _,x in ipairs(mtu_def) do local mtu = x["mtu"] local txt = x["txt"] if self.tcp_opt_mtu == mtu then return txt end if self.tcp_opt_mtu < mtu then return string.format("unknown-%i", self.tcp_opt_mtu) end end return string.format("unknown-%i", self.tcp_opt_mtu) end ---------------------------------------------------------------------------------------------------------------- function split(str, regexp) local tab = {} local i = 0 for x in string.gfind(str, regexp) do i = i+1 tab[i] = x end return tab end function join(delim, arr, idx) local i = idx local o = "" while arr[i] ~= nil do if o ~= "" then o = o .. delim end o = o .. arr[i] i = i + 1 end return o end function sig_unpack(signature) local s = {} local tab = split(signature, "[^:]+") s.wwww = tab[1] s.ttt = tonumber(tab[2]) s.D = tab[3] s.ss = tab[4] s.OOO = split(tab[5], "[^,]+") s.QQ = tab[6] -- split(tab[6], ".") s.OS = tab[7] s.details = join(":" ,tab, 8) s.full = signature return s end function sig_getindex_OOO(t) local s = "" for k,v in ipairs(t) do local t = string.find(v, "^[NEST?]") -- no W and M if t ~= nil then s = s .. v .. "," end end return s end function sig_getindex(s) return string.format("%s|%s|%s|%s", s.D, s.ss, sig_getindex_OOO(s.OOO), s.QQ ) end function sigs_load(filename) local sigs = {} local f = io.open("scripts/" .. filename, "r") if f == nil then io.write("P0F file NOT FOUND " .. filename .. "\n") end -- io.write("Loading p0f signature file\n") while true do local line = f:read("*line") if line == nil then break end if string.find(line, "^#") == nil and string.find(line, ":") ~= nil then -- io.write(line .. "!\n") local sig = sig_unpack(line) local idx = sig_getindex(sig) if sigs[idx] == nil then sigs[idx] = {} end table.insert(sigs[idx], sig) -- io.write(string.format("%s\t%s\n", idx , line)) end end f:close() return sigs end function test_opt(a, b) -- a=['Xnnn', 'X%nnn', 'X*'] b=[Xnnn] if a == b then return true end if string.len(a)<2 then return false end local a1 = string.sub(a, 1, 1) local b1 = string.sub(b, 1, 1) local a2 = string.sub(a, 2, 2) local b2 = string.sub(b, 2, 2) local a2s = string.sub(a, 2) local b2s = string.sub(b, 2) -- can't be % local a3s = string.sub(a, 3) local b3s = string.sub(b, 3) if a1 ~= b1 then return false end if a2s == "*" then return true end if a2 == "%" then if tonumber(b2s) % tonumber(a3s) == 0 then return true else return false end end return false end function sig_compare(t, s) if test_opt(t.wwww, s.wwww)~= true then return false, string.format("wwww %s!=%s", t.wwww, s.wwww) end if t.ttt < s.ttt or s.ttt < t.ttt-40 then return false, string.format("ttl %i>=%i>=%i", t.ttt, s.ttt, t.ttt-40) end if t.D ~= s.D then return false, string.format("D %s!=%s", t.D, s.D) end if t.ss ~= s.ss then return false, string.format("ss %s!=%s", t.ss, s.ss) end if t.QQ ~= s.QQ then return false, string.format("QQ %s!=%s", t.QQ, s.QQ) end return sig_compare_OOO(t.OOO, s.OOO) end function sig_compare_OOO(to, so) for k,v in ipairs(to) do local a = v local b = so[k] if test_opt(a,b) ~= true then return false, string.format("Opt %s!=%s", a, b) end end return true end function sig_test(sigs, tsignature) local ts = sig_unpack(tsignature) local idx = sig_getindex(ts) local t = split(tsignature, "[^|]+") if sigs[idx] == nil then return string.format("UNKNOWN [%s] (%s)", t[1], t[2]) end local out = {} for k,sig in ipairs(sigs[idx]) do local t, err = sig_compare(sig, ts) if t == true then table.insert(out, sig) -- else -- io.write(string.format("%s\t\t%s\n", sig.full, err)) -- match debugging end end if out[1] == nil then return string.format("UNKNOWN [%s] (%s)", t[1], t[2]) end local s = "" local dist for k, sig in ipairs(out) do dist = tonumber(sig.ttt) - tonumber(ts.ttt) s = s .. string.format("%s %s|", sig.OS, sig.details) end s = string.sub(s, 1, string.len(s)-1) return string.format("%s (distance: %i, %s)", s, dist, t[2]) end ---------------------------------------------------------------------------------------------------------------- portrule = function(host, port) return true end -- index is on target_ip, target_port -- packet is from target (so target is source) callback = function(packetsz, layer2, layer3) local tcp = Tcp:new(layer3, packetsz-string.len(layer2)) local a = tcp.ip_bin_src .. string.format("%i", tcp.tcp_sport) return a end function make_index(target_ip, target_port) local a = target_ip .. string.format("%i", target_port) return a end action = function(host, port) local pcap = nmap.new_socket() local conn = nmap.new_socket() local status, packetsz, layer2, layer3 local sigs = nmap.registry["p0f_sigs"] local _, o, tcp, localport if sigs == nil then sigs = sigs_load("p0fa.fp") nmap.registry["p0f_sigs"] = sigs end -- index is on source_ip, target_ip, target_port pcap:pcap_open(host.interface, 256, 0, callback, "tcp and (tcp[tcpflags] & tcp-ack) != 0 and (tcp[tcpflags] & tcp-syn) != 0") pcap:set_timeout(5000) -- 5 seconds? enough? pcap:pcap_register( make_index(host.bin_ip, port.number) ) conn:connect(host.ip, port.number) status , packetsz, layer2, layer3 = pcap:pcap_receive() if status then tcp = Tcp:new(layer3, packetsz-string.len(layer2)) o = sig_test(sigs, tcp:to_p0f()) else -- timeouted o = "Failed to capture SYN+ACK" end conn:close() return o end