文章总结: 本文主要分析银狐远控的免杀技术与Shellcode修复思路,详细讲解了恶意软件如何通过PEB遍历技术动态解析API函数以规避导入地址表检测。文章阐述了获取PEB、定位kernel32.dll及解析关键函数的完整流程,提供了C++代码示例,并提及通过Hash替代字符串以增强隐蔽性的进阶技巧,为理解恶意软件隐蔽执行机制提供了理论与实践基础。 综合评分: 87 文章分类: 恶意软件,免杀,逆向分析
银狐远控免杀与shellcode修复思路分析 01
原创
安全研究员
CppGuide
2025年12月24日 11:29 上海
银狐生成被控端有三种形式,分别是生成被控exe、dll和shellcode,由于原版的生成shellcode功能已经失效。
加上与一些做红蓝对抗的朋友的交流,这里单纯地从技术上介绍一下银狐生成shellcode和免杀的一些技术和思路。
特别申明:
1. 本文介绍的内容仅做技术上的交流,请勿使用本文介绍的技术做其他用途,违者与本号无关。
2. 作者不提供任何支持生成可用shellcode和免杀版本的银狐源码,有此需求的读者请勿联系作者。
银狐被控端在生成被控程序时,使用了一些技术来隐藏自己,这个系列将介绍相关技术,当然,也仅仅是作技术上的交流,不可用于其他用途。
银狐生成exe和dll的入口工程是上线模块,而生成shellcode的入口工程是执行代码。
要看懂执行代码这个工程的代码,需要一些安全工程的知识。
当然,银狐源码也是学习C++编程、网络编程和安全工程等知识非常好的材料。
本文为这个系列的第一篇,先介绍第一个使用技术,本篇先介绍理论知识,下一篇我们将结合银狐执行代码工程中的具体代码逻辑来佐证这篇的理论知识。
好了,现在开始引入本文的主题——恶意软件如何在不使用导入表的情况下解析API。
恶意软件作者不断改进他们的策略,以领先于分析人员。许多现代恶意软件样本使用的一种技巧叫做“PEB遍历”。我想写这篇文章,帮助恶意软件分析的初学者清晰理解这种技术,因为尽管它被广泛使用,但新手往往对其存在误解。
在本文中,我们将逐步解析什么是PEB遍历、恶意软件为何使用它以及它如何运作,包括一个完整的C++示例。
为什么恶意软件要避开导入地址表(IAT)
在深入探讨PEB之前,让我们先弄清楚其“原因”。
在分析恶意软件时,无论是静态分析(查看代码)还是动态分析(运行代码),分析人员通常都会检查导入地址表(IAT),以了解使用了哪些Windows API函数。这些导入的函数会泄露有关恶意软件行为的线索。
例如,如果你在导入地址表(IAT)中看到CreateRemoteThread、WriteProcessMemory或WinExec,这可能意味着存在进程注入、代码执行或新进程生成的情况。
那么对于恶意软件作者来说,问题出在哪里呢?
导入地址表(IAT)会暴露无遗。
恶意软件作者不希望分析人员仅通过查看导入地址表(IAT)就能猜到他们的代码在做什么。因此,他们不会预先声明应用程序编程接口(API)调用(这会填满导入地址表),而是使用一种狡猾的技术——遍历进程环境块(PEB),在运行时解析这些调用。
PEB代表进程环境块。它是Windows操作系统为每个进程在内存中创建的数据结构。PEB包含关于该进程的各种有用信息,例如:
- 已加载模块(DLL)的基地址
- 进程参数和环境变量
- 进程是否正在被调试
- 以及其他信息。
PEB结构如下:
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;
PEB的Ldr成员指向一个PEB_LDR_DATA结构,该结构包含所有已加载模块的详细信息。其中一个关键字段InMemoryOrderModuleList是一个链表,用于定位这些DLL的基地址。
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
在这种结构中,一个进程(例如恶意软件)会使用InMemoryOrderModuleList来遍历所有已加载的模块。这个链表中的每个条目都对应一个模块,并由LDR_DATA_TABLE_ENTRY结构表示,该结构包含了关于该模块的详细信息。
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
USHORT LoadCount;
USHORT TlsIndex;
LIST_ENTRY HashLinks;
PVOID SectionPointer;
ULONG CheckSum;
ULONG TimeDateStamp;
PVOID LoadedImports;
PVOID EntryPointActivationContext;
PVOID PatchInformation;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
对于恶意软件而言,已加载模块是最令人关注的部分——尤其是kernel32.dll和ntdll.dll。这些模块包含了恶意软件运行所需的关键API。
访问PEB结构
恶意软件如何访问PEB结构?一种常见的方法是通过内联汇编进行直接访问:
//for 32 bit architecture
#include <stdio.h>
#include <Windows.h>
int main() {
PVOID peb;
__asm {
mov eax, fs:[0x30]
mov peb, eax
}
printf("PEB Address: %p\n", peb);
return 0;
}
在这个示例中,__asm关键字允许在C代码中直接嵌入汇编指令。该汇编块通过fs段寄存器访问线程环境块(TEB)。在32位Windows系统上,fs寄存器指向TEB,而TEB内的偏移量0x30保存着指向PEB的指针。
在恶意软件中,PEB(进程环境块)包含有关进程中已加载模块(DLL)的信息,例如kernel32.dll和ntdll.dll——这些对恶意软件来说很重要。通过遍历PEB,恶意软件可以找到这些DLL的基地址,然后定位其中的特定函数。
例如,恶意软件通常会在kernel32.dll中寻找两个关键函数:
- LoadLibraryA(dllName):将DLL加载到进程中。
- GetProcAddress(hmodule, functionName):查找已加载的DLL中某个函数的地址。
PEB遍历的主要目标是在kernel32.dll中找到LoadLibraryA的地址,以便恶意软件能够加载其他库并动态调用它们的函数。
以下是PEB遍历如何查找LoadLibraryA和GetProcAddress,然后使用它们来调用MessageBoxA的简单分步流程:
- 获取当前进程的PEB。
- 访问PEB内部的加载器数据(称为PEB_LDR_DATA)。
- 遍历已加载模块列表,找到kernel32.dll的条目。
- 从该条目获取kernel32.dll的基地址。
- 手动查看kernel32.dll的导出表,以找到LoadLibraryA和GetProcAddress的地址。
- 使用LoadLibraryA加载user32.dll。
- 使用GetProcAddress在user32.dll中找到MessageBoxA的地址。
- 最后,调用MessageBoxA来显示消息“success”。
C++ 代码如下:
#include <stdio.h>
#include <windows.h>
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
USHORT LoadCount;
USHORT TlsIndex;
LIST_ENTRY HashLinks;
PVOID SectionPointer;
ULONG CheckSum;
ULONG TimeDateStamp;
PVOID LoadedImports;
PVOID EntryPointActivationContext;
PVOID PatchInformation;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
typedef struct _PEB_LDR_DATA {
ULONG Length;
BOOLEAN Initialized;
HANDLE SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, * PPEB_LDR_DATA;
typedef struct _PEB {
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
BOOLEAN SpareBool;
HANDLE Mutant;
PVOID ImageBaseAddress;
PPEB_LDR_DATA Ldr;
} PEB, * PPEB;
typedef FARPROC(WINAPI* GETPROCADDRESS)(HMODULE, LPCSTR);
typedef HMODULE(WINAPI* LOADLIBRARYA)(LPCSTR);
typedef int (WINAPI* MESSAGEBOXA)(HWND, LPCSTR, LPCSTR, UINT);
PVOID GetProcAddressKernel32(HMODULE hModule, LPCSTR lpProcName) {
PIMAGE_DOS_HEADER pDOSHeader = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS pNTHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hModule + pDOSHeader->e_lfanew);
PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule + pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
DWORD* pAddressOfFunctions = (DWORD*)((BYTE*)hModule + pExportDirectory->AddressOfFunctions);
DWORD* pAddressOfNames = (DWORD*)((BYTE*)hModule + pExportDirectory->AddressOfNames);
WORD* pAddressOfNameOrdinals = (WORD*)((BYTE*)hModule + pExportDirectory->AddressOfNameOrdinals);
for (DWORD i = 0; i < pExportDirectory->NumberOfNames; i++) {
char* functionName = (char*)((BYTE*)hModule + pAddressOfNames[i]);
if (strcmp(functionName, lpProcName) == 0) {
return (PVOID)((BYTE*)hModule + pAddressOfFunctions[pAddressOfNameOrdinals[i]]);
}
}
return NULL;
}
int main() {
PEB* peb;
PLDR_DATA_TABLE_ENTRY module;
LIST_ENTRY* listEntry;
HMODULE kernel32baseAddr = NULL;
GETPROCADDRESS ptrGetProcAddress = NULL;
LOADLIBRARYA ptrLoadLibraryA = NULL;
MESSAGEBOXA ptrMessageBoxA = NULL;
__asm {
mov eax, fs: [0x30]
mov peb, eax
}
listEntry = peb->Ldr->InLoadOrderModuleList.Flink;
do {
module = CONTAINING_RECORD(listEntry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
char baseDllName[256];
size_t i;
for (i = 0; i < module->BaseDllName.Length / sizeof(WCHAR) && i < sizeof(baseDllName) - 1; i++) {
baseDllName[i] = (char)module->BaseDllName.Buffer[i];
}
baseDllName[i] = '\0';
if (_stricmp(baseDllName, "kernel32.dll") == 0) {
kernel32baseAddr = (HMODULE)module->DllBase;
}
listEntry = listEntry->Flink;
} while (listEntry != &peb->Ldr->InLoadOrderModuleList);
if (kernel32baseAddr) {
ptrGetProcAddress = (GETPROCADDRESS)GetProcAddressKernel32(kernel32baseAddr, "GetProcAddress");
ptrLoadLibraryA = (LOADLIBRARYA)GetProcAddressKernel32(kernel32baseAddr, "LoadLibraryA");
HMODULE user32Base = ptrLoadLibraryA("user32.dll");
ptrMessageBoxA = (MESSAGEBOXA)ptrGetProcAddress(user32Base, "MessageBoxA");
ptrMessageBoxA(NULL, "success", NULL, MB_OK);
}
return 0;
}
代码执行效果如下图:
解释一下上述代码:
- Struct Definitions: 结构体定义部分,定义了Windows内部数据结构,如
PEB、PEB_LDR_DATA和LDR_DATA_TABLE_ENTRY。这些结构表示进程环境块及其模块加载器数据,使程序能够遍历进程内已加载的DLL列表。 - GetProcAddressKernel32函数:手动解析DLL(在本例中为
kernel32.dll)的导出表,以通过名称查找函数的地址。它: - 读取DLL的PE头。
- 定位导出目录。
- 遍历导出的函数名。
- 如果找到请求的函数,则返回其地址。
- 内联汇编(访问 PEB):使用汇编指令从fs段寄存器(偏移量0x30)获取当前进程的PEB地址。PEB包含进程范围的信息,包括已加载的模块。
- 遍历模块列表:访问PEB的加载器数据(
Ldr)以遍历InLoadOrderModuleList,这是一个加载到进程中的所有DLL的链表。对于每个模块: - 将其 Unicode 名称转换为 ASCII。
- 将其与
"kernel32.dll"进行比较。 - 找到
kernel32.dll后,保存其基地址。 - 解析重要函数:使用
GetProcAddressKernel32手动获取kernel32.dll中GetProcAddress和LoadLibraryA的地址。这绕过了正常的导入表和API调用。 - 加载user32.dll并查找MessageBoxA:调用已解析的
LoadLibraryA来动态加载user32.dll。然后调用已解析的GetProcAddress以获取user32.dll中MessageBoxA的地址。 - 调用MessageBoxA:使用函数指针指向
MessageBoxA来显示一个内容为"success"的消息框,以此证明函数的手动解析和调用工作正常。
更进一步
由于我们在上述代码中有这样一行:
if (_stricmp(baseDllName, "kernel32.dll") == 0)
这行通过比较kernel32.dll这样的字符串去获取模块的基地址,导致生成的程序文件中存在"kernel32.dll"这样固定的字符串,这样程序如果被静态分析也容易暴露,为了更加隐蔽,我们通过定义一个hash函数,对于我们需要查找的字符串进行hash,然后通过比较hash值来确定是不是我们要找的字符串,这样编译后的静态程序中就不会有这样的字符串了,程序行为更加隐蔽。银狐中也使用了这样的技术,在下篇文章中我们将详细介绍。
对我们上述PEB遍历程序进行逆向分析(Release版本)
这样的话,我们就在我们的程序中虽然用到了MessageBoxA这样的API,但是我们看不到我们的程序有任何导入表。
我们可以用相关工具查看一下:
可以看到,MessageBoxAPI 不可见,因为我们使用PEB遍历动态解析了该API。
本文到此结束。我们介绍了恶意软件如何在运行时利用PEB解析API函数,从而避开导入地址表。理解这种技术至关重要,因为它常被用于逃避检测和掩盖恶意行为。
本篇文章是下一篇的基础部分,下一篇文章我们将结合银狐生成shellcode具体代码来介绍这一技术的使用,这也是免杀和修复shellcode功能的第一步。
源码获取
如果对银狐(winos)有兴趣,可以通过下面的方式获取全套源码:
关注后回复【winos】即可获取源码
推荐阅读
银狐远控问题排查与修复——Viusal Studio集成Google Address Sanitizer排查内存问题
银狐远控代码中差异屏幕bug修复
银狐远程屏幕内存优化方法探究
银狐远程软件bug修复记录 第03篇
银狐远程软件 UDP 断线无法重连的bug排查和修复
银狐远程软件代理映射功能优化思路分享
银狐远程软件去后门方法
银狐远控一键编译调试与开发教程
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:CppGuide 安全研究员《银狐远控免杀与shellcode修复思路分析 01》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论