freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

从零讲解隐藏导入表
2025-03-13 13:04:22
所属地 四川省

导入表

在 Windows 操作系统中,可执行文件(如.exe文件)和动态链接库(DLL,.dll文件)常常需要调用其他 DLL 中的函数来实现特定功能。程序的导入表(Import Table)就是用于记录这些外部依赖信息的一种数据结构

首先我们这里用一个实验来解释一下什么是程序的导入表

#include <windows.h>
#include <stdio.h>

int main() {
    // 调用 MessageBoxA 函数(ANSI 版本)
    int result = MessageBoxA(NULL, "这是控制台程序中弹出的消息框。", "消息提示", MB_OKCANCEL);

    return 0;
}

这里就是直接用messagebox函数,输出一个弹窗,众所周知,messagebox函数是存在user32.dll里面的,理论上讲,我们通过查看这个程序的导入表,就能看到它调用了user32.dll。现在我们用CFF Explorer去看看

image-20250301104051441

在导入表这里可以看到确实是有user32.dll以及messageboxa函数

image-20250301104130938

相对隐藏

隐藏的方法很简单

在普通的 Windows 程序中,当编译器编译代码时,如果代码里调用了某个 DLL 中的函数(如MessageBoxA),编译器会在生成的可执行文件(PE 文件)里添加相应的导入表项。

我们可以不依赖编译器自动生成的导入表来调用MessageBoxA函数,而是手动完成。

手动加载dll,然后在dll中找到函数地址,把函数地址赋值给指针,然后调用该函数。

首先我们要做的就是自己定义一个messageboxa函数的指针类型,我们需要看messagebox要传入哪些参数进去,然后用结构体去定义。至于在哪里去看,当然是翻官方的文档了,文档地址如下:MessageBoxA 函数 (winuser.h) - Win32 apps | Microsoft Learn

image-20250301105200911

我们直接根据他的描述,定一个新的messagebox的类型,fnMessageBoxA就是我们定义的新名字

// 定义 MessageBoxA 函数指针类型
typedef UINT(CALLBACK* fnMessageBoxA)(
    HWND hWnd,
    LPCSTR lpText,
    LPCSTR lpCaption,
    UINT uType
    );

然后就是加载dll,使用LoadLibraryA函数即可

HMODULE hUser32 = LoadLibraryA("user32.dll");

然后从dll中寻找函数地址,具体代码和解释如下

简单的讲,就是先通过dos头的偏移量获取到nt头的地址,然后nt头里面就记录了记录了导出表的位置。在找到导出表之后,继续找到导出表里面的名称表,函数地址表。然后直接遍历即可

