位置:首页 > 安全分类 > WEB安全
【技术分享】结合实例浅析壳编写的流程与难点
前言
- 前段时间自己学习了一下壳的编写,发现网上相关资料不是非常全,而且讲解顺序不太对胃口。因此参考了《黑客免杀攻防》中的代码对DLL型壳编写的结构进行了一次归纳整理,并附上相应代码解析。
一、壳的简介
1.1 壳的简介
- 在日常生活中我们所接触到的大量瓜果蔬菜存在壳,例如花生壳,瓜子壳等,这些壳是为了保护自身而存在。同理,对于我们在逆向工程中接触到的壳同样是出于保护自身减少被破解的可能性而研发的。
- 一般大部分的壳可以分为两种,加密壳与压缩壳。压缩壳以通过压缩算法减少程序体积为目的,而加密壳则以干扰逆向分析为主要目的。但是两者都会对静态分析产生较大的影响
- 例如恶意代码分析中常见的特征匹配的办法,以其中最广为使用的yara为例,其工作的原理就是匹配一串特殊的数据,也就是特征。这段数据是一个恶意软件的核心功能代码,而且正常代码,甚至包括其他恶意代码几乎没有这段代码。而哪怕是一个简单的压缩壳都有可能导致这段特征产生严重的变形进而干扰分析。
1.2 壳的原理
- 一般原始程序结构如下。在操作系统加载该程序后,程序从PE头指定的OEP(入口点)开始执行
而加壳之后,程序的.text段就被加密了(有些壳还会加密IAT表,资源段等等),程序的入口点指向壳添加的代码的入口(Stub的入口),Stub将程序的更改恢复(例如解密.text,IAT等等),最后执行完后跳转到原始的OEP。
- 在执行到原先的OEP后,实际上程序就与加壳前没有差别了(除了添加的Stub的区块),而Stub的区块也并非必须。此处先介绍一个概念,区块对齐。在内存中与在硬盘中,程序都存在一个对齐值,例如内存中是SectionAlignment,文件中是FileAlignment。当一个区块的大小不是对齐值的整数倍时,会被填充成对齐值的整数部分。也就是说在这种情况下区块可能存在一部分的空余空间。我们可以将Stub填写到该空余空间中,这样文件大小就不会改变,这也是不少病毒的感染做法。
1.3 壳的编写流程以及其中的问题
- 壳的编写流程非常简单,流程如下:
- 编写Stub代码将原程序加载到内存,加密.text段(或更多,例如资源,IAT等等)在原程序中添加一个Section,将恢复原程序的代码(Stub)添加到原程序中。在Stub的Section结尾添加一个跳转,跳转到原始的OEP。将程序的OEP修改到Stub的起始地址。这个过程看似没有什么难度,实际上存在一些比较难以实现的点。DLL编译过程中生成的引导代码会导致代码不可控,大概率出现不
- 涉及到的地址会出现很大问题(jmp短跳转用的是相对地址,但是长跳转和大部分call用的是绝对地址,移植到原程序后会出现对应不上的问题)
- 由于无法将DLL的导入表移植过去,DLL中的API调用将全部无效。
- 加密text段时我们无法获得目标程序的信息,也就无法指定起始与结束加密位置
- 编写的Stub格式代码如果纯粹用汇编写,之后可移植性不强,因此建议编译为dll格式。
- 如果将stub的dll拷贝到原程序中
- 由于程序在内存中和在硬盘中的状态不同(主要体现在FileAlignment与SectionAlignment的不同上,SectionAlignment一般大于FileAlignment),进行FileOffset与VirtualAddress转换时容易引起混淆。实际上读入内存时无论是使用硬盘中的布局或者内存中的布局都是可以的,但需要注意两者的不同
- 那么如何解决?
- 引导代码的问题,我们可以加上naked的关键字不生成引导代码,因为我们的DLLMain不需要执行,所以实际上不会影响。
- PE文件PE头存在一个重定位的Table,我们可以根据这张表修复重定位,具体修复位置根据我们的程序基址与DLL的基址来决定。
- 对于导入表不存在的问题,我们使用fs的特性定位GetProcAddress,之后通过GetProcAddress获取API地址,动态加载API。
- 对于无法获得信息的问题,我们导出一个全局变量来获取信息,使得用于加壳的程序(不是被加壳程序)能够从外部读取并写入这个全局变量。
- 对于移植性,我们的Stub编译为DLL格式
- 对于DLL拷贝的问题
- 对于程序内存与硬盘中不同的布局,我们直接指定加载原程序的方式
二、壳代码的编写
该部分主要包括:
- 初始化,将所有区块合并
- 获取.text段必要信息,导出变量
- 添加API调用
2.1 初始化操作
- 之前我们已经提过使用编写DLL的方式来完成Stub的实现。在此为了方便起见,我们只使用异或来加密.text段。
- 由于我们移植stub时候只需要一个代码段,所以dll编译的时候我们指定多个段合并
#pragma comment(linker, "/merge:.data=.text") // 将.data合并到.text#pragma comment(linker, "/merge:.rdata=.text") // 将.rdata合并到.text#pragma comment(linker, "/section:.text,RWE") // 将.text段的属性设置为可读、可写、可执行
2.2 获取加密.text时的必要信息
之后我们导出一个全局变量用于获取信息
typedef struct _GLOBAL_PARAM{ BOOL bShowMessage; // 是否显示MessageBox DWORD dwOEP; // 程序入口点 PBYTE lpStartVA; // 起始.text段地址 PBYTE lpEndVA; // 结束.text段地址}GLOBAL_PARAM, *PGLOBAL_PARAM;extern "C"__declspec(dllexport) GLOBAL_PARAM g_stcParam;
之后我们手动定义一个入口函数,如我们前面所提到的原因所说,我们需要将dll的加载代码去掉,所以我们使用naked关键字修饰
#pragma comment(linker, "/entry:"StubEntryPoint"") // 指定程序入口函数为StubEntryPoint()void start(){//这里存放对被加壳文件的操作}void __declspec(naked) StubEntryPoint(){ __asm sub esp, 0x50; // 分配0x50的栈空间 start(); // 执行Stub代码 __asm add esp, 0x50; // 平衡堆栈 __asm retn;}
显然,我们的Stub的核心之一就是解密,我们需要编写一个解密函数,从导入变量中获取起始地址与结束地址,之后解密。
void Decrypt(){// 在导出的全局变量中读取需解密区域的起始与结束VA PBYTE lpStart = g_stcParam.lpStartVA; PBYTE lpEnd = g_stcParam.lpEndVA;while (lpStart < lpEnd) { *lpStart ^= 0x15; lpStart++; }}
这样我们的原始的Stub就完成了。
2.3 添加API调用功能
- 但是这样还不够,为了方便扩展,我们需要添加对API调用的支持。例如,我们的程序可能会出现一些兼容性问题,那么让它触发异常时ExitProcess(0)就能提高兼容性。
- 这一部分主要包括
- 获取kernel32.dll基地址
- 根据kernel32.dll基地址获取GetProcAddress基地址
- 根据GetProcAddress获得API地址并定义API指针
2.3.1 获取kernel32.dlll的基地址
- 我们在第一部分的最后一段已经提到过,由于没有导入表,我们需要通过GetProcAddress动态获得API地址后调用。而GetProcAddress位于kernel32.dll中,所以我们的问题就从调用API转换为了获取kernel32的地址。
- 查询资料可得,fs寄存器指向TEB,而TEB结构我们可以在Windbg中查看。
kd> dt _tebnt!_TEB +0x000 NtTib : _NT_TIB +0x01c EnvironmentPointer : Ptr32 Void +0x020 ClientId : _CLIENT_ID +0x028 ActiveRpcHandle : Ptr32 Void +0x02c ThreadLocalStoragePointer : Ptr32 Void +0x030 ProcessEnvironmentBlock : Ptr32 _PEB
PEB在TEB偏移0x30的位置上,而查看PEB结构如下
kd> dt _pebnt!_PEB +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x003 SpareBool : UChar +0x004 Mutant : Ptr32 Void +0x008 ImageBaseAddress : Ptr32 Void +0x00c Ldr : Ptr32 _PEB_LDR_DATA
从PEB中获得_PEB_LDR_DATA,windbg查看LDR_DATA定义
0:000> dt 0x77847880 _PEB_LDR_DATA ntdll!_PEB_LDR_DATA +0x000 Length : 0x30 +0x004 Initialized : 0x1 '' +0x008 SsHandle : (null) +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x3126f8 - 0x36bd60 ] +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x312700 - 0x36bd68 ] +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x312798 - 0x36bd70 ] +0x024 EntryInProgress : (null) +0x028 ShutdownInProgress : 0 '' +0x02c ShutdownThreadId : (null)
从MSDN上获得对InInitializationOrderModuleList的描述(MSDN上对_PEB_LDR_DATA定义与windbg上有所不同,此处以windbg为准)
The head of a doubly-linked list that contains the loaded modules for the process. Each item in the list is a pointer to an LDR_DATA_TABLE_ENTRY structure.
查看MSDN的LDR_DATA_TABLE_ENTRY的定义
typedef struct _LDR_DATA_TABLE_ENTRY {PVOID Reserved1[2];LIST_ENTRY InMemoryOrderLinks;PVOID Reserved2[2];PVOID DllBase;PVOID EntryPoint;PVOID Reserved3;UNICODE_STRING FullDllName;BYTE Reserved4[8];PVOID Reserved5[3];union {ULONG CheckSum;PVOID Reserved6;};ULONG TimeDateStamp;} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
此时我们就看到了我们希望得到的DllBase,我们可以遍历搜索,当名字与”kernel32.dll”重合时获取DllBase,而由于每个操作系统中的Dll加载顺序恒定,kernel32.dll一般都是第二个加载的。因此我们可以通过如下方式获取
extern DWORD GetKernel32Base(); // 获取Kernel32.dll的模块基址DWORD GetKernel32Base(){DWORD dwKernel32Addr = 0;__asm{push eaxmov eax,dword ptr fs:[0x30] // eax = PEB的地址mov eax,[eax+0x0C] // eax = 指向PEB_LDR_DATA结构的指针mov eax,[eax+0x1C] // eax = 模块初始化链表的头指针InInitializationOrderModuleListmov eax,[eax] // eax = 列表中的第二个条目mov eax,[eax+0x08] // eax = 获取到的Kernel32.dll基址mov dwKernel32Addr,eaxpop eax}return dwKernel32Addr;}
- 2.3.2 获取GetProcAddress的地址
- 获得基址之后,我们根据PE结构从kernel32.dll中的导出表中获得”GetProcAddress”
- 我们可以通过查询资料得到,IAT表所属的IMAGE_DIRECTORY_ENTRY_EXPORT位于IMAGE_NT_HEADER->IMAGE_OPTIONAL_HEADER->DataDirectory,那我们先获取导出表
DWORD dwAddrBase = GetKernel32Base(); // 1. 获取DOS头、NT头PIMAGE_DOS_HEADER pDos_Header;PIMAGE_NT_HEADERS pNt_Header;pDos_Header = (PIMAGE_DOS_HEADER)dwAddrBase;pNt_Header = (PIMAGE_NT_HEADERS)(dwAddrBase + pDos_Header->e_lfanew); // 2. 获取导出表项PIMAGE_DATA_DIRECTORY pDataDir;PIMAGE_EXPORT_DIRECTORY pExport;pDataDir = pNt_Header->OptionalHeader.DataDirectory; pDataDir = &pDataDir[IMAGE_DIRECTORY_ENTRY_EXPORT];pExport = (PIMAGE_EXPORT_DIRECTORY)(dwAddrBase + pDataDir->VirtualAddress);...
然后我们查看IMAGE_EXPORT_DIRECTORY的结构,如下所示。此处需要注意的是AddressOf…三个都是指针
typedef struct _IMAGE_EXPORT_DIRECTORY {DWORD Characteristics;DWORD TimeDateStamp;WORD MajorVersion;WORD MinorVersion;DWORD Name;DWORD Base;DWORD NumberOfFunctions;DWORD NumberOfNames;DWORD AddressOfFunctions; // RVA from base of imageDWORD AddressOfNames; // RVA from base of imageDWORD AddressOfNameOrdinals; // RVA from base of image} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
三个指针结构如下所示
根据上述结构,我们可以根据函数名字遍历链表获得GetProcAddress地址
// 3、获取导出表的必要信息 DWORD dwModOffset = pExport->Name; // 模块的名称 DWORD dwFunCount = pExport->NumberOfFunctions; // 导出函数的数量 DWORD dwNameCount = pExport->NumberOfNames; // 导出名称的数量 PDWORD pEAT = (PDWORD)(dwAddrBase + pExport->AddressOfFunctions); // 获取地址表的RVA PDWORD pENT = (PDWORD)(dwAddrBase + pExport->AddressOfNames); // 获取名称表的RVA PWORD pEIT = (PWORD)(dwAddrBase + pExport->AddressOfNameOrdinals); //获取索引表的RVA// 4、获取GetProAddress函数的地址for (DWORD i = 0; i < dwFunCount; i++) {if (!pEAT[i]) {continue; }// 4.1 获取序号 DWORD dwID = pExport->Base + i;// 4.2 变量EIT 从中获取到 GetProcAddress的地址for (DWORD dwIdx = 0; dwIdx < dwNameCount; dwIdx++) {// 序号表中的元素的值 对应着函数地址表的位置if (pEIT[dwIdx] == i) {//根据序号获取到名称表中的名字 DWORD dwNameOffset = pENT[dwIdx]; char * pFunName = (char*)(dwAddrBase + dwNameOffset);//判断是否是GetProcAddress函数if (!strcmp(pFunName, "GetProcAddress")) {// 获取EAT的地址 并将GetProcAddress地址返回 DWORD dwFunAddrOffset = pEAT[i];return dwAddrBase + dwFunAddrOffset; } } } }
2.3.3获取API
- 那么之后我们使用typedef定义需要的API指针,然后用GetProcAddress获得的API地址给指针赋值,之后我们就可以使用这些API了,另:自己定义的指针名不能与API名相同
LPGETPROCADDRESS g_funGetProcAddress = nullptr; LPLOADLIBRARYEX g_funLoadLibraryEx = nullptr; LPEXITPROCESS g_funExitProcess = nullptr; LPMESSAGEBOX g_funMessageBox = nullptr; LPGETMODULEHANDLE g_funGetModuleHandle = nullptr; LPVIRTUALPROTECT g_funVirtualProtect = nullptr; bool InitializationAPI(){ HMODULE hModule; // 1. 初始化基础API 这里使用的是LoadLibraryExW g_funGetProcAddress = (LPGETPROCADDRESS)GetGPAFunAddr(); g_funLoadLibraryEx = (LPLOADLIBRARYEX)g_funGetProcAddress((HMODULE)GetKernel32Base(), "LoadLibraryExW"); // 2. 初始化其他API hModule = NULL; if (!(hModule = g_funLoadLibraryEx(L"kernel32.dll", NULL, NULL))) return false; g_funExitProcess = (LPEXITPROCESS)g_funGetProcAddress(hModule, "ExitProcess"); hModule = NULL; if (!(hModule = g_funLoadLibraryEx(L"user32.dll", NULL, NULL))) return false; g_funMessageBox = (LPMESSAGEBOX)g_funGetProcAddress(hModule, "MessageBoxW"); hModule = NULL; if (!(hModule = g_funLoadLibraryEx(L"kernel32.dll", NULL, NULL))) return false; g_funGetModuleHandle = (LPGETMODULEHANDLE)g_funGetProcAddress(hModule, "GetModuleHandleW"); hModule = NULL; if (!(hModule = g_funLoadLibraryEx(L"kernel32.dll", NULL, NULL))) return false; g_funVirtualProtect = (LPVIRTUALPROTECT)g_funGetProcAddress(hModule, "VirtualProtect"); return true;
- 之后我们完善一下我们的start函数
extern __declspec(dllexport) GLOBAL_PARAM g_stcParam={0};void start(){// 1. 初始化所有APIif (!InitializationAPI()) return; // 2. 解密宿主程序 Decrypt(); // 3. 跳转到OEP __asm jmp g_stcParam.dwOEP; }
这样一来我们就完成了DLL的Stub部分
三、Stub的植入
之后我们就需要将Stub植入了。
但在此之前我们需要先将带加壳文件加载入内存并预处理文件
- 这一部分主要包括:
- 加载目标文件
- 目标文件预处理
- 植入Stub
- 保存文件
3.1 加载目标文件
- 由于我们只是普通的加载目标文件而不是按照内存的格式展开,所以加载过程非常简单,但之后操作要注意将所有VirtualAddress的地址转换为FileOffset。
// 2. 获取文件信息,并映射进内存中if (INVALID_HANDLE_VALUE == (hFile_In = CreateFile(strPath, GENERIC_READ, FILE_SHARE_READ,NULL, OPEN_EXISTING, 0, NULL))) {return false; }if (INVALID_FILE_SIZE == (dwFileSize = GetFileSize(hFile_In, NULL))) { CloseHandle(hFile_In);return false; }if (!(lpFileImage = VirtualAlloc(NULL, dwFileSize * 2, MEM_COMMIT, PAGE_READWRITE))) { CloseHandle(hFile_In);return false; } DWORD dwRet;if (!ReadFile(hFile_In, lpFileImage, dwFileSize, &dwRet, NULL)) { CloseHandle(hFile_In); VirtualFree(lpFileImage, 0, MEM_RELEASE);return false; }
3.2目标文件预处理
- 所谓目标文件预处理就是将.text段加密,之后运行时由植入的Stub部分解密后才会恢复正常。
- 那么我们只需要将获取的目标文件信息传入即可执行,代码如下:
// 3. 获取PE文件信息 objProcPE.GetPeInfo(lpFileImage, dwFileSize, &stcPeInfo); // 4. 获取目标文件代码段的起始结束信息 // 读取第一个区段的相关信息,并将其加密(默认第一个区段为代码段) PBYTE lpStart = (PBYTE)(stcPeInfo.pSectionHeader->PointerToRawData + (DWORD)lpFileImage); PBYTE lpEnd = (PBYTE)((DWORD)lpStart + stcPeInfo.pSectionHeader->SizeOfRawData); PBYTE lpStartVA = (PBYTE)(stcPeInfo.pSectionHeader->VirtualAddress + stcPeInfo.dwImageBase); PBYTE lpEndVA = (PBYTE)((DWORD)lpStartVA + stcPeInfo.pSectionHeader->SizeOfRawData); // 5. 对文件进行预处理 Pretreatment(lpStart, lpEnd, stcPeInfo); void Pretreatment(PBYTE lpCodeStart, PBYTE lpCodeEnd, PE_INFO stcPeInfo) { // 1. 加密指定区域while (lpCodeStart < lpCodeEnd) { *lpCodeStart ^= 0x15; lpCodeStart++; } // 2. 给第一个区段附加上可写属性 PDWORD pChara = &(stcPeInfo.pSectionHeader->Characteristics); *pChara = *pChara | IMAGE_SCN_MEM_WRITE; }
- 那么之后我们只需将Stub植入就可以保存文件了(上述处理PE结构使用了一个CProcessingPE类,定义之后详细给出)
3.3植入Stub
- 植入Stub可以说是DLL型壳中的最大难点。其主要包括以下部分
- 将Stub读入缓冲区
- 在原文件中增加区块
- 修复Stub的重定位
- 将Stub复制到原文件中
- 计算并设置新的OEP
- 其中由于篇幅所限,本人仅仅介绍增加区块以及Stub重定位修复部分。
3.3.1区块增加
- PE文件中的区块与PE结构中的SectionHeader有关,所以为了增加一个Section,我们首先应该定位到SectionTable的起始位置。
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(m_pNt_Header);// 1. 获取基本信息DWORD dwDosSize = m_pDos_Header->e_lfanew;DWORD dwPeSize = sizeof(IMAGE_NT_HEADERS32);DWORD dwStnSize = m_pNt_Header->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER);DWORD dwHeadSize = dwDosSize+dwPeSize+dwStnSize;
我们查询一下关于PE文件的SectionTable的结构,单个SectionHeader的结构如下
#define IMAGE_SIZEOF_SHORT_NAME 8typedef struct _IMAGE_SECTION_HEADER{0X00 BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //节(段)的名字.text/.data/.rdata/等//由于长度固定8字节,所以可以没有结束符0X08 union{ DWORD PhysicalAddress; //物理地址 DWORD VirtualSize; //虚拟大小 }Misc;0X0C DWORD VirtualAddress; //块的RVA,相对虚拟地址0X10 DWORD SizeOfRawData; //该节在文件对齐后的尺寸大小(FileAlignment的整数倍)0X14 DWORD PointerToRawData; //节区在文件中的偏移量//0X18 DWORD PointerToRelocations; //重定位偏移(obj中使用)//0X1C DWORD PointerToLinenumbers; //行号表偏移(调试用)//0X20 WORD NumberOfRelocations; //重定位项目数(obj中使用)//0X22 WORD NumberOfLinenumbers; //行号表中行号的数目0X24 DWORD Characteristics; //节属性(按bit位设置属性)} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;#define IMAGE_SIZEOF_SECTION_HEADER 40
- 那么我们应该相应的组装一个类似的结构,然后增加区块数目并将组装的SectionHeader赋值到目标文件中。
// 2.4 组装一个新的区段头 IMAGE_SECTION_HEADER stcNewSect = {0}; CopyMemory(stcNewSect.Name, szVarName, 7); // 区段名称 stcNewSect.Misc.VirtualSize = dwVirtualSize; // 虚拟大小 stcNewSect.VirtualAddress = dwVStart; // 虚拟地址 stcNewSect.SizeOfRawData = dwSizeOfRawData; // 文件大小 stcNewSect.PointerToRawData = dwFStart; // 文件地址 stcNewSect.Characteristics = dwChara; // 区段属性 // 2.5 写入指定位置 CopyMemory( (PVOID)((DWORD)m_dwFileDataAddr+dwHeadSize), &stcNewSect, sizeof(IMAGE_SECTION_HEADER) ); // 3. 修改区段数目字段NumberOfSections m_pNt_Header->FileHeader.NumberOfSections++;
- 之后我们需要将SizeOfImage大小增加我们Stub的大小(要求按照SectionAlignment对齐)并将我们的Stub复制进新的区块。
// 4. 修改PE文件的景象尺寸字段SizeOfImage m_pNt_Header->OptionalHeader.SizeOfImage += dwVirtualSize;// 5. 返回新区段的详细信息、大小,以及可直接访问的地址 CopyMemory(pNewSection, &stcNewSect, sizeof(IMAGE_SECTION_HEADER)); *lpSize = dwSizeOfRawData;return (PVOID)(m_dwFileDataAddr+dwFStart); }
3.3.2修复重定位
- 修复重定位决定了我们的Stub能否在目标程序中正常运行,同样,我们首先需要先定位RelocTable。
// 1. 获取映像基址与代码段指针DWORD dwImageBase;PVOID lpCode;dwImageBase = m_pNt_Header->OptionalHeader.ImageBase;lpCode = (PVOID)( (DWORD)m_dwFileDataAddr + RVAToOffset(m_pNt_Header->OptionalHeader.BaseOfCode) );
- 之后我们查看一下RelocTable的结构
- 每个IMAGE_BASE_RELOCATION元素包含了VirtualAddress、SizeOfBlock,后边跟着数目不定的重定位项,所以重定位项的数量n就等于(SizeOfBlock-sizeof IMAGE_BASE_RELOCATION)÷2。
- 那么我们只需要遍历重定位表,逐个进行修复。注意,此处重定位表的地址与实际内存地址不同,应该先减去基址计算为偏移,之后加上当前基址计算。
// 2. 获取重定位表在内存中的地址 PIMAGE_DATA_DIRECTORY pDataDir; PIMAGE_BASE_RELOCATION pReloc; pDataDir = m_pNt_Header->OptionalHeader.DataDirectory+IMAGE_DIRECTORY_ENTRY_BASERELOC; pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)m_dwFileDataAddr + RVAToOffset(pDataDir->VirtualAddress)); // 3. 遍历重定位表,并对目标代码进行重定位while ( pReloc->SizeOfBlock && pReloc->SizeOfBlock < 0x100000 ) {// 3.1 取得重定位项TypeOffset与其数量 PWORD pTypeOffset = (PWORD)((DWORD)pReloc+sizeof(IMAGE_BASE_RELOCATION)); DWORD dwCount = (pReloc->SizeOfBlock-sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); // 3.2 循环检查重定位项for ( DWORD i=0; i {if ( !*pTypeOffset ) continue; // 3.2.1 获取此重定位项指向的指针 DWORD dwPointToRVA = (*pTypeOffset&0x0FFF)+pReloc->VirtualAddress; PDWORD pPtr = (PDWORD)(RVAToOffset(dwPointToRVA)+(DWORD)m_dwFileDataAddr);// 3.2.2 计算重定位增量值 DWORD dwIncrement = dwLoadImageAddr - dwImageBase;// 3.2.3 修复需重定位的地址数据 *((PDWORD)pPtr) += dwIncrement; pTypeOffset++; } // 3.3 指向下一个重定位块,开始另一次循环 pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pReloc + pReloc->SizeOfBlock); }
由此我们成功修复了重定位表
3.4 CProcessingPE
- 在此前植入Stub的过程中我们为了方便手动定义了一个CProcessingPE类,下面附上这个类的成员以及方法供参考。由于较为重要的方法此前都已经有介绍,此处就不在赘述。
CProcessingPE::CProcessingPE(void) { ZeroMemory(&m_stcPeInfo, sizeof(PE_INFO)); } CProcessingPE::~CProcessingPE(void) { }/********************************************************************** 相对虚拟地址(RVA)转文件偏移(Offset)* 此函数负责将传入的RVA转换为Offset。** 注意:此转换函数并未考虑到所有细节,但是在绝大多数情况可以正常运转。** 参数:* ULONG uRvaAddr:RVA地址值* * 返回值:* DWORD:成功返回Offset,失败则返回0*********************************************************************/ DWORD CProcessingPE::RVAToOffset(ULONG uRvaAddr) { PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(m_pNt_Header); for (DWORD i=0; iFileHeader.NumberOfSections; i++) { if((pSectionHeader[i].VirtualAddress <= uRvaAddr) && (pSectionHeader[i].VirtualAddress + pSectionHeader[i].SizeOfRawData > uRvaAddr)) { return (pSectionHeader[i].PointerToRawData + (uRvaAddr - pSectionHeader[i].VirtualAddress)); } } return 0; }/********************************************************************** 文件偏移(Offset)转相对虚拟地址(RVA)* 此函数负责将传入的Offset转换为RVA。** 注意:此转换函数并未考虑到所有细节,但是在绝大多数情况可以正常运转。** 参数:* ULONG uOffsetAddr:Offset地址值* * 返回值:* DWORD:成功返回RVA地址,失败则返回0*********************************************************************/ DWORD CProcessingPE::OffsetToRVA(ULONG uOffsetAddr) { PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(m_pNt_Header); for (DWORD i=0; iFileHeader.NumberOfSections; i++) { if((pSectionHeader[i].PointerToRawData <= uOffsetAddr) && (pSectionHeader[i].PointerToRawData + pSectionHeader[i].SizeOfRawData > uOffsetAddr)) { return (pSectionHeader[i].VirtualAddress + (uOffsetAddr - pSectionHeader[i].PointerToRawData)); } } return 0; }/********************************************************************** 获取PE文件信息* 此函数负责获取目标文件的关键PE信息。** 参数:* LPVOID lpImageData:目标文件所在缓存区的指针* DWORD dwImageSize:目标文件的大小* PPE_INFO pPeInfo :[OUT]用于传出目标文件的关键PE信息* * 返回值:* BOOL:成功返回true,失败则返回false*********************************************************************/ BOOL CProcessingPE::GetPeInfo(LPVOID lpImageData, DWORD dwImageSize, PPE_INFO pPeInfo) { // 1、判断映像指针是否有效 if ( m_stcPeInfo.dwOEP ) { CopyMemory(pPeInfo, &m_stcPeInfo, sizeof(PE_INFO)); return true; } else { if ( !lpImageData ) return false; m_dwFileDataAddr = (DWORD)lpImageData; m_dwFileDataSize = dwImageSize; } // 2. 获取基本信息 // 2.1 获取DOS头、NT头 m_pDos_Header = (PIMAGE_DOS_HEADER)lpImageData; m_pNt_Header = (PIMAGE_NT_HEADERS)((DWORD)lpImageData+m_pDos_Header->e_lfanew); // 2.2 获取OEP m_stcPeInfo.dwOEP = m_pNt_Header->OptionalHeader.AddressOfEntryPoint; // 2.3 获取映像基址 m_stcPeInfo.dwImageBase = m_pNt_Header->OptionalHeader.ImageBase; // 2.4 获取关键数据目录表的内容 PIMAGE_DATA_DIRECTORY lpDataDir = m_pNt_Header->OptionalHeader.DataDirectory; m_stcPeInfo.pDataDir = lpDataDir; CopyMemory(&m_stcPeInfo.stcExport, lpDataDir+IMAGE_DIRECTORY_ENTRY_EXPORT, sizeof(IMAGE_DATA_DIRECTORY)); // 2.5 获取区段表与其他详细信息 m_stcPeInfo.pSectionHeader = IMAGE_FIRST_SECTION(m_pNt_Header); // 3. 检查PE文件是否有效 if ( (m_pDos_Header->e_magic!=IMAGE_DOS_SIGNATURE) || (m_pNt_Header->Signature!=IMAGE_NT_SIGNATURE) ) { // 这不是一个有效的PE文件 return false; } // 4. 传出处理结果 CopyMemory(pPeInfo, &m_stcPeInfo, sizeof(PE_INFO)); return true; }/********************************************************************** 修复重定位项* 此函数负责修复映像的重定位项,此函数依赖于RVAToOffset函数。* * 注意:* 1. dwLoadImageAddr指的并非是其本身ImageBase的值,而是其被加载后的预* 计模块基址。* 2. 此重定位函数并未考虑到修复类型问题,如果要提高兼容性,应该分别对* 三种重定位类型进行区别对待。** 参数:* DWORD dwLoadImageAddr:此映像被加载后的预计模块基址* * 返回值:无*********************************************************************/ void CProcessingPE::FixReloc(DWORD dwLoadImageAddr) { // 1. 获取映像基址与代码段指针 DWORD dwImageBase; PVOID lpCode; dwImageBase = m_pNt_Header->OptionalHeader.ImageBase; lpCode = (PVOID)( (DWORD)m_dwFileDataAddr + RVAToOffset(m_pNt_Header->OptionalHeader.BaseOfCode) ); // 2. 获取重定位表在内存中的地址 PIMAGE_DATA_DIRECTORY pDataDir; PIMAGE_BASE_RELOCATION pReloc; pDataDir = m_pNt_Header->OptionalHeader.DataDirectory+IMAGE_DIRECTORY_ENTRY_BASERELOC; pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)m_dwFileDataAddr + RVAToOffset(pDataDir->VirtualAddress)); // 3. 遍历重定位表,并对目标代码进行重定位 while ( pReloc->SizeOfBlock && pReloc->SizeOfBlock < 0x100000 ) { // 3.1 取得重定位项TypeOffset与其数量 PWORD pTypeOffset = (PWORD)((DWORD)pReloc+sizeof(IMAGE_BASE_RELOCATION)); DWORD dwCount = (pReloc->SizeOfBlock-sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); // 3.2 循环检查重定位项 for ( DWORD i=0; iVirtualAddress; PDWORD pPtr = (PDWORD)(RVAToOffset(dwPointToRVA)+(DWORD)m_dwFileDataAddr); // 3.2.2 计算重定位增量值 DWORD dwIncrement = dwLoadImageAddr - dwImageBase; // 3.2.3 修复需重定位的地址数据 *((PDWORD)pPtr) += dwIncrement; pTypeOffset++; } // 3.3 指向下一个重定位块,开始另一次循环 pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pReloc + pReloc->SizeOfBlock); } }/********************************************************************** 获取PE文件信息* 此函数负责获取目标文件的关键PE信息。** 参数:* LPVOID lpImageData:目标文件所在缓存区的指针* DWORD dwImageSize:目标文件的大小* PPE_INFO pPeInfo :[OUT]用于传出目标文件的关键PE信息* * 返回值:* BOOL:成功返回true,失败则返回false*********************************************************************/ PVOID CProcessingPE::GetExpVarAddr(LPCTSTR strVarName) { // 1、获取导出表地址,并将参数strVarName转为ASCII形式,方便对比查找 CHAR szVarName[MAX_PATH] = {0}; PIMAGE_EXPORT_DIRECTORY lpExport = (PIMAGE_EXPORT_DIRECTORY)(m_dwFileDataAddr + RVAToOffset(m_stcPeInfo.stcExport.VirtualAddress)); WideCharToMultiByte(CP_ACP, NULL, strVarName, -1, szVarName, _countof(szVarName), NULL, FALSE); // 2、循环读取导出表输出项的输出函数,并依次与szVarName做比对,如果相同,则取出相对应的函数地址 for (DWORD i=0; iNumberOfNames; i++) { PDWORD pNameAddr = (PDWORD)(m_dwFileDataAddr+RVAToOffset(lpExport->AddressOfNames+i)); PCHAR strTempName = (PCHAR)(m_dwFileDataAddr + RVAToOffset(*pNameAddr)); if ( !strcmp(szVarName, strTempName) ) { PDWORD pFunAddr = (PDWORD)(m_dwFileDataAddr+RVAToOffset(lpExport->AddressOfFunctions+i)); return (PVOID)(m_dwFileDataAddr + RVAToOffset(*pFunAddr)); } } return 0; }/********************************************************************** 添加区段函数* 此函数负责在目标文件中添加一个自定义的区段。** 注:* 此函数并未考虑到目标函数存在附加数据等细节问题。** 参数:* LPCTSTR strName :新区段的名称* DWORD dwSize :新区段的最小体积* DWORD dwChara :新区段的属性* PIMAGE_SECTION_HEADER pNewSection:[OUT]新区段的段结构指针* PDWORD lpSize :[OUT]新区段的最终大小* * 返回值:* PVOID:成功返回指向新区段现在所在内存的指针*********************************************************************/ PVOID CProcessingPE::AddSection(LPCTSTR strName, DWORD dwSize, DWORD dwChara, PIMAGE_SECTION_HEADER pNewSection, PDWORD lpSize) { PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(m_pNt_Header); // 1. 获取基本信息 DWORD dwDosSize = m_pDos_Header->e_lfanew; DWORD dwPeSize = sizeof(IMAGE_NT_HEADERS32); DWORD dwStnSize = m_pNt_Header->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER); DWORD dwHeadSize = dwDosSize+dwPeSize+dwStnSize; // 2. 在区段表中加入新区段的信息 // 2.1 获取基本信息 CHAR szVarName[7] = {0}; DWORD dwFileAlign = m_pNt_Header->OptionalHeader.FileAlignment; // 文件粒度 DWORD dwSectAlign = m_pNt_Header->OptionalHeader.SectionAlignment; // 区段粒度 WORD dwNumOfsect = m_pNt_Header->FileHeader.NumberOfSections; // 区段数目 // 2.2 获取最后一个区段的信息 IMAGE_SECTION_HEADER stcLastSect = {0}; CopyMemory(&stcLastSect, &pSectionHeader[dwNumOfsect-1], sizeof(IMAGE_SECTION_HEADER)); // 2.3 根据区段粒度计算相应地址信息 DWORD dwVStart = 0; // 虚拟地址起始位置 DWORD dwFStart = stcLastSect.SizeOfRawData + stcLastSect.PointerToRawData; // 文件地址起始位置 if ( stcLastSect.Misc.VirtualSize%dwSectAlign ) dwVStart = (stcLastSect.Misc.VirtualSize / dwSectAlign+1) * dwSectAlign + stcLastSect.VirtualAddress; else dwVStart = (stcLastSect.Misc.VirtualSize / dwSectAlign ) * dwSectAlign + stcLastSect.VirtualAddress; DWORD dwVirtualSize = 0; // 区段虚拟大小 DWORD dwSizeOfRawData = 0; // 区段文件大小 if ( dwSize%dwSectAlign) dwVirtualSize = (dwSize / dwSectAlign+1) * dwSectAlign; else dwVirtualSize = (dwSize / dwSectAlign ) * dwSectAlign; if ( dwSize%dwFileAlign ) dwSizeOfRawData = (dwSize / dwFileAlign+1) * dwFileAlign; else dwSizeOfRawData = (dwSize / dwFileAlign ) * dwFileAlign; WideCharToMultiByte(CP_ACP, NULL, strName, -1, szVarName, _countof(szVarName), NULL, FALSE); // 2.4 组装一个新的区段头 IMAGE_SECTION_HEADER stcNewSect = {0}; CopyMemory(stcNewSect.Name, szVarName, 7); // 区段名称 stcNewSect.Misc.VirtualSize = dwVirtualSize; // 虚拟大小 stcNewSect.VirtualAddress = dwVStart; // 虚拟地址 stcNewSect.SizeOfRawData = dwSizeOfRawData; // 文件大小 stcNewSect.PointerToRawData = dwFStart; // 文件地址 stcNewSect.Characteristics = dwChara; // 区段属性 // 2.5 写入指定位置 CopyMemory( (PVOID)((DWORD)m_dwFileDataAddr+dwHeadSize), &stcNewSect, sizeof(IMAGE_SECTION_HEADER) ); // 3. 修改区段数目字段NumberOfSections m_pNt_Header->FileHeader.NumberOfSections++; // 4. 修改PE文件的景象尺寸字段SizeOfImage m_pNt_Header->OptionalHeader.SizeOfImage += dwVirtualSize; // 5. 返回新区段的详细信息、大小,以及可直接访问的地址 CopyMemory(pNewSection, &stcNewSect, sizeof(IMAGE_SECTION_HEADER)); *lpSize = dwSizeOfRawData; return (PVOID)(m_dwFileDataAddr+dwFStart); }/********************************************************************** 修改目标文件OEP* 此函数负责修改目标文件OEP。** 参数:* DWORD dwOEP:新OEP* * 返回值:无*********************************************************************/ void CProcessingPE::SetOEP(DWORD dwOEP) { m_pNt_Header->OptionalHeader.AddressOfEntryPoint = dwOEP; }
总结
壳的编写看似简单,实际有不少难点。本文中没有涉及IAT表的加密以及一系列反调试,混淆手段,只是作为一个壳的原型方便读者了解壳编写的流程以及其中的难点,为下一步深入学习打下基础。
本人纯属萌新,本文如有错漏,希望各位大佬不吝指正。
本文参考的代码来自于《黑客免杀攻防》,如有需要源码可以自行前往华章图 书的官网下载。