不那么菜菜的ROP
栈上的不那么简单小玩意们
Stack pivoting
stack pivoting (堆栈转移),基本思想是利用已有的内存区域或数据结构(例如堆)来构造一个新的堆栈,然后将程序的控制流转移到该堆栈上执行。通过将堆栈指针 (ESP/RSP) 设置为新的堆栈地址,并在该堆栈上构造合适的函数调用帧,可以控制程序的执行路径。
通常在内存布局受限或某些保护机制存在的情况下使用。
利用stack pivoting有以下几个特征:
- 栈可以控制的空间过小不足以构造完整的rop链
- 开启了 PIE 保护,栈地址未知,我们可以将栈劫持到已知的区域
说到底栈迁移就是 'a'*offset+p64(fake_stack)+P64(leave)
首先我们需要了解函数在执行结束后会执行的 leave;ret
这条汇编代码的含义:
1 |
|
在实际的程序汇编代码中,我们还需要利用如下图所示的代码:
rax, [rbp+buf]
我们要知道这句汇编在干嘛,其实就是把 rbp+buf 的地址给到 rax。我们都知道栈溢出是先溢出到 rbp 再到 rip 的,我们可以通过栈溢出的方式控制 rbp 的值,在下面又将会把 rax 的值赋给 rsi ,通过这条汇编命令我们就可以控制 rbp 和 rsi 。
像上图的例子,假设我们通过溢出控制了 rbp 为 0x123000,那么 read 命令就相当于在执行 read(0,0x123000+buf,xx)
,也就是读入的数据被写入了 0x123000。至此我们就可以实现任意地址的写入。
但是这样只适用于我们只利用一次漏洞的情况,如果我们多次利用漏洞就需要为程序完整的构造一个栈结构,实现 “ 把栈搬走 ” 。
大致的通用思路就是:
第一次栈迁移修改 rbp 让接下来的输入指向我们想要写的地方
第二次栈迁移修改 rsp 让程序正常
第三次栈迁移进行攻击 rop 链的构造(例如泄露 libc ,修改 GOT 地址,在栈上布置 shell rop 链)
第四次重复第一次的操作
第五次构造 getshell rop 链
如果需要进行多次 rop 攻击,除了最后一次都需要重复 1、2 操作恢复栈结构
🌰_1:
发现了一个靶场:ROP Emporium,选择pivot-x_86
给的压缩包里还有一个动态链接库 libpivot32.so
,checksec 一下甚至出现了没见过的玩意 RUNPATH: '.'
1 |
|
这个 RUNPATH: ‘.’ 的意思是可执行文件的运行时路径(run-time search path)设置为当前目录(’.’),也就是它运行时会使用题目所给的动态链接库
直接在linux里运行一下看看,这个程序有有两个输入。
反编译一下这个程序的main函数,复制一下源码写个注释
1 |
|
发现main函数里没有什么漏洞可以被我们利用,但是在pwnme
函数里发现read()
没有输入长度的验证
其中在第二个输入溢出时,只有(0x38-0x28-4)
即12个字节的缓冲区可以利用,无法直接在栈上构造rop链。题目也有提示,我们需要将rop链存到buf中,再将栈转移到buf上。
我们发现puts一条文本Call ret2win() from libpivot
,没有在这个可执行文件中发现这个函数,但是题目还给了一个动态链接库 libpivot32.so
,发现这是一个后门函数,我们可以控制程序跳转到这里执行为我们打开flag。
需要注意的是程序执行到我们填入的leave;ret
指令之前,自己也执行了一次该指令。也就是说这个指令被执行了两次
由于每次ret都会使得esp+4,所以,伪造的ebp的地址要减去4。
1 |
|
🌰_2:
[X-CTF Quals 2016 - b0verfl0w](https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/stackprivot/X-CTF Quals 2016 - b0verfl0w)
题目拿到手先查看一下基本信息
查看一下源码发现vul函数有栈溢出漏洞
但是可控制的范围很小只有50-32-4=14个字节,我们就考虑 stack pivoting 。由于程序本身并没有开启堆栈保护,所以我们可以在栈上布置 shellcode 并执行。
构造完shellcode之后,我们需要对 esp 进行操作,使其指向 shellcode 处,并且直接控制程序跳转至 esp 处。
查找一下可以利用的gadget
其中0x08048504
有直接跳转到esp的片段,
修改栈上的返回地址为jmp esp
的地址,这样程序就会跳转到栈顶指针所指向的地址处执行payload。
构造结构如下:
shellcode+padding=0x20,fake ebp=0x4,jmp_esp=0x4
为了保证栈上有足够的空间执行payload,我们能将栈指针向下调整,即sub esp, 0x28;jmp esp。
1 |
|
Stack smash
canary保护有多种绕过方式,其中Stack smash就是绕过canary保护的一种方式。之前做过的canary保护绕过是填充canary最后的字节并之后暴力枚举或是直接泄露,从而避免canary保护程序报错。Stack smash这个方法并不在乎是否会引发canary报错,而是利用报错的内容。
在程序启动canary保护之后,如果发现canary被修改的话就会执行 _stack_chk_fail
函数来打印 argv[0] 指针所指向的字符串,正常情况下这个指针指向程序名。如果我们利用栈溢出覆盖 argv[0] 为我们想要输出的字符串地址,那么在 _fortify_fail
函数中就会输出我们想要的信息。
这个方法适用于glibc-2.31以前的程序,之后的程序不会打印 argv[0] 指针所指向的字符串
1 |
|
以2015年32C3 CTF readme 为例
触发canary保护后程序会输出一段报错
开始解题,反汇编一下源码。
程序中有两次输入,第一次输入赋值给v3后不对其进行任何操作;第二次输入赋值给v1,将其不断赋值给byte_600D20这个数组。
双击查看数组
也就是说这道题我们只需要拿flag而不是拿shell,而且v2变量接收第二次输入的字符串,并且会不断覆盖原有的flag内容。
在这之后还有一条语句
1 |
|
这条伪代码的原型是这样的
1 |
|
意思是从内存指针ptr指向的位置直到向后num字节都被value取代。
结合 byte_600D20[v0++] = v1;
,我们题目中的函数意思就是无论你是否进行第二次输入,在程序结束后 flag 的位置都将被替换,我们就无法通过直接修改 argv[0] 的值获得 flag 。
但是flag被映射了两次,0x600D21中的flag被修改,那我们就把 argv[0] 指向 0x400d21。
看了很多大佬的解析也没搞明白,程序中有两个load段,为什么第二个load段中的flag,会被映射到第一个load 段里🥲
查找argv[0]的位置,一共找到三种方法:
- 方法一:
用peda挂载文件,先在第一个gets处下断点
利用find命令查找与文件名有关的地址,再计算当前的rsp指针与其的距离
- 方法二
也是利用argv[0]指向程序名的特点寻找,直接下断点,查找指向程序名的内存指针。
- 方法三
用命令p & __libc_argv[0]
1 |
|
我们要写入536个字节也就是0x218个字节才能将argv[0]覆盖掉,所以payload构成应该是0x218字节的填充物加上我们目标flag的地址。
1 |
|
但是没有输出,是因为程序把错误信号发送给了执行程序的终端里,我们需要修改环境变量让错误信息通过网络传到我们的终端里。
所以我们要利用第二次的输入,将 LIBC_FATAL_STDERR_=1
写入到环境变量中。在第一个payload当中我们已经把指针指向了argv[0],需要将指针再次指向第二次输入点,结果如下:
1 |
|
原来栈溢出不是想象中的辣么简单捏
栈上的东西终于整理好噜😊