Instrumenting a Chess game with FЯIDA

Instrumenting dreamchess for Linux to play against StockFish, the famous chess engine.

Table of contents

In this entry the open-source DreamChess [LICENSE] game will be reversed and instrumented with Frida. The goal is to simulate a human playing against the game CPU by intercepting the binary’s function calls and leveraging existing byte code to input moves. The human agent is simulated through a well known open source chess engine called StockFish. For this exercise, the binary was reversed as if there is no knowledge of the source code.

Requirements

Tools were installed in a Ubuntu machine version 5.4.0-37-generic.

NameDescriptionInstall
FridaBinary instrumentation tool.pip3 install frida-tools
Radare2Reverse engineering framework and command-line toolsClone & build. Instructions
DreamchessChess game to be instrumented.sudo apt-get install dreamchess
StockFishChess engine to simulate the human playing the game.sudo apt-get install stockfish
StockFish PyPIPython wrapper for StockFish.pip install stockfish

Static binary analysis

The first step is to understand the characteristics of our target binary (dreamchess). To analyze, radare2 will be used. The binary was loaded, and the basic info was dumped:

[0x0000be00]> iI
arch     x86
baddr    0x0
binsz    210516
bintype  elf
bits     64
canary   true
class    ELF64
crypto   false
endian   little
havecode true
intrp    /lib64/ld-linux-x86-64.so.2
laddr    0x0
lang     c
linenum  false
lsyms    false
machine  AMD x86-64 architecture
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      true
relocs   false
relro    full
rpath    NONE
sanitiz  false
static   false
stripped true
subsys   linux
va       true

During this analysis, the important aspects to note are the architecture and if the binary has been stripped. Architecture can give us an idea of the ABI which determines the calling convention, and if the binary has been stripped it means that there is no debug information that can aid during debugging and reversing. Unfortunately in this case, as most production binaries, debug symbols have been stripped.

NOTE: A description of all the attributes output by iI can be found here.

The next step in the analysis process is to visualize the functions the binary possess. This will help identify calls that can be intercepted to know the state of the board, and that can be used to introduce input to the program. To do this afl was used in radare2.

0x0000be00]> afl
0x0000be00    1 46           entry0
0x000242c0   27 335  -> 318  sym.move_selector
0x0001a2e0    1 120          sym.gg_scrollbarv_create
0x00027440    1 9            sym.pipe_unix_exit
0x000277c0    3 60   -> 53   sym.msgbuf_exit
0x0000ab90    1 11           sym.imp.free
0x0001ae90    1 14           sym.gg_system_draw_char
0x00023890    1 183          sym.start_piece_move
0x0000b380    1 11           sym.imp.SDL_GetTicks
...
0x0001a610    1 94           sym.gg_seperatorh_create
0x00025ab0    1 11           sym.get_black_in_check
0x0001b460   21 839  -> 815  sym.gg_vbox_input
0x0001a8c0    1 16           sym.gg_signal_exit
...
0x00020f40    4 145  -> 140  fcn.00020f40
0x00020fe0    4 107  -> 102  fcn.00020fe0
0x00021050    4 85   -> 80   fcn.00021050
0x00021590    4 75           fcn.00021590
0x00022910    1 89           fcn.00022910
0x00023020    6 259  -> 254  fcn.00023020
0x00022970   64 1359 -> 1310 fcn.00022970

Interestingly, even though the binary was stripped there are plenty of functions that map into named symbols. This is because, even though symbols were stripped, these functions are being exported by the binary resulting in this information being exposed.

As observed with radare2 previously, and with objdump. There are no debug symbols:

$ objdump --syms /usr/games/dreamchess 

/usr/games/dreamchess:     file format elf64-x86-64

SYMBOL TABLE:
no symbols

Although, if the exports are listed, it is possible to observe the functions detected by radare2:

$ objdump -TC /usr/games/dreamchess 

/usr/games/dreamchess:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000      DF *UND*    0000000000000000  GLIBC_2.2.5 wait
0000000000000000      DF *UND*    0000000000000000  GLIBC_2.2.5 strdup
0000000000000000      DF *UND*    0000000000000000              XML_SetUserData
0000000000000000      DF *UND*    0000000000000000              glClearDepth
0000000000000000      DF *UND*    0000000000000000  GLIBC_2.2.5 select
0000000000000000      DF *UND*    0000000000000000              glTexCoord2fv
0000000000000000      DF *UND*    0000000000000000  GLIBC_2.14  memcpy
0000000000000000      DF *UND*    0000000000000000              XML_ParserFree
0000000000000000      DF *UND*    0000000000000000  GLIBC_2.7   __isoc99_sscanf
0000000000000000      DF *UND*    0000000000000000              SDL_SetWindowFullscreen
0000000000000000      DF *UND*    0000000000000000              glScalef
0000000000000000      DF *UND*    0000000000000000              glMatrixMode
0000000000000000      DF *UND*    0000000000000000  GLIBC_2.2.5 realloc
0000000000000000      DF *UND*    0000000000000000              glHint
...

This is not necessarily a programmer mistake, since there are cases where it is desired to export the functions so other programs can link against and reuse the functionality. But usually executables hide their exports by compiling with -fvisibility=hidden. More information on this can be found here.

This will ease when looking for functions of interest. As an initial naive approach, the export list will be filtered to find functions that contain keywords such as board, move, and game. Since these can give us hints on how a game is created, the board is configured and how to detect movements of the pieces on the board. Which will help instrumenting the binary later on.

Filter exports with “board”:

[0x0000be00]> iE ~board
163 0x0000b430 GLOBAL FUNC       SDL_GetKeyboardState
192  0x00026590 0x00026590 GLOBAL FUNC   30       get_saved_board
283  0x0000c660 0x0000c660 GLOBAL FUNC   272      board_setup
284  0x00023c00 0x00023c00 GLOBAL FUNC   48       load_board
303  0x000260a0 0x000260a0 GLOBAL FUNC   608      dialog_vkeyboard_create
331  0x00026090 0x00026090 GLOBAL FUNC   11       get_vkeyboard_enabled
399  0x00025a40 0x00025a40 GLOBAL FUNC   12       get_board
625  0x00026070 0x00026070 GLOBAL FUNC   22       toggle_vkeyboard_enabled

Filter exports with “move”:

[0x0000be00]> iE ~move
166  0x000242c0 0x000242c0 GLOBAL FUNC   335      move_selector
170  0x00023890 0x00023890 GLOBAL FUNC   183      start_piece_move
195  0x0000e2b0 0x0000e2b0 GLOBAL FUNC   31       game_get_move_list
212  0x0000c770 0x0000c770 GLOBAL FUNC   217      move_to_fullalg
226  0x0001de50 0x0001de50 GLOBAL FUNC   820      draw_move_lists
254  0x0000de90 0x0000de90 GLOBAL FUNC   54       game_move_now
272  0x0000ded0 0x0000ded0 GLOBAL FUNC   47       game_want_move
314  0x0000dfc0 0x0000dfc0 GLOBAL FUNC   120      game_make_move
321  0x0000cc50 0x0000cc50 GLOBAL FUNC   486      move_set_attr
332  0x0000cfa0 0x0000cfa0 GLOBAL FUNC   584      move_to_san
390  0x0000c850 0x0000c850 GLOBAL FUNC   338      make_move
417  0x00024240 0x00024240 GLOBAL FUNC   123      move_camera
424  0x0000e1c0 0x0000e1c0 GLOBAL FUNC   231      game_make_move_str
435  0x0001d650 0x0001d650 GLOBAL FUNC   1055     get_move
448  ---------- 0x00038bc0 GLOBAL OBJ    8        move
480  ---------- 0x00038ba0 GLOBAL OBJ    28       san_move
486  0x0000ce40 0x0000ce40 GLOBAL FUNC   351      fullalg_to_move
525  0x0000de30 0x0000de30 GLOBAL FUNC   89       game_retract_move
564  0x0000c9b0 0x0000c9b0 GLOBAL FUNC   134      move_is_valid
607  0x0000d1f0 0x0000d1f0 GLOBAL FUNC   98       san_to_move

