找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2123

积分

0

好友

294

主题
发表于 2025-12-25 03:42:01 | 查看: 29| 回复: 0

银狐远控在生成被控端时,提供了三种形式:可执行文件(exe)、动态链接库(dll)和shellcode。目前,原版生成shellcode的功能已失效。结合与安全研究同行的技术探讨,本文将从纯技术角度,深入分析银狐实现shellcode生成及规避安全检测(免杀)所采用的核心思路。

银狐被控端在生成过程中,运用了多种技术来隐藏自身行为,本系列文章将逐一剖析这些技术。需要明确的是,所有讨论仅限于技术交流范畴。

在银狐的源码工程中,生成exe和dll的入口是上线模块,而生成shellcode的入口则是执行代码工程。

图片

要深入理解执行代码工程,需要具备一定的Windows安全编程知识。同时,银狐源码本身也是学习C++、网络编程以及安全工程实践的优质材料。

作为系列的第一篇,本文将聚焦于一项基础且关键的技术:恶意软件如何在不依赖导入表的情况下动态解析并调用API函数,这也是深入理解免杀与shellcode修复的第一步。本篇侧重理论梳理,后续文章将结合银狐源码中的具体实现进行验证。

恶意软件作者为对抗分析,不断演进其技术。其中,“PEB遍历”被现代恶意软件广泛采用。本文旨在清晰地阐述这项技术的工作原理,帮助初学者破除常见的误解。

我们将逐步拆解三个核心问题:什么是PEB遍历为何要使用它以及它是如何工作的,并辅以完整的C++示例代码。

为何恶意软件要规避导入地址表(IAT)?

在深入PEB之前,有必要理解其动机。

无论是静态分析(逆向代码)还是动态分析(运行调试),安全分析师通常会优先检查程序的导入地址表(IAT)。IAT列出了程序运行时需要调用的所有外部Windows API函数,这些信息直接暴露了程序的行为意图。

例如,如果在IAT中发现CreateRemoteThreadWriteProcessMemoryWinExec等函数,很可能意味着该程序具备进程注入、内存写入或创建新进程的能力。

那么,问题出在哪里?

IAT让一切变得过于透明。 恶意软件作者不希望分析师轻易通过查看IAT就推测出其功能。因此,他们放弃在编译时声明API函数(这样会填充IAT),转而采用一种更隐蔽的方法:在运行时通过遍历进程环境块(PEB) 来动态获取所需API的地址。

什么是PEB?

PEB进程环境块,是Windows操作系统为每个运行的进程在内存中维护的一个关键数据结构。它包含了关于该进程的大量信息,例如:

  • 已加载模块(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;

其中,Ldr成员指向一个PEB_LDR_DATA结构,该结构内包含了所有已加载模块的链表信息。

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;        // DLL的基地址
    PVOID      EntryPoint;
    ULONG      SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName; // DLL的短名称(如kernel32.dll)
    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;

恶意软件最感兴趣的是DllBase(模块基地址)和BaseDllName。通过比对名称,它可以定位到关键的系统DLL,如kernel32.dllntdll.dll,这些DLL包含了实现其功能所需的核心API。

如何访问PEB?

在32位系统中,一种常见的方法是使用内联汇编直接访问:

// for 32 bit architecture
#include <stdio.h>
#include <Windows.h>

int main() {
    PVOID peb;
    __asm {
        mov eax, fs:[0x30]  // FS寄存器指向TEB,TEB+0x30处为PEB指针
        mov peb, eax
    }
    printf(“PEB Address: %p\n”, peb);
    return 0;
}

__asm块通过fs段寄存器访问线程环境块(TEB),在偏移0x30处即可获得PEB的指针。

图片
图片

PEB遍历的核心流程

PEB遍历的首要目标通常是找到kernel32.dll中的两个关键函数:

  1. LoadLibraryA:用于在运行时加载其他DLL。
  2. GetProcAddress:用于获取已加载DLL中指定函数的地址。

一旦获得这两个函数,恶意软件就可以加载任何DLL并调用其中的任何函数,完全摆脱对IAT的依赖。

以下是一个简化的步骤,演示如何通过PEB遍历找到MessageBoxA并调用它:

  1. 获取当前进程的PEB地址。
  2. 通过PEB找到PEB_LDR_DATA结构。
  3. 遍历InLoadOrderModuleList链表,查找BaseDllName”kernel32.dll”的模块条目。
  4. 从该条目中获取kernel32.dll的基地址(DllBase)。
  5. 手动解析kernel32.dll的PE头部和导出表,找到LoadLibraryAGetProcAddress函数的地址。
  6. 调用LoadLibraryA加载user32.dll
  7. 调用GetProcAddress获取user32.dllMessageBoxA的地址。
  8. 调用MessageBoxA显示消息。

以下是完整的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);

