"Back to 2007" writeup X-MAS CTF 2019

ctfwriteup

1576875004058


Generally, when I see a problem that has 3 solves and is worth 500 points with a day left in the competition, I stay away. This time, however, my teammate hgarrereyn said something that stuck with me: "honestly some of these chals are really easy people just don't try them". It holds a lot of truth, because that same logic would have applied to me. It's a ginormous pcap file that seems really complicated, with little solves. Nonetheless, he inspired me to give it a shot and I'm really glad he did.

Upon opening Wireshark and inspecting the first few packets, you can see some plain text: "TSource Engine Query". After googling, we found this page on Valve server queries. Cool! We get to reverse engineer Valve's server network structures. However, these are just standard queries for finding game servers, so likely the player was viewing a list of servers at this time. Moving forward, at some point we can see a bunch of HTTP requests for sound files, such as "GET /sound/admin_plugin/bestman.wav". If you've ever played a source game like Counter Strike: Source or TF2, you would know that when joining a custom server, you often download a bunch of sound files.

So far we have followed the player through searching for available servers, then joining a server. We also know that the client is playing Team Fortress 2, because you can see the names of common TF2 maps in the query. Now, in the packet capture, we can actually skip about 50% of it. There is a large stretch of TCP packets in the middle, which would never be used for a game server. Moving down to packet 21114, we see another stretch of UDP packets, all between two IP's: 192.168.1.100 and 162.254.196.68. Some basic deduction shows that 192.168.1.100 is the client, and 162.254.196.68 is the server.

The UDP stream between 192.168.1.100 and 162.254.196.68 is huge. 375kB. This is very likely to be the client and the game server communicating with each other. At first glance, I was surprised to see some plain text in the stream.

So of course I tried some really cheesy stuff, like using strings to find "XX--MM...". No luck. At this point, I thought my next step would be to try to find documentation on how the game servers communicate, and filter out packages that have chat in them. I did a lot of research, but found little to no specifications. Then, I had a brilliant idea. Why don't I capture my own packets, go into TF2, join an empty server, and check out how they look?

So I did exactly that. I loaded up TF2 and set it into windowed mode with low resolution so I could have it side by side with Wireshark. At the time, I also had Discord and my internet browser running, so Wireshark was filled with those packets. Once I found a suitable server, I used a filter to only show packets to and from them: udp.port == 27223 . Still, when in the game, I am constantly receiving updates on the game state. I discovered that most basic game state updates were less than 100 bytes, so I added another filter udp.length > 100.

I typed the letter A into the server as much as I could, and I saw that I sent this packet to the server:

This was really interesting. Instead of 414141 (the hex code for the letter 'A'), I sent a bunch of 141414. So, somehow, the bits of the message are not evenly aligned to fit 8 bytes per section. I tried the same with a bunch of B's, and got 242424 instead of 424242.

Based on the wording of the problem, I believe that the client himself is not sending messages, but rather he is receiving them. So more importantly, what do I get back from the server when I type a bunch of A's?

What? A bunch of a0's?

I thought about this for a while and decided to convert the hex to ascii (using Weastie.com's esteemed base converter). a0 a0 a0 a0 corresponds to 10100000 10100000 10100000 10100000 10100000 in binary. Well... the letter A in binary is 01000001. So, it actually looks like things are in order! After manual testing, if I convert a0 a0 a0 a0 to binary, and add seven 0's to the start of the binary code, and then convert it back to ascii, I get A A A A.

So now we know why using strings doesn't work: everything is bitshifted. From here, I had two routes. The first route was to try to understand the header bytes of a chat message, then search for those in the original pcap. Or, the "big brain" stupid yet genius idea I had, was to convert the entire conversation to literal binary (1's and 0's), and search for "XX--MMAA...". This is not something that I am particularly proud of, but it actually worked.

First, extract the conversation with tshark: tshark -r doomsday.pcapng -T fields -e "data" -R "udp.stream eq 53 and ip.src eq 212.83.129.138" -2 > tmp.out. This command filters out the specific udp stream, specifically the data section of each packet, and only what the server sends to the client (we don't care what the client sends). It create's a file named tmp.out that has each packet's data section literally written out in hexadecimal. I wrote a simple Python script to make a new file that literally had the bits written out as 1's and 0's.

f = open('tmp.out').read().split('\n')

out = ""
for line in f:
        out += ''.join(format(ord(x), 'b') for x in line.decode('hex'))
        out += '\n'

open('tmp.bin', 'w').write(out)

Next, I wrote a script so bad that I won't even paste it, but essentially it searches for the binary codes of the letters XX--MMAAA (note that in the challenge description, it says every letter is doubled). This actually didn't find anything, so I thought maybe the message was split up into sections. I tried searching for just MMAASS and I got it! I traced it back to packet 23867, which consisted of this:

Interesting, you can see a lot of double bytes, indicating that this is probably a message! I pasted it into my base converter, added seven 0's to the front, deleted all the unimportant characters, and I got this: YY-MMAASS{{00uu''ss__ff55mm33^^oo11ffhhuu^^tt00oo11ffhhuu______||. That's.. the flag? Why does it start with "YY" and not end in "}}"?

Unfortunately, I spent hours looking elsewhere until I realized something very stupid. A lot of those characters are just... off by one. That's all. Y is one more than X, | is one less than }. At this point, my beloved teammate poortho played a quick guessing game, randomly increasing or decreasing each letter by one until it made sense. At the end of it all, he submitted the flag X-MAS{1t's_g4m3_n1ght_t0n1ght___}.


Some concluding notes:

  • Don't be afraid to try a challenge just because it has few solves
  • Recreating the scenario of a CTF challenge can do wonders
  • Positive encouragement from a teammate can go a long way