Filter exports with “game”:

[0x0000be00]> iE ~game
195  0x0000e2b0 0x0000e2b0 GLOBAL FUNC   31       game_get_move_list
249  0x00015110 0x00015110 GLOBAL FUNC   12       get_ingame_style
250  0x00013a50 0x00013a50 GLOBAL FUNC   615      dialog_ingame_create
254  0x0000de90 0x0000de90 GLOBAL FUNC   54       game_move_now
272  0x0000ded0 0x0000ded0 GLOBAL FUNC   47       game_want_move
314  0x0000dfc0 0x0000dfc0 GLOBAL FUNC   120      game_make_move
374  0x0000dd40 0x0000dd40 GLOBAL FUNC   110      game_view_prev
424  0x0000e1c0 0x0000e1c0 GLOBAL FUNC   231      game_make_move_str
458  0x0000dfb0 0x0000dfb0 GLOBAL FUNC   11       game_get_engine_error
469  0x0000dfa0 0x0000dfa0 GLOBAL FUNC   11       game_set_engine_error
493  0x0000dcb0 0x0000dcb0 GLOBAL FUNC   137      game_view_next
523  0x0000e050 0x0000e050 GLOBAL FUNC   360      game_load
525  0x0000de30 0x0000de30 GLOBAL FUNC   89       game_retract_move
552  0x0000ddb0 0x0000ddb0 GLOBAL FUNC   118      game_undo
582  0x0000e040 0x0000e040 GLOBAL FUNC   15       game_quit
606  0x0000df00 0x0000df00 GLOBAL FUNC   150      game_save
617  0x00025a90 0x00025a90 GLOBAL FUNC   11       get_game_stalemate
620  0x000215e0 0x000215e0 GLOBAL FUNC   1264     dialog_title_newgame_create

This approach has been fruitful. As some functions look promising, such as make_move, get_board, game_load, etc.

Also, there are some other functions, not necessarily related to the movement of the pieces or configuration, such as san_to_move that are interesting as well, as they include terms related to chess programming. In this particular one, san probably refers to the standard algebraic notation, which can help translate internal structures to a human-readable form.

Dynamic binary analysis

Since some functions of interest have already been detected, it is possible to move to a dynamic approach. In this phase frida-trace will be used to understand when different functions of interest are being called.

Determining piece movements

In order to instrument the binary to play by itself, knowledge of how the program moves the pieces is required. As these calls need to be intercepted when the CPU issues a move and issued when the external engine that simulates the human responding.

To determine this, the program will be interacted with during a chess game session while frida-trace is running while filtering for function calls with move.

The following command was issued to start tracing:

$ frida-trace  -i *move* dreamchess
...
62 ms  get_move()
63 ms  game_want_move()
69 ms  draw_move_lists()
69 ms     | game_get_move_list()
78 ms  get_move()
78 ms  game_want_move()
82 ms  draw_move_lists()
...

Many function calls are being printed constantly, even without user interaction, probably due to the UI calling these while refreshing the screen. These functions will be excluded from the trace to isolate the ones being called only during user interaction. Resulting in the following command line:

frida-trace -i *move*  -x *get_move -x *game_want_move -x *draw_move_lists -x *game_get_move_list -x *_dbus_list_remove_link dreamchess

When the CPU moves a piece, it generates the following trace:

Started tracing 226 functions. Press Ctrl+C to stop.                    
           /* TID 0x974a */
  8091 ms  san_to_move()
  8091 ms  fullalg_to_move()
  8091 ms     | move_is_valid()
  8091 ms     |    | make_move()
  8091 ms     | move_set_attr()
  8091 ms     |    | make_move()
  8091 ms     |    | move_is_valid()
  8091 ms     |    | move_is_valid()

This was achieved by starting a game where the CPU plays as whites (plays first).

Now the inverse will be done. A new chess game will be started where the user plays the first move.

After the first piece is move, it is possible to see the following trace:

 23874 ms  game_make_move()
 23874 ms     | move_is_valid()
 23874 ms     |    | make_move()
 23874 ms     | move_set_attr()
 23874 ms     |    | make_move()
...

By comparing these two, it is possible to see that san_to_move is called only when the CPU issues a move, and game_make_move when the human moves.

Identifying CPU piece movements

The next step is to identify when a CPU movement is completed. So when instrumenting the binary a callback can be set to respond to the movement issued by the game.

To understand this, a GDB session will be attached to the game process, and as a starting point, a breakpoint will be set in san_to_move as this function is only called during CPU piece movements.

$ ps -au | grep dreamchess
     38730  360  2.4 2136836 196628 pts/3  Rl+  Jun19 7076:14 /usr/games/dreamchess
     49888  0.0  0.5  59264 40352 pts/2    S+   11:12   0:02 r2 /usr/games/dreamchess
     52224  0.0  0.0  17664   736 pts/5    S+   13:45   0:00 grep --color=auto dreamchess
$ gdb -pid 38730
...
b san_to_move

By doing multiple moves, it is possible to appreciate that only when the CPU starts a movement this breakpoint is hit, inline to what is observed with frida-trace.

Although by the end of the execution of this function call, the piece is not yet moved on the board. This can be appreciated by hitting the breakpoint and continuing to the end of the function by issuing a fin gdb command.

Additional to this, it seems that the function is directly called from main, as seen in the image above. In order to understand when a move is finalized, main will be disassembled around san_to_move function call, to understand the next steps the program is taking and analyze them dynamically with GDB to appreciate when a CPU movement is completed.

To ease the disassembly, radare2 was used:

[0x0000b460]> axt sym.san_to_move
main 0xbb92 [CALL] call sym.san_to_move
sym.game_make_move_str 0xe22b [CALL] call sym.san_to_move
[0x0000bb92]> s 0xbb92
[0x0000bb92]> pd 40
│      ╎╎   0x0000bb92      e859160000     call sym.san_to_move
│      ╎╎   0x0000bb97      488b7c2408     mov rdi, qword [dest]
│      ╎╎   0x0000bb9c      4885c0         test rax, rax
│      ╎╎   0x0000bb9f      4989c7         mov r15, rax
│     ┌───< 0x0000bba2      7421           je 0xbbc5
│     │╎╎   ; CODE XREF from main @ 0xbbd3
│    ┌────> 0x0000bba4      31ff           xor edi, edi
│    ╎│╎╎   0x0000bba6      e8f5730000     call sym.audio_play_sound
│    ╎│╎╎   0x0000bbab      4c89ff         mov rdi, r15                ; int64_t arg1
│    ╎│╎╎   0x0000bbae      be01000000     mov esi, 1                  ; int64_t arg2
│    ╎│╎╎   0x0000bbb3      e888200000     call fcn.0000dc40
│    ╎│╎╎   0x0000bbb8      4c89ff         mov rdi, r15                ; void *ptr
│    ╎│╎╎   0x0000bbbb      e8d0efffff     call sym.imp.free           ; void free(void *ptr)
│    ╎│└──< 0x0000bbc0      e90bfdffff     jmp 0xb8d0
│    ╎│ ╎   ; CODE XREF from main @ 0xbba2
│    ╎└───> 0x0000bbc5      4c89f6         mov rsi, r14
│    ╎  ╎   0x0000bbc8      e873120000     call sym.fullalg_to_move
│    ╎  ╎   0x0000bbcd      4989c7         mov r15, rax
│    ╎  ╎   0x0000bbd0      4885c0         test rax, rax
│    └────< 0x0000bbd3      75cf           jne 0xbba4
│       ╎   0x0000bbd5      4c89f1         mov rcx, r14
│       ╎   0x0000bbd8      488d15a1ca01.  lea rdx, str.Failed_to_parse_move_string___s ; 0x28680 ; "Failed to parse move string '%s'"
│       ╎   0x0000bbdf      be38020000     mov esi, 0x238
│       ╎   0x0000bbe4      31c0           xor eax, eax
│       ╎   0x0000bbe6      488d3d2bca01.  lea rdi, str.build_dreamchess_2gtGVK_dreamchess_0.3.0_dreamchess_src_dreamchess.c ; 0x28618 ; "/build/dreamchess-2gtGVK/dreamchess-0.3.0/dreamchess/src/dreamchess.c"
│       ╎   0x0000bbed      e80e190000     call sym.dbg_error
│       └─< 0x0000bbf2      e9d9fcffff     jmp 0xb8d0
...

