[NDH 2016] [FORENSICS 200 – I’M AFRAID OF A GH0ST NAMED POISON IVY] Write Up

Description

You must find the flag.

Download the pcap : poisonIvy.pcap

Resolution

Hey fellas,

Sometimes, there are challenges you can’t resolve. Yes, you know, there is always one or two challenges that made you insane, that you just can’t find out a way to solve them.
Challenges that make you cry during weeks.
Challenges that haunt your nightmares. Even nightmares have nightmares because of these challenges.

This challenge is one of them.

Ok, maybe we overreacted a bit during the introduction, but anyway, we worked hard on this one and didn’t find the flag.
We tried it after the CTF, because it was a cool chall (even if it seemed a bit buggy on some steps), and because as yoda said “Do or do not, there is no try”.
We found the flag on wednesday, but we were proud to find it 🙂

Sooooo, ladies and gentlemeeeeeeeeen, here iiiiiis the only write up eveeeeeeeer for this chaaaaaaaaaall

The challenge consisted only of a pcap file, as you can see on the screenshot below.

 

a "classic" pcap
a “classic” pcap

We opened it and after checking the three way handshake and the termination, we saw nothing weird on these packets.
We then checked at the other packets, the ones containing data.

NdH16 magic number ?
NdH16 magic number ?

As you see on the screenshot, there isn’t a wireshark dissector, and the only meaningful string was NdH16 (for “Nuit du Hack 2016”, obviously).
As there wasn’t any useful data, we did a bit of google by looking at the “gh0st” string.

The data was always beginning with the string “NdH16”. By looking at the challenge name, we went on google, in order to find information about this “gh0st”. We rapidly found this PDF which was explaining the packet structure of the gh0st protocol. Here is the most important part :

The first 5 bytes contain the initialization string “Gh0st”. While this is the default string, numerous other initialization strings have been reported, so this string should not be relied upon for identification.
The next 4 bytes contain a Little-Endian integer representing the packet length.
The 4 bytes after that contain a Little-Endian integer representing the packet length once the payload is uncompressed.
This header is followed by a variable length payload, compressed with the Zlib compression algorithm

So, the struct is easy to understand :

  • identifier : 5 bytes, usually “gh0st”
  • packet length : 4 bytes
  • uncompressed length : 4 bytes
  • zlib-ed payload : variable

So, if our tought were correct, we were in front of the gh0st protocol. Here the magic number wasn’t gh0st but Ndh16 (which is good about the bytes length), the length of the packet (0x7C), and the length of the uncompressed payload (0x64).

Why not give it a try ? All we need to do is removing the header and uncompressing the payload. With a little (and ugly, as usual) python script, we stripped the first 13 bytes and tried to uncompress the payload :


h = open('poisonIvy.pcap', 'rb')
pcap = h.read()
begin = pcap.find('NdH16')
end = pcap.find('\x00\x00\x00\x00\x00\x00\x00\x00', begin)
data1 = pcap[begin:end]
data1 = zlib.decompress(data1[13:])

h2 = open('data1.txt', 'wb')
h2.write(data1)
h2.close()
h.close()

We tried it… and…

thelsd@thelsd-PC ~/ndh $ python camellia_ndh.py 
Traceback (most recent call last):
 File "camellia_ndh.py", line 14, in <module>
    data1 = zlib.decompress(data1[13:])
zlib.error: Error -3 while decompressing data: invalid stored block lengths

WAAAAAT! B-B-B-BUT WHY!? IT HAS TO WORK! IT’S TOO PERFECT TO BE FALSE!
So, after we stopped crying, we started to check where the issue could be. Let’s look from the beginning.
We know that our magic bytes is 5 bytes long and could be anything. We know that our packet length must be 124 bytes long (0x7C), and we know that the uncompressed data should be 100 bytes long (0x64). “NdH16” is 5 bytes, and as we can’t uncompress the zlib-ed data, the third header is useless for now. The only thing that have sense is the packet length…

