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) |
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/