Well previously I blogged about actually parsing DnsCat traffic, this blog post will be about converting it into an actual Wireshark post-dissector. As with dissecting DnsCat traffic using LUA I’ve also never written a wireshark post-dissector up until now. This is how it will finally look like:

Things you should know(read: things that could/should be improved) about this post-dissector:
- It assumes you are tunneling plain ascii (dnscat –listen –exec ‘/bin/sh’)
- It will only decode incoming&outgoing packets if you use the wireshark development version
- I think it would be more efficient if this would have been a chained-dissector
- It’s only been tested locally (dnscat –dns 127.0.0.1)
- It will happily parse every DNS packet it encounters
Just as the previous post, this one will contain the source code (pastebin) and the references at the end of the post. Now let’s get going with building our post-dissector.
There isn’t really much to tell about fitting my previous code into a wireshark post-dissector. The wireshark documentation pages are easy to follow. The two main pages I’ve used while developing the post-dissector are:
http://wiki.wireshark.org/Lua/Dissectors
http://www.wireshark.org/docs/wsug_html_chunked/wsluarm.html
The above two pages include more or less all you need to know about wireshark and LUA to be able to develop your own post-dissector. Another helpful resource is the #wireshark channel on freenode. Really helpful people over there, the channel isn’t very active but it sure is helpful. During the development of the post-dissector I had a problem dissecting response packets, since the data I needed wasn’t made available by the DNS dissector. So I tried a few dirty hacks with LUA but failed miserably. I then decided to seek help in the #wireshark channel. The response was really great a patch was made to fix the issue I stumbled upon, which in turn made it possibly for my dissector to also parse the dns response packets. The patch made the following available:
dns.resp.primaryname
Of course this means that to run this post-dissector you need to use the wireshark development version right from their SVN server. You can skip this step and just configure wireshark to use LUA if you don’t care about the dns response packets getting parsed.
Getting & Compiling Wireshark
Assuming you already have subversion installed you can perform the following command to obtain the wireshark sources:
svn checkout http://anonsvn.wireshark.org/wireshark/trunk wireshark
Ones it’s finished you need to obtain some prerequisites before you can compile Wireshark. The following blog has a good how to about it:
http://bredsaal.dk/compiling-wireshark-from-source-on-ubuntu-9-10
Short version (quote from the blog):
First of, you have to install some libraries and tools for building wireshark:
sudo aptitude install autoconf libgtk2.0-dev libglib2.0-dev libgeoip-dev libpcre3-dev libpcap0.8-dev libtool byacc flex subversionNow, it’s just a matter of compiling the source code:
cd wireshark
./autogen.sh
./configure
make
Like the above blog says be prepared to wait a loonnggggg time. Ones it’s done you can continue with the next step, enabling LUA.
Enabling LUA
You need to edit ‘/etc/wireshark/init.lua‘ and comment out the following line( use — for comments):
disable_lua = true; do return end;
Using the post-dissector
So ones all of the above have completed you can just fire up Wireshark with the appropriate commandline arguments and watch the magic happen:
./wireshark <pcapfile.pcap> -X lua_script:<our-post-dissector.lua>
Remember the post-dissector doesn’t detect if the traffic actually is DnsCat traffic, so if you feed it something else it will happily try and decode it and probably get fubar.
Code
-- postdissector to make dnscat traffic more human readable
-- DiabloHorn https://diablohorn.wordpress.com
-- Thanks to #wireshark on freenode for the quick and excellent response,
-- which resulted into the patch for the dns dissector that made access to dns.resp.primaryname possible
-- http://www.skullsecurity.org/wiki/index.php/Dnscat#Structure
-- required libs
local bit = require("bit")
-- info
print("dnscat postdissector loaded")
-- we need these fields from the dns packets
dc_dns_name = Field.new("dns.qry.name")
--this will only work if you have the developer version which includes a patch
dc_dns_rname = Field.new("dns.resp.primaryname")
dc_udp_dport = Field.new("udp.dstport")
dc_udp_sport = Field.new("udp.srcport")
-- declare our postdissector
dc_pd = Proto("dnscat","dnscat postdissector")
-- our fields
dc_tunneldata = ProtoField.string("dc_pd.tunneldata","Encoded Tunnel Data")
dc_td_sig = ProtoField.string("dc_pd.td_sig","Signature")
dc_td_flags = ProtoField.string("dc_pd.td_flags","Flags")
dc_td_ident = ProtoField.string("dc_pd.td_ident","Identifier")
dc_td_sess = ProtoField.string("dc_pd.td_session","Session")
dc_td_seqnum = ProtoField.string("dc_pd.td_seqnum","SeqNum")
dc_td_count = ProtoField.string("dc_pd.td_count","Count")
dc_td_err = ProtoField.string("dc_pd.td_error","Error")
dc_td_data = ProtoField.string("dc_pd.td_data","Data")
dc_td_gar = ProtoField.string("dc_pd.td_garbage","Garbage")
dc_td_dom = ProtoField.string("dc_pd.td_domain","Domain")
-- add our fields
dc_pd.fields = {dc_tunneldata,dc_td_sig,dc_td_flags,dc_td_ident,dc_td_sess,dc_td_seqnum,dc_td_count,dc_td_err,dc_td_data,dc_td_gar,dc_td_dom}
-- dissect each packet
function dc_pd.dissector(buffer,pinfo,tree)
local udpsport = dc_udp_sport()
local udpdport = dc_udp_dport()
local dnsqryname = dc_dns_name()
--this will only work if you have the developer version which includes a patch
local dnsresname = dc_dns_rname()
local subtree
local parsed = {}
subtree = tree:add(dc_pd,"dnscat data")
if tostring(udpdport) == "53" then
subtree:add(dc_tunneldata,tostring(dnsqryname))
parsed = parseDC(tostring(dnsqryname))
end
--this will only work if you have the developer version which includes a patch
if tostring(udpsport) == "53" then
subtree:add(dc_tunneldata,tostring(dnsresname))
parsed = parseDC(tostring(dnsresname))
end
subtree:add(dc_td_sig,tostring(parsed.signature))
subtree:add(dc_td_flags,tostring(parsed.flags))
subtree:add(dc_td_ident,tostring(parsed.identifier))
subtree:add(dc_td_sess,tostring(parsed.session))
subtree:add(dc_td_seqnum,tostring(parsed.seqnum))
subtree:add(dc_td_count,tostring(parsed.count))
subtree:add(dc_td_err,tostring(parsed.err))
subtree:add(dc_td_data,tostring(parsed.asciidata))
subtree:add(dc_td_gar,tostring(parsed.garbage))
subtree:add(dc_td_dom,tostring(parsed.domain))
end -- end dissector function
-- main dissecting logic
-- 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(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
local 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 main dissecting logic
-- register ourselfs
register_postdissector(dc_pd)
References
http://wiki.wireshark.org/Lua/Dissectors
http://www.wireshark.org/docs/wsug_html_chunked/wsluarm.html
http://wiki.wireshark.org/Lua
http://wiki.wireshark.org/Lua/Examples
http://www.skullsecurity.org/wiki/index.php/Dnscat
http://www.wowpedia.org/Pattern_matching
http://bitop.luajit.org/api.html
http://bredsaal.dk/compiling-wireshark-from-source-on-ubuntu-9-10
So it works here is the final post-dissector.
First make sure you compile wireshark, if you are on ubuntu follow the steps described over here: http://bredsaal.dk/compiling-wireshark-from-source-on-ubuntu-9-10 in short: Quote
First of, you have to install some libraries and tools for building wireshark:
sudo aptitude install autoconf libgtk2.0-dev libglib2.0-dev libgeoip-dev libpcre3-dev libpcap0.8-dev libtool byacc flex subversion Now, it’s just a matter of compiling the source code: cd wireshark After you’ve done that you can use the following post-dissector to view dnscat data outgoing and incomming. Code:
-- postdissector to make dnscat traffic more human readable
-- http://www.skullsecurity.org/wiki/index.php/Dnscat#Structure
-- DiabloHorn https://diablohorn.wordpress.com
-- due to wireshark weirdness...or me not getting it, this only decodes requests
-- required libs
local bit = require("bit")
-- info
print("dnscat postdissector loaded")
-- we need these fields from the dns packets
dc_dns_name = Field.new("dns.qry.name")
--this will only work if you have the developer version which includes a patch
dc_dns_rname = Field.new("dns.resp.primaryname")
dc_udp_dport = Field.new("udp.dstport")
dc_udp_sport = Field.new("udp.srcport")
-- declare our postdissector
dc_pd = Proto("dnscat","dnscat postdissector")
-- our fields
dc_tunneldata = ProtoField.string("dc_pd.tunneldata","Encoded Tunnel Data")
dc_td_sig = ProtoField.string("dc_pd.td_sig","Signature")
dc_td_flags = ProtoField.string("dc_pd.td_flags","Flags")
dc_td_ident = ProtoField.string("dc_pd.td_ident","Identifier")
dc_td_sess = ProtoField.string("dc_pd.td_session","Session")
dc_td_seqnum = ProtoField.string("dc_pd.td_seqnum","SeqNum")
dc_td_count = ProtoField.string("dc_pd.td_count","Count")
dc_td_err = ProtoField.string("dc_pd.td_error","Error")
dc_td_data = ProtoField.string("dc_pd.td_data","Data")
dc_td_gar = ProtoField.string("dc_pd.td_garbage","Garbage")
dc_td_dom = ProtoField.string("dc_pd.td_domain","Domain")
-- add our fields
dc_pd.fields = {dc_tunneldata,dc_td_sig,dc_td_flags,dc_td_ident,dc_td_sess,dc_td_seqnum,dc_td_count,dc_td_err,dc_td_data,dc_td_gar,dc_td_dom}
-- dissect each packet
function dc_pd.dissector(buffer,pinfo,tree)
local udpsport = dc_udp_sport()
local udpdport = dc_udp_dport()
local dnsqryname = dc_dns_name()
--this will only work if you have the developer version which includes a patch
local dnsresname = dc_dns_rname()
local subtree
local parsed = {}
subtree = tree:add(dc_pd,"dnscat data")
if tostring(udpdport) == "53" then
subtree:add(dc_tunneldata,tostring(dnsqryname))
parsed = parseDC(tostring(dnsqryname))
end
--this will only work if you have the developer version which includes a patch
if tostring(udpsport) == "53" then
subtree:add(dc_tunneldata,tostring(dnsresname))
parsed = parseDC(tostring(dnsresname))
end
subtree:add(dc_td_sig,tostring(parsed.signature))
subtree:add(dc_td_flags,tostring(parsed.flags))
subtree:add(dc_td_ident,tostring(parsed.identifier))
subtree:add(dc_td_sess,tostring(parsed.session))
subtree:add(dc_td_seqnum,tostring(parsed.seqnum))
subtree:add(dc_td_count,tostring(parsed.count))
subtree:add(dc_td_err,tostring(parsed.err))
subtree:add(dc_td_data,tostring(parsed.asciidata))
subtree:add(dc_td_gar,tostring(parsed.garbage))
subtree:add(dc_td_dom,tostring(parsed.domain))
end -- end dissector function
-- main dissecting logic
-- 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(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
local 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 main dissecting logic
-- register ourselfs
register_postdissector(dc_pd)
|

Quote
Modify
Remove
Split Topic
Cool that looks really nice. At least python is easier to handle datatypes :p
I converted your LUA script to Python here: http://micksmix.wordpress.com/2013/01/14/dnscat-traffic-parser-dissector-decoder/