// 从指定模块中查找指定名称的函数地址
// 参数:
// hModule: 要查找函数的模块句柄,通常由 LoadLibrary 或 GetModuleHandle 等函数获取
// functionName: 要查找的函数的名称,以字符串形式传入
// 返回值:
// 如果找到目标函数,返回其地址(FARPROC 类型);如果未找到或模块句柄无效,返回 NULL
FARPROC GetFunctionAddress(HMODULE hModule, const char* functionName) {
    // 获取模块的导出表信息
    // 将模块句柄强制转换为 PIMAGE_DOS_HEADER 类型,以便访问 DOS 头信息
    // 每个 PE 文件(如 DLL、EXE)开头都有一个 DOS 头,包含基本信息如文件签名等
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule;

    // pDosHeader->e_lfanew 表示 NT 头相对于文件起始位置的偏移量
    // 通过将模块句柄加上这个偏移量,定位到 NT 头的位置
    // NT 头包含了更多关于 PE 文件的详细信息,如可选头、节表等
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)hModule + pDosHeader->e_lfanew);

    // pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
    // 表示导出表相对于模块基地址的虚拟地址
    // 通过将模块句柄加上这个虚拟地址,定位到导出表的位置
    // 导出表记录了模块中导出的函数和变量的信息
    PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)hModule + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    // 获取导出函数的名称表、地址表和序号表

    // pExportDirectory->AddressOfNames 是导出表中名称表的相对虚拟地址
    // 通过将模块句柄加上这个地址,得到名称表的实际地址
    // 名称表是一个数组,每个元素是一个指向函数名称字符串的偏移量
    PDWORD pNames = (PDWORD)((DWORD_PTR)hModule + pExportDirectory->AddressOfNames);

    // pExportDirectory->AddressOfNameOrdinals 是导出表中名称序号表的相对虚拟地址
    // 名称序号表也是一个数组,每个元素是一个序号,用于关联名称表和函数地址表
    PWORD pNameOrdinals = (PWORD)((DWORD_PTR)hModule + pExportDirectory->AddressOfNameOrdinals);

    // pExportDirectory->AddressOfFunctions 是导出表中函数地址表的相对虚拟地址
    // 函数地址表存储了每个导出函数的实际地址
    PDWORD pFunctions = (PDWORD)((DWORD_PTR)hModule + pExportDirectory->AddressOfFunctions);

    // 遍历导出函数名称表,查找目标函数
    // pExportDirectory->NumberOfNames 表示导出表中函数名称的数量
    for (DWORD i = 0; i < pExportDirectory->NumberOfNames; i++) {
        // pNames[i] 是当前函数名称相对于模块基地址的偏移量
        // 通过将模块句柄加上这个偏移量,得到函数名称字符串的实际地址
        const char* currentFunctionName = (const char*)((DWORD_PTR)hModule + pNames[i]);

        // 使用 strcmp 函数比较当前函数名称和要查找的函数名称
        // 如果两者相等,说明找到了目标函数
        if (strcmp(currentFunctionName, functionName) == 0) {
            // pNameOrdinals[i] 存储了当前函数的序号
            // 获取当前函数的序号
            WORD ordinal = pNameOrdinals[i];

            // pFunctions[ordinal] 是目标函数相对于模块基地址的偏移量
            // 通过将模块句柄加上这个偏移量,得到目标函数的实际地址
            // 并将其作为 FARPROC 类型返回
            return (FARPROC)((DWORD_PTR)hModule + pFunctions[ordinal]);
        }
    }
    // 如果遍历完所有函数名称都没有找到目标函数
    // 则返回 NULL 表示查找失败
    return NULL;
}

最后找到函数地址之后,转换成我们自己定义的指针,然后直接调用即可

// 从 user32.dll 中查找 MessageBoxA 函数的地址
    FARPROC messageBoxAddr = GetFunctionAddress(hUser32, "MessageBoxA");
    
    // 将函数地址转换为函数指针类型
    fnMessageBoxA myMessageBoxA = (fnMessageBoxA)messageBoxAddr;

    // 调用 MessageBoxA 函数
    myMessageBoxA(NULL, "11111", "1111", MB_OK);

    // 释放加载的 DLL
    FreeLibrary(hUser32);

最后完整代码如下

#include <windows.h>
#include <stdio.h>
typedef UINT(CALLBACK* fnMessageBoxA)(
    HWND hWnd,
    LPCSTR lpText,
    LPCSTR lpCaption,
    UINT uType
    );
FARPROC GetFunctionAddress(HMODULE hModule, const char* functionName) {

    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule;
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)hModule + pDosHeader->e_lfanew);
    PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)hModule + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    PDWORD pNames = (PDWORD)((DWORD_PTR)hModule + pExportDirectory->AddressOfNames);
    PWORD pNameOrdinals = (PWORD)((DWORD_PTR)hModule + pExportDirectory->AddressOfNameOrdinals);
    PDWORD pFunctions = (PDWORD)((DWORD_PTR)hModule + pExportDirectory->AddressOfFunctions);

    for (DWORD i = 0; i < pExportDirectory->NumberOfNames; i++) {
        const char* currentFunctionName = (const char*)((DWORD_PTR)hModule + pNames[i]);
        if (strcmp(currentFunctionName, functionName) == 0) {
            WORD ordinal = pNameOrdinals[i];
            return (FARPROC)((DWORD_PTR)hModule + pFunctions[ordinal]);
        }
    }

    return NULL;
}