There are a couple of familiar functions in this disassembly. For example, after calling san_to_move the binary might jump to 0x0000bbc5 where fullalg_to_move is called, similar to what was observed in frida-trace. Then, when successful, it jumps back and executes from instruction 0x0000bba4 and calls into audio_play_sound and fcn.0000dc40.

As an initial approach gdb will be used to single-step through main after the execution of san_to_move.

Thread 1 "dreamchess" hit Breakpoint 1, 0x000055852612c1f0 in san_to_move ()
(gdb) fin
Run till exit from #0  0x000055852612c1f0 in san_to_move ()
0x000055852612ab97 in main ()
(gdb) si
0x000055852612ab9c in main ()
(gdb) si
0x000055852612ab9f in main ()
(gdb) si
0x000055852612aba2 in main ()
(gdb) si
0x000055852612abc5 in main ()
(gdb) si
0x000055852612abc8 in main ()
(gdb) si
0x000055852612be40 in fullalg_to_move ()
(gdb) fin
Run till exit from #0  0x000055852612be40 in fullalg_to_move ()
0x000055852612abcd in main ()
(gdb) si
0x000055852612abd0 in main ()
(gdb) si
0x000055852612abd3 in main ()
(gdb) si
0x000055852612aba4 in main ()
(gdb) si
0x000055852612aba6 in main ()
(gdb) si
0x0000558526131fa0 in audio_play_sound ()
(gdb) fin
Run till exit from #0  0x0000558526131fa0 in audio_play_sound ()
0x000055852612abab in main ()
(gdb) si
0x000055852612abae in main ()
(gdb) si
0x000055852612abb3 in main ()
(gdb) si
0x000055852612cc40 in ?? ()
(gdb) fin
Run till exit from #0  0x000055852612cc40 in ?? ()
0x000055852612abb8 in main ()

At the end of the function 0x000055852612cc40, the piece was finally moved. This function corresponds to fcn.0000dc40 mentioned above. This can be proved by subtracting the offset of the module to the function address:

(gdb) info proc map 
process 38730
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
      0x55852611f000     0x558526129000     0xa000        0x0 /usr/games/dreamchess
...
(gdb) p/x 0x000055852612cc40 - 0x55852611f000
$1 = 0xdc40

As seen above the offset matches to what radare2 indicated as function name.

As the next step, this call will be monitored with frida-trace, to understand when it is called.

frida-trace -a dreamchess\!0xdc40 dreamchess

By interacting with the system. It seems that this function is called any time a piece is moved from either side (human, CPU).

This function can be intercepted to know when a move is completed. Afterward, based on the ordering of the moves, it is possible to determine if the move was done by the CPU or the human.

Initiating moves

The next step is to find a function that can be instrumented to send moves to the program. This to simulate the human playing against the machine.

From the initial frida-trace, it was determined that every time a user moves a piece the function game_make_move was called. Based on the name it seems to be a way of issuing piece movements.

By using gdb it is possible to call functions defined in the binary, although these might require arguments. To find if they do, and what they might be, radare2 can be used to both disassemble the function and find cross-references that indicate the usage.

As an initial step, radare2 will be used to dissasemble the function:

[0x000247c0]> s sym.game_make_move
[0x0000dfc0]> pdf
       ╎╎   ; CALL XREF from sym.game_make_move_str @ 0xe23e
       ╎╎   ; CALL XREF from fcn.00024660 @ 0x24840
┌ 115: sym.game_make_move (int64_t arg1);
│      ╎╎   ; arg int64_t arg1 @ rdi
│      ╎╎   0x0000dfc0      f30f1efa       endbr64
│      ╎╎   0x0000dfc4      55             push rbp
│      ╎╎   0x0000dfc5      4889fd         mov rbp, rdi                ; arg1
│      ╎╎   0x0000dfc8      e873fcffff     call fcn.0000dc40
│      ╎╎   0x0000dfcd      85c0           test eax, eax
│     ┌───< 0x0000dfcf      7427           je 0xdff8
│     │╎╎   0x0000dfd1      488b05486602.  mov rax, qword [0x00034620] ; [0x34620:8]=0
│     │╎╎   0x0000dfd8      486315496602.  movsxd rdx, dword [0x00034628] ; [0x34628:4]=0
│     │╎╎   0x0000dfdf      488d3df4d601.  lea rdi, [0x0002b6da]       ; "%s\n"
│     │╎╎   0x0000dfe6      5d             pop rbp
│     │╎╎   0x0000dfe7      488b74d0f8     mov rsi, qword [rax + rdx*8 - 8]
│     │╎╎   0x0000dfec      31c0           xor eax, eax
│     │└──< 0x0000dfee      e9edf2ffff     jmp sym.comm_send
..
│     │ ╎   ; CODE XREF from sym.game_make_move @ 0xdfcf
│     └───> 0x0000dff8      488b05196602.  mov rax, qword [0x00034618] ; [0x34618:8]=0
│       ╎   0x0000dfff      4889ee         mov rsi, rbp
│       ╎   0x0000e002      488b4010       mov rax, qword [rax + 0x10]
│       ╎   0x0000e006      488b7810       mov rdi, qword [rax + 0x10]
│       ╎   0x0000e00a      e861e7ffff     call sym.move_to_fullalg
│       ╎   0x0000e00f      488d3d02a601.  lea rdi, str.build_dreamchess_2gtGVK_dreamchess_0.3.0_dreamchess_src_dreamchess.c ; 0x28618 ; "/build/dreamchess-2gtGVK/dreamchess-0.3.0/dreamchess/src/dreamchess.c" ; int64_t arg1
│       ╎   0x0000e016      be08010000     mov esi, 0x108              ; int64_t arg2
│       ╎   0x0000e01b      488d156da401.  lea rdx, str.Ignoring_illegal_move__s ; 0x2848f ; "Ignoring illegal move %s" ; int64_t arg3
│       ╎   0x0000e022      4889c5         mov rbp, rax
│       ╎   0x0000e025      4889c1         mov rcx, rax                ; int64_t arg4
│       ╎   0x0000e028      31c0           xor eax, eax
│       ╎   0x0000e02a      e811f6ffff     call sym.dbg_warn
│       ╎   0x0000e02f      4889ef         mov rdi, rbp
│       ╎   0x0000e032      5d             pop rbp
└       └─< 0x0000e033      e958cbffff     jmp sym.imp.free

Radare2 already shows the signature that it expects the function to havesym.game_make_move (int64_t arg1);). By going through the function, it seems to be calling the function fcn.0000dc40 mentioned in the previous section. This further indicates that game_make_move could be the function used by the user to initiate a move.

Cross references of game_make_move can help understanding the parameter that it receives:

