Flare-On 2019 - dnschess (Level 4)

13 minute read

To advance my reverse engineering skills I decided to try my hands on the 2019 version of theFlare-On CTF. The challenges seem to get really tough and only have a few finishers. As the CTF is over there are already public solutions, for example by the developers themselve. In this blogpost I want to show my solution for the dnschess level (number 4), that I just finished.

For this level we get the two ELF binaries ChessUI and ChessAI.so, the pcap capture.pcap and following message:

Some suspicious network traffic led us to this unauthorized chess program running on an Ubuntu desktop. This appears to be the work of cyberspace computer hackers. You’ll need to make the right moves to solve this one. Good luck!

Based on the naming of the binaries we can guess that the ChessUI file shows a User Interface that uses functions of the ChessAI.so file. As it is unlikely that we will find any artificial intelligence in a simple CTF challenge, this will most likely implement some kind of game logic. We will execute ChessUI to get a first look at the game:

As soon as we make the first move we get the message DeepFLARE has resigned and our game ends:

We will have to take a deeper look at the binaries to find out what is happening here.

#Reversing the binaries I will use Radare2 with Cutter to disassemble the binaries.

In the ChessUI ELF file we can immediately see alot of GTK-Functions. GTK is a library that can be used for GUIs on Linux Operating Systems. We won’t analyse these functions, as I have no clue how GTK works. Instead we will take a look at the Strings section in Cutter. We find following interesting strings:

We already saw some of these like the Window-Title Chess Blaster 3000. They also support our assumption that ChessAI.so is used inside this binary, as we see the string ./ChessAI.so which refers to the file in the same directory and an error message if it fails to load. We don’t see any ChessAI imports, so the library will be loaded at runtime. Disassembling ChessAI.so with radare2 shows us three functions:

1. getAiGreeting
2. getAiName
3. getNextMove

If we look back at the strings of ChessUI we can see, that all three functions seem to be loaded and therefore could be used in the game. The first two functions are easy to analyze, as they will both just return a string. getAiName returns “DeepFLARE” and getAiGreeting returns “Finally a worthy opponent. Let us begin”.

The function getNextMove is more complex and will probably reveal how the game works. We will first take a look at the graph of this functions:

The graph indicates that nested if-clauses were used after the first bigger code block. This is most likely done to check some results from operations in the first block and returning early, if they don’t match the expected results (like in error handling). If all checks out we once again reach a bigger code block and return after that.

By looking at the first block of code we can see that the third and fourth argument passed to the function are concatenated together and to the fixed string .game-of-thrones.flare-on.com. This new string is then passed to gethostbyname which is the Linux function for making a DNS-Request.

After the DNS-Request, the nested if-clause begins. The first check is to see if the request was succesful and else fail and return 2 (seems to be an error code the ChessUI will work with). In the case of a succesful request there will be three further checks. If one of them fails, the function will return with the value 2. The assembly code for the checks is as following:

Before the next if-clause there is actually some setup, as we move to a 24 byte offset into our response from gethostbyname. The return value will be a hostent structure (source) and at the specified offset we will be at the h_addr variable - the first returned IP-Address of the DNS request. OThe upcoming checks are all done on this value.

The first check on the returned IP is done by comparing the first byte (the first octet of the IP-Adress) with 0x7f. In Cutter we can right click this value and set its base to either binary (to get 0111111b) or to IP Address and get 127.0.0.0. So the first octet of the returned IP-Adress has to be 127.

The next check ANDs the last octet of our IP-Adress (add rax, 3) with 1 and then calls test eax, eax followed by the conditional jump jne. This means that we will take the jump if the last bit of the last octet is 1. As the jump indicates a failure, for the check to pass the last bit of the last octet has to be 0. (I first mixed that up, so be sure to check out exact syntax if needed here)

The last check works on the third octet of the IP-Address (add rax, 2). Not the whole octet is used though. By using movzx eax, al only the last nibble (four bits) of the octet is inside eax. On the nibble a logical AND 16 is executed. The returning value is compared to var_64h which is the first argument passed to the function.

If all checks pass we move to the next bigger code block. In the code block we can see two xor operations on the second octet of our IP-Adress with a fixed value out of the rodata-section. This indicates some kind of decoding or deobfuscation. Instead of analysing this function block any further, we will turn our attention back to executing the game. We can use gdb to debug the running ChessUI binary. We will have to find out why the game fails after our first chess move and also what is the first argument passed to the function getNextMove. Therefore we will set breakpoints at the calls to getNextMove and gethostbyname (to determine the response to our DNS request and see why the checks fail).