Oh…
But wait…
Let’s have a look at the previous screenshot…
Wireshark, it says that the data is… 126 BYTES LONG! NOT 124! We have two ghost bytes (you see the joke? Ghost bytes? In the gh0st protocol? Urmmm… Forget it!)
We have to find these bytes.
Let’s begin by the beginning. How is the zlib header made? After a bit of googling, we found this stackoverflow topic (http://stackoverflow.com/questions/9050260/what-does-a-zlib-header-look-like) which explains that the zlib magic bytes are one of these :

– 7801
– 789C
– 78DA

Now, matrix style, we have to find these bytes on the raw data.

 

The biiiig red quares
The biiiig red quares

OH MY GOD! DO YOU SEE IT? THE MAGIC NUMBER! IT IS IN DOUBLE!!! That’s why our script was screwing us! Ok, now that we know it, we just have to strip two more bytes. We launched our script (the previous one, only modified to get from the 15th byte), and…


thelsd@thelsd-PC ~/ndh $ python camellia_ndh.py
è¥*÷‚UáøãÔm:iø1Zp½çöC½d¥…Fö\WƒÃƒlÉô6 /Êg÷K6Ì;C6îß›Óâm¡oY½ªª
ÛÙòÖÿ<%&÷‰Îäë>øÔ…ÓÚy‰Äéû

Nice. Junk data. Exactly what we were not waiting for. Following our PDF, the data was just zlib-ed, after uncompressing it, we were waiting for something else (the PDF was talking of a structure beginning with a command_id).

Well, well, well, you know, as usual, google and google again, but this time on the Poison Ivy RAT.

Finally, we found three another docs, very useful. The first one from the FireEye company, the second one by Conix Security, and the third one by Aris, a CTF player who also wrote a write up about a Poison Ivy challenge 🙂

By reading carefully these papers, we saw that our first PDF was incomplete. Even if the gh0st protocol is only using zlib, the RAT named Poison Ivy (which uses the gh0st protocol) also implements some cryptography AFTER the data is uncompressed.
Just see how it works :

  1. The RAT admin choose a master key, which is embedded to the RAT
  2. The infected machine sends to the C&C 100 random bytes.
  3. The C&C server receives it, crypt it with the Camellia algorithm (with the master key) and sends the ciphered data to the infected machine
  4. The infected machine crypts the the previously sent 100 random bytes with the master key
  5. The infected machine checks if the ciphered data sent by the server is equal to the locally calculated cipher.
  6. If yes, the handshake is over, else, the infected machine drops the connection (at least, we suppose, but that’s not the point)

So, to sum up, our first packet should be our random bytes, the second is the response from the server, and the third is the real data, after the handshake had been validated.
If everything is correct, our uncompressed data should be 100 bytes long :

thelsd@thelsd-PC ~/ndh $ wc -c data1.txt 
100 data1.txt

Cool! We’re on the good way! The data in data1.txt is the 100 bytes.
Now, we have to extract the second packet payload, in order to have the encrypted string, and to find the master key.
Following the documentation, the password is usually “admin” (\x00 padded to 32 bytes).
Let’s try it!


h = open('poisonIvy.pcap', 'rb')
pcap = h.read()
begin = pcap.find('NdH16')
end = pcap.find('\x00\x00\x00\x00\x00\x00\x00\x00', begin)
data1 = pcap[begin:end]
data1 = zlib.decompress(data1[15:])
data1 = data1+('\x00'*28) # padded to 128 bytes for the camellia cipher

h2 = open('data1.txt', 'wb')
h2.write(data1)
h2.close()
h.close()


begin = pcap.find('NdH16', end)
end = pcap.find('\x00\x00\x00\x00\x00\x00\x00\x00', begin)
data2 = pcap[begin:end]
data2 = zlib.decompress(data2[15:])

h2 = open('data2.txt', 'wb')
h2.write(data2)
h2.close()
h.close()

w = 'admin'
w = w+"\x00"*(32-len(w))

c = camellia.CamelliaCipher(key=w, mode=camellia.MODE_ECB)
en = c.encrypt(data1)
if en == data2:
print('FOUND')
print(w)
print(en)
print(data2)

As you may imagine, it wasn’t that easy, and the ‘admin’ key was not working. We tried some other strings, as “Ndh16”, “NdH16”, “NDH16”, etc., but none of them were working.
You already know the next step : Bruteforce. We took the darkc0de.lst dictionnary and tried all the keys.


h = open('poisonIvy.pcap', 'rb')
pcap = h.read()
begin = pcap.find('NdH16')
end = pcap.find('\x00\x00\x00\x00\x00\x00\x00\x00', begin)
data1 = pcap[begin:end]
data1 = zlib.decompress(data1[15:])
data1 = data1+('\x00'*28) # padded to 128 bytes for the camellia cipher

h2 = open('data1.txt', 'wb')
h2.write(data1)
h2.close()
h.close()


begin = pcap.find('NdH16', end)
end = pcap.find('\x00\x00\x00\x00\x00\x00\x00\x00', begin)
data2 = pcap[begin:end]
data2 = zlib.decompress(data2[15:])

h2 = open('data2.txt', 'wb')
h2.write(data2)
h2.close()
h.close()

dc = open('darkc0de.lst', 'r').readlines()
for i in dc:
w = i.replace("\n","").replace("\r","")
w = w[:32]
w = w+"\x00"*(32-len(w))

c = camellia.CamelliaCipher(key=w, mode=camellia.MODE_ECB)
en = c.encrypt(data1)
if en == data2:
print('FOUND')
print(w)


thelsd@thelsd-PC ~/ndh $ python camellia_ndh.py
FOUND
casper

YES!
Actually, another challenge was using bruteforce with the darkc0de dictionnary, so we had good hope that it would work here. Anyway, now we know that the first packet was the random bytes, the second packet was the encrypted random bytes, and that the master key was “casper”. There was only one packet, and we had everthing that was needed. We just have to decompress and decrypt it. So here is the script :


begin = pcap.find('NdH16', end)
end = pcap.find('\x00\x00\x00\x00\x00\x00\x00\x00', begin)
data3 = pcap[begin:end]
data3 = zlib.decompress(data3[15:])

h2 = open('data3.txt', 'wb')
h2.write(data3)
h2.close()
h.close()

w = 'casper'+"\x00"*(32-len('casper'))
c = camellia.CamelliaCipher(key=w, mode=camellia.MODE_ECB)
de = c.decrypt(data3+"\x00"*(64-len(data3)))

print(de)


thelsd@thelsd-PC ~/ndh $ python camellia_ndh.py
�qG��W��E�T�pҷ)_Yoy��uT�yg�b��Ե؊�fX��41��4����g9���e