[0x0000dfc0]> axt sym.game_make_move
sym.game_make_move_str 0xe23e [CALL] call sym.game_make_move
fcn.00024660 0x24840 [CALL] call sym.game_make_move

By looking at game_make_move_str dissasemble it is possible to know how game_make_move is called:

[0x0000d1f0]> s sym.game_make_move_str
[0x0000e1c0]> pdf
            ; CALL XREF from sym.yyparse @ 0x10e3e
┌ 228: sym.game_make_move_str (int64_t arg1, int64_t arg2);
│           ; var int64_t var_30h @ rsp+0x138
│           ; arg int64_t arg1 @ rdi
│           ; arg int64_t arg2 @ rsi
│           0x0000e1c0      f30f1efa       endbr64
│           0x0000e1c4      4156           push r14
│           0x0000e1c6      488d1512a301.  lea rdx, str.Parsing_move_string___s ; 0x284df ; "Parsing move string '%s'"
│           0x0000e1cd      b926000000     mov ecx, 0x26               ; '&'
│           0x0000e1d2      4155           push r13
│           0x0000e1d4      4189f5         mov r13d, esi               ; arg2
│           0x0000e1d7      4154           push r12
│           0x0000e1d9      4989fc         mov r12, rdi                ; arg1
│           0x0000e1dc      55             push rbp
│           0x0000e1dd      4881ec480100.  sub rsp, 0x148
│           0x0000e1e4      64488b042528.  mov rax, qword fs:[0x28]
│           0x0000e1ed      488984243801.  mov qword [var_30h], rax
│           0x0000e1f5      31c0           xor eax, eax
│           0x0000e1f7      488b051a6402.  mov rax, qword [0x00034618] ; [0x34618:8]=0
│           0x0000e1fe      4889e7         mov rdi, rsp
│           0x0000e201      4989e6         mov r14, rsp
│           0x0000e204      488b4010       mov rax, qword [rax + 0x10]
│           0x0000e208      488b7010       mov rsi, qword [rax + 0x10]
│           0x0000e20c      31c0           xor eax, eax
│           0x0000e20e      f348a5         rep movsq qword [rdi], qword ptr [rsi]
│           0x0000e211      4c89e1         mov rcx, r12
│           0x0000e214      be39010000     mov esi, 0x139
│           0x0000e219      488d3df8a301.  lea rdi, str.build_dreamchess_2gtGVK_dreamchess_0.3.0_dreamchess_src_dreamchess.c ; 0x28618 ; "/build/dreamchess-2gtGVK/dreamchess-0.3.0/dreamchess/src/dreamchess.c"
│           0x0000e220      e85bf5ffff     call sym.dbg_log
│           0x0000e225      4c89e6         mov rsi, r12                ; int64_t arg2
│           0x0000e228      4c89f7         mov rdi, r14                ; int64_t arg1
│           0x0000e22b      e8c0efffff     call sym.san_to_move
│           0x0000e230      4889c5         mov rbp, rax
│           0x0000e233      4885c0         test rax, rax
│       ┌─< 0x0000e236      7438           je 0xe270
│       │   ; CODE XREF from sym.game_make_move_str @ 0xe281
│      ┌──> 0x0000e238      4889ef         mov rdi, rbp
│      ╎│   0x0000e23b      4489ee         mov esi, r13d
│      ╎│   0x0000e23e      e87dfdffff     call sym.game_make_move
│      ╎│   0x0000e243      4889ef         mov rdi, rbp                ; void *ptr
│      ╎│   0x0000e246      e845c9ffff     call sym.imp.free           ; void free(void *ptr)
│      ╎│   ; CODE XREF from sym.game_make_move_str @ 0xe2a0
│     ┌───> 0x0000e24b      488b84243801.  mov rax, qword [var_30h]
│     ╎╎│   0x0000e253      644833042528.  xor rax, qword fs:[0x28]
│    ┌────< 0x0000e25c      7544           jne 0xe2a2
│    │╎╎│   0x0000e25e      4881c4480100.  add rsp, 0x148
│    │╎╎│   0x0000e265      5d             pop rbp
│    │╎╎│   0x0000e266      415c           pop r12
│    │╎╎│   0x0000e268      415d           pop r13
│    │╎╎│   0x0000e26a      415e           pop r14
│    │╎╎│   0x0000e26c      c3             ret
..
│    │╎╎│   ; CODE XREF from sym.game_make_move_str @ 0xe236
│    │╎╎└─> 0x0000e270      4c89e6         mov rsi, r12                ; char *s
│    │╎╎    0x0000e273      4c89f7         mov rdi, r14                ; int64_t arg1
│    │╎╎    0x0000e276      e8c5ebffff     call sym.fullalg_to_move
│    │╎╎    0x0000e27b      4889c5         mov rbp, rax
│    │╎╎    0x0000e27e      4885c0         test rax, rax
│    │╎└──< 0x0000e281      75b5           jne 0xe238
│    │╎     0x0000e283      4c89e1         mov rcx, r12
│    │╎     0x0000e286      488d15f3a301.  lea rdx, str.Failed_to_parse_move_string___s

By looking at the disassembly, it can be observed that the return value of san_to_move is passed as the first argument of game_make_move if it is not 0. Otherwise, the return value of fullalg_to_move is passed as input, as long it is not zero either. If both functions san_to_move and fullalg_to_move return 0 an error message is printed.

By just analyzing the name of the san_to_move function, it can be inferred that it takes a move in SAN notation, and transforms to an internal representation of a move. By disassembling the function, it is possible to gain a better understanding of what it does:

[0x0000e1c0]> s sym.san_to_move

[0x0000d1f0]> pdf
            ; CALL XREF from main @ 0xbb92
            ; CALL XREF from sym.game_make_move_str @ 0xe22b
┌ 96: sym.san_to_move (int64_t arg1, int64_t arg2);; arg int64_t arg1 @ rdi
│           ; arg int64_t arg2 @ rsi
│           0x0000d1f0      f30f1efa       endbr64
│           0x0000d1f4      4155           push r13
│           0x0000d1f6      4989fd         mov r13, rdi                ; arg1
│           0x0000d1f9      4889f7         mov rdi, rsi                ; int64_t arg1
│           0x0000d1fc      4154           push r12
│           0x0000d1fe      4989f4         mov r12, rsi                ; arg2
│           0x0000d201      55             push rbp
│           0x0000d202      e889a10100     call sym.san_parse
│           0x0000d207      4885c0         test rax, rax
│       ┌─< 0x0000d20a      7424           je 0xd230
│       │   0x0000d20c      4889c5         mov rbp, rax
│       │   0x0000d20f      4c89ef         mov rdi, r13                ; int64_t arg1
│       │   0x0000d212      4889c6         mov rsi, rax                ; int64_t arg2
│       │   0x0000d215      e826f8ffff     call fcn.0000ca40
│       │   0x0000d21a      4889ef         mov rdi, rbp                ; void *ptr
│       │   0x0000d21d      4989c4         mov r12, rax
│       │   0x0000d220      e86bd9ffff     call sym.imp.free           ; void free(void *ptr)
│       │   ; CODE XREF from sym.san_to_move @ 0xd250
│      ┌──> 0x0000d225      4c89e0         mov rax, r12
│      ╎│   0x0000d228      5d             pop rbp
│      ╎│   0x0000d229      415c           pop r12
│      ╎│   0x0000d22b      415d           pop r13
│      ╎│   0x0000d22d      c3             ret
..
│      ╎│   ; CODE XREF from sym.san_to_move @ 0xd20a
│      ╎└─> 0x0000d230      4c89e1         mov rcx, r12
│      ╎    0x0000d233      488d15ceaf01.  lea rdx, str.Failed_to_parse_SAN_move_string___s ; 0x28208 ; "Failed to parse SAN move string '%s'"
│      ╎    0x0000d23a      be88020000     mov esi, 0x288
│      ╎    0x0000d23f      31c0           xor eax, eax
│      ╎    0x0000d241      488d3da0ae01.  lea rdi, str.build_dreamchess_2gtGVK_dreamchess_0.3.0_dreamchess_src_board.c ; 0x280e8 ; "/build/dreamchess-2gtGVK/dreamchess-0.3.0/dreamchess/src/board.c"
│      ╎    0x0000d248      4531e4         xor r12d, r12d
│      ╎    0x0000d24b      e830050000     call sym.dbg_log
└      └──< 0x0000d250      ebd3           jmp 0xd225

