Foreword/Disclamer
I didn’t manage to solve this challenge the first time…
After sinking in 30 hours and having enough (I took the completionist route for this one) I decided to look at the solution.
Now several months later, I reapproach this challenge with the intention of writing a full walkthrough, this post is gonna be a long one, so strap in
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!
tl;dr
- Analyse the
.pcap
file and the functions - Figure out the relationship between the program checks and traffic
(I got severely stuck here) - Replay the right actions (as the flavortext suggests)
- Get the flag
FULL Walkthrough
We are given three files to work with (excluding the message):
Lets launch the ChessUI
and see what happens:
That’s weird, I don’t think the game should act this way…
Thankfully there is a capture.pcap
which shows a previous game session, lets open it up in Wireshark
now:
Some key things to take away from this pcap file:
- It is played with DNS requests and DNS responses, hence the name
dnschess
- None of the requests/responses go outside of the local/internal network
- The Source/Destination of packets are within the
192.168.122/24
subnet - While the wonky subdomains all resolve to within the
127.0.0.0/8
subnet
- The Source/Destination of packets are within the
- Each movement set ie. Rook to C3-C6 will resolve to a different internal IP
- The Nameserver used points to localhost! This further strengthes the theory that everything is run internally
We also have a quick peek at the ChessUI
disassembly:
The first thing that we notice is that because the ELF is stripped, 99% of the original function names are not present. A new aspect of the challenge is to go through each function and make assumptions about its role in the program and name it accordingly…
For clarity I will go into detail with ChessAI.so
, ChessUI
is full of functions which are not critical to the challenge and mostly handles UI etc…
We see that some symbols are already given, usually this means that those functions are ready for export and used by our main program ChessUI — |
getAiName
and getAiGreeting
are fairly straightforward functions which pretty much just return a string from memory.
Lets inspect each of the unknown functions above…
As a preface, constructors and destructors specified in ELF headers are used as part of the initialisation and termination on runtime for programs as well as on dynamic linking for shared objects.
start
By using the xrefs (cross-referrence) function, we can see below that start
is the entry point into the program; It is also called by sub_1100
:
sub_1100
This sub_1100
is actually called as part of the _fini_array
of functions during destruction — as shown under ELF Termination Function Table :
sub_10C0 and sub_1140
sub_10C0
is called by sub_1140
, which in turn is called as a part of the ELF Initalization Function Table:
sub_1020
One thing to note is that sub_1020
is a PLT jump function, I had no idea until @sgtSkid
told me this…
When we view where it is referrenced from, it is easy to see that in the .plt
section it sets the index into the GOT and makes a jump there to referrence whatever is needed.
Side Note:
cs:qword_4010
means a quad-word (8-byte) value in a code segment register at that address.
Here we can see that the GOT jumps align nicely with the offsets in the previous screenshot:
sub_1145
As shown below this function is called by getNextMove
twice.
We can see in the comment that a "-"
is appended to a dest
string.
(This screenshot will make more sense coming back from reading below…)
In getNextMove
there are two calls to sub_1145
and also the resulting string has .game-of-thrones.flare-on.com
appended at the end.
After we have examined getNextMove
, we can reliably infer that the sub_1145
function does the following: (see blue text in screenshot)
- takes a source string
- appends a dash to it
- appends a alphabet [a-h] to it
- appends a number [1-8] to it
As the function is called twice, the code block constructs the final string like so:
- rook
- rook-c3
- rook-c3-c6
- rook-c3-c6.game-of-thrones.flare-on.com
This is part of creating a serialised string of the chess piece being placed in a new position, which is made to be comformant to subdomain convention!
getNextMove
What is really interesting is the getNextMove
function, which is essential to the operation of the ChessUI
program! This function is rather big so I will break it up visually into three sections:
Section 1
Side Note:
fs:28h
is usually a stack cookie, we can see that at the beginning it is stored invar_8
and at the end it is xored with itself, this is a anti stack-smashing technique where a mismatch will result in a jmp to the code block that calls__stack_chk_fail
The middle bit of this section was previously explored as the string construction code which translates player moves into a hostname format. I won’t go into any further detail there
What is interesting is at the front of the function right after the epilogue, this section of code is where arguments are passed in:
From previous posts we might recognise this as standard Linux fastcall convention, where args are passed in the order of edi,esi,edx,ecx,r8,r9
Out of curiousity lets debug the application and figure out what types and what values are passed into the getNextMove
function…
As we can see in the teal section, this code path is the exact same behaviour as seen in the gif above. The red section is the part we are interested in, as these values ultumately end up as args for getNextMove
…
We know from getNextMove
that rsi has to contain a string that correlates to the chessPiece type. However we can see [rdi+8]
.
This hints that rdi
is most likely a pointer to a struct of some sort.
During the debugging process, I moved a chess piece from D2 to D3, then inspected the struct in heap memory:
As we can see the index changed from 25 to 26. After a bit of guesswork, we can now see how the chessboard is laid out in memory. (Yellow = Unknown)
With this knowledge, we now know how the positions are calculated back in the sub_1145
/append_pos
function!
As of now the purpose and identity of r8
and edi
is unknown…
In getNextMove
a global variable dword_D2120
is written to edi
.
My current guess is that dword_D120
/(current_round)
is a round counter.
- When the game first starts
(startGame)
, the variable is initialised to 0 - After each move is registered this variable is incremented by 1
- This variable is also read and used for some comparison in
getNextMove()
The next mystery is r8
, it stores rsp
, which is the stack frame pointer
Tracing through the functions shows that it is passed in to getNextMove
:
For now we will treat this value as an array
, and we explore it further in Section 3…
[Extra] Discovering more…
We look at how the registerMove
function is being called:
We see that it is being loaded into rsi
as an argument for _g_timeout_add
. As @sgtSkid
points out, this is usually a callback function. Looking at the documentation shows that it takes three arguments, not only that… this function will call the given function every interval until it returns False.
Here is a peek at what the code block looks like after I reversed it:
Through tracing upwards, we can discover that:
-
movePiece
struct is of size 264 -
[rbp+0]
=movePiece.old_index
-
[rbp+4]
=movePiece.new_index
-
dword_D110
/gameSwitchValue
stores value for a switch table for updating the game’s responses
-
_g_type_cast_ctype_ptr
is actually a savedgameState
struct with lots of info.- This is evident when the current function is called with two arguments:
- The “fresh” gameState returned from a function call
- A “stored” state assumed to be from the previous round
- This is evident when the current function is called with two arguments:
Lets look closer at the functions strcpy_chessPiece
and load_chess_images
When we dynamically reverse the strcpy_chessPiece
function, we see that the following [rdi+92]
values make no sense when looked at statically.
We then set a breakpoint and look at how the values change across the selection of every single tile:
It turns out that this value is a sort of chessPieceID
, that can be used to infer three things:
- Individual pieces of identical types from each other (ie. pawn1 and pawn2)
- Whether the piece is black or white
- What type the piece is
We can prove this by insepcting the not so aptly named strcpy_chessPiece
and load_chess_images
functions —
As an example we take the ID of the Rook at H1, it has uniqueID 0x17
= 23
:
Section 2
I got stuck for a fair a bit here but got help from @sgtSkid
again who pointed out — What does gethostbyname()
return?
By checking out the man pages, we see that a hostent
is returned,
or NULL if an error occurs…
Therefore we know that var_60
is a pointer to NULL or new hostent
struct:
Combining this info with the wireshark analysis earlier, the way we can interact with the program is to have the hostnames resolve to their relevant IP addresses, because the AI will isstantly resign if the resolution fails!
The simple way to do this is by editing the local /etc/hosts
file like so:
127.150.96.223 rook-c3-c6.game-of-thrones.flare-on.com
127.252.212.90 knight-g1-f3.game-of-thrones.flare-on.com
127.215.177.38 pawn-c2-c4.game-of-thrones.flare-on.com
127.118.118.207 knight-c7-d5.game-of-thrones.flare-on.com
127.89.38.84 bishop-f1-e2.game-of-thrones.flare-on.com
127.109.155.97 rook-a1-g1.game-of-thrones.flare-on.com
127.217.37.102 bishop-c1-f4.game-of-thrones.flare-on.com
127.49.59.14 bishop-c6-a8.game-of-thrones.flare-on.com
127.182.147.24 pawn-e2-e4.game-of-thrones.flare-on.com
127.0.143.11 king-g1-h1.game-of-thrones.flare-on.com
127.227.42.139 knight-g1-h3.game-of-thrones.flare-on.com
127.101.64.243 king-e5-f5.game-of-thrones.flare-on.com
127.201.85.103 queen-d1-f3.game-of-thrones.flare-on.com
127.200.76.108 pawn-e5-e6.game-of-thrones.flare-on.com
127.50.67.23 king-c4-b3.game-of-thrones.flare-on.com
127.157.96.119 king-c1-b1.game-of-thrones.flare-on.com
127.99.253.122 queen-d1-h5.game-of-thrones.flare-on.com
127.25.74.92 bishop-f3-c6.game-of-thrones.flare-on.com
127.168.171.31 knight-d2-c4.game-of-thrones.flare-on.com
127.148.37.223 pawn-c6-c7.game-of-thrones.flare-on.com
127.108.24.10 bishop-f4-g3.game-of-thrones.flare-on.com
127.37.251.13 rook-d3-e3.game-of-thrones.flare-on.com
127.34.217.88 pawn-e4-e5.game-of-thrones.flare-on.com
127.57.238.51 queen-a8-g2.game-of-thrones.flare-on.com
127.196.103.147 queen-a3-b4.game-of-thrones.flare-on.com
127.141.14.174 queen-h5-f7.game-of-thrones.flare-on.com
127.238.7.163 pawn-h4-h5.game-of-thrones.flare-on.com
127.230.231.104 bishop-e2-f3.game-of-thrones.flare-on.com
127.55.220.79 pawn-g2-g3.game-of-thrones.flare-on.com
127.184.171.45 knight-h8-g6.game-of-thrones.flare-on.com
127.196.146.199 bishop-b3-f7.game-of-thrones.flare-on.com
127.191.78.251 queen-d1-d6.game-of-thrones.flare-on.com
127.159.162.42 knight-b1-c3.game-of-thrones.flare-on.com
127.184.48.79 bishop-f1-d3.game-of-thrones.flare-on.com
127.127.29.123 rook-b4-h4.game-of-thrones.flare-on.com
127.191.34.35 bishop-c1-a3.game-of-thrones.flare-on.com
127.5.22.189 bishop-e8-b5.game-of-thrones.flare-on.com
127.233.141.55 rook-f2-f3.game-of-thrones.flare-on.com
127.55.250.81 pawn-a2-a4.game-of-thrones.flare-on.com
127.53.176.56 pawn-d2-d4.game-of-thrones.flare-on.com
Side note: The program includes certain optimization features that squishes code together I suspect. IDA makes the wrong assumption about the flow, which threw me off the first couple of times.
Brute force
Before thinking of the clean solution, it is actually possible to filter out all the player moves from the pcap
file and brute-force the ordering through process of elimination. This is time consuming but definitely one way to get the flag without having to understand much of the inner workings of the program.
As an example, I worked my way down the pcap
file and tried all the moves that seemed to be playable right from the beginning of the match:
Eventually near the very end, I found one move which worked!
That was Pawn D2-D4. With this information, we can actually begin to understand using dynamic analysis what constitutes as a correct and wrong move, since we have a control group for both positive and negative outcome.
Now we can start to disect the meat of Section 2,
which is a series of fancy math checks accompanied by early exits:
By setting a breakpoint and using dynamic analysis, we can see what is really being done for the first check:
We inspect the contents at the address 0x5586B2FE36E2
to 0x5586B2FE3710
, we can jump within the Hex view by pressing “G” in IDA and passing in the hex address in memory we want:
After jumping to the address, we see the IP address in string form just above the hostname!
In the checks, the value used is the IP address in network byte order.
The second check tests whether the lowest byte is even or not (using mod 2):
The third check is the most fascincating one imo — It takes the value,
mods it by 16 and checks if it matches the current round number:
This is most likely a system to ensure that certain moves are not performed out of order, as this would screw up the flag decryption process
With this knowledge, we can analyse all of the IP addresses,
eliminate non-suitable ones, and order them correctly!
From the list of mappings above,
we get a subset by eliminating all that have a lowest byte with odd value:
127.252.212.90 knight-g1-f3.game-of-thrones.flare-on.com
127.215.177.38 pawn-c2-c4.game-of-thrones.flare-on.com
127.89.38.84 bishop-f1-e2.game-of-thrones.flare-on.com
127.217.37.102 bishop-c1-f4.game-of-thrones.flare-on.com
127.49.59.14 bishop-c6-a8.game-of-thrones.flare-on.com
127.182.147.24 pawn-e2-e4.game-of-thrones.flare-on.com
127.200.76.108 pawn-e5-e6.game-of-thrones.flare-on.com
127.99.253.122 queen-d1-h5.game-of-thrones.flare-on.com
127.25.74.92 bishop-f3-c6.game-of-thrones.flare-on.com
127.108.24.10 bishop-f4-g3.game-of-thrones.flare-on.com
127.34.217.88 pawn-e4-e5.game-of-thrones.flare-on.com
127.141.14.174 queen-h5-f7.game-of-thrones.flare-on.com
127.230.231.104 bishop-e2-f3.game-of-thrones.flare-on.com
127.159.162.42 knight-b1-c3.game-of-thrones.flare-on.com
127.53.176.56 pawn-d2-d4.game-of-thrones.flare-on.com
After that, we can extract the move order from the third byte:
[4] 127.252.212.90 knight-g1-f3.game-of-thrones.flare-on.com
[1] 127.215.177.38 pawn-c2-c4.game-of-thrones.flare-on.com
[6] 127.89.38.84 bishop-f1-e2.game-of-thrones.flare-on.com
[5] 127.217.37.102 bishop-c1-f4.game-of-thrones.flare-on.com
[11] 127.49.59.14 bishop-c6-a8.game-of-thrones.flare-on.com
[3] 127.182.147.24 pawn-e2-e4.game-of-thrones.flare-on.com
[12] 127.200.76.108 pawn-e5-e6.game-of-thrones.flare-on.com
[13] 127.99.253.122 queen-d1-h5.game-of-thrones.flare-on.com
[10] 127.25.74.92 bishop-f3-c6.game-of-thrones.flare-on.com
[8] 127.108.24.10 bishop-f4-g3.game-of-thrones.flare-on.com
[9] 127.34.217.88 pawn-e4-e5.game-of-thrones.flare-on.com
[14] 127.141.14.174 queen-h5-f7.game-of-thrones.flare-on.com
[7] 127.230.231.104 bishop-e2-f3.game-of-thrones.flare-on.com
[2] 127.159.162.42 knight-b1-c3.game-of-thrones.flare-on.com
[0] 127.53.176.56 pawn-d2-d4.game-of-thrones.flare-on.com
After sorting, we then get the move list!
[0] 127.53.176.56 pawn-d2-d4.game-of-thrones.flare-on.com
[1] 127.215.177.38 pawn-c2-c4.game-of-thrones.flare-on.com
[2] 127.159.162.42 knight-b1-c3.game-of-thrones.flare-on.com
[3] 127.182.147.24 pawn-e2-e4.game-of-thrones.flare-on.com
[4] 127.252.212.90 knight-g1-f3.game-of-thrones.flare-on.com
[5] 127.217.37.102 bishop-c1-f4.game-of-thrones.flare-on.com
[6] 127.89.38.84 bishop-f1-e2.game-of-thrones.flare-on.com
[7] 127.230.231.104 bishop-e2-f3.game-of-thrones.flare-on.com
[8] 127.108.24.10 bishop-f4-g3.game-of-thrones.flare-on.com
[9] 127.34.217.88 pawn-e4-e5.game-of-thrones.flare-on.com
[10] 127.25.74.92 bishop-f3-c6.game-of-thrones.flare-on.com
[11] 127.49.59.14 bishop-c6-a8.game-of-thrones.flare-on.com
[12] 127.200.76.108 pawn-e5-e6.game-of-thrones.flare-on.com
[13] 127.99.253.122 queen-d1-h5.game-of-thrones.flare-on.com
[14] 127.141.14.174 queen-h5-f7.game-of-thrones.flare-on.com
Follow the move order above, defeat the AI, and get the flag
Section 3
This section is more of a bonus, just to inspect how the flag decryption process occurs:
The flag is decrypted two bytes at a time, per round,
with the 2nd byte of the IP address as the key.
This key byte is also the only one that isn’t subjected to any checks above!
The array
variable is passed into the function by referrence, and is populated with values:
- the value of the 3rd byte divided by 16
- the value of the lowest byte in the IP address divided by 2
- the reply given by the AI
This is then used in the make_another_move
code block, which explains the previously unknown stack variables!
What I learned
- Don’t overthink the challenge
- My first instinct was that I needed to beat a computer AI at chess
- Second guess was somehow I would need to learn all the game mechanics and cheat to win, ie. TP hacks etc
- In the end its actually super easy. Just replay everythhing in the pcap file to win
-
dlsym
means that function importing/exporting is happening:
The function calls are then saved to global variables and can be seen referrenced throughout the program
-
Never underestimate dynamic analysis
-
G
for address jumping in IDA -
Network byte ordering from int32
-
IDA will actually tell you when fastcall is being used, as well as function args:
- Completionist approach isn’t the best for solving the challenge, but allows you to learn a whole lot!