In the debug output we see that a DNS request for the domain pawn-e2-e4.game-of-thrones.flare-on.com is made. If we try to do this with dig, we can see that there is no such public record. As we can’t resolve the DNS query our game fails. The argument passed to getNextMove is 0. (TIL: Linux passes arguments in registers and not on the stack) As I kind of forgot about the capture file we were given, I first thought that maybe while the challenge was online the DNS lookups worked or if I had to reconstruct the records myself. After thinking far too much about how to proceed, the capture file came back into my mind.

#Analysing the pcap By opening the pcap file with tcpdump we get following output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
[twenska@theshire Level4]$ tcpdump -r capture.pcap 
reading from file capture.pcap, link-type EN10MB (Ethernet)
00:06:27.480580 IP theshire.56668 > 192.168.122.29.domain: 44029+ [1au] A? rook-c3-c6.game-of-thrones.flare-on.com. (80)
00:06:27.481658 IP 192.168.122.29.domain > theshire.56668: 44029* 1/1/2 A 127.150.96.223 (146)
00:06:28.503371 IP theshire.46055 > 192.168.122.29.domain: 28101+ [1au] A? knight-g1-f3.game-of-thrones.flare-on.com. (82)
00:06:28.504374 IP 192.168.122.29.domain > theshire.46055: 28101* 1/1/2 A 127.252.212.90 (148)
00:06:29.527159 IP theshire.38192 > 192.168.122.29.domain: 41956+ [1au] A? pawn-c2-c4.game-of-thrones.flare-on.com. (80)
00:06:29.529462 IP 192.168.122.29.domain > theshire.38192: 41956* 1/1/2 A 127.215.177.38 (146)
00:06:30.544231 IP theshire.51010 > 192.168.122.29.domain: 50443+ [1au] A? knight-c7-d5.game-of-thrones.flare-on.com. (82)
00:06:30.544622 IP 192.168.122.29.domain > theshire.51010: 50443* 1/1/2 A 127.118.118.207 (148)
00:06:31.555401 IP theshire.40892 > 192.168.122.29.domain: 18583+ [1au] A? bishop-f1-e2.game-of-thrones.flare-on.com. (82)
00:06:31.556119 IP 192.168.122.29.domain > theshire.40892: 18583* 1/1/2 A 127.89.38.84 (148)
00:06:32.575087 IP theshire.45382 > 192.168.122.29.domain: 6273+ [1au] A? rook-a1-g1.game-of-thrones.flare-on.com. (80)
00:06:32.576106 IP 192.168.122.29.domain > theshire.45382: 6273* 1/1/2 A 127.109.155.97 (146)
00:06:33.590079 IP theshire.41867 > 192.168.122.29.domain: 56013+ [1au] A? bishop-c1-f4.game-of-thrones.flare-on.com. (82)
00:06:33.590477 IP 192.168.122.29.domain > theshire.41867: 56013* 1/1/2 A 127.217.37.102 (148)
00:06:34.601187 IP theshire.38792 > 192.168.122.29.domain: 46221+ [1au] A? bishop-c6-a8.game-of-thrones.flare-on.com. (82)
00:06:34.601655 IP 192.168.122.29.domain > theshire.38792: 46221* 1/1/2 A 127.49.59.14 (148)
00:06:35.621196 IP theshire.40594 > 192.168.122.29.domain: 40484+ [1au] A? pawn-e2-e4.game-of-thrones.flare-on.com. (80)
00:06:35.622341 IP 192.168.122.29.domain > theshire.40594: 40484* 1/1/2 A 127.182.147.24 (146)
00:06:36.644215 IP theshire.60795 > 192.168.122.29.domain: 26833+ [1au] A? king-g1-h1.game-of-thrones.flare-on.com. (80)
00:06:36.645268 IP 192.168.122.29.domain > theshire.60795: 26833* 1/1/2 A 127.0.143.11 (146)
00:06:37.660181 IP theshire.49282 > 192.168.122.29.domain: 6489+ [1au] A? knight-g1-h3.game-of-thrones.flare-on.com. (82)
00:06:37.660547 IP 192.168.122.29.domain > theshire.49282: 6489* 1/1/2 A 127.227.42.139 (148)
00:06:38.679294 IP theshire.33900 > 192.168.122.29.domain: 56854+ [1au] A? king-e5-f5.game-of-thrones.flare-on.com. (80)
00:06:38.680414 IP 192.168.122.29.domain > theshire.33900: 56854* 1/1/2 A 127.101.64.243 (146)
00:06:39.702765 IP theshire.40441 > 192.168.122.29.domain: 24001+ [1au] A? queen-d1-f3.game-of-thrones.flare-on.com. (81)
00:06:39.703852 IP 192.168.122.29.domain > theshire.40441: 24001* 1/1/2 A 127.201.85.103 (147)
00:06:40.718199 IP theshire.42236 > 192.168.122.29.domain: 19650+ [1au] A? pawn-e5-e6.game-of-thrones.flare-on.com. (80)
00:06:40.718702 IP 192.168.122.29.domain > theshire.42236: 19650* 1/1/2 A 127.200.76.108 (146)
00:06:41.729267 IP theshire.38456 > 192.168.122.29.domain: 43811+ [1au] A? king-c4-b3.game-of-thrones.flare-on.com. (80)
00:06:41.729769 IP 192.168.122.29.domain > theshire.38456: 43811* 1/1/2 A 127.50.67.23 (146)
00:06:42.749022 IP theshire.52472 > 192.168.122.29.domain: 65075+ [1au] A? king-c1-b1.game-of-thrones.flare-on.com. (80)
00:06:42.750118 IP 192.168.122.29.domain > theshire.52472: 65075* 1/1/2 A 127.157.96.119 (146)
00:06:43.764216 IP theshire.43209 > 192.168.122.29.domain: 51109+ [1au] A? queen-d1-h5.game-of-thrones.flare-on.com. (81)
00:06:43.764675 IP 192.168.122.29.domain > theshire.43209: 51109* 1/1/2 A 127.99.253.122 (147)
00:06:44.775357 IP theshire.57530 > 192.168.122.29.domain: 50373+ [1au] A? bishop-f3-c6.game-of-thrones.flare-on.com. (82)
00:06:44.775800 IP 192.168.122.29.domain > theshire.57530: 50373* 1/1/2 A 127.25.74.92 (148)
00:06:45.794537 IP theshire.55935 > 192.168.122.29.domain: 12744+ [1au] A? knight-d2-c4.game-of-thrones.flare-on.com. (82)
00:06:45.795752 IP 192.168.122.29.domain > theshire.55935: 12744* 1/1/2 A 127.168.171.31 (148)
00:06:46.809538 IP theshire.52575 > 192.168.122.29.domain: 35679+ [1au] A? pawn-c6-c7.game-of-thrones.flare-on.com. (80)
00:06:46.809941 IP 192.168.122.29.domain > theshire.52575: 35679* 1/1/2 A 127.148.37.223 (146)
00:06:47.821285 IP theshire.36338 > 192.168.122.29.domain: 19954+ [1au] A? bishop-f4-g3.game-of-thrones.flare-on.com. (82)
00:06:47.821983 IP 192.168.122.29.domain > theshire.36338: 19954* 1/1/2 A 127.108.24.10 (148)
00:06:48.841464 IP theshire.39637 > 192.168.122.29.domain: 64565+ [1au] A? rook-d3-e3.game-of-thrones.flare-on.com. (80)
00:06:48.842483 IP 192.168.122.29.domain > theshire.39637: 64565* 1/1/2 A 127.37.251.13 (146)
00:06:49.855678 IP theshire.52046 > 192.168.122.29.domain: 15337+ [1au] A? pawn-e4-e5.game-of-thrones.flare-on.com. (80)
00:06:49.856142 IP 192.168.122.29.domain > theshire.52046: 15337* 1/1/2 A 127.34.217.88 (146)
00:06:50.864146 IP theshire.43552 > 192.168.122.29.domain: 40882+ [1au] A? queen-a8-g2.game-of-thrones.flare-on.com. (81)
00:06:50.864704 IP 192.168.122.29.domain > theshire.43552: 40882* 1/1/2 A 127.57.238.51 (147)
00:06:51.873040 IP theshire.59665 > 192.168.122.29.domain: 3488+ [1au] A? queen-a3-b4.game-of-thrones.flare-on.com. (81)
00:06:51.873407 IP 192.168.122.29.domain > theshire.59665: 3488* 1/1/2 A 127.196.103.147 (147)
00:06:52.881901 IP theshire.37173 > 192.168.122.29.domain: 60061+ [1au] A? queen-h5-f7.game-of-thrones.flare-on.com. (81)
00:06:52.882301 IP 192.168.122.29.domain > theshire.37173: 60061* 1/1/2 A 127.141.14.174 (147)
00:06:53.893584 IP theshire.60263 > 192.168.122.29.domain: 27118+ [1au] A? pawn-h4-h5.game-of-thrones.flare-on.com. (80)
00:06:53.894003 IP 192.168.122.29.domain > theshire.60263: 27118* 1/1/2 A 127.238.7.163 (146)
00:06:54.913486 IP theshire.47613 > 192.168.122.29.domain: 31251+ [1au] A? bishop-e2-f3.game-of-thrones.flare-on.com. (82)
00:06:54.914424 IP 192.168.122.29.domain > theshire.47613: 31251* 1/1/2 A 127.230.231.104 (148)
00:06:55.928362 IP theshire.60888 > 192.168.122.29.domain: 24559+ [1au] A? pawn-g2-g3.game-of-thrones.flare-on.com. (80)
00:06:55.928888 IP 192.168.122.29.domain > theshire.60888: 24559* 1/1/2 A 127.55.220.79 (146)
00:06:56.940220 IP theshire.58182 > 192.168.122.29.domain: 53313+ [1au] A? knight-h8-g6.game-of-thrones.flare-on.com. (82)
00:06:56.940650 IP 192.168.122.29.domain > theshire.58182: 53313* 1/1/2 A 127.184.171.45 (148)
00:06:57.951602 IP theshire.43360 > 192.168.122.29.domain: 44738+ [1au] A? bishop-b3-f7.game-of-thrones.flare-on.com. (82)
00:06:57.951943 IP 192.168.122.29.domain > theshire.43360: 44738* 1/1/2 A 127.196.146.199 (148)
00:06:58.963320 IP theshire.52033 > 192.168.122.29.domain: 42247+ [1au] A? queen-d1-d6.game-of-thrones.flare-on.com. (81)
00:06:58.963907 IP 192.168.122.29.domain > theshire.52033: 42247* 1/1/2 A 127.191.78.251 (147)
00:06:59.975164 IP theshire.37276 > 192.168.122.29.domain: 15407+ [1au] A? knight-b1-c3.game-of-thrones.flare-on.com. (82)
00:06:59.975608 IP 192.168.122.29.domain > theshire.37276: 15407* 1/1/2 A 127.159.162.42 (148)
00:07:00.986281 IP theshire.51370 > 192.168.122.29.domain: 12723+ [1au] A? bishop-f1-d3.game-of-thrones.flare-on.com. (82)
00:07:00.986733 IP 192.168.122.29.domain > theshire.51370: 12723* 1/1/2 A 127.184.48.79 (148)
00:07:01.997173 IP theshire.33446 > 192.168.122.29.domain: 28898+ [1au] A? rook-b4-h4.game-of-thrones.flare-on.com. (80)
00:07:01.997722 IP 192.168.122.29.domain > theshire.33446: 28898* 1/1/2 A 127.127.29.123 (146)
00:07:03.009044 IP theshire.49376 > 192.168.122.29.domain: 42761+ [1au] A? bishop-c1-a3.game-of-thrones.flare-on.com. (82)
00:07:03.009586 IP 192.168.122.29.domain > theshire.49376: 42761* 1/1/2 A 127.191.34.35 (148)
00:07:04.020142 IP theshire.37391 > 192.168.122.29.domain: 13100+ [1au] A? bishop-e8-b5.game-of-thrones.flare-on.com. (82)
00:07:04.020640 IP 192.168.122.29.domain > theshire.37391: 13100* 1/1/2 A 127.5.22.189 (148)
00:07:05.031244 IP theshire.41091 > 192.168.122.29.domain: 33344+ [1au] A? rook-f2-f3.game-of-thrones.flare-on.com. (80)
00:07:05.031775 IP 192.168.122.29.domain > theshire.41091: 33344* 1/1/2 A 127.233.141.55 (146)
00:07:06.050850 IP theshire.49855 > 192.168.122.29.domain: 39520+ [1au] A? pawn-a2-a4.game-of-thrones.flare-on.com. (80)
00:07:06.051814 IP 192.168.122.29.domain > theshire.49855: 39520* 1/1/2 A 127.55.250.81 (146)
00:07:07.073839 IP theshire.51265 > 192.168.122.29.domain: 54817+ [1au] A? pawn-d2-d4.game-of-thrones.flare-on.com. (80)
00:07:07.074958 IP 192.168.122.29.domain > theshire.51265: 54817* 1/1/2 A 127.53.176.56 (146)

