HTB: FlagCasino
Fla-zino?
Table of Contents
Introduction
Although this is not the first reveng
challenge I have solved in the
last month, I noticed many improvements while solving it. It always
feels good when you make progress.
I work with radare2
, as it was suggested to me as a very good way
for a newbie to learn: it may have a steep learning curve, but it
allows you to see things in greater depth.
, , (\____/) (_oo_) (O) __||__ \) []/______\[] / / \______/ \/ / /__\ (\ /____\ --------------------- [ ** WELCOME TO ROBO CASINO **] [*** PLEASE PLACE YOUR BETS ***] [ * CORRECT *] [ * INCORRECT * ] [ *** ACTIVATING SECURITY SYSTEM - PLEASE VACATE *** ] [ ** HOUSE BALANCE $0 - PLEASE COME BACK LATER ** ]
Starting
Through a first look at the disassembled code through graph visual mode
VV @ sym.main
, we see that only at the very surface is this a program imitating a casino:
- In the first part, it produces some output for the end user.
- Then we have a while loop that exits if the house balance is 0.
- If the house balance is not 0: We are then tasked
with providing a bet, and the return code of
scanf
is tested to see if we can proceed. - If we have properly passed input: The input now gets checked:
srand
(with a specific seed?)rand
- Some comparison takes place here, given which we:
- Get correct
- Get incorrect -> exits
If we get correct, we just go back to step 2 again, but it is strange since the flag is supposed to be given somewhere here.
- It could very well have something to do with
var_4h
.
Now, could it be that the proper input sequence is the flag? -> Yep, this is the case, tested with HTB{.
- It could very well have something to do with
Understanding the loop
- First, we get the last byte of
[var_5h]
, move it with sign extension toeax
, and then toedi
, to be used as the seed insrand
. After that, a random value is generated throughrand
. We need to validate that[var_5h]
remains the same in each iteration, because that would in turn mean thatrand
ALWAYS returns the same value. - After getting the rand value, we also get the
[var_4h]
variable intordx
(two-step process). - Then, we get the address of
obj.check
and compare the value of:rcx
(which is[signextended(dword [var_4h])*4]
).edx
, which isdword [rcx + obj.check]
.
Compare these two together: If not equal, exit.
0x5b39eabbe1f5 movzx eax, byte [var_5h] 0x5b39eabbe1f9 movsx eax, al 0x5b39eabbe1fc mov edi, eax 0x5b39eabbe1fe call sym.imp.srand ;[5] ; void srand(int seed) 0x5b39eabbe203 call sym.imp.rand ;[6] ; int rand(void) 0x5b39eabbe208 mov edx, dword [var_4h] 0x5b39eabbe20b movsxd rdx, edx 0x5b39eabbe20e lea rcx, [rdx*4] 0x5b39eabbe216 lea rdx, obj.check ; 0x5b39eabc1080 0x5b39eabbe21d mov edx, dword [rcx + rdx] 0x5b39eabbe220 cmp eax, edx 0x5b39eabbe222 jne 0x5b39eabbe232
Nice, so we now need to understand:
- Where
rand()
output is used: Its output is saved ateax
. - What exactly is
var_4h
: Could be our index, and it makes sense, but at the same point, it does not? A byte increase makes sense for the*4
part, but then why not just have it+4
? - What exactly is
obj.check
:obj.check
is an array or data structure?
- Where
Asm notes
movzx
move with zero extend.movsx
move with sign extend.movsxd
move with sign extend to a 64-bit register.
Obj.check hexdump
[0x71025cdd1540]> px @ obj.check - offset - 8081 8283 8485 8687 8889 8A8B 8C8D 8E8F 0123456789ABCDEF 0x5b39eabc1080 be28 4b24 0578 f70a 17fc 0d11 a1c3 af07 .(K$.x.......... ...
Decompilation
So far, we got to a really nice place with static binary analysis:
- We know that the input is the flag, the flag will not be returned in any other way by the executable
We know that
srand
is called along with var4hNot going to lie, I thought of going for debugging instead of decompiling, but
- I did not want to waste time atm figuring how debugging works in r2
- GDB for some reason failed to insert breakpoints
So…decompiling, and not even done properly, but I need practice here
First things first, we see that: There are a lot of strange assignments, like:
*(*0x20 + -0x18) = 0x56c847aba199;
How does it even dereference 0x20
… How is it set?
I started removing these, whilst also making a point of seeing the instruction
at that address. At the end I was left with pretty readable code, except for
the user input: I could not exactly see how it gets transformed to be used by
srand()
. Turns out, even though I modified the code so that it more closely
resembles C
: I do not have the location of the check object so… it does
not really work that way.
int main(void) { int loopIndex; unsigned int userInput; // First step puts("[ ** WELCOME TO ROBO CASINO **]"); // obj.banner puts(" , ,\n (\\____/)\n (_oo_)\n (O)\n __||__ " "\\)\n []/______\\[] /\n / \\______/ \\/\n / /__\\\n(\\ " "/____\\\n---------------------"); puts("[*** PLEASE PLACE YOUR BETS ***]"); loopIndex = 0; // Second step while (1) { // House balance check if (29 < loopIndex) { // Length is 30 puts("[ ** HOUSE BALANCE $0 - PLEASE COME BACK LATER ** ]"); return 0; } // Step 3 printf("> "); // Prompt if ( scanf("%c", &userInput) != 1) break; srand(userInput); // pending some transformation if (rand() != *(loopIndex * 4 + obj.check)) { puts("[ * INCORRECT * ]"); puts("[ *** ACTIVATING SECURITY SYSTEM - PLEASE VACATE *** ]"); exit(0xfffffffe); } puts("[ * CORRECT *]"); loopIndex = loopIndex + 1; } exit(0xffffffff); }
Implementing the payload
Now, simply following the decompiled code we see that any character we
entered is fed into srand()
as a seed, and then the first rand()
with
that seed is checked to be equal to the object at address: loopIndex*4 + obj.check
This means that each printable character has the same address to be checked against: If we compute this association once, and store it in a dictionary we can retrieve the results without extra computations for each index. Thus, the payload can easily be produced:
from pwn import * from ctypes import CDLL # shoutout to ckrielle for mentioning this trick at the last HtB meetup libc = CDLL("libc.so.6") zino = ELF("./casino") # Create the dictionary, printable characters are from 40 to 177, see ~man ascii~ dic = {} for i in range(40,177): libc.srand(i) dic[ libc.rand() ] = chr(i) # Now loop get the actual characters answ = "" objCheck = zino.sym.check # obj.check address for i in range(30): # we know that the flag is 30 characters long, >0x1d answ += dic[ zino.u32( 4 * i + objCheck )] print(answ)
Summary
- Start by looking at the disassembly. Visual graph mode helps a lot in understanding the flow of the program.
- Do not overlook loop control variables: I did this and, as a result,
I got stuck (notice that I had not included the step increase in
loopIndex
in the disassembly snippet I focused on). - After decompilation, take your time: Use a plain buffer and analyze step by step how it differs from what you have already understood through assembly.
- If you encounter strange memory assignments added by Ghidra (or any decompiler, for that matter), check the addresses they point to. If they reference another command, it is highly likely they do absolutely nothing.
- More variables than those that actually exist may appear in the decompiled snippet. This could be due to name dependencies being handled through register renaming. In any case, variables might need to be merged or discarded.
Today was a rest day after an exam. I won’t have much time to polish something to publish for a while, so…
Figure 1: Until we meet again