int main() {
    HMODULE hUser32 = LoadLibraryA("user32.dll");
    FARPROC messageBoxAddr = GetFunctionAddress(hUser32, "MessageBoxA");
    fnMessageBoxA myMessageBoxA = (fnMessageBoxA)messageBoxAddr;
    myMessageBoxA(NULL, "11111", "1111", MB_OK);
    FreeLibrary(hUser32);
    return 0;
}

image-20250301112414928

这个时候再用CFF去看就看不到user32.dll了,但是这只是相对隐藏掉了。

image-20250303152802155

但是如果直接去搜exe里面的字符串,还是能看到messagebox这个函数。我们用die工具去进行搜索。可以看到同样还是有这么个东西

image-20250303153147824

加密

我们可以通过加密的方式去隐藏这个字符串。直接使用大部分网站的密码存储思路,通过hash加密,然后对比加密后的数据来判断两个数据是不是一样的。

那我们这里在遍历的时候,每次遍历到的函数名的时候就hash加密,然后和hash加密后的messageboxa进行对比。

我们这里简单的写一个hash算法,然后通过这个hash算法去进行比对

#include <stdio.h>
#include <string.h>

// 旋转哈希函数
unsigned int rotatingHash(const char* str) {
    unsigned int hash = 0;
    int i;
    for (i = 0; str[i] != '\0'; i++) {
        hash = (hash << 4) ^ (hash >> 28) ^ str[i];
    }
    return hash;
}

int main() {
    const char* input = "MessageBoxA";
    unsigned int hash = rotatingHash(input);
    printf(" '%s' is: %u\n", input, hash);
    return 0;
}

image-20250303154225254

MessageBoxA的hash值是1460732901

#include <windows.h>
#include <stdio.h>

typedef UINT(CALLBACK* fnMessageBoxA)(
    HWND hWnd,
    LPCSTR lpText,
    LPCSTR lpCaption,
    UINT uType
    );

unsigned int rotatingHash(const char* str) {
    unsigned int hash = 0;
    int i;
    for (i = 0; str[i] != '\0'; i++) {
        hash = (hash << 4) ^ (hash >> 28) ^ str[i];
    }
    return hash;
}

static LPVOID getAPIAddr(HMODULE h, DWORD myHash) {
    PIMAGE_DOS_HEADER img_dos_header = (PIMAGE_DOS_HEADER)h;
    PIMAGE_NT_HEADERS img_nt_header = (PIMAGE_NT_HEADERS)((LPBYTE)h + img_dos_header->e_lfanew);
    PIMAGE_EXPORT_DIRECTORY img_edt = (PIMAGE_EXPORT_DIRECTORY)(
        (LPBYTE)h + img_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    PDWORD fAddr = (PDWORD)((LPBYTE)h + img_edt->AddressOfFunctions);
    PDWORD fNames = (PDWORD)((LPBYTE)h + img_edt->AddressOfNames);
    PWORD fOrd = (PWORD)((LPBYTE)h + img_edt->AddressOfNameOrdinals);

    for (DWORD i = 0; i < img_edt->AddressOfFunctions; i++) {
        LPSTR pFuncName = (LPSTR)((LPBYTE)h + fNames[i]);
        if (rotatingHash(pFuncName) == myHash) {
            printf("successfully found! %s - %d\n", pFuncName, myHash);
            return (LPVOID)((LPBYTE)h + fAddr[fOrd[i]]);
        }
    }
    return NULL;
}

void main() {
    HMODULE mod = LoadLibraryA("user32.dll");
    LPVOID addr = getAPIAddr(mod, 1460732901);
    printf("0x%p\n", addr);
    fnMessageBoxA myMessageBoxA = (fnMessageBoxA)addr;
    myMessageBoxA(NULL, "这是弹窗", "嘻嘻嘻", MB_OK);
}

image-20250303154629935

成功弹窗

image-20250303154707733

这时候不管cff还是die搜字符串,都看不到我们调用的函数了

image-20250303154801461

image-20250303154818167

# c语言 # 免杀 # 免杀木马 # 静态免杀
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
  • 0 文章数
  • 0 关注者
文章目录