The function seems to receive two arguments, possibly of pointer type or int64. This can be further analyzed by setting a breakpoint in gdb and going through the execution.

Thread 1 "dreamchess" hit Breakpoint 1, 0x000055852612c1f0 in san_to_move ()
(gdb) x/10x $rsi
0x5585269396f5:    0x33663167    0x00000000    0x00000000    0x00000000
0x558526939705:    0x31000000    0x00000001    0x10000000    0x85268202
0x558526939715:    0x10000055    0x85262020
(gdb) x/s $rsi
0x5585269396f5:    "g1f3"
(gdb) x/10wx $rdi
0x7ffe4bbaad90:    0x00000000    0x00000006    0x00000002    0x00000004
0x7ffe4bbaada0:    0x00000008    0x0000000a    0x00000004    0x00000002
0x7ffe4bbaadb0:    0x00000006    0x00000000
Run till exit from #0  0x000055852612c1f0 in san_to_move ()
0x000055852612ab97 in main ()
(gdb) x $rax
0x0:    Cannot access memory at address 0x0
(gdb) p $rax
$1 = 0

It seems that the first argument of the function (rdi), is a pointer to the heap that contains some unknown structure. The second argument (rsi) is a pointer to the heap, containing a string that represents the move made by the CPU (“g1f3”). Although it is not in SAN notation, it is in coordinate notation. The return value is 0, maybe denoting failure or an invalid pointer.

In this case, as stated previously the program will continue and call fullalg_to_move, which by paying a closer look at the disassembly of game_make_move_str is taking the same arguments as san_to_move. By setting a breakpoint in this function it is possible to see that this one returns a valid pointer, which is then given to game_make_move:

Thread 1 "dreamchess" hit Breakpoint 2, 0x000055852612be40 in fullalg_to_move ()
(gdb)  x/s $rsi
0x5585269396f5:    "g1f3"
(gdb) x/10x $rdi
0x7ffe4bbaad90:    0x00    0x00    0x00    0x00    0x06    0x00    0x00    0x00
0x7ffe4bbaad98:    0x02    0x00
(gdb) fin
Run till exit from #0  0x000055852612be40 in fullalg_to_move ()
0x000055852612abcd in main ()
(gdb) p $rax
$2 = 94030378063664
(gdb) x/10wx $rax
0x558527485730:    0x00000006    0x00000015    0x0000000c    0x00000000
0x558527485740:    0x00000000    0x00000000    0x00000041    0x00000000
0x558527485750:    0x2746c8b0    0x00005585

By knowing this, it is possible to simulate a call togame_make_move by first using fullalg_to_move to transform a move represented in coordinate notation to a consumable pointer. But before doing this, the first argument of fullalg_to_move needs to be understood.

By making an educated guess, to generate a move the state of the board plus the move representation is needed (as you need to translate the coordinates from the coordinate notation to actual board pieces). Since the move representation is already given as the second parameter, probably the first is a pointer to a struct that represents the state of the board.

From the list of externs obtained in the “Static binary analysis” section, there is a function called get_board, which might be promising to get such struct.

By disassembling this function in radare2, it can be observed that it receives no arguments and returns a pointer.

[0x0000b460]> s sym.get_board
[0x00025a40]> pdf
            ; XREFS: CALL 0x0001da98  CALL 0x0001daa4  CALL 0x0001dab2  CALL 0x0001dac3  CALL 0x0001dad6  CALL 0x0001dc20  
            ; XREFS: CALL 0x0001dc2b  CALL 0x0001dc3a  CALL 0x0001dc4c  CALL 0x0001dc60  CALL 0x0001dec0  CALL 0x0001e00d  
            ; XREFS: CALL 0x0001e0aa  CALL 0x0001e1e1  CALL 0x0001e280  CALL 0x00026796  
┌ 12: sym.get_board ();
│           0x00025a40      f30f1efa       endbr64
│           0x00025a44      488d05751101.  lea rax, [0x00036bc0]
└           0x00025a4b      c3       

Knowing all this, it is possible to attempt to move one of the pieces by doing the following:

  1. Obtain the board pointer by calling get_board.
  2. Allocate memory to store the move representation string.
  3. Set the move to be issued.
  4. Call fullalg_to_move to obtain a pointer to the move to be made.
  5. Call game_make_move to issue the user move.

This was done in gdb to prototype:

call (uint64_t*)get_board()
call (uint64_t*)malloc(25)
set {char[5]}$2 = "e2e4"
call (uint64_t*)fullalg_to_move($1, $2)
call (uint64_t*)game_make_move($3)

The piece was moved by issuing these gdb commands as seen in the image above.

Understanding CPU moves

From section “Identifying CPU piece movements” it was stated that each time a piece is moved in the board the function fcn.0000dc40 is called. By looking at the function call in main, additional information such as the arguments passed to the function can be known:

│ ││╎││╎╎   0x0000bb92      e859160000     call sym.san_to_move
│ ││╎││╎╎   0x0000bb97      488b7c2408     mov rdi, qword [dest]
│ ││╎││╎╎   0x0000bb9c      4885c0         test rax, rax
│ ││╎││╎╎   0x0000bb9f      4989c7         mov r15, rax
│ ────────< 0x0000bba2      7421           je 0xbbc5
│ ││╎││╎╎   ; CODE XREF from main @ 0xbbd3
│ ────────> 0x0000bba4      31ff           xor edi, edi
│ ││╎││╎╎   0x0000bba6      e8f5730000     call sym.audio_play_sound
│ ││╎││╎╎   0x0000bbab      4c89ff         mov rdi, r15                ; int64_t arg1
│ ││╎││╎╎   0x0000bbae      be01000000     mov esi, 1                  ; int64_t arg2
│ ││╎││╎╎   0x0000bbb3      e888200000     call fcn.0000dc40
│ ││╎││╎╎   0x0000bbb8      4c89ff         mov rdi, r15                ; void *ptr
│ ││╎││╎╎   0x0000bbbb      e8d0efffff     call sym.imp.free           ; void free(void *ptr)
│ ────────< 0x0000bbc0      e90bfdffff     jmp 0xb8d0
│ ││╎││╎╎   ; CODE XREF from main @ 0xbba2
│ ────────> 0x0000bbc5      4c89f6         mov rsi, r14
│ ││╎││╎╎   0x0000bbc8      e873120000     call sym.fullalg_to_move
│ ││╎││╎╎   0x0000bbcd      4989c7         mov r15, rax
│ ││╎││╎╎   0x0000bbd0      4885c0         test rax, rax
│ ────────< 0x0000bbd3      75cf           jne 0xbba4

In this case, by looking at the disassembly, it is possible to observe that the return value of san_to_move and fullalg_to_move is passed to fcn.0000dc40 as first argument and 1 as the second argument.

From section “Initiating moves” it was determined that san_to_move and fullalg_to_move return a pointer representing a move that was translated from either san (standard arithmetic notation) or coordinate notation.

From the exports, it is possible to find the inverse of these functions: move_to_san and move_to_fullalg.

move_to_fullalg seems to be a better choice to decypher the internal move representation, as chess engines prefer this form of notation. By using radare2 it is possible to extract the arguments to the function:

