RPG - Part I (IDA writeup - EKO 2019)
October 27, 2019
rpg
is a buggy game where the player can attack to and defend from attacks of monsters.
Let’s see if we can know how it works.
It is a 64 bits ELF binary:
$ file rpg # byexample: +norm-ws
rpg: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux),
statically linked, for GNU/Linux 2.6.32,
BuildID[sha1]=<...>, stripped
We expect some part of libc
in the binary and no symbol at all.
So our first task is to find what pieces of the binary are of the game and which aren’t like stdlib
.
Strings as Starting Points
Let’s see what strings we can find:
$ strings -a -16 rpg | head -14
Monster %s attack to %s with %d damage.
%s has stopped the attack.
%s is dead. GG WP.
Monster %s defends with %d of defense.
%s hit a critical to %s.
0) Create player
1) Update player name
2) Delete player
3) Player attack
4) Player defend
5) Get current time
User created, first delete it.
Enter player name:
User not created.
Running the program we can map those strings to a mental model of what it does:
If the player was not created, the options 1 to 4 print User not created.
; if the player was created selecting 0 again yields a User created, first delete it.
Other strings shown up. Of course, I’m speculating the conversion specifier like %s
and %d
:
%s defends.
%s roar to %s.
Name: %s
Lives: %d
ctrl+F12
opens the Strings window listing all the strings that IDA found. Pressing enter
in one of the strings, IDA shows us the memory where it was found.
Guessing Functions from their Arguments
From there we can go to the locations of the binary that have a reference to each string selecting the label (aMonsterSAttack
) and pressing x
.
The de facto calling convention in Linux 64 bits says that the first six parameters are passed to the callee in registers rdi
, rsi
, rdx
, rcx
, r8
, r9
(xmm0
, xmm1
, xmm2
, xmm3
, xmm4
, xmm5
, xmm6
and xmm7
for floating point arguments).
With this we could assume that the next call after referencing aMonsterSAttack
is a printf
-like function.
This is what we got:
To get the pointer to the players name the code does a second indirection: possibly we are dealing with an attribute of a struct
.
printf
-like function call with four arguments: the format string, the name of the monster, name of the player and the damage.
What about the code that reads the player’s name?
Three calls happen after the print of the message. One of them should be a read
like function:
sub_4117C0
: unlikely, it only receives one parameter (cs:off_6CC840
)sub_400D16
: more likely, it receives a buffer and a size (100h
)sub_425850
: unlikely, it receives the buffer but not the size because the size previously set inesi
is not preserved between calls so it could be garbage. Besides,sub_425850
is not call when the user needs to select an option in the main menu so it is unlikely that it is aread
like.
If you said, ey!, may be there is a buffer overflow there. No. Double click in sbuf
to go to the Stack view and right click on sbuf
and select Convert to Array
: based on IDA analysis there are at least 264 bytes (greater than 100h
o 256).
Ok, if the second call (sub_400D16
) is fgets?
, the third should be a strdup?
call.
Why?
The name is stored in the stack so the only way to make it to survive is copying it to the heap or other global place. strdup
will do the trick.
Keep Guessing
The entire block configures the hypothetical player_struct
setting the name of the player at the offset 8 (like we saw before) and the lives of the player at the offset 0:
mov byte ptr [rax], 10h
mov rax, [rbp+player_struct] ; player's lives (= 16)
No, there are not memory leaks here. The delete player function is a good boy.
If this is correct, mov cs:player_struct, rax
saves the pointer to the struct globally, struct allocated at the begin of the block.
The key note here is that we don’t know. We can just guess.
But guessing is good, and the guess in one side may give use the context to understand other pieces of code.
Magic Numbers
The begin of the game gives us more clues
I bet this is something like srand(time(NULL) >> 8)
.
We found srand
, and we found the global state of the PRNG at 6CC100h
, now we can find who updates that global state: the guy will probably be the rand
function.
Nice Findings
The get time function is quite short: it is just a call to the date
program and no command injection is possible.
However this gives us two pieces of information:
- we know the time of the remote machine: we can break things like
srand(time())
. - we know the position of the
system
function (0x411070
) and acall
to it (0x401258
).
The player attack code has a preamble of several operations but after that, there is a interesting section.
It choose a monster name and if it is NULL
, jumps to a block that copy the player’s name into the heap and stores it in the array that initially has the names of the monsters.
Then it is passed as the first argument of a indirect call. Initially the only function that should be called is monster_attack
but we may point to somewhere.
Interesting things:
monster_attack
does not free its argument so we may have a memory leak which content is controlled by us.monster_attack
does free theplayer_struct
but it doesn’t setis_user_created
to 0 so we may have a double free if we calldelete_player
later.- unfortunately
monster_attack
may return 0 that makesplayer_attack
to callexit()
; only under a specific pathmonster_attack
returns 1.
Something similar happen in monster_defense
and roar
functions with the exception that roar
returns always 0.
Next Steps
We found a memory leak and a double free but we don’t have a real crash. We just reviewed the code.
We need to keep exploring this, here are some ideas for a future post: