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”