[0x0000b460]> axt sym.move_to_fullalg
sym.move_to_san 0xd1b1 [CALL] call sym.move_to_fullalg
fcn.0000dc40 0xda53 [CALL] call sym.move_to_fullalg
sym.game_make_move 0xe00a [CALL] call sym.move_to_fullalg
[0x0000b460]> s sym.game_make_move
[0x0000dfc0]> pdf
       ╎╎   ; CALL XREF from sym.game_make_move_str @ 0xe23e
       ╎╎   ; CALL XREF from fcn.00024660 @ 0x24840
┌ 115: sym.game_make_move (int64_t arg1);
│      ╎╎   ; arg int64_t arg1 @ rdi
│      ╎╎   0x0000dfc0      f30f1efa       endbr64
│      ╎╎   0x0000dfc4      55             push rbp
│      ╎╎   0x0000dfc5      4889fd         mov rbp, rdi                ; arg1
│      ╎╎   0x0000dfc8      e873fcffff     call fcn.0000dc40
│      ╎╎   0x0000dfcd      85c0           test eax, eax
│     ┌───< 0x0000dfcf      7427           je 0xdff8
│     │╎╎   0x0000dfd1      488b05486602.  mov rax, qword [0x00034620] ; [0x34620:8]=0
│     │╎╎   0x0000dfd8      486315496602.  movsxd rdx, dword [0x00034628] ; [0x34628:4]=0
│     │╎╎   0x0000dfdf      488d3df4d601.  lea rdi, [0x0002b6da]       ; "%s\n"
│     │╎╎   0x0000dfe6      5d             pop rbp
│     │╎╎   0x0000dfe7      488b74d0f8     mov rsi, qword [rax + rdx*8 - 8]
│     │╎╎   0x0000dfec      31c0           xor eax, eax
│     │└──< 0x0000dfee      e9edf2ffff     jmp sym.comm_send
│     │ ╎   ; CODE XREF from sym.game_make_move @ 0xdfcf
│     └───> 0x0000dff8      488b05196602.  mov rax, qword [0x00034618] ; [0x34618:8]=0
│       ╎   0x0000dfff      4889ee         mov rsi, rbp
│       ╎   0x0000e002      488b4010       mov rax, qword [rax + 0x10]
│       ╎   0x0000e006      488b7810       mov rdi, qword [rax + 0x10]
│       ╎   0x0000e00a      e861e7ffff     call sym.move_to_fullalg

From this cross reference found in game_make_move, it is possible to see that the second parameter passed to move_to_fullalg is the first argument given to game_make_move, which as seen in the previous section is the internal representation of a move.

Similar to their counterpart, it is probable that move_to_fullalg required the state of the board to do a transformation between move representations. This can be verified by instrumenting the call in gdb:

(gdb) call (uint64_t*)get_board()
$2 = (uint64_t *) 0x56202b11dbc0
(gdb) call (uint64_t*)move_to_fullalg($2, $rdi)
$3 = (uint64_t *) 0x56202c3e3480
(gdb) x/s 0x56202c3e3480
0x56202c3e3480:    "b1c3"

This seems to prove the stated hypothesis.

Determining initial game state

In order to find out if the CPU player is going to make the first move, i.e; if Dreamchess CPU is playing with the white pieces, it is imperative to find which function is in charge of setting up the configurations selected in the game menu, such as the type of players, difficulty, and level:

To achieve this frida-trace was used. frida-trace provides different options to make the tracing more granular, to exclude or include specific functions, for example. As explained before, since most videogames use a graphical interface, it is inherent that many functions related to the GUI (Graphical User Interface) such as the renderization and the mouse listeners are constantly called to refresh the game UI, it is better to exclude those calls from the tracing to facilitate the search of the function that provides the configuration of the current game and to include only the dreamchess module. In addition to facilitate the search of the target function, frida will perform better when using a more granular specification for the tracing.

$ frida-trace -I dreamchess -x "gg_*" -x "*draw*" -x "yy*" -x "*gl*"  -x "*mouse*" -x "*screen*" -x "*text_character*" -x "*ui*" dreamchess 

Even after excluding several functions related to the GUI and user interaction, there are still plenty of functions being traced. To reduce the number of functions to analyze, the tracing was performed twice in two different contexts. The first tracing was performed without initializing the game (before clicking Start Game) and the second one was performed while the game was initialized (before, during, and after clicking Start Game). Both of the results from the tracing were written into different files and they were compared using diff to see what functions are not present before starting the game.

#### Before initializing the game 
$ frida-trace -I dreamchess -x "gg_*" -x "*draw*" -x "yy*" -x "*gl*"  -x "*mouse*" -x "*screen*" -x "*text_character*" dreamchess > before_start.txt
$ cat before_start.txt | grep ms | awk {'print $3'} | sort | uniq > before_start_clean.txt
$
#### After initializing the game 
$ frida-trace -I dreamchess -x "gg_*" -x "*draw*" -x "yy*" -x "*gl*"  -x "*mouse*" -x "*screen*" -x "*text_character*" dreamchess > after_start.txt
$ cat after_start.txt | grep ms | awk {'print $3'} | sort | uniq > after_start_clean.txt
$
### Diffing the files
$ diff before_start_clean.txt after_start_clean.txt

As a result, a reduced set of functions was obtained. This set of functions could be seen as a snapshot of the functions running at the time of initializing a new game:

0a1,2
> |
> audio_play_sound()
2a5,11
> board_setup()
> ch_userdir()
> comm_init()
> comm_poll()
> comm_send()
> config_get_option()
> config_save()
3a13,25
> dbg_log()
> dialog_title_newgame_create()
> find_square()
> fullalg_to_move()
> game_get_engine_error()
> game_get_move_list()
> game_want_move()
> get_backdrop()
> get_black_in_check()
> get_black_in_checkmate()
> get_black_name()
> get_black_piece()
> get_board()
4a27
> get_config()
5a29,41
> get_egg_req()
> get_fading_out()
> get_game_stalemate()
> get_menu_style()
> get_move()
> get_piece_moving_done()
> get_show_egg()
> get_turn_counter()
> get_white_in_check()
> get_white_in_checkmate()
> get_white_name()
> get_white_piece()
> go_3d()
6a43,60
> history_init()
> history_play()
> load_theme()
> make_move()
> move_is_valid()
> move_set_attr()
> move_to_fullalg()
> move_to_san()
> render_scene_3d()
> reset_3d()
> reset_transition()
> resize_window()
> san_to_fan()
> san_to_move()
> set_fade_start()
> set_set_loading()
> start_piece_move()
> text_height()
7a62
> transition_update()

Based on the name of the functions, an educated guess to dismiss some of them was made, for instance, the ones related to the function that reproduces a sound when a new game starts or the function that is constantly looking for a checkmate, and so forth. One of the functions that stand out was dialog_title_newgame_create() essentially because it contained references to strings that are used in the game’s menu just before initializing a game, such as "Players", "Difficulty" and "Level".