We see 40 DNS requests and 40 responses to them. The requests look like the one we generated in the debug output and seem to indicate our moves. (figure, current field, new field) The returned IP-Addresses all start with 127, which means all of them will pass atleast one of our checks. But we can already spot some IP-Addresses that would be invalid, e.g. the first one - 127.150.96.223 as response to rook-c3-c6.game-of-thrones.flare-on.com. We can determine this easily by looking at the last octet, 223 in this case. According to our reverse engineering before, the last bit has to be zero and that means that only even numbers can be valid. So all IPs with an even number in the last octet will fail our game.

It seems that “the right moves” (as inidicated in the challenge message) could be in this pcap file, along with some invalid moves. We can write a Python script that corresponds to the checks we found in the game binary and to filter out invalid ones. The following script does the trick(its probably not the easiest or “best” way, but hey its MY way):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import subprocess
import re

regex = re.compile('[0-9a-z\.\-]*\s[\(\)0-9]*$')

result = subprocess.run(['tcpdump -r capture.pcap'], stdout=subprocess.PIPE, shell=True)
ips = result.stdout.decode('utf-8')
ips=ips.split("\n")
list = []

for line in ips:
        ip =  regex.search(line)
        if(ip!=None):
                ip = ip.group()
                ip = ip.split()[0]
                list.append(ip)

