diff --git a/scripts/p2p-conficker.nse b/scripts/p2p-conficker.nse new file mode 100644 index 000000000..78274d44e --- /dev/null +++ b/scripts/p2p-conficker.nse @@ -0,0 +1,634 @@ +description = [[ +Check if a host is infected with Conficker.C or higher, based on Conficker's peer to peer communication. + +When Conficker.C and higher infect a system, it opens four ports: two TCP and two UDP. The ports are +random, but are seeded with the current week and the IP of the infected host. By determining the algorithm, +one can check if these four ports are open, and can probe them for more data. + +Once the open ports are found, communication can be initiated using Conficker's custom peer to peer protocol. +If a valid response is received, then a valid Conficker infection has been found. + +This check won't work properly on a multihomed or NATed system -- the open ports will be based on a nonpublic IP. +The argument checkall tells Nmap to attempt communication with every open port (much like a version +check) and the argument realip tells Nmap to base its port generation on the given ip address instead +of the actual ip. See the args section for more information. + +By default, this will run against a system that has a standard Windows port open (445, 139, 137). The arguments +checkall and checkconficker will both perform checks regardless of which port is open, see the args section for +more information. + +Note: Ensure your clock is correct (within a week) before using this script! + +The majority of research for this script was done by Symantec Security Response, and some was taken +from public sources (most notably the port blacklisting was found by David Fifield). A big thanks goes +out to everybody who contributed! +]] + +--
nmap -p445 -T4 --script=p2p-conficker --script-args=realip=\"192.168.1.65\" x.x.x.x
+-- @args checkconficker If set to '1' or 'true', the script will always run on active hosts, +-- it doesn't matter if any open ports were detected. +-- +-- @usage +-- # Default modes: +-- nmap --script p2p-conficker.nse -p445 +-- sudo nmap -sU -sS --script p2p-conficker.nse -p U:137,T:139 +-- +-- # Run regardless of which port is open +-- nmap --script p2p-conficker.nse -p445 --script-args=checkconficker=1 +-- +-- # Check every port (slow!) +-- nmap --script p2p-conficker.nse -p- --script-args=checkall=1 +-- +-- # Base checks on a different ip address (NATed) +-- nmap --script p2p-conficker.nse -p445 --script-args=realip=\"192.168.1.65\" +-- +-- @output +-- Clean machine: +-- Host script results: +-- | p2p-conficker: Checking for Conficker.C or higher... +-- | | Check 1 (port 44329/tcp): CLEAN (Couldn't connect) +-- | | Check 2 (port 33824/tcp): CLEAN (Couldn't connect) +-- | | Check 3 (port 31380/udp): CLEAN (Failed to receive data) +-- | | Check 4 (port 52600/udp): CLEAN (Failed to receive data) +-- |_ |_ 0/4 checks: Host is CLEAN or ports are blocked +-- +-- Infected machine: +-- Host script results: +-- | p2p-conficker: Checking for Conficker.C or higher... +-- | | Check 1 (port 18707/tcp): INFECTED (Received valid data) +-- | | Check 2 (port 65273/tcp): INFECTED (Received valid data) +-- | | Check 3 (port 11722/udp): INFECTED (Received valid data) +-- | | Check 4 (port 12690/udp): INFECTED (Received valid data) +-- |_ |_ 4/4 checks: Host is likely INFECTED +-- +----------------------------------------------------------------------- + +author = "Ron Bowes (with research from Symantec Security Response)" +copyright = "Ron Bowes" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"default","safe"} +-- Set the runlevel to 2. This means this script will run last, but it will also run in parallel with smb-check-vulns.nse, +-- which will generally be run at the same time. So, by setting this to 2, we increase our parallelism. +runlevel = 2 + +require 'smb' +require 'stdnse' +require 'ipOps' + +-- Max packet size +local MAX_PACKET = 0x2000 + +-- Flags +local mode_flags = +{ + FLAG_MODE = bit.lshift(1, 0), + FLAG_LOCAL_ACK = bit.lshift(1, 1), + FLAG_IS_TCP = bit.lshift(1, 2), + FLAG_IP_INCLUDED = bit.lshift(1, 3), + FLAG_UNKNOWN0_INCLUDED = bit.lshift(1, 4), + FLAG_UNKNOWN1_INCLUDED = bit.lshift(1, 5), + FLAG_DATA_INCLUDED = bit.lshift(1, 6), + FLAG_SYSINFO_INCLUDED = bit.lshift(1, 7), + FLAG_ENCODED = bit.lshift(1, 15) +} + +---For a hostrule, simply use the 'smb' ports as an indicator, unless the user overrides it +hostrule = function(host) + if(smb.get_port(host) ~= nil) then + return true + elseif(nmap.registry.args.checkall == "true" or nmap.registry.args.checkall == "1") then + return true + elseif(nmap.registry.args.checkconficker == "true" or nmap.registry.args.checkconficker == "1") then + return true + end + + return false +end + +-- Multiply two 32-bit integers and return a 64-bit product. The first return +-- value is the low-order 32 bits of the product and the second return value is +-- the high-order 32 bits. +-- +--@param u First number (0 <= u <= 0xFFFFFFFF) +--@param v Second number (0 <= v <= 0xFFFFFFFF) +--@return 64-bit product of u*v, as a pair of 32-bit integers. +local function mul64(u, v) + -- This is based on formula (2) from section 4.3.3 of The Art of + -- Computer Programming. We split u and v into upper and lower 16-bit + -- chunks, such that + -- u = 2**16 u1 + u0 and v = 2**16 v1 + v0 + -- Then + -- u v = (2**16 u1 + u0) * (2**16 v1 + v0) + -- = 2**32 u1 v1 + 2**16 (u0 v1 + u1 v0) + u0 v0 + assert(0 <= u and u <= 0xFFFFFFFF) + assert(0 <= v and v <= 0xFFFFFFFF) + local u0, u1 = bit.band(u, 0xFFFF), bit.rshift(u, 16) + local v0, v1 = bit.band(v, 0xFFFF), bit.rshift(v, 16) + -- t uses at most 49 bits, which is within the range of exact integer + -- precision of a Lua number. + local t = u0 * v0 + (u0 * v1 + u1 * v0) * 65536 + return bit.band(t, 0xFFFFFFFF), u1 * v1 + bit.rshift(t, 32) +end + +---Rotates the 64-bit integer defined by h:l left by one bit. +-- +--@param h The high-order 32 bits +--@param l The low-order 32 bits +--@return 64-bit rotated integer, as a pair of 32-bit integers. +local function rot64(h, l) + local i + + assert(0 <= h and h <= 0xFFFFFFFF) + assert(0 <= l and l <= 0xFFFFFFFF) + + local tmp = bit.band(h, 0x80000000) -- tmp = h & 0x80000000 + h = bit.lshift(h, 1) -- h = h << 1 + h = bit.bor(h, bit.rshift(l, 31)) -- h = h | (l >> 31) + l = bit.lshift(l, 1) + if(tmp ~= 0) then + l = bit.bor(l, 1) + end + + h = bit.band(h, 0xFFFFFFFF) + l = bit.band(l, 0xFFFFFFFF) + + return h, l +end + + +---Check if a port is Blacklisted. Thanks to David Fifield for determining the purpose of the "magic" +-- array: +-- +-- +-- Basically, each bit in the blacklist array represents a group of 32 ports. If that bit is on, those ports +-- are blacklisted and will never come up. +-- +--@param port The port to check +--@return true if the port is blacklisted, false otherwise +local function is_blacklisted_port(port) + local r, l + + local blacklist = { 0xFFFFFFFF, 0xFFFFFFFF, 0xF0F6BFBB, 0xBB5A5FF3, 0xF3977011, 0xEB67BFBF, 0x5F9BFAC8, 0x34D88091, 0x1E2282DF, 0x573402C4, 0xC0000084, 0x03000209, 0x01600002, 0x00005000, 0x801000C0, 0x00500040, 0x000000A1, 0x01000000, 0x01000000, 0x00022A20, 0x00000080, 0x04000000, 0x40020000, 0x88000000, 0x00000180, 0x00081000, 0x08801900, 0x00800B81, 0x00000280, 0x080002C0, 0x00A80000, 0x00008000, 0x00100040, 0x00100000, 0x00000000, 0x00000000, 0x10000008, 0x00000000, 0x00000000, 0x00000004, 0x00000002, 0x00000000, 0x00040000, 0x00000000, 0x00000000, 0x00000000, 0x00410000, 0x82000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000008, 0x80000000, }; + + r = bit.rshift(port, 5) + l = bit.lshift(1, bit.band(r, 0x1f)) + r = bit.rshift(r, 5) + + return (bit.band(blacklist[r + 1], l) ~= 0) +end + +---Generates the four random ports that Conficker uses, based on the current time and the IP address. +-- +--@param ip The IP address as a 32-bit little endian integer +--@param seed The seed, based on the time (floor((time - 345600) / 604800)) +--@return An array of four ports; the first and third are TCP, and the second and fourth are UDP. +local function prng_generate_ports(ip, seed) + local ports = {0, 0, 0, 0} + local v1, v2 + local port1, port2, shift1, shift2 + local i + local magic = 0x015A4E35 + + stdnse.print_debug(1, "Conficker: Generating ports based on ip (0x%08x) and seed (%d)", ip, seed) + + v1 = -(ip + 1) + repeat + -- Loop 10 times to generate the first pair of ports + for i = 0, 9, 1 do + v1, v2 = mul64(bit.band(v1, 0xFFFFFFFF), bit.band(magic, 0xFFFFFFFF)) + + -- Add 1 to v1, handling overflows + if(v1 ~= 0xFFFFFFFF) then + v1 = v1 + 1 + else + v1 = 0 + v2 = v2 + 1 + end + + v2 = bit.rshift(v2, i) + + ports[(i % 2) + 1] = bit.bxor(bit.band(v2, 0xFFFF), ports[(i % 2) + 1]) + end + until(is_blacklisted_port(ports[1]) == false and is_blacklisted_port(ports[2]) == false and ports[1] ~= ports[2]) + + -- Update the accumlator with the seed + v1 = bit.bxor(v1, seed) + + -- Loop 10 more times to generate the second pair of ports + repeat + for i = 0, 9, 1 do + v1, v2 = mul64(bit.band(v1, 0xFFFFFFFF), bit.band(magic, 0xFFFFFFFF)) + + -- Add 1 to v1, handling overflows + if(v1 ~= 0xFFFFFFFF) then + v1 = v1 + 1 + else + v1 = 0 + v2 = v2 + 1 + end + + v2 = bit.rshift(v2, i) + + ports[(i % 2) + 3] = bit.bxor(bit.band(v2, 0xFFFF), ports[(i % 2) + 3]) + end + until(is_blacklisted_port(ports[3]) == false and is_blacklisted_port(ports[4]) == false and ports[3] ~= ports[4]) + + return {ports[1], ports[2], ports[3], ports[4]} +end + +---Calculate a checksum for the data. This checksum is appended to every Conficker packet before the random noise. +-- The checksum includes the key and data, but not the noise and optional length. +-- +--@param data The data to create a checksum for. +--@return An integer representing the checksum. +local function p2p_checksum(data) + local pos, i + local hash = string.len(data) + + stdnse.print_debug(2, "Conficker: Calculating checksum for %d-byte buffer", string.len(data)) + + -- Get the first character + pos, i = bin.unpack(" 0xFFFFFFFF) then + -- Handle overflows + key2 = key2 + (bit.rshift(key1, 32)) + key2 = bit.band(key2, 0xFFFFFFFF) + key1 = bit.band(key1, 0xFFFFFFFF) + end + end + + return buf +end + +---Decrypt the packet, verify it, and parse it. This function will fail with an error if the packet can't be +-- parsed properly (likely means the port is being used for something else), but will return successfully +-- without checking the packet's checksum (although it does calculate the checksum). It's up to the calling +-- function to decide if it cares about the checksum. +-- +--@param packet The packet, without the optional length (if it's TCP). +--@return (status, result) If status is true, result is a table (including 'hash' and 'real_hash'). If status +-- is false, result is a string that indicates why the parse failed. +function p2p_parse(packet) + local pos = 1 + local data = {} + + -- Get the key + pos, data['key1'], data['key2'] = bin.unpack("prng_generate_ports, or from unidentified ports) +--@return (status, reason, data) Status indicates whether or not Conficker is suspected to be present (truefalse = no Conficker). If status is true, data is the table of information returned by +-- Conficker. +local function conficker_check(ip, port, protocol) + local status, packet + local socket + local response + + status, packet = p2p_create_packet(protocol) + if(status == false) then + return false, packet + end + + -- Try to connect to the first socket + socket = nmap.new_socket() + socket:set_timeout(5000) + status, response = socket:connect(ip, port, protocol) + if(status == false) then + return false, "Couldn't establish connection (" .. response .. ")" + end + + -- Send the packet + socket:send(packet) + + -- Read a response (2 bytes minimum, because that's the TCP length) + status, response = socket:receive_bytes(2) + if(status == false) then + return false, "Couldn't receive bytes: " .. response + elseif(response == "ERROR") then + return false, "Failed to receive data" + elseif(response == "TIMEOUT") then + return false, "Timeout" + elseif(response == "EOF") then + return false, "Couldn't connect" + end + + -- If it's TCP, get the length and make sure we have the full packet + if(protocol == "tcp") then + local length + _, length = bin.unpack(" (string.len(response) - 2) do + local response2 + + status, response2 = socket:receive_bytes(2) + if(status == false) then + return false, "Couldn't receive bytes: " .. response2 + elseif(response2 == "ERROR") then + return false, "Failed to receive data" + elseif(response2 == "TIMEOUT") then + return false, "Timeout" + elseif(response2 == "EOF") then + return false, "Couldn't connect" + end + + response = response .. response2 + end + + -- Remove the 'length' bytes + response = string.sub(response, 3) + end + + -- Close the socket + socket:close() + + local status, result = p2p_parse(response) + + if(status == false) then + return false, "Data received, but wasn't Conficker data: " .. result + end + + if(result['hash'] ~= result['real_hash']) then + return false, "Data received, but checksum was invalid (possibly INFECTED)" + end + + return true, "Received valid data", result +end + +local function go(host) + local tcp_ports = {} + local udp_ports = {} + local response = " \n" + local i + local port, protocol + local count = 0 + local checks = 0 + + -- Generate a complete list of valid ports + if(nmap.registry.args.checkall == "true" or nmap.registry.args.checkall == "1") then + for i = 1, 65535, 1 do + if(not(is_blacklisted_port(i))) then + local tcp = nmap.get_port_state(host, {number=i, protocol="tcp"}) + if(tcp ~= nil and tcp.state == "open") then + tcp_ports[i] = "true" + end + + local udp = nmap.get_port_state(host, {number=i, protocol="udp"}) + if(udp ~= nil and (udp.state == "open" or udp.state == "open|filtered")) then + udp_ports[i] = "true" + end + end + end + end + + -- Generate ports based on the ip and time + local seed = math.floor((os.time() - 345600) / 604800) + local ip = host.ip + + -- Use the provided IP, if it exists + if(nmap.registry.args.realip ~= nil) then + ip = nmap.registry.args.realip + end + + -- Reverse the IP's endianness + ip = ipOps.todword(ip) + ip = bin.pack(">I", ip) + _, ip = bin.unpack("