[0x00024af0]> afl ~dialog_title_newgame_create
0x00020720    1 1260         sym.dialog_title_newgame_create
[0x00024af0]> pdf @ sym.dialog_title_newgame_create~..
...
0x00020742      488d3da79200.  lea rdi, qword str.Players: ; 0x299f0 ; "Players:"
   0x00020749      4889c3         mov rbx, rax
   0x0002074c      e8cf84ffff     call sym.gg_label_create
   0x00020751      31ff           xor edi, edi
   0x00020753      4989c4         mov r12, rax
   0x00020756      e8d5a6ffff     call sym.gg_vbox_create
   0x0002075b      4889c5         mov rbp, rax
   0x0002075e      e8dd55ffff     call sym.gg_container_get_class_id
   0x00020763      4c8d05f28100.  lea r8, qword str.gg_container_t ; 0x2895c ; "gg_container_t"
   0x0002076a      b988000000     mov ecx, 0x88
   0x0002076f      4889ef         mov rdi, rbp
   0x00020772      488d15179300.  lea rdx, qword str.build_dreamchess_w2udAl_dreamchess_0.3.0_dreamchess_src
   0x00020779      89c6           mov esi, eax
   0x0002077b      e85094ffff     call sym.gg_check_cast
   0x00020780      4c89e6         mov rsi, r12
   0x00020783      4889c7         mov rdi, rax
   0x00020786      e8e556ffff     call sym.gg_container_append
   0x0002078b      488d3d679200.  lea rdi, qword str.Difficulty: ; 0x299f9 ; "Difficulty:"
   0x00020792      e88984ffff     call sym.gg_label_create
   0x00020797      4989c4         mov r12, rax
   0x0002079a      e8a155ffff     call sym.gg_container_get_class_id
   0x0002079f      4c8d05b68100.  lea r8, qword str.gg_container_t ; 0x2895c ; "gg_container_t"
   0x000207a6      b98b000000     mov ecx, 0x8b
   0x000207ab      4889ef         mov rdi, rbp
   0x000207ae      488d15db9200.  lea rdx, qword str.build_dreamchess_w2udAl_dreamchess_0.3.0_dreamchess_src
   0x000207b5      89c6           mov esi, eax
   0x000207b7      e81494ffff     call sym.gg_check_cast
   0x000207bc      4c89e6         mov rsi, r12
   0x000207bf      4889c7         mov rdi, rax
   0x000207c2      e8a956ffff     call sym.gg_container_append
   0x000207c7      488d3d379200.  lea rdi, qword str.Level:   ; 0x29a05 ; "Level:"
   0x000207ce      e84d84ffff     call sym.gg_label

After that function is executed, some functions related to seting up the configuration of the game are executed:

2206 ms  dialog_title_newgame_create()
  2206 ms     | set_pgn_slot()
  2206 ms     | config_get_option()
  2206 ms     |    | option_group_find_option()
  2206 ms     | config_get_option()
  2206 ms     |    | option_group_find_option()
  2206 ms     | config_get_option()
  2206 ms     |    | option_group_find_option()
  2207 ms     | get_menu_style()
  2207 ms  audio_poll()
  2207 ms  get_credits()
  ...
  4320 ms  get_col()
  4321 ms  get_col()
  4321 ms  audio_poll()
  4321 ms  convert_event()
  4321 ms  set_set_loading()
  4321 ms  config_get_option()
  4321 ms     | option_group_find_option()
  4321 ms  config_get_option()
  4321 ms     | option_group_find_option()
  4321 ms  config_get_option()
  4321 ms     | option_group_find_option()
  4321 ms  dbg_log()
  4322 ms  get_egg_req()
  4322 ms  get_config()
  4322 ms  get_config()
  4322 ms  get_config()
  4322 ms  get_config()
  4322 ms  config_save()
  4322 ms     | option_group_save_xml()
  4322 ms     |    | ch_userdir()
  4322 ms  audio_poll()

After those functions are executed, the game starts rendering and setting up the parameters set in the dialog menu using the functions config_get_option() and get_config. The function get_config is self-explanatory, it just reads from an object called config.

[0x0000b3f0]> s sym.get_config
[0x00024af0]> pdf
            ; XREFS: CALL 0x00020530  CALL 0x0002053c  CALL 0x0002058b  CALL 0x00020596  CALL 0x000205a8  
            ; XREFS: CALL 0x000205b3  CALL 0x000205c8  CALL 0x000205d3  CALL 0x00025761  CALL 0x000257a2  
            ; XREFS: CALL 0x000257e4  CALL 0x00025802  
┌ 8: sym.get_config ();
│ bp: 0 (vars 0, args 0)
│ sp: 0 (vars 0, args 0)
│ rg: 0 (vars 0, args 0)
│           0x00024af0      488d05993501.  lea rax, qword obj.config   ; 0x38090
└           0x00024af7      c3             ret
[0x00024af0]> 

By attaching a gdb session to the running process (gdb -p <PID>), setting up a breakpoint in the function get_config ((gdb) b get_config) and then continuing the program until the function is called, it is possible to analyze the contents of the object:

The function get_config is called several times before starting the game, but the important one is the last call since, at that moment, the object contains all the configuration parameters, so the hexdump show below was made after the last time get_config

For the configuration Players: Human vs CPU, Difficulty: Normal, Level: 1

0x55b609af1090 <config>:    0x00000000    0x00000001    0x00000000    0x00000001

For the configuration Players: CPU vs Human, Difficulty: Normal, Level: 3

0x55b609af1090 <config>:    0x00000001    0x00000000    0x00000000    0x00000003

For the configuration Players: Human vs Human, Difficulty: Normal, Level: 5

0x55b609af1090 <config>:    0x00000000    0x00000000    0x00000000    0x00000005

The first two 4-byte words change depending on the Players setting. If the first 4-byte word is 0x0 and the second 4-byte word is 0x1, it means that the Human is going to play with the white pieces. Likewise, If the first 4-byte word is 0x1 and the second 4-byte word is 0x0, it means that the Human is going to play with the black pieces. By understanding, the goal to know if the Dreamchess CPU is playing with the white pieces or with the black pieces, has been accomplished. This is going to be very useful when instrumenting the game later.

Interacting with chess engines

Most of the chess engines implement UCI (Universal Chess Interface). And some engines expose a UCI command-line interface, such as the case of StockFish. Through UCI you can request a chess engine to calculate the best possible moves based on the state of the board.

There are two different ways to represent the state of a board in a UCI compliant chess engine. Either through FEN (Forsyth–Edwards Notation) or a list of coordinate notated moves.

In this case, as the functions that were found can translate internal representations into coordinate notated moves, the second approach will be done.

To exemplify this, a simple set of commands will be sent to StockFish to request for the next move:

uci  
position startpos moves e2e4 d7d6
go ponder
stop

In this case, with the uci command the engine is started. With position startpos e2e4 d7d6, it is indicated that the position to be evaluated is from a start position move the piece in e2 to e4, then move d7 to d6. Then go ponder indicated the engine to analyze the current position and stop to finish calculating and return an answer.

$ stockfish
Stockfish 11 64 by T. Romstad, M. Costalba, J. Kiiski, G. Linscott
uci
id name Stockfish 11 64
id author T. Romstad, M. Costalba, J. Kiiski, G. Linscott

option name Debug Log File type string default 
option name Contempt type spin default 24 min -100 max 100
option name Analysis Contempt type combo default Both var Off var White var Black var Both
option name Threads type spin default 1 min 1 max 512
option name Hash type spin default 16 min 1 max 131072
option name Clear Hash type button
option name Ponder type check default false
option name MultiPV type spin default 1 min 1 max 500
option name Skill Level type spin default 20 min 0 max 20
option name Move Overhead type spin default 30 min 0 max 5000
option name Minimum Thinking Time type spin default 20 min 0 max 5000
option name Slow Mover type spin default 84 min 10 max 1000
option name nodestime type spin default 0 min 0 max 10000
option name UCI_Chess960 type check default false
option name UCI_AnalyseMode type check default false
option name UCI_LimitStrength type check default false
option name UCI_Elo type spin default 1350 min 1350 max 2850
option name SyzygyPath type string default <empty>
option name SyzygyProbeDepth type spin default 1 min 1 max 100
option name Syzygy50MoveRule type check default true
option name SyzygyProbeLimit type spin default 7 min 0 max 7
uciok
position startpos moves e2e4 d7d6
go ponder
...
stop
bestmove d2d4 ponder g8f6

