Is everything in life completely random? Are we unable to change our fate? Or maybe we can change the future and even manipulate randomness?! Is luck even a thing? Try your “luck”!
Category: Misc
Solvers: t0b1, lmarschk
Writeup
In this challenge we get a binary and can spawn a docker container. Downloading and running the binary yields the following output.
π Cosy Casino π
Current cosy coins: 69.69
1. Generate lucky number.
2. Play game.
3. Claim prize.
4. Exit.
Afterwards, we took a look into the decompiled sources of the program. The main function is straight forward.
void main(void) {
setup();
welcome();
generate();
do {
menu();
} while( true );
}
The generate
functions sounds interesting and contains the following.
if (_flag == 0) {
printf("\nLength of number (1-32): ");
__isoc99_scanf(&DAT_00102073,&local_44);
if ((local_44 < 0x22) && (-1 < local_44)) {
memset(local_38,0,(long)local_44);
local_3c = open("/dev/urandom",0);
if (local_3c < 0) {
fwrite("\nError opening /dev/urandom, exiting..\n",1,0x27,stderr);
/* WARNING: Subroutine does not return */
exit(0x69);
}
read(local_3c,local_38,(long)local_44);
while (local_40 < local_44) {
while (local_38[local_40] == '\0') {
read(local_3c,local_38 + local_40,1);
}
local_40 = local_40 + 1;
}
strcpy(lucky_number,local_38);
close(local_3c);
puts("\nLucky number generated successfuly! Try your luck!");
}
else {
puts("\nInvalid size!");
}
} else {
_flag = 0;
local_3c = open("/dev/urandom",0);
if (local_3c < 0) {
fwrite("\nError opening /dev/urandom, exiting..\n",1,0x27,stderr);
/* WARNING: Subroutine does not return */
exit(0x22);
}
read(local_3c,lucky_number,0x21);
close(local_3c);
}
On the first call reads 33 bytes from /dev/urandom
into the lucky_number
. Every other call of that function will allow us to enter a length and read bytes equal to that length from /dev/urandom
. Those bytes are then copied using strcpy
to the lucky_number
. As part of its functionality to copy strings, strcopy
makes sure that the string it copies is terminated correctly. Therefore, at the end of every copied string it will append a null-byte. With this null-byte we know one byte of the copied string (the last one).
Looking at the menu
function reveals nothing special. Depending on our input number, the corresponding function is called. We can see that the generate
function is called again if we choose the first menu item.
The claim
function is of more interest. It contains the logic, that will output the flag.
if ((prize_flag == 0) && (cosy_coins <= 100.0)) {
puts("\nNo prizes available!");
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
puts("\nEnjoy your prize!!\n");
cosy_coins = cosy_coins + 269.0;
local_40 = 0;
__fd = open("./flag.txt",0);
if (__fd < 0) {
fwrite("\nError opening flag, exiting..\n",1,0x1f,stderr);
/* WARNING: Subroutine does not return */
exit(0x6969);
}
read(__fd,local_38,0x21);
while (local_40 < 0x21) {
local_38[local_40] = local_38[local_40] ^ lucky_number[local_40];
local_40 = local_40 + 1;
}
close(__fd);
printf("%s",local_38);
If we have more than 100 coins, we get to enjoy our price. The program opens the flag.txt
and XORs its content with the lucky_number
generated from /dev/urandom
. The result is printed to us.
The last function we haven’t looked at yet is the play
function. It contains the following code.
puts("\nHow many coins do you want to bet?");
__isoc99_scanf(&DAT_001020e4,&subtracted_coins);
cosy_coins = cosy_coins - subtracted_coins;
iVar1 = coin_check(cosy_coins);
if (iVar1 != 0) {
dev_urandom = open("/dev/urandom",0);
if (dev_urandom < 0) {
fwrite("\nError opening /dev/urandom, exiting..\n",1,0x27,stderr);
/* WARNING: Subroutine does not return */
exit(0x22);
}
read(dev_urandom,rigged_number,0x31);
close(dev_urandom);
iVar1 = strcmp(lucky_number,rigged_number);
if (iVar1 == 0) {
puts("\nYou won! Claim your reward!");
prize_flag = 1;
}
else {
puts("\nYou lost! Try again!");
}
}
We can first input a number of coins we want to bet. Those coins are read using scanf("%f")
and subtracted from our total amount of coins. We can simply input -100
to avoid loosing coins, but instead earning coins. That way we can increase our coins over 100 to pass the check in the claim
function. The function then reads some more bytes from /dev/urandom
and checks if both random numbers are equal. However this seems to be a dead end and not exploitable. Even if those numbers were magically the same, we still must know the random number to reverse the XOR of the flag.
Now we know that we can generate as much random numbers as we want and claim the flag xored with our lucky_number
once we have more than 100 coins. We also know that strcpy
is used to copy the new random number into the lucky_number
.
As strcpy
always appends a null byte, we can generate numbers of length 31, 30, 29… 0, to null the whole lucky_number
buffer. Afterwards the key will consist only of zeros and XORing the flag will simply reveal the flag itself.
This process can be automated using the following script, however we entered the values manually during the challenge.
#!/usr/bin/python3
from pwn import *
local = False
proc = process('./rigged_lottery') if local else remote('docker.hackthebox.eu', 12345)
payload = ''.join(['1\n%d\n' % (31-i) for i in range(32)])
payload += '2\n-100\n3\n'
proc.send(payload)
proc.recvuntil('Enjoy your prize!!')
print(proc.recvall())
Using the script or manually inputting the values gives us the flag. HTB{strcpy_0nly_c4us3s_tr0ubl3!}