For the ones who don’t know DnsCat it’s an awsome tool and even has metasploit shellcodes. Here’s a little quote from the website:
dnscat is designed in the spirit of netcat, allowing two hosts over the Internet to talk to each other. The major difference between dnscat and netcat, however, is that dnscat routes all traffic through the local (or a chosen) DNS server. This has several major advantages:
- Bypasses pretty much all network firewalls
- Bypasses many local firewalls
- Doesn’t pass through the typical gateway/proxy and therefore is stealthy
Which brings us to my original quest of finding a nice protocol to experiment with Wireshark dissector prototyping using LUA. I decided to try and make a dissector for DnsCat traffic. This post won’t contain the dissector, instead it contains a stand alone LUA DnsCat traffic parser(main dissector logic). Reason for this is that I’m not entirely happy with the dissector as it is right now, I want to try and improve some things on it before publishing it. To be able to run this parser you’ll need to install an additional LUA library though, since LUA doesn’t have native support for bitwise operators. You can get the library from the following website:
The installation is pretty straightforward, if you are on ubuntu you could also just do the following:
sudo apt-get install liblua5.1-bitop0
Ones you have that in place you should be able to run the parser without any problems. You can get the parser from pastebin or at the end of this post. If you are really impatient you can throw together a quick dissector yourself and just reuse this code for the dissecting part. I’ve also included some references at the end of this post that I’ve used while developing the parser.
Code
#!/usr/bin/env lua --[[ Author: DiabloHorn https://diablohorn.wordpress.com DnsCat traffic decoder, can be easily converted to a wireshark dissector :) This could be written a lot more efficient, but hey it's one of my first serious LUA scripts. ]]-- -- testdata local testdata = {"dnscat.21.fgnlglmh.7718fc12.0.dgqt.directdns","dnscat.31.hihuznqd.2cedf05e.1.636174202f6574632f69737375650a.tvtm.directdns", "dnscat.29.gnwbuhls.699c9c36.3.sryi.directdns", "dnscat.21.gnwbuhls.699c9c2f.1.ffgchfgohehfcadbdacodadecodbcaemfefdcafmgocafmgmakak.rypw.directdns"} -- requires the bitopt library to be installed local bit = require("bit") -- start base parser -- split the request into an array of subdomain function getsubs(data) -- empty table to hold the subs local subs = {} for sub in data:gmatch("[^%.]+") do table.insert(subs,sub) end return subs end -- decode the flags to human readable strings function decodeflags(data) -- protocol flags local FLAG_STREAM = 0x00000001 -- deprecated local FLAG_SYN = 0x00000002 local FLAG_ACK = 0x00000004 -- end of deprecated local FLAG_RST = 0x00000008 local FLAG_HEX = 0x00000010 local FLAG_SESSION = 0x00000020 local FLAG_IDENTIFIER = 0x00000040 -- convert string to number local hFlags = tonumber(data,16) --setup the flags table local Flags = {} -- st=nil,sy=nil,ac=nil,rs=nil,he=nil,se=nil,id=nil -- let's see which are set if bit.band(hFlags,FLAG_STREAM) ~= 0 then table.insert(Flags,"stream") end -- deprecated if bit.band(hFlags,FLAG_SYN) ~= 0 then table.insert(Flags,"syn") end if bit.band(hFlags,FLAG_ACK) ~= 0 then table.insert(Flags,"ack") end -- end of deprecated if bit.band(hFlags,FLAG_RST) ~= 0 then table.insert(Flags,"rst") end if bit.band(hFlags,FLAG_HEX) ~= 0 then table.insert(Flags,"hex") end if bit.band(hFlags,FLAG_SESSION) ~= 0 then table.insert(Flags,"session") end if bit.band(hFlags,FLAG_IDENTIFIER) ~= 0 then table.insert(Flags,"identifier") end return Flags end -- decode the error code to something human readable -- overcomplicated...but hey I wanted to use the bitopt lib again function decoderr(data) local ERR_SUCCESS = 0x00000000 local ERR_BUSY = 0x00000001 local ERR_INVSTATE = 0x00000002 local ERR_FIN = 0x00000003 local ERR_BADSEQ = 0x00000004 local ERR_NOTIMPLEMENTED = 0x00000005 local ERR_TEST = 0xFFFFFFFF local err = {} local herr = tonumber(data,16) if bit.tobit(ERR_SUCCESS) == bit.tobit(herr) then table.insert(err,"success") end if bit.tobit(ERR_BUSY) == bit.tobit(herr) then table.insert(err,"busy") end if bit.tobit(ERR_INVSTATE) == bit.tobit(herr) then table.insert(err,"invalidstate") end if bit.tobit(ERR_FIN) == bit.tobit(herr) then table.insert(err,"confin") end if bit.tobit(ERR_BADSEQ) == bit.tobit(herr) then table.insert(err,"badseqnum") end if bit.tobit(ERR_NOTIMPLEMENTED) == bit.tobit(herr) then table.insert(err,"notimplemented") end if bit.tobit(ERR_TEST) == bit.tobit(herr) then table.insert(err,"contest") end return err end -- decode netbios data to ascii function decodenetbios(data) local ldata = data:upper() local dec = "" for sub in ldata:gmatch("%u%u") do -- perform operation in decimal and convert final value from hex XX to decimal --local decnum = tonumber(((sub:byte(1)-65) .. (sub:byte(2)-65)),16) -- Thanks to Animal for making me realize the concat has to be with hexnumbers local decnum = tonumber((bit.tohex(sub:byte(1)-65,1) .. bit.tohex(sub:byte(2)-65,1)),16) print(bit.tohex(sub:byte(1)-65,1)) print(decnum,sub) if decnum > 31 and decnum < 127 then dec = dec .. string.char(decnum) else dec = dec .. "." end decnum = 0 sub = "" end return dec end -- decode hex data to ascii function decodehex(data) local dec = "" for sub in data:gmatch("%x%x") do decnum = tonumber(sub,16) if decnum > 31 and decnum < 127 then dec = dec .. string.char(decnum) else dec = dec .. "." end end return dec end -- main flow implementation -- lacks implementation of syn/ack since it's deprecated function parseDC(data) local finalparsed = {} local x = getsubs(data) finalparsed["signature"] = x[1] table.remove(x,1) finalparsed["flags"] = table.concat(decodeflags(x[1]),",") table.remove(x,1) if finalparsed["flags"]:find("identifier") ~= nil then finalparsed["identifier"] = x[1] table.remove(x,1) if finalparsed["flags"]:find("session") ~= nil then finalparsed["session"] = x[1] table.remove(x,1) end elseif finalparsed["flags"]:find("session") ~= nil then finalparsed["session"] = x[1] table.remove(x,1) end if finalparsed["flags"]:find("stream") ~= nil then finalparsed["seqnum"] = x[1] table.remove(x,1) end if finalparsed["flags"]:find("rst") ~= nil then finalparsed["err"] = table.concat(decoderr(x[1]),",") table.remove(x,1) finalparsed["garbage"] = x[1] finalparsed["domain"] = x[2] else finalparsed["count"] = x[1] table.remove(x,1) -- if you wonder the character # == len() finalparsed["garbage"] = x[#x-1] finalparsed["domain"] = x[#x] table.remove(x,(#x)) table.remove(x,(#x)) -- so we either got data or we don't finalparsed["asciidata"] = "" while #x > 0 do if finalparsed["flags"]:find("hex") == nil then finalparsed["asciidata"] = finalparsed["asciidata"] .. decodenetbios(x[1]) else finalparsed["asciidata"] = finalparsed["asciidata"] .. decodehex(x[1]) end table.remove(x,1) end end -- end of rst check return finalparsed end -- end of parseDC function -- end of base parser for i,v in ipairs(testdata) do local fp = parseDC(v) print("TestData: " .. v) print("Signature: " .. tostring(fp.signature)) print("Flags: " .. tostring(fp.flags)) print("Identifier: " .. tostring(fp.identifier)) print("Session: " .. tostring(fp.session)) print("Seqnum: " .. tostring(fp.seqnum)) print("Count: " .. tostring(fp.count)) print("Error: " .. tostring(fp.err)) print("Data: " .. tostring(fp.asciidata)) print("Garbage: " .. tostring(fp.garbage)) print("Domain: " .. tostring(fp.domain)) end
References
http://www.skullsecurity.org/wiki/index.php/Dnscat
http://www.wowpedia.org/Pattern_matching
http://bitop.luajit.org/api.html
http://lua-users.org/wiki/
http://www.lua.org/manual/5.1/
One thought on “Lua based DnsCat traffic parser”