Opening the capture in Wireshark reveals a lot of DNS traffic (and 4 ARP requests): it definitely looks like a DNS tunnel.
Overview of the DNS tunnel data
Basically DNS works with client requests (DNS QR) and server responses (DNS RR), so the real data are in QR.qname and RR.rdata respectively.Using our favourite network packet manipulation tool Scapy, we can quickly have an overview of the DNS tunnel:
p = rdpcap('bottle.cap') for i in range(8): if not p[i].haslayer(DNS): continue if DNSQR in p[i]: if DNSRR in p[i] and len(p[i][DNSRR].rdata)>0: # downstream/server print "S[%i]: %r" % (i,p[i][DNSRR].rdata) else: # upstream/client print "C[%i]: %r" % (i,p[i][DNSQR].qname)
C[0]: 'vaaaakardli.pirate.sea.' S[1]: 'VACKD\x03\xc5\xe9\x01' C[2]: 'laegpumiplhhpz12ynd1efljwlkjcgwy.pirate.sea.' S[3]: '10.20.30.1-10.20.30.3-1130-24' C[4]: 'yrbi02.pirate.sea.' S[5]: '\x00\x00\x00\x00\xff\xff\xff\xffUUUU\xaa\xaa\xaa\xaa\x81c\xc8 \xd2\xc7|\xb2\x17_O\xce\xc9I-R!a\xa9q %\xb3\x06s\xe6\xd8D0yPW\xbf' C[6]: 'zi03aA-Aaahhh-Drink-mal-ein-J\xe4germeister-.pirate.sea.' S[7]: 'zi03aA-Aaahhh-Drink-mal-ein-J\xe4germeister-.' [...]View the entire overview here.
Identify DNS tunnel protocol version
Several DNS tunneling softwares exists, the most well-known being iodine we will start with it. Extract the source of latest iodine version 0.6.0-rc1 then read the kindly provided doc/proto_* files to understand how iodine DNS tunneling works. In proto_00000502.txt we read that the first packet to be transmitted sends protocol version:Version: Client sends: - First byte v or V - Rest encoded with base32: - 4 bytes big endian protocol version - CMC Server replies: - 4 chars: - VACK (version ok), followed by login challenge - VNAK (version differs), followed by server protocol version - VFUL (server has no free slots), followed by max users - 4 byte value: means login challenge/server protocol version/max users - 1 byte userid of the new user, or any byte if not VACKLooks like it is indeed iodine because we have something like that:
C[0]: 'vaaaakardli.pirate.sea.' S[1]: 'VACKD\x03\xc5\xe9\x01'We see that the client sends its version, then the server replies OK and gives the login challenge 'D\x03\xc5\xe9'.
In order to know which protocol version is used, we need to decode "aaaakardli" in base32. However big problem, iodine is using its own base32/64/128 charset and not the one defined in the RFC! In order not to reimplement its base32/64/128 versions and to be quicker, I made a little C program (encoder.c) directly using iodine's base32/64/128 routines that I can then call from python using subprocess module:
from subprocess import Popen,PIPE def encoder(base,encode="",decode=""): # base=[32,64,128] p = Popen(["./encoder", str(base), "e" if len(encode)>0 else "d"], stdin=PIPE, stdout=PIPE) p.stdin.write(encode if len(encode)>0 else decode) return p.communicate()[0]So it gives:
>>> "0x%08x" % unpack(">I",encoder(32,decode="aaaakardli")[:4]) '0x00000502'Good, it is using protocol version 502, the latest.
Login sequence, break the password!
Next, we should have the login sequence:Login: Client sends: - First byte l or L - Rest encoded with base32: - 1 byte userid - 16 bytes MD5 hash of: (first 32 bytes of password) xor (8 repetitions of login challenge) - CMC Server replies: - LNAK means not accepted - x.x.x.x-y.y.y.y-mtu-netmask means accepted (server ip, client ip, mtu, netmask bits)Indeed we have:
C[2]: 'laegpumiplhhpz12ynd1efljwlkjcgwy.pirate.sea.' S[3]: '10.20.30.1-10.20.30.3-1130-24'Client sends the MD5 hash, which gets accepted and the server replies the network info. We can extract the MD5 hash this way:
>>> encoder(32,decode="aegpumiplhhpz12ynd1efljwlkjcgwya")[1:17].encode("hex") '0cfa310f59cefcef9868f642ad365a92'
Would be fun to break the password, and maybe it is the flag? Sadly we cannot use our rainbow tables because it is using a challenge (and it is a very good practice), but we can still try to attack it via a dictionary:
from hashlib import md5 from struct import pack,unpack def xor(a,b): return "".join(chr(ord(a[i])^ord(b[i%len(b)])) for i in range(len(a))) def crack_password(hash, challenge, dic): for line in open(dic): if hash == md5(xor(line.strip().ljust(32,'\x00'),challenge)).digest().encode('hex'): print "\o/ Password: %r" % line.strip()We try with all the english words:
>>> crack_password("0cfa310f59cefcef9868f642ad365a92", "D\x03\xc5\xe9", "english.txt") \o/ Password: 'swordfish'Wow, we are lucky! But dammit, it is not the flag :) it was worth trying anyway.
Analyze DNS tunnel data headers
We go on reading the overview of DNS tunnel data along with the protocol specification:U[16]: 'sbhi1c.pirate.sea.' D[17]: 'Base128'This indicates that the upstream data will be base128 encoded. We will use the C program (encoder.c) and python wrapper previously mentioned to decode it. Obviously, before the base128 decode we have to remove the top domain as well as the inner dots.
About the data, the following is very interesting:
Data: Upstream data header: 3210 432 10 43 210 4321 0 43210 +----+---+--+--+---+----+-+-----+ |UUUU|SSS|FF|FF|DDD|GGGG|L|UDCMC| +----+---+--+--+---+----+-+-----+ Downstream data header: 7 654 3210 765 4321 0 +-+---+----+---+----+-+ |C|SSS|FFFF|DDD|GGGG|L| +-+---+----+---+----+-+ UUUU = Userid L = Last fragment in packet flag SS = Upstream packet sequence number FFFF = Upstream fragment number DDD = Downstream packet sequence number GGGG = Downstream fragment number C = Compression enabled for downstream packet UDCMC = Upstream Data CMC, 36 steps a-z0-9, case-insensitive Upstream data packet starts with 1 byte ASCII hex coded user byte; then 3 bytes Base32 encoded header; then 1 char data-CMC; then comes the payload data, encoded with the chosen upstream codec. Downstream data starts with 2 byte header. Then payload data, which may be compressed. In NULL responses, downstream data is always raw.To implement header decoding, it is easier to look directly at the code (iodined.c for the server handling client packets, and client.c for the client handling server packets). At the server side, we notice that again it is using a custom base32 decoder (base32_5to8). We are eventually able to decode all the header fields with the following code:
def b32_8to5(a): return "abcdefghijklmnopqrstuvwxyz012345".find(a.lower()) def up_header(p): return { "userid": int(p[0],16), "up_seq": (b32_8to5(p[1]) >> 2) & 7, "up_frag": ((b32_8to5(p[1]) & 3) << 2) | ((b32_8to5(p[2]) >> 3) & 3), "dn_seq": (b32_8to5(p[2]) & 7), "dn_frag": b32_8to5(p[3]) >> 1, "lastfrag": b32_8to5(p[3]) & 1 } def dn_header(p): return { "compress": ord(p[0]) >> 7, "up_seq": (ord(p[0]) >> 4) & 7, "up_frag": ord(p[0]) & 15, "dn_seq": (ord(p[1]) >> 1) & 15, "dn_frag": (ord(p[1]) >> 5) & 7, "lastfrag": ord(p[1]) & 1, }We try it with this small piece of code:
p = rdpcap('bottle.cap') datasent = False for i in range(20,100): if not p[i].haslayer(DNS): continue if DNSQR in p[i]: if DNSRR in p[i] and len(p[i][DNSRR].rdata)>0: # downstream/server d = p[i][DNSRR].rdata print "D[%i]: %r" % (i,d[:30]+(" [...]" if len(d)>30 else "")) if datasent: print " %r" % dn_header(d) else: # upstream/client d = p[i][DNSQR].qname print "U[%i]: %r" % (i,d[:30]+(" [...]" if len(d)>30 else "")) if d[0].lower() in "0123456789abcdef": print " %r" % up_header(d) datasent = True else: datasent = FalseAnd we can see it works fine for the first data packets:
U[46]: '1eaba82\xca2hb\xbe\xeeY\xd6wgi\xcf\xe2\xde4yp1\xccC\xc8I\xe1 [...]' {'userid': 1, 'up_seq': 1, 'lastfrag': 1, 'dn_seq': 0, 'up_frag': 0, 'dn_frag': 0} D[47]: '\x90!x\xdac`\xe0`pe`\xb0a`p`p`{5\x81KD\x8e\x11\x88\x99\xd9v\xdc [...]' {'up_seq': 1, 'lastfrag': 1, 'dn_seq': 0, 'up_frag': 0, 'dn_frag': 1, 'compress': 1}
Decompression
We are almost ready to extract the DNS tunnel data, the last thing to do is to decompress the data. For upstream packets it is the data after the base128 decode, for downstream packets it is the raw data (because here it is using type NULL DNS queries). Just like for the encoder, I made a small C program (uncompress.c) using the same function uncompress() from the zlib, and a python wrapper:def uncompress(s): p = Popen(["./uncompress"], stdin=PIPE, stdout=PIPE) p.stdin.write(s) if p.wait() == 0: return p.communicate()[0] else: return False
We try it on the first upstream packet p=U[46] (not fragmented as we have seen previously):
>>> # skip header, remove top domain and undotify >>> d = p[5:-len(".pirate.sea.")].replace(".","") >>> u = uncompress(encoder(128,decode=d)) >>> u '\x00\x00\x08\x00E\x00\x00<\x01\x12@\x00@\x06\xe9~\n\x14\x1e\x03 \n\x14\x1e\x01\xdb\x9c\x06\xb8\\s\xed+\x00\x00\x00\x00\xa0\x02\x11 \x08\xd0\r\x00\x00\x02\x04\x04B\x04\x02\x08\n\x00\x05\xec9\x00\x00 \x00\x00\x01\x03\x03\x05'Oh oh, we recognize the E (0x45) of what could be an IP packet starting at offset 4:
>>> IP(u[4:]) <IP version=4L ihl=5L tos=0x0 len=60 id=274 flags=DF frag=0L ttl=64 proto=tcp chksum=0xe97e src=10.20.30.3 dst=10.20.30.1 options=[] |<TCP sport=56220 dport=1720 seq=1551101227 ack=0 dataofs=10L reserved=0L flags=S window=4360 chksum=0xd00d urgptr=0 options=[('MSS', 1090), ('SAckOK', ''), ('Timestamp', (388153, 0)), ('NOP', None), ('WScale', 5)] |>>Looks like a valid packet! Good, we are able to decode upstream packets.
Same thing for the downstream packet p=D[47] (not fragmented as we have seen previously):
>>> d = p[2:] # skip the two byte header >>> u = uncompress(d) >>> u '\x00\x00\x08\x00E\x00\x00<\x00\x00@\x00@\x06\xea\x90\n\x14\x1e\x01 \n\x14\x1e\x03\x06\xb8\xdb\x9c\xfd\x82\x8b\x08\\s\xed,\xa0\x12\x10 \xd8\x86b\x00\x00\x02\x04\x04B\x04\x02\x08\n\x00\x10\xc1-\x00\x05 \xec9\x01\x03\x03\x06' >>> IP(u[4:]) <IP version=4L ihl=5L tos=0x0 len=60 id=0 flags=DF frag=0L ttl=64 proto=tcp chksum=0xea90 src=10.20.30.1 dst=10.20.30.3 options=[] |<TCP sport=1720 dport=56220 seq=4253190920L ack=1551101228 dataofs=10L reserved=0L flags=SA window=4312 chksum=0x8662 urgptr=0 options=[('MSS', 1090), ('SAckOK', ''), ('Timestamp', (1098029, 388153)), ('NOP', None), ('WScale', 6)] |>>Looks like a valid packet! Good, we are also able to decode downstream packets.
Extract DNS tunnel packets
Seeing that packets were captured in a LAN, we can reasonably assume packets are in the correct order, thus the only thing that remains is to handle fragmentation and reassembly.With all the previously defined functions, it gives:
input, output = "bottle.cap", "extracted.cap" topdomain = ".pirate.sea." upstream_encoding = 128 p = rdpcap(input) dn_pkt, up_pkt = '', '' datasent = False E = [] for i in range(len(p)): if not p[i].haslayer(DNS): continue if DNSQR in p[i]: if DNSRR in p[i] and len(p[i][DNSRR].rdata)>0: # downstream/server d = p[i][DNSRR].rdata if datasent: # real data and no longer codec/fragment checks dn_pkt += d[2:] if dn_header(d)['lastfrag'] and len(dn_pkt)>0: u = uncompress(dn_pkt) if not u: raise Exception("Error dn_pkt %i: %r" % (i,dn_pkt)) E += [IP(u[4:])] dn_pkt = '' else: # upstream/client d = p[i][DNSQR].qname if d[0].lower() in "0123456789abcdef": datasent = True up_pkt += d[5:-len(topdomain)].replace(".","") if up_header(d)['lastfrag'] and len(up_pkt)>0: u = uncompress(encoder(upstream_encoding,decode=up_pkt)) if not u: raise Exception("Error up_pkt %i: %r" % (i,up_pkt)) E += [IP(u[4:])] up_pkt = '' wrpcap(output, E) print "Successfully extracted %i packets into %s" % (len(E), output)The full script is available here.
$ python extract_dns.py Successfully extracted 200 packetsWoohoo! The extracted network capture is available here.
VoIP?
We see nothing interesting in the VoIP/H.323 signalisation: looks like a "root" user is having a conversation with an Answering Machine using product OpenH323 Project OpenAM version 1.1.18 (thanks Wireshark for dissecting all these fields!).Then we see RTP traffic containing GSM audio. One can easily extract it by using his favourite network packet manipulation tool - guess, Scapy! - by just concatenating all the payloads (we assume here too that there is no disorder):
A = [] for p in rdpcap("extracted.cap"): if UDP in p and p[UDP].sport==5000: # we see in Wireshark that GSM audio starts at offset 12 of UDP payload A += [str(p[UDP].payload)[12:]] open("audio.gsm","w").write("".join(A))The full script is available here, as well as the resulting audio.gsm file.
Then how do we listen to a raw GSM audio file? For those who worked on Defcon 18 quals forensics 300 (see writeup by team routards) or followed my twitter (@stalkr_), we already know SoX - Sound eXchange, the Swiss Army knife of sound processing programs. Note for debian users: install package sox but also libsox-fmt-all to have GSM format as well.
Then just convert it to any format you can read, for instance audio.wav:
$ sox -t gsm audio.gsm audio.wav
Final challenge: understand the answering machine
In the audio file, you can hear distinctly "Password" then the password, something like "freebewzo chimp". The most difficult challenge for us was to understand what was being said! We went mad trying stuff on the scoreboard and finally sent our audio file to a fluxfingers admin who told us what we were not able to understand correctly: freebooter chimp! :)Thank you fluxfingers for this awesome CTF with great challenges.
Thanks also to the person who worked with me on this challenge, it was fun ;)
And also congrats to iodine guys because it is a great software!
very nice writeup and good job!
ReplyDelete