--- -- Facilities for manipulating raw packets. -- -- @author Marek Majkowski -- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html local ipOps = require "ipOps" local stdnse = require "stdnse" local string = require "string" local unittest = require "unittest" _ENV = stdnse.module("packet", stdnse.seeall) ---------------------------------------------------------------------------------------------------------------- --- Get an 8-bit integer at a 0-based byte offset in a byte string. -- @param b A byte string. -- @param i Offset. -- @return An 8-bit integer. local function u8(b, i) return b:byte(i+1) end --- Get a 16-bit integer at a 0-based byte offset in a byte string. -- @param b A byte string. -- @param i Offset. -- @return A 16-bit integer. local function u16(b, i) return (">I2"):unpack(b, i+1) end --- Get a 32-bit integer at a 0-based byte offset in a byte string. -- @param b A byte string. -- @param i Offset. -- @return A 32-bit integer. local function u32(b,i) return (">I4"):unpack(b, i+1) end --- Set an 8-bit integer at a 0-based byte offset in a byte string -- (big-endian). -- @param b A byte string. -- @param i Offset. -- @param num Integer to store. local function set_u8(b, i, num) local s = string.char(num & 0xff) return b:sub(0+1, i+1-1) .. s .. b:sub(i+1+1) end --- Set a 16-bit integer at a 0-based byte offset in a byte string -- (big-endian). -- @param b A byte string. -- @param i Offset. -- @param num Integer to store. local function set_u16(b, i, num) return b:sub(0+1, i+1-1) .. (">I2"):pack(num) .. b:sub(i+1+2) end --- Set a 32-bit integer at a 0-based byte offset in a byte string -- (big-endian). -- @param b A byte string. -- @param i Offset. -- @param num Integer to store. local function set_u32(b,i, num) return b:sub(0+1, i+1-1) .. (">I4"):pack(num) .. b:sub(i+1+4) end --- Calculate a standard Internet checksum. -- @param b Data to checksum. -- @return Checksum. function in_cksum(b) local sum = 0 -- Pad to even length, then sum up string.gsub(b .. ("\0"):rep(#b % 2), "..", function(twobytes) sum = sum + (">I2"):unpack(twobytes) end) local shifted = sum >> 16 while shifted > 0 do sum = (sum & 0xffff) + shifted shifted = sum >> 16 end sum = ~sum sum = (sum & 0xffff) -- truncate to 16 bits return sum end -- ip protocol field IPPROTO_IP = 0 -- Dummy protocol for TCP IPPROTO_HOPOPTS = 0 -- IPv6 hop-by-hop options IPPROTO_ICMP = 1 -- Internet Control Message Protocol IPPROTO_IGMP = 2 -- Internet Group Management Protocol IPPROTO_IPIP = 4 -- IPIP tunnels (older KA9Q tunnels use 94) IPPROTO_TCP = 6 -- Transmission Control Protocol IPPROTO_EGP = 8 -- Exterior Gateway Protocol IPPROTO_PUP = 12 -- PUP protocol IPPROTO_UDP = 17 -- User Datagram Protocol IPPROTO_IDP = 22 -- XNS IDP protocol IPPROTO_DCCP = 33 -- Datagram Congestion Control Protocol IPPROTO_RSVP = 46 -- RSVP protocol IPPROTO_GRE = 47 -- Cisco GRE tunnels (rfc 1701,1702) IPPROTO_IPV6 = 41 -- IPv6-in-IPv4 tunnelling IPPROTO_ROUTING = 43 -- IPv6 routing header IPPROTO_FRAGMENT= 44 -- IPv6 fragmentation header IPPROTO_ESP = 50 -- Encapsulation Security Payload protocol IPPROTO_AH = 51 -- Authentication Header protocol IPPROTO_ICMPV6 = 58 -- ICMP for IPv6 IPPROTO_DSTOPTS = 60 -- IPv6 destination options IPPROTO_BEETPH = 94 -- IP option pseudo header for BEET IPPROTO_PIM = 103 -- Protocol Independent Multicast IPPROTO_COMP = 108 -- Compression Header protocol IPPROTO_SCTP = 132 -- Stream Control Transport Protocol IPPROTO_UDPLITE = 136 -- UDP-Lite (RFC 3828) ICMP_ECHO_REQUEST = 8 ICMP_ECHO_REPLY = 0 ICMP6_ECHO_REQUEST = 128 ICMP6_ECHO_REPLY = 129 MLD_LISTENER_QUERY = 130 MLD_LISTENER_REPORT = 131 MLD_LISTENER_REDUCTION = 132 ND_ROUTER_SOLICIT = 133 ND_ROUTER_ADVERT = 134 ND_NEIGHBOR_SOLICIT = 135 ND_NEIGHBOR_ADVERT = 136 ND_REDIRECT = 137 MLDV2_LISTENER_REPORT = 143 ND_OPT_SOURCE_LINKADDR = 1 ND_OPT_TARGET_LINKADDR = 2 ND_OPT_PREFIX_INFORMATION = 3 ND_OPT_REDIRECTED_HEADER = 4 ND_OPT_MTU = 5 ND_OPT_RTR_ADV_INTERVAL = 7 ND_OPT_HOME_AGENT_INFO = 8 ETHER_TYPE_IPV4 = 0x0800 ETHER_TYPE_IPV6 = 0x86dd ETHER_TYPE_PPPOE_DISCOVERY = 0x8863 ETHER_TYPE_PPPOE_SESSION = 0x8864 ETHER_TYPE_EAPOL = 0x888e ETHER_TYPE_ATAOE = 0x88a2 ---------------------------------------------------------------------------------------------------------------- -- Frame is a class Frame = {} function Frame:new(frame, force_continue) local mac_dst, mac_src, ether_type, packet if frame and #frame >= 14 then local pos mac_dst, mac_src, ether_type, pos = ("c6c6>I2"):unpack(frame) packet = frame:sub(pos, -1) if #packet == 0 then packet = nil end end local o = Packet:new(packet, packet and #packet or 0, force_continue) o.build_ether_frame = self.build_ether_frame o.frame_buf = frame o.mac_dst = mac_dst o.mac_src = mac_src o.ether_type = ether_type return o end --- Build an Ethernet frame. -- @param mac_dst six-byte string of the destination MAC address. -- @param mac_src six-byte string of the source MAC address. -- @param ether_type IEEE 802 ethertype as a 16-bit integer (0x0800 for IPv4) -- @param packet string of the payload. -- @return frame string of the Ether frame. function Frame:build_ether_frame(mac_dst, mac_src, ether_type, packet) self.mac_dst = mac_dst or self.mac_dst self.mac_src = mac_src or self.mac_src self.ether_type = ether_type or self.ether_type self.buf = packet or self.buf if not self.ether_type then return nil, "Unknown packet type." end self.frame_buf = self.mac_dst..self.mac_src..(">I2"):pack(self.ether_type)..self.buf end ---------------------------------------------------------------------------------------------------------------- -- Packet is a class Packet = {} --- Create a new Packet object. -- @param packet Binary string with packet data. -- @param packet_len Packet length. It could be more than -- #packet. -- @param force_continue whether an error in parsing headers should be fatal or -- not. This is especially useful when parsing ICMP packets, where a small ICMP -- payload could be a TCP header. The problem is that parsing this payload -- normally would fail because the TCP header is too small. -- @return A new Packet. function Packet:new(packet, packet_len, force_continue) local o = setmetatable({}, {__index = Packet}) if not packet then return o end o.buf = packet o.packet_len = packet_len o.ip_v = string.byte(o.buf) >> 4 if o.ip_v == 4 and not o:ip_parse(force_continue) then return nil elseif o.ip_v == 6 and not o:ip6_parse(force_continue) then return nil end if o.ip_v == 6 then while o:ipv6_is_extension_header() do if o.ip6_data_offset >= o.packet_len or not o:ipv6_ext_header_parse(force_continue) then stdnse.debug1("Error while parsing IPv6 extension headers.") return o end end o.ip_p = o.ip6_nhdr end if o.ip_p == IPPROTO_TCP then if not o:tcp_parse(force_continue) then stdnse.debug1("Error while parsing TCP packet\n") end elseif o.ip_p == IPPROTO_UDP then if not o:udp_parse(force_continue) then stdnse.debug1("Error while parsing UDP packet\n") end elseif o.ip_p == IPPROTO_ICMP then if not o:icmp_parse(force_continue) then stdnse.debug1("Error while parsing ICMP packet\n") end elseif o.ip_p == IPPROTO_ICMPV6 then if not o:icmpv6_parse(force_continue) then stdnse.debug1("Error while parsing ICMPv6 packet\n") end end return o end --- Convert Version, Traffic Class and Flow Label to a 4-byte string. -- @param ip6_tc Number stands for Traffic Class. -- @param ip6_fl Number stands for Flow Label. -- @return The first four-byte string of an IPv6 header. local function ipv6_hdr_tc_fl(ip6_tc, ip6_fl) return (6 << 28) + ((ip6_tc & 0xFF) << 20) + (ip6_fl & 0xFFFFF) end --- Build an IPv6 packet. -- @param src 16-byte string of the source IPv6 address. -- @param dsr 16-byte string of the destination IPv6 address. -- @param nx_hdr integer that represents next header. -- @param h_limit integer that represents hop limit. -- @param t_class integer that represents traffic class. -- @param f_label integer that represents flow label. function Packet:build_ipv6_packet(src, dst, nx_hdr, payload, h_limit, t_class, f_label) self.ether_type = ETHER_TYPE_IPV6 self.ip_v = 6 self.ip_bin_src = src or self.ip_bin_src self.ip_bin_dst = dst or self.ip_bin_dst self.ip6_nhdr = nx_hdr or self.ip6_nhdr self.l4_packet = payload or self.l4_packet self.ip6_tc = t_class or self.ip6_tc or 1 self.ip6_fl = f_label or self.ip6_fl or 1 self.ip6_hlimit = h_limit or self.ip6_hlimit or 255 self.ip6_plen = #(self.exheader or "")+#(self.l4_packet or "") self.buf = (">I4I2BBc16c16"):pack( ipv6_hdr_tc_fl(self.ip6_tc, self.ip6_fl), self.ip6_plen, --payload length self.ip6_nhdr, --next header self.ip6_hlimit, --hop limit self.ip_bin_src, --Source self.ip_bin_dst) .. --dest (self.exheader or "").. (self.l4_packet or "") end --- Return true if and only if the next header is an known extension header. -- @param nhdr Next header. function Packet:ipv6_is_extension_header(nhdr) self.ip6_nhdr = nhdr or self.ip6_nhdr if self.ip6_nhdr == IPPROTO_HOPOPTS or self.ip6_nhdr == IPPROTO_DSTOPTS or self.ip6_nhdr == IPPROTO_ROUTING or self.ip6_nhdr == IPPROTO_FRAGMENT then return true end return nil end --- Count IPv6 checksum. -- @return the checksum. function Packet:count_ipv6_pseudoheader_cksum() local pseudoheader = (">c16c16I2xxxB"):pack( self.ip_bin_src, self.ip_bin_dst, #self.l4_packet, self.ip6_nhdr) local ck_content = pseudoheader .. self.l4_packet return in_cksum(ck_content) end --- Set ICMPv6 checksum. function Packet:set_icmp6_cksum(check_sum) self.l4_packet = set_u16(self.l4_packet, 2, check_sum) end --- Build an ICMPv6 header. -- @param icmpv6_type integer that represent ICMPv6 type. -- @param icmpv6_code integer that represent ICMPv6 code. -- @param icmpv6_payload string of the payload -- @param ip_bin_src 16-byte string of the source IPv6 address. -- @param ip_bin_dst 16-byte string of the destination IPv6 address. function Packet:build_icmpv6_header(icmpv6_type, icmpv6_code, icmpv6_payload, ip_bin_src, ip_bin_dst) self.ip6_nhdr = IPPROTO_ICMPV6 self.icmpv6_type = icmpv6_type or self.icmpv6_type self.icmpv6_code = icmpv6_code or self.icmpv6_code self.icmpv6_payload = icmpv6_payload or self.icmpv6_payload self.ip_bin_src = ip_bin_src or self.ip_bin_src self.ip_bin_dst = ip_bin_dst or self.ip_bin_dst self.l4_packet = ("BBxx"):pack(self.icmpv6_type, self.icmpv6_code) .. (self.icmpv6_payload or "") local check_sum = self:count_ipv6_pseudoheader_cksum() self:set_icmp6_cksum(check_sum) end --- Build an ICMPv6 Echo Request frame. -- @param mac_src six-byte string of source MAC address. -- @param mac_dst sis-byte string of destination MAC address. -- @param ip_bin_src 16-byte string of source IPv6 address. -- @param ip_bin_dst 16-byte string of destination IPv6 address. -- @param id integer that represents Echo ID. -- @param sequence integer that represents Echo sequence. -- @param data string of Echo data. -- @param tc integer that represents traffic class of IPv6 packet. -- @param fl integer that represents flow label of IPv6 packet. -- @param hop-limit integer that represents hop limit of IPv6 packet. function Packet:build_icmpv6_echo_request(id, sequence, data, mac_src, mac_dst, ip_bin_src, ip_bin_dst, tc, fl, hop_limit) self.mac_src = mac_src or self.mac_src self.mac_dst = mac_dst or self.mac_dst self.ip_bin_src = ip_bin_src or self.ip_bin_src self.ip_bin_dst = ip_bin_dst or self.ip_bin_dst self.traffic_class = tc or 1 self.flow_label = fl or 1 self.ip6_hlimit = hop_limit or 255 self.icmpv6_type = ICMP6_ECHO_REQUEST self.icmpv6_code = 0 self.echo_id = id or self.echo_id or 0xdead self.echo_seq = sequence or self.echo_seq or 0xbeef self.echo_data = data or self.echo_data or "" self.icmpv6_payload = (">I2I2"):pack(self.echo_id, self.echo_seq) .. self.echo_data end --- Set an ICMPv6 option message. function Packet:set_icmpv6_option(opt_type,msg) return string.char(opt_type, (#msg+2)/8) .. msg end --- Build an IPv4 packet. -- @param src 4-byte string of the source IP address. -- @param dst 4-byte string of the destination IP address. -- @param payload string containing the IP payload -- @param dsf byte that represents the differentiated services field -- @param id integer that represents the IP identification -- @param flags integer that represents the IP flags -- @param off integer that represents the IP offset -- @param ttl integer that represent the IP time to live -- @param proto integer that represents the IP protocol function Packet:build_ip_packet(src, dst, payload, dsf, id, flags, off, ttl, proto) self.ether_type = ETHER_TYPE_IPV4 self.ip_v = 4 self.ip_bin_src = src or self.ip_bin_src self.ip_bin_dst = dst or self.ip_bin_dst self.l3_packet = payload or self.l3_packet self.ip_dsf = dsf or self.ip_dsf or 0 self.ip_p = proto or self.ip_p self.flags = flags or self.flags or 0 -- should be split into ip_rd, ip_df, ip_mv self.ip_id = id or self.ip_id or 0xbeef self.ip_off = off or self.ip_off or 0 self.ip_ttl = ttl or self.ip_ttl or 255 self.buf = (">BBI2I2BBBBI2c4c4"):pack( (self.ip_v << 4) + 20 / 4, -- version and header length self.ip_dsf, #self.l3_packet + 20, self.ip_id, self.flags, self.ip_off, self.ip_ttl, self.ip_p, 0, -- checksum self.ip_bin_src, --Source self.ip_bin_dst --dest ) self.buf = set_u16(self.buf, 10, in_cksum(self.buf)) self.buf = self.buf .. self.l3_packet end --- Build an ICMP header. -- @param icmp_type integer that represent ICMPv6 type. -- @param icmp_code integer that represent ICMPv6 code. -- @param icmp_payload string of the payload -- @param ip_bin_src 16-byte string of the source IPv6 address. -- @param ip_bin_dst 16-byte string of the destination IPv6 address. function Packet:build_icmp_header(icmp_type, icmp_code, icmp_payload, ip_bin_src, ip_bin_dst) self.icmp_type = icmp_type or self.icmp_type self.icmp_code = icmp_code or self.icmp_code self.icmp_payload = icmp_payload or self.icmp_payload self.ip_bin_src = ip_bin_src or self.ip_bin_src self.ip_bin_dst = ip_bin_dst or self.ip_bin_dst self.l3_packet = ("BBxx"):pack(self.icmp_type, self.icmp_code) .. (self.icmp_payload or "") self.l3_packet = set_u16(self.l3_packet, 2, in_cksum(self.l3_packet)) end --- Build an ICMP Echo Request frame. -- @param mac_src six-byte string of source MAC address. -- @param mac_dst sis-byte string of destination MAC address. -- @param ip_bin_src 16-byte string of source IPv6 address. -- @param ip_bin_dst 16-byte string of destination IPv6 address. -- @param id integer that represents Echo ID. -- @param seq integer that represents Echo sequence. -- @param data string of Echo data. -- @param dsf integer that represents differentiated services field. function Packet:build_icmp_echo_request(id, seq, data, mac_src, mac_dst, ip_bin_src, ip_bin_dst) self.mac_src = mac_src or self.mac_src self.mac_dst = mac_dst or self.mac_dst self.ip_p = IPPROTO_ICMP self.ip_bin_src = ip_bin_src or self.ip_bin_src self.ip_bin_dst = ip_bin_dst or self.ip_bin_dst self.icmp_type = ICMP_ECHO_REQUEST self.icmp_code = 0 self.echo_id = id or self.echo_id or 0xdead self.echo_seq = seq or self.echo_seq or 0xbeef self.echo_data = data or self.echo_data or "" self.icmp_payload = (">I2I2"):pack(self.echo_id, self.echo_seq) .. self.echo_data end -- Helpers local function _hex_str (x) return string.char(tonumber(x, 16)) end --- Convert a MAC address string (like "00:23:ae:5d:3b:10") to -- a raw six-byte long. -- @param str MAC address string. -- @return Six-byte string. function mactobin(str) if not str then return nil, "MAC was not specified." end return (str:gsub("(%x%x)[^%x]?", _hex_str)) end --- Generate the link-local IPv6 address from the MAC address. -- @param mac MAC address string. -- @return Link-local IPv6 address string. function mac_to_lladdr(mac) if not mac then return nil, "MAC was not specified." end local interfier = string.char((string.byte(mac,1) | 0x02))..string.sub(mac,2,3).."\xff\xfe"..string.sub(mac,4,6) local ll_prefix = ipOps.ip_to_str("fe80::") return string.sub(ll_prefix,1,8)..interfier end --- Get an 8-bit integer at a 0-based byte offset in the packet. -- @param index Offset. -- @return An 8-bit integer. function Packet:u8(index) return u8(self.buf, index) end --- Get a 16-bit integer at a 0-based byte offset in the packet. -- @param index Offset. -- @return A 16-bit integer. function Packet:u16(index) return u16(self.buf, index) end --- Get a 32-bit integer at a 0-based byte offset in the packet. -- @param index Offset. -- @return An 32-bit integer. function Packet:u32(index) return u32(self.buf, index) end --- Return part of the packet contents as a byte string. -- @param index The beginning of the part of the packet to extract. The index -- is 0-based. If omitted the default value is 0 (beginning of the string) -- @param length The length of the part of the packet to extract. If omitted -- the remaining contents from index to the end of the string are returned. -- @return A string. function Packet:raw(index, length) if not index then index = 0 end if not length then length = #self.buf-index end return self.buf:sub(index+1, index+1+length-1) end --- Set an 8-bit integer at a 0-based byte offset in the packet. -- (big-endian). -- @param index Offset. -- @param num Integer to store. function Packet:set_u8(index, num) self.buf = set_u8(self.buf, index, num) return self.buf end --- Set a 16-bit integer at a 0-based byte offset in the packet. -- (big-endian). -- @param index Offset. -- @param num Integer to store. function Packet:set_u16(index, num) self.buf = set_u16(self.buf, index, num) return self.buf end --- Set a 32-bit integer at a 0-based byte offset in the packet. -- (big-endian). -- @param index Offset. -- @param num Integer to store. function Packet:set_u32(index, num) self.buf = set_u32(self.buf, index, num) return self.buf end --- Parse an IP packet header. -- @param force_continue Ignored. -- @return Whether the parsing succeeded. function Packet:ip_parse(force_continue) self.ip_offset = 0 if #self.buf < 20 then -- too short stdnse.debug2("Packet.ip_parse: too short") return false end self.ip_v = (self:u8(self.ip_offset + 0) & 0xF0) >> 4 self.ip_hl = (self:u8(self.ip_offset + 0) & 0x0F) -- header_length or data_offset if self.ip_v ~= 4 then -- not ip stdnse.debug2("Packet.ip_parse: Not IPv4") return false end self.ip = true 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 = (self.ip_off & 0x8000)~=0 -- true/false self.ip_df = (self.ip_off & 0x4000)~=0 self.ip_mf = (self.ip_off & 0x2000)~=0 self.ip_off = (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 = ipOps.str_to_ip(self.ip_bin_src) -- formatted string self.ip_dst = ipOps.str_to_ip(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 --- Parse an IPv6 packet header. -- @param force_continue Ignored. -- @return Whether the parsing succeeded. function Packet:ip6_parse(force_continue) self.ip6_offset = 0 if #self.buf < 40 then -- too short return false end self.ip_v = (self:u8(self.ip6_offset + 0) & 0xF0) >> 4 if self.ip_v ~= 6 then -- not ipv6 return false end self.ip6 = true self.ip6_tc = (self:u16(self.ip6_offset + 0) & 0x0FF0) >> 4 self.ip6_fl = (self:u8(self.ip6_offset + 1) & 0x0F)*65536 + self:u16(self.ip6_offset + 2) self.ip6_plen = self:u16(self.ip6_offset + 4) self.ip6_nhdr = self:u8(self.ip6_offset + 6) self.ip6_hlimt = self:u8(self.ip6_offset + 7) self.ip_bin_src = self:raw(self.ip6_offset + 8, 16) self.ip_bin_dst = self:raw(self.ip6_offset + 24, 16) self.ip_src = ipOps.str_to_ip(self.ip_bin_src) self.ip_dst = ipOps.str_to_ip(self.ip_bin_dst) self.ip6_data_offset = 40 return true end --- Pare an IPv6 extension header. Just jump over it at the moment. -- @param force_continue Ignored. -- @return Whether the parsing succeeded. function Packet:ipv6_ext_header_parse(force_continue) local ext_hdr_len = self:u8(self.ip6_data_offset + 1) ext_hdr_len = ext_hdr_len*8 + 8 self.ip6_data_offset = self.ip6_data_offset + ext_hdr_len self.ip6_nhdr = self:u8(self.ip6_data_offset) return true end --- Set the payload length field. -- @param plen Payload length. function Packet:ip6_set_plen(plen) self:set_u16(self.ip6_offset + 4, plen) self.ip6_plen = plen end --- Set the header length field. function Packet:ip_set_hl(len) self:set_u8(self.ip_offset + 0, (self.ip_v << 4) | (len & 0x0F)) self.ip_v = (self:u8(self.ip_offset + 0) & 0xF0) >> 4 self.ip_hl = (self:u8(self.ip_offset + 0) & 0x0F) -- header_length or data_offset end --- Set the packet length field. -- @param len Packet length. function Packet:ip_set_len(len) self:set_u16(self.ip_offset + 2, len) self.ip_len = len end --- Set the packet identification field. -- @param id packet ID. function Packet:ip_set_id(id) self:set_u16(self.ip_offset + 4, id) self.ip_id = id end --- Set the TTL. -- @param ttl TTL. function Packet:ip_set_ttl(ttl) self:set_u8(self.ip_offset + 8, ttl) self.ip_ttl = ttl end --- Set the checksum. -- @param checksum Checksum. function Packet:ip_set_checksum(checksum) self:set_u16(self.ip_offset + 10, checksum) self.ip_sum = checksum end --- Count checksum for packet and save it. function Packet:ip_count_checksum() self:ip_set_checksum(0) local csum = in_cksum( self.buf:sub(0, self.ip_offset + self.ip_hl*4) ) self:ip_set_checksum(csum) end --- Set the source IP address. -- @param binip The source IP address as a byte string. function Packet:ip_set_bin_src(binip) local nrip = u32(binip, 0) self:set_u32(self.ip_offset + 12, nrip) self.ip_bin_src = self:raw(self.ip_offset + 12,4) -- raw 4-bytes string end --- Set the destination IP address. -- @param binip The destination IP address as a byte string. function Packet:ip_set_bin_dst(binip) local nrip = u32(binip, 0) self:set_u32(self.ip_offset + 16, nrip) self.ip_bin_dst = self:raw(self.ip_offset + 16,4) end --- Set the IP options field (and move the data, count new length, -- etc.). -- @param ipoptions IP options. function Packet:ip_set_options(ipoptions) -- packet = + ipoptions + local buf = self.buf:sub(0+1,self.ip_offset + 20) .. ipoptions .. self.buf:sub(self.ip_data_offset+1) self.buf = buf -- set ip_len self:ip_set_len(self.buf:len()) -- set ip_hl self:ip_set_hl(5 + ipoptions:len()/4) -- set data offset correctly 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 if self.tcp then self.tcp_offset = self.ip_data_offset elseif self.icmp then self.icmp_offset = self.ip_data_offset end end --- Get a short string representation of the IP header. -- @return A string representation of the IP header. function Packet:ip_tostring() return string.format( "IP %s -> %s", self.ip_src, self.ip_dst) end --- Parse IP/TCP options into a table. -- @param offset Offset at which options start. -- @param length Length of options. -- @return Table of options. function Packet:parse_options(offset, length) 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 --- Get a short string representation of the packet. -- @return A string representation of the packet. function Packet:tostring() if self.tcp then return self:tcp_tostring() elseif self.udp then return self:udp_tostring() elseif self.icmp then return self:icmp_tostring() elseif self.ip then return self:ip_tostring() end return "" end ---------------------------------------------------------------------------------------------------------------- --- Parse an ICMP packet header. -- @param force_continue Ignored. -- @return Whether the parsing succeeded. function Packet:icmp_parse(force_continue) self.icmp_offset = self.ip_data_offset if #self.buf < self.icmp_offset + 8 then -- let's say 8 bytes minimum return false end self.icmp = true self.icmp_type = self:u8(self.icmp_offset + 0) self.icmp_code = self:u8(self.icmp_offset + 1) self.icmp_sum = self:u16(self.icmp_offset + 2) if self.icmp_type == 3 or self.icmp_type == 4 or self.icmp_type == 11 or self.icmp_type == 12 then self.icmp_payload = true self.icmp_r0 = self:u32(self.icmp_offset + 4) self.icmp_payload_offset = self.icmp_offset + 8 if #self.buf < self.icmp_payload_offset + 24 then return false end self.icmp_payload = Packet:new(self.buf:sub(self.icmp_payload_offset+1), self.packet_len - self.icmp_payload_offset, true) end return true end --- Get a short string representation of the ICMP header. -- @return A string representation of the ICMP header. function Packet:icmp_tostring() return self:ip_tostring() .. " ICMP(" .. self.icmp_payload:tostring() .. ")" end ---------------------------------------------------------------------------------------------------------------- --- Parse an ICMPv6 packet header. -- @param force_continue Ignored. -- @return Whether the parsing succeeded. function Packet:icmpv6_parse(force_continue) self.icmpv6_offset = self.ip6_data_offset if #self.buf < self.icmpv6_offset + 8 then -- let's say 8 bytes minimum return false end self.icmpv6 = true self.icmpv6_type = self:u8(self.icmpv6_offset + 0) self.icmpv6_code = self:u8(self.icmpv6_offset + 1) if self.icmpv6_type == ND_NEIGHBOR_SOLICIT then self.ns_target = self:raw(self.icmpv6_offset + 8, 16) end return true end ---------------------------------------------------------------------------------------------------------------- -- Parse a TCP packet header. -- @param force_continue Whether a short packet causes parsing to fail. -- @return Whether the parsing succeeded. function Packet:tcp_parse(force_continue) self.tcp = true self.tcp_offset = self.ip_data_offset or self.ip6_data_offset if #self.buf < self.tcp_offset + 4 then return false end self.tcp_sport = self:u16(self.tcp_offset + 0) self.tcp_dport = self:u16(self.tcp_offset + 2) if #self.buf < self.tcp_offset + 20 then if force_continue then return true else return false end end self.tcp_seq = self:u32(self.tcp_offset + 4) self.tcp_ack = self:u32(self.tcp_offset + 8) self.tcp_hl = (self:u8(self.tcp_offset+12) & 0xF0) >> 4 -- header_length or data_offset self.tcp_x2 = (self:u8(self.tcp_offset+12) & 0x0F) self.tcp_flags = self:u8(self.tcp_offset + 13) self.tcp_th_fin = (self.tcp_flags & 0x01)~=0 -- true/false self.tcp_th_syn = (self.tcp_flags & 0x02)~=0 self.tcp_th_rst = (self.tcp_flags & 0x04)~=0 self.tcp_th_push = (self.tcp_flags & 0x08)~=0 self.tcp_th_ack = (self.tcp_flags & 0x10)~=0 self.tcp_th_urg = (self.tcp_flags & 0x20)~=0 self.tcp_th_ece = (self.tcp_flags & 0x40)~=0 self.tcp_th_cwr = (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 if self.ip_len then self.tcp_data_length = self.ip_len - self.tcp_offset - self.tcp_hl*4 else self.tcp_data_length = self.ip6_plen - self.tcp_hl*4 end self:tcp_parse_options() return true end --- Get a short string representation of the TCP packet. -- @return A string representation of the TCP header. function Packet:tcp_tostring() return string.format( "TCP %s:%i -> %s:%i", self.ip_src, self.tcp_sport, self.ip_dst, self.tcp_dport ) end --- Parse options for TCP header. function Packet: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 --- Set the TCP source port. -- @param port Source port. function Packet:tcp_set_sport(port) self:set_u16(self.tcp_offset + 0, port) self.tcp_sport = port end --- Set the TCP destination port. -- @param port Destination port. function Packet:tcp_set_dport(port) self:set_u16(self.tcp_offset + 2, port) self.tcp_dport = port end --- Set the TCP sequence field. -- @param new_seq Sequence. function Packet:tcp_set_seq(new_seq) self:set_u32(self.tcp_offset + 4, new_seq) self.tcp_seq = new_seq end --- Set the TCP flags field (like SYN, ACK, RST). -- @param new_flags Flags, represented as an 8-bit number. function Packet:tcp_set_flags(new_flags) self:set_u8(self.tcp_offset + 13, new_flags) self.tcp_flags = new_flags end --- Set the urgent pointer field. -- @param urg_ptr Urgent pointer. function Packet:tcp_set_urp(urg_ptr) self:set_u16(self.tcp_offset + 18, urg_ptr) self.tcp_urp = urg_ptr end --- Set the TCP checksum field. -- @param checksum Checksum. function Packet:tcp_set_checksum(checksum) self:set_u16(self.tcp_offset + 16, checksum) self.tcp_sum = checksum end --- Count and save the TCP checksum field. function Packet:tcp_count_checksum() self:tcp_set_checksum(0) local proto = self.ip_p local length = self.buf:len() - self.tcp_offset local b = self.ip_bin_src .. self.ip_bin_dst .. "\0" .. (">BI2"):pack(proto, length) .. self.buf:sub(self.tcp_offset+1) self:tcp_set_checksum(in_cksum(b)) end --- Map an MTU to a link type string. Stolen from p0f. -- @return A string describing the link type. function Packet: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 ---------------------------------------------------------------------------------------------------------------- -- Parse a UDP packet header. -- @param force_continue Whether a short packet causes parsing to fail. -- @return Whether the parsing succeeded. function Packet:udp_parse(force_continue) self.udp = true self.ip_p = self.ip_p or IPPROTO_UDP self.udp_offset = self.ip_data_offset or self.ip6_data_offset if #self.buf < self.udp_offset + 4 then return false end self.udp_sport = self:u16(self.udp_offset + 0) self.udp_dport = self:u16(self.udp_offset + 2) if #self.buf < self.udp_offset + 8 then if force_continue then return true else return false end end self.udp_len = self:u16(self.udp_offset + 4) self.udp_sum = self:u16(self.udp_offset + 6) return true end --- Get a short string representation of the UDP packet. -- @return A string representation of the UDP header. function Packet:udp_tostring() return string.format( "UDP %s:%i -> %s:%i", self.ip_src, self.udp_sport, self.ip_dst, self.udp_dport ) end --- -- Set the UDP source port. -- @param port Source port. function Packet:udp_set_sport(port) self:set_u16(self.udp_offset + 0, port) self.udp_sport = port end --- -- Set the UDP destination port. -- @param port Destination port. function Packet:udp_set_dport(port) self:set_u16(self.udp_offset + 2, port) self.udp_dport = port end --- -- Set the UDP payload length. -- @param len UDP payload length. function Packet:udp_set_length(len) self:set_u16(self.udp_offset + 4, len) self.udp_len = len end --- -- Set the UDP checksum field. -- @param checksum Checksum. function Packet:udp_set_checksum(checksum) self:set_u16(self.udp_offset + 6, checksum) self.udp_sum = checksum end --- -- Count and save the UDP checksum field. function Packet:udp_count_checksum() self:udp_set_checksum(0) local pseudo_header local payload = self.buf:sub(self.udp_offset+1) if self.ip_v == 4 then pseudo_header = (">c4 c4 x B I2"):pack(self.ip_bin_src, self.ip_bin_dst, IPPROTO_UDP, #payload) elseif self.ip_v == 6 then pseudo_header = (">c16 c16 I4 xxx B"):pack(self.ip_bin_src, self.ip_bin_dst, #payload, IPPROTO_UDP) end self:udp_set_checksum(in_cksum(pseudo_header .. payload)) end if not unittest.testing() then return _ENV end test_suite = unittest.TestSuite:new() -- Byte setting functions test_suite:add_test(unittest.equal(set_u8("abc", 1, 0x41), "aAc"), "set_u8") test_suite:add_test(unittest.equal(set_u16("abcd", 2, 0x4142), "abAB"), "set_u16") test_suite:add_test(unittest.equal(set_u32("abcdefg", 0, 0x41424344), "ABCDefg"), "set_u32") -- Packet parsing local packet1 = "\x45\x00\x00\x62\xaf\xbd\x40\x00\xe3\x06\x03\xf3\x03\x5e\x1e\xa5\xc0\xa8\x01\x3a\x01\xbb\xee\x3e\x74\xd2\x61\xbe\xd5\x66\xb1\x09\x80\x18\x00\x7a\x94\x22\x00\x00\x01\x01\x08\x0a\x73\xab\x53\x92\x05\xe3\x08\xc3\x17\x03\x03\x00\x29\x99\xff\x5d\x17\xe4\x26\x14\xb8\x53\xe3\x76\xdc\xba\xf9\x55\xf7\x52\x5f\xa2\x78\xc3\x4e\x9a\x31\x44\x2d\x67\x9c\x16\xea\x71\xf1\xdb\x0a\xdd\xc1\x92\x46\xa7\xdf\xde" local pkt_parsed = Packet:new(packet1, #packet1, false) test_suite:add_test (unittest.not_nil(pkt_parsed), "parse packet") test_suite:add_test(unittest.equal(pkt_parsed:raw(), packet1), "parse to raw") -- Checksum tests pkt_parsed:ip_count_checksum() test_suite:add_test(unittest.equal(pkt_parsed:raw(), packet1), "IP checksum") pkt_parsed:tcp_count_checksum() test_suite:add_test(unittest.equal(pkt_parsed:raw(), packet1), "TCP checksum") -- TODO: UDP parsing/checksum -- TODO: IPv6 parsing, ICMPv6 checksum -- Basically, we need a lot more test coverage here. return _ENV;