纠结了一下到底要叫什么名字,因为这里打算提到 pwn 的时候遇到的各种奇技淫巧(或者说各种很棒的思路),着重想提一下 shellcode 和 ROP。
充满技巧的shellcode
ROP
关于ROP,其实在不同的环境下,有很多的构造方法,当然使用合适的工具也是很重要的一环,比如说linux下的ROPGadget
,windows下的mona
都是很好的选择
Windows 绕过 DEP
实验条件
- 32bit PE 文件
- 没有打开
SafeSEH
和GS
- 本地演示,假设知道各个module加载时候的地址
目标
弹出计算器
思路
一个典型的栈溢出,DEP导致我们不能跳转到栈上,这种情况下windows下常见思路有两种
- WinExec执行calc.exe
- 将栈这段地址修改为可执行,然后
jmp esp
这里针对第二种讨论。
给出一个之前看到的很棒的思路(用python生成ROP):
1 | def create_rop_chain(): |
这个写法真的很巧妙。我们来一步步分析它的行为
首先我们已知当前是栈溢出,那么我们的目标有两个
- 调用
VirtualProtect
- 跳转到可执行的位置上执行
shellcode
关于VirtualProtect
,这里给出函数原型
1 | VirtualProtect( |
通过这个函数,我们能够修改栈的权限。观察函数的参数,发现其中有两个变量的数据是固定的,另外两个则是要根据运行时的程序确定,此时我想到的问题就是:
- 如最大确定当前的
shellcode
的位置?或者说,如何将esp
的值传入到栈中?
(关于lpOldProtect参数,我们可以直接找一个固定的可写的地址即可,dll中这类地址很多)
我构想过如下思路:
- 利用
mov reg32, esp; ret --> push reg32; ret;
如何?
但是直接这么用的话不能保证传参是连续的,因为有过一次ret之后,说明下一个地址也是存在栈中的,会卡住后面的参数传参
可以看到,如果这样处理的话,虽然我们可以传入一个当前的esp,但是不能保证之后的参数能够按照我们的想法赋值,所以这个思路不同
- 利用
push esp; pop ebp; ret --> xchg eax, ebp; ret
这个思路倒是可以,不过这样的话同样需要满足参数在栈中相邻这个要求,这样就比较复杂。
pusha
pusha
可以说是神器一号,pusha这个指令会将所有的寄存器保存在栈中,包括esp(为调用pusha之前的esp的值),如果说我们首先将需要传递的参数按照pusha
的推入顺序放在指定的寄存器中,那么调用pusha
的时候,所有的参数都将会是相邻的
pusha
1 | push eax; |
以下这几步做的就是将VirtualProtect
的参数传入的操作。
1 | 0x77933b03, # POP EBX # RETN [ntdll.dll] |
(漏了lPAddress?回想一下栈中的位置以及pusha传入的参数)
我们假设我们已经调用了pusha
,那么我们可以想象到,第二个问题就是
- 如何去调用
VirtualProtect
呢?
(纠错:此时 esp 指向的是 edi 的位置)
(有点违和?想一想pusha的时候,栈中本来的gadget是不是被各个寄存器给覆盖掉了?)
上图是按照给出的shellcode重现的情况(部分未分析的地方没有提到)。由图可知,我们根本没有办法调用到那个VirtualProtect
!所以我们不能直接的把函数地址填写到栈上,我们可以采取一种稍微绕弯的gadget
1 | mov eax, &VirtualProtect; |
按照上面的写法,我们可以算是调用了VirtualProtect
函数,而且最关键的是,这样处理的时候函数调用的时候不需要紧密联系栈,此时只需要关心栈中的数据就可以了。于是我们为了满足栈中的处理,可以使用下面的gadget
1 | 0x6ad696ed, # POP EDI # RETN [MSVCR110.dll] |
加入了这些数据之后我们可以看栈现在的情况
此时的esp
正好指向一个距离参数为esp + 0x8
的位置,并且发生了函数调用,完全就是模拟了call VirtualProtect
。太棒了,这样我们就能够调用VirtualProtect
函数了!
(如果将寄存器倒过来放,强行构成一个函数调用栈?看后面提到的VirtualProtect
的特性)
- 但是调用结束之后呢?我们有一个
gadget
的位置,要用这个来调整 esp 吗?
VirtualProtect
返回的ret和普通的函数返回不太一样,返回为
1 | ret 0x10 |
这个指令的意思是将返回地址交还给eip之后,esp += 0x10
这样的话上图中的esp
正好就会落在eax
的位置上,并且此时的eip的值为&VirtualProtect
,也就是说最后一个寄存器EBP
的值可以开始调用了:
1 | 0x6add435d, # POP EBP # RETN [MSVCR110.dll] |
通过pop ret
,让esp正好落在了jmp esp
上,实现了shellcode
的调用
附上当初为了思考的时候画的结构图
1 | stack bottom |