counter = 1
while(counter <= len(list)):
        octets = list[counter].split('.')
        if(int(octets[0]) == 127):
                if(int(octets[3])%2 == 0):
                        if(int(octets[2])%16 == iteration):
                                print(list[counter]+'\t'+list[counter-1])
        counter+=2

Running it will return following output:

1
2
3
[twenska@theshire Level4]$ python3 script.py 
reading from file capture.pcap, link-type EN10MB (Ethernet)
127.53.176.56	pawn-d2-d4.game-of-thrones.flare-on.com.

This IP-Address will match all our checks for the first move! An open question stays what we will do after the first move. We can assume that the only dynamic check (comparison with getNextMoves first argument) will change and therefore reveal the next IP-Address and hostname. We add the IP and hostname to our /etc/hosts file and start the game with gdb. We will once again attach to the function getNextMove, then run the program, make our first move and then a random second one:

As we can see the first move works. At the second call to getNextMove the value 1 is passed. So most likely this will be a counter relating to the number of moves! This is the information we needed to modify our script to find all the valid IPs and hostnames from the pcap.

#Putting it all together We have to edit our script, so that the check for the third octet compares to the number of moves. We also have to change our loop a little bit, to start searching from the start for every move. I came up with following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import subprocess
import re

regex = re.compile('[0-9a-z\.\-]*\s[\(\)0-9]*$')

result = subprocess.run(['tcpdump -r capture.pcap'], stdout=subprocess.PIPE, shell=True)
ips = result.stdout.decode('utf-8')
ips=ips.split("\n")
list = []

for line in ips:
        ip =  regex.search(line)
        if(ip!=None):
                ip = ip.group()
                ip = ip.split()[0]
                list.append(ip)

counter = 1
move = 0
while(counter <= len(list)):
        octets = list[counter].split('.')
        if(int(octets[0]) == 127):
                if(int(octets[3])%2 == 0):
                        if(int(octets[2])%16 == move):
                                print(list[counter]+'\t'+list[counter-1])
                                move+=1
                                counter = -1
        counter+=2

We have to add the IPs and corresponding hostnames to our /etc/hosts file and then play the game with the moves in the order of the script output. This will result in us checkmating the computer enemy and also getting the flag:

The flag seems to have been obfuscated in the rodata-section of the ChessAI.so file and gets decoded in the code section we identified beforehand. The decoding seems to be based on the second octet of the IP-Address, so theoretically we could have reversed this completely statically.