There are many wrappers for integrating StockFish into diverse programming languages. As Frida is usually run through Python scripts, PyPI StockFish was used.

The previous example can be done in a python script as follows:

$ python3
Python 3.8.2 (default, Apr 27 2020, 15:53:34) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 
>>> from stockfish import Stockfish
>>> 
>>> stockfish = Stockfish("/usr/games/stockfish")
>>> stockfish.set_position(['e2e4', 'd7d6'])
>>> print(stockfish.get_best_move_time(1000))
d2d4

Instrumenting with Frida

Frida is a dynamic code instrumentation toolkit that lets you inject snippets of JavaScript into binaries. It allows debugging with full access to memory, it is able to intercept function calls and execute native functions inside the process. Frida injects the javascript engine duktape to accomplish those tasks. More details about Frida’s API can be found on its official website

The next JS was used to make possible the instrumentation of Dreamchess and make it play against a crafted agent, which uses Stockfish to determine the best moves to play.

var next_move = "" // Used to store the asynchronous value returned by the python script
var ptr_mem_fullalg = Memory.alloc(32) // Allocate memory to store the string representation given by the Python script
var reset = 0; // Used to know if a new game has started

// Pointers to the functions that are going to be used
var ptr_move = Process.getModuleByName("dreamchess").base.add(0xdc40); // Function to intercept the movement of a piece
var ptr_game_make_move = Module.findExportByName(null, "game_make_move"); // This function is used by Dreamchess to recieve movements from the user, in this case, the movements are going to be passed from StockFish
var ptr_get_board = Module.findExportByName(null, "get_board"); // It returns the current board.
var ptr_fullalg_to_move = Module.findExportByName(null, "fullalg_to_move"); // Transforms strings in the notation used by StockFish (coordinate notation) into a dreamchess struct 
var ptr_move_to_fullalg = Module.findExportByName(null, "move_to_fullalg"); //T he move made by dreamchess is converted into an coordinate notation to be processed by StockFish (or other engines)
var ptr_history_init = Module.findExportByName(null, "history_init");// It is called every time a new game is initialized
var ptr_get_config = Module.findExportByName(null, "get_config"); // It returns the configuration of the current game
var ptr_game_want_move = Module.findExportByName(null, "game_want_move");// The function is called when the board has been set up and it is ready be played. It is only going to be used when "Human" player is playing with the White pieces

// NativeFunction wrapper of the functions previously defined as function pointers
var fn_game_make_move = new NativeFunction(ptr_game_make_move, 'pointer', ['pointer', 'pointer']); 
var fn_get_board = new NativeFunction(ptr_get_board, 'pointer', []); 
var fn_fullalg_to_move = new NativeFunction(ptr_fullalg_to_move, 'pointer', ['pointer', 'pointer']);
var fn_move_to_fullalg = new NativeFunction(ptr_move_to_fullalg, 'pointer', ['pointer', 'pointer']);
var fn_get_config = new NativeFunction(ptr_get_config, 'pointer', []);

// Intercept history_init to notify the engine that it needs to reset the board, and to indicate a new game has started
Interceptor.attach(ptr_history_init,
{
    onLeave: function (retval) 
    {   
        send("reset");
        reset = 1; // Set the reset flag, as this is a new game
    }
});

// Intercept the first call to game_want_move. Check if user is playing as whites and issue the first move
Interceptor.attach(ptr_game_want_move,
{
    onLeave: function (retval) 
    {   
        // Game is set up
        if (reset)
        {
            
            var config = fn_get_config();//The function that reads the object config
            var whiteCPU = config.readUInt() //Read the first 4 bytes, if it contains a 0x1, then CPU is playing with White pieces
            var blackCPU = config.add(4).readUInt() //Read the next 4 bytes, if it contains a 0x1, then CPU is playing with Black pieces
            console.log("Started game! CPU Whites? " + whiteCPU); //If CPU is playing with white pieces, prints 1 otherwise prints 0

            // If CPU is playing with Blacks, Human starts
            if (blackCPU)
            {
                send(""); // Send an empty move to make StockFish return a move 
                recv(get_move).wait(); //wait until receive a move from stockfish
                
                var board = fn_get_board(); // Since StockFish, playing as Human in this scneario, the board is needed.  
                ptr_mem_fullalg.writeUtf8String(next_move); // The move is written into the memory allocated previously
                var ptr_next_move = fn_fullalg_to_move(board, ptr_mem_fullalg); // fn_fullalg_to_move transforms from coordinate notation into a struct used by Dreamchess
                console.log(ptr_next_move); // Prints the move that is going to be made
                fn_game_make_move(ptr_next_move, board); // Actually makes the move
            }
            // Catch the error in case the game mode was set as Human vs Human
            else if (!whiteCPU && !blackCPU)
            {
                console.log("Human vs Human? ... Nothing to do here");
            }
            reset = 0; // To avoid this function is called again unless a new game starts
        }
    }
});

// Intercept calls when a piece is moved. Since 'user' moves are instrumented by Frida, only CPU calls are intercepted.
Interceptor.attach(ptr_move, 
{
    onEnter: function (args) 
    {
        var board = fn_get_board();
        var ptr_move = fn_move_to_fullalg(board, args[0]); // Transforms from Dreamches notation into coordinate notation
        var current_move = ptr_move.readUtf8String(); // Reads the movement from Dreamchess that has already been transformed into coordinate notation that later is going to be send to StockFish
        send(current_move); // Send the move to StockFish
        this.recv_wait = recv(get_move); // Store the recv object to avoid busy waiting state
    },
    onLeave: function (retval) 
    {
        var board = fn_get_board(); //The board information needs to be requested again since the state of the board has changed since a move has just been made
        this.recv_wait.wait(); //Wait until StockFish responds with a move

        ptr_mem_fullalg.writeUtf8String(next_move); //Write the move given by StockFish into the buffer 
        var ptr_next_move = fn_fullalg_to_move(board, ptr_mem_fullalg); //fn_fullalg_to_move transforms from coordinate notation into a struct for Dreamchess
        fn_game_make_move(ptr_next_move, board); // Makes the move
    }
});

// Callback when python provides a move to be made
function get_move(m)
{
    next_move =  m //The movement is saved
}

Frida also implements a Python API to perform bindings. Since there is a straight forward Stockfish API in Python, as mentioned in the previous section, a python agent was created to load the JS script into the dreamchess process and handle transactions to the external chess engine.

# Imports the libraries, frida and stockfish for Python
import frida
import sys
from stockfish import Stockfish
# Path to engine
stockfish = Stockfish("/usr/games/stockfish")
# Define a list to store the moves from the current game
moves = []

# Read the agent source
with open("chess.js", "r") as f:agent = f.read()
# Create a session frida 
session = frida.attach("dreamchess")
# Create a script and loads it into frida
script = session.create_script(agent)
script.load()

# Receives a function from the JS script
def incoming(message, data):
    print(message['payload'])
    # If it is performing a reset, it will clean the "moves" list
    if message['payload'] == "reset": 
        moves.clear()
    # Otherwise, it is sending a move
    else:
        # Appends the new move to the list
        moves.append(message['payload'])
        # All the moves made so far are going to be sent to StockFish
        stockfish.set_position(moves)
        # Analyzes the move that was played by the oponent during 1 second and returns the best move according to StockFish
        move = stockfish.get_best_move_time(1000)
        # Appends the issued move to the list
        moves.append(move)
        print(move)
        # Sends the move to the JS agent injected in the process
        script.post(move)
        
# Every time Frida sends a message, it shall call the "incoming" function
script.on("message", incoming)

# Block so that the program does not quit.
sys.stdin.read()

Result

Happy hacking :)


© 2020. All rights reserved.

Powered by Hydejack v8.5.1