Oh nooooo, not agaiiiiin. Junk data one more time…
We checked the doc, and it wasn’t really clear at this time. After the handshake, there should be two packets sent by the C&C, used to create a second connection, and then this new connection is used to send commands. But these packets weren’t matching our third packet. We tried different things but anything was working.

And then… the solution came up! There was some ghost bytes in the two first packets… If there was also ghost bytes in this one?
We tried to strip the first byte


w = 'casper'+"\x00"*(32-len('casper'))
data3 = data3[1:len(data3)]
c = camellia.CamelliaCipher(key=w, mode=camellia.MODE_ECB)
de = c.decrypt(data3+"\x00"*(64-len(data3)))

print(de)

We ran it…


thelsd@thelsd-PC ~/ndh $ python camellia_ndh.py
ndh2k16_babafafa58941��4����g9���e1��4����g9���e

WOOOOOOOOT! THE FLAG! IT WAS FINALLY HERE!!! SOOO COOOOOOL!!!

Anyway, as we found this flag way too late (we worked on it every evening, three days after the CTF), we don’t know if we have the full flag, why there is some junk data after the flag, if the last step was bugged or if we missed something, and obvisously, we didn’t had the points, but, the important thing is that we made it !

Flag was (at least we hope so ^^) : ndh2k16_babafafa58941

3 thoughts on “[NDH 2016] [FORENSICS 200 – I’M AFRAID OF A GH0ST NAMED POISON IVY] Write Up”

  1. Ah nice guys!

    i would like to apologize, i have just checked it tonight, and in my client/server implementation i still used my previous send_test function (so not the final one) to make that pcap file (eheh comment). Hence, the double \x78\x9c is indeed not correct, and at the third packet, i wanted to reproduce the structure of payload, but instead of DWORD,i used to put a single byte… Glad you got it with my mistakes (i will get you some beer!)

    just a note: the junk you get at the end is due to the length you use for decrypt, you have a key of 32 bytes, and you make the decrypt on a length of 64 bytes (so on some undetermined data). By the way, the 0x20 acting for payload length in the third packet was here to say it’s 32 bytes long.

    another note: on the stegano chall, the strings command on the image returns “code rate is 0.571”, that would have been the hint for the size of generator matrix used.

    regards,
    Big5

  2. Hello Big5,

    Thanks for your feedback, I waited it for a looooong time 🙂
    Even if there was some bugs, it was a really nice challenge. I loved to bang my head against the wall during hours.
    Concerning the junk data at the end of the decrypted text, the goal for me was to find something (at least the beginning of the flag), so I didn’t racked my brain to strip the junk.
    I hope you’ll do other networking challenges for the next NDH (but without bug please :p)

    Oh, and I’m always ready for beer, just tell me when and where, I’ll be there 🙂

    Enjoy

    The lsd

Leave a Reply

Your email address will not be published. Required fields are marked *