// 手动解析DLL导出表,根据函数名查找地址
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;

    // 内联汇编获取PEB地址
    __asm {
        mov eax, fs: [0x30]
        mov peb, eax
    }

    // 遍历已加载模块列表,寻找kernel32.dll
    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;
            break;
        }
        listEntry = listEntry->Flink;
    } while (listEntry != &peb->Ldr->InLoadOrderModuleList);

    if (kernel32baseAddr) {
        // 手动解析并获取关键函数地址
        ptrGetProcAddress = (GETPROCADDRESS)GetProcAddressKernel32(kernel32baseAddr, “GetProcAddress”);
        ptrLoadLibraryA = (LOADLIBRARYA)GetProcAddressKernel32(kernel32baseAddr, “LoadLibraryA”);

        // 动态加载user32.dll并获取MessageBoxA地址
        HMODULE user32Base = ptrLoadLibraryA(“user32.dll”);
        ptrMessageBoxA = (MESSAGEBOXA)ptrGetProcAddress(user32Base, “MessageBoxA”);

        // 调用MessageBoxA
        ptrMessageBoxA(NULL, “success”, NULL, MB_OK);
    }
    return 0;
}

代码执行效果:
图片

代码解析:

  1. 结构体定义:定义了Windows内部数据结构(PEB, PEB_LDR_DATA, LDR_DATA_TABLE_ENTRY),以便程序能够访问和遍历进程的模块列表。
  2. GetProcAddressKernel32函数:手动解析指定DLL(这里指kernel32.dll)的PE头部和导出表,通过函数名称找到其内存地址,绕过了系统的GetProcAddress调用。
  3. 内联汇编获取PEB:通过FS寄存器直接获取当前进程的PEB地址,这是理解Windows系统底层机制的关键。
  4. 遍历模块列表:通过PEB找到加载器数据,并遍历InLoadOrderModuleList链表。将每个模块的Unicode名称转换为ASCII后与”kernel32.dll”比较,找到后记录其基地址。
  5. 动态解析与调用:使用自定义的解析函数获取LoadLibraryAGetProcAddress的地址,然后利用它们加载user32.dll并找到MessageBoxA,最终成功调用。

进阶隐蔽技巧

在上面的代码中,存在这样一行比较语句:

if (_stricmp(baseDllName, “kernel32.dll”) == 0)

这会导致编译后的二进制文件中明文存在”kernel32.dll”字符串,在静态分析中极易被发现。为了增强隐蔽性,一种常见的免杀技术是使用哈希(Hash)函数。例如,对需要查找的字符串(如”kernel32.dll””LoadLibraryA”)预先计算一个哈希值。在遍历比较时,不再比较字符串本身,而是比较计算出的哈希值。这样,最终的二进制文件中就不会留下明文的API或模块名称字符串。银狐远控的源码中也应用了此类技术,我们将在后续文章中详细分析。

逆向分析验证(Release版本)

对上述程序编译后的Release版本进行逆向分析,可以观察到其内部执行流程。
图片
图片
图片

关键点在于,尽管程序成功调用了MessageBoxA,但其导入表中并未显示该函数。使用PE工具查看导入表:
图片
可以看到,MessageBoxA并不可见,因为它是在运行时通过PEB遍历动态解析的,没有在编译时声明依赖。

总结

本文详细阐述了恶意软件如何利用PEB遍历技术在运行时动态解析API函数地址,从而完全规避导入地址表(IAT),达到隐藏行为意图的目的。理解这项基础技术对于安全分析与防御至关重要。本篇是后续分析银狐shellcode生成与免杀机制的理论基础,下一篇文章我们将深入银狐“执行代码”工程的源码,具体看其如何实现并优化这一技术。




上一篇:AI年度进展与AGI路径深度分析:技术瓶颈与经济影响展望
下一篇:iPhone 18 Pro前瞻:A20 Pro芯片与七大升级,涵盖设计、性能与拍摄
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 20:16 , Processed in 0.354334 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表