软件加壳的原理及实现
Posted lcword
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件加壳的原理及实现相关的知识,希望对你有一定的参考价值。
加壳的实现
我是个初学者,所知有限,难免会有错误,如果有人发现了错误,还请指正。
先大致说一下加壳的原理,即在原PE文件(后面称之为宿主文件)上加一个新的区段(也就是壳),然后从这个新的区段上开始运行;也就算是成功的加上了壳;下面我们就说一下具体的实现。
这个工程有两个项目,一个用来生成壳的Win32项目(dll类型),另一个是实现加壳的MFC项目;
加壳的项目界面是用MFC实现的,除了原有的类外,添加了两个新类,一个用于PE操作,
一个用于加壳。
下面说下加壳过程的实现:
先将原PE文件读取到内存;获取头文件信息,获得.text区段信息,然后对代码段进行加密(简单的异或加密);随后再用LoadLibrary将生成的壳(是一个dll)加载到内存;我们需要在壳的程序里对宿主PE进行解密,并且还要修复重定位,所以要把一些必要的数据存储到加载的壳里面;
申请空间将dll(壳)拷贝一份(此处大家可能会疑惑为什么要拷贝一份,因为我在以LoadLibrary加载进来的壳里直接修改需要重定位的地址信息时,程序运行会出错);
然后申请内存,大小是原宿主PE文件和壳的大小的和;先将宿主程序拷贝进去;
然后修复重定位信息,这个地方应该重点说明一下,我是直接把展开的整个dll拷贝到新PE文件里,并且打算壳的部分利用系统的重定位(每次加载PE文件的时候,如果重定位没有关掉,系统会进行一次重定位),所以在拷贝之前,要把需要重定位的地址修改成在新PE中的虚拟地址,并且我们的壳是通过LoadLibrary的方式加载的,已经被重定位过,我又是直接拷贝过来的,所以修改地址的时候要注意,后面会说到如何修改。
然后,合并PE文件和壳;设置新的OEP,既然加壳,当然要从壳的我们规定的开始位置开始执行,需要将其改成相对新PE开始位置的偏移,原理和修复重定位差不多,不过一个是相对虚拟地址,一个是虚拟地址。
如图:
相对虚拟地址=1+2;
如果修复重定位的话就某一地址的相对虚拟地址再加个默认基址;
下面是代码实现部分:
bool CPack::Pack(WCHAR * szPath)
{
CPe objPe;
//读取要被加壳的PE文件
DWORD dwReadFilSize = 0;
HANDLE hFile = CreateFile(szPath,GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
DWORD dwFileSize = GetFileSize(hFile, NULL);
char * pFileBuf = new char[dwFileSize];
memset(pFileBuf, 0, dwFileSize);
ReadFile(hFile, pFileBuf, dwFileSize, &dwReadFilSize, NULL);
//获取PE头文件信息
PEHEADERINFO pPeHead = { 0 };
objPe.GetPeHeaderinfo(pFileBuf, &pPeHead);
//加密
IMAGE_SECTION_HEADER pTxtSection;
objPe.GetSectionInfo(pFileBuf, &pTxtSection, ".text");
objPe.XorCode((LPBYTE)(pTxtSection.PointerToRawData + pFileBuf), pTxtSection.SizeOfRawData);
//用loadLibrary加载壳文件
HMODULE pLoadStubBuf = LoadLibrary(L"..\Release\Stub.dll");
//存储必要的信息
PPACKINFO PackInfoAdd = (PPACKINFO)GetProcAddress((HMODULE)pLoadStubBuf, "g_PackInfo");
PackInfoAdd->dwOriStartPoint = pPeHead.pOptionHeader->AddressOfEntryPoint; //需要跳转的OEP
PackInfoAdd->dwImageBase = pPeHead.pOptionHeader->ImageBase; //默认加载基址
PackInfoAdd->dwXorCode = pTxtSection.VirtualAddress; //加密代码段地址
PackInfoAdd->dwXorKey = 0xE; //加密密钥
PackInfoAdd->dwXorSize = pTxtSection.SizeOfRawData; //加密大小
PackInfoAdd->stcPeRelocDir = pPeHead.pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; //重定位表信息
PackInfoAdd->dwSizeOfImage = pPeHead.pOptionHeader->SizeOfImage; //原PE的大小
//拷贝一份
MODULEINFO stcModInfo = { 0 };
GetModuleInformation(GetCurrentProcess(), pLoadStubBuf, &stcModInfo, sizeof(MODULEINFO));
char * pStubBuf = new char[stcModInfo.SizeOfImage];
memset(pStubBuf, 0, stcModInfo.SizeOfImage);
memcpy(pStubBuf, pLoadStubBuf, stcModInfo.SizeOfImage);
//申请新空间存储新PE
int NewPeSize = objPe.GetAddSectionSize(pFileBuf, (char*)pLoadStubBuf, stcModInfo.SizeOfImage);
char * pNewPeBuf = new char[NewPeSize];
memset(pNewPeBuf, 0, NewPeSize);
memcpy(pNewPeBuf, pFileBuf, dwFileSize);
//修改重定位表
objPe.FixReloc(pStubBuf, pNewPeBuf);
// 添加一个区段
objPe.AddSection(pNewPeBuf, pStubBuf, stcModInfo.SizeOfImage);
//设置入口点
//自己定义的壳的开始位置的原始偏移
DWORD Offset = PackInfoAdd->dwStartPoint - (DWORD)pLoadStubBuf;
//相对于新PE的起始位置的偏移
unsigned int NewOep = Offset + pPeHead.pOptionHeader->SizeOfImage;
objPe.SetOep(pNewPeBuf, NewOep);
//保存成文件
SavePackFile(szPath, pNewPeBuf, NewPeSize);
//释放内存
delete[]pFileBuf;
delete[]pStubBuf;
delete[]pNewPeBuf;
return true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
下面我们就重点说一下修复重定位,通过重定位表找到能存储着需要重定位地址的地址,然后我们把这个地址存储的数据给改了就行了;其实修改重定位信息,简而言之就是把需要重定位的地址改成基于新基址的虚拟地址,这样程序在运行时才能根据真正的加载基址进行重定位;像我们这种情况,需要算出目标地址相对于壳的起始位置的偏移,加上宿主PE的默认加载基址,再加上这个壳加到PE之后的作为新区段的相对虚拟地址,也就是基于新PE的虚拟地址了。
并且由于相对虚拟地址是从0开始,所以头文件里的sizeofimage就是这个新区段的RVA。
比如说一个在内存中展开后大小(sizeofimage)为5000的宿主文件,相对虚拟地址为5000的地方就是新区段的起始位置(vitualaddress)。
如图:
还有几点应该注意,壳的部分我们要用系统进行重定位,在循环中要修改壳需要重定位的页的起始位置相对虚拟地址(重定位表的每一个PIMAGE_BASE_RELOCATION结构体变量描述的都是某个区段一个页的重定位信息),最后还要将重定位表的指针指向壳的重定位表,并且将重定位表的大小改成壳的重定位表的大小。
代码实现如下:
bool CPe::FixReloc(char* StubBuf, char*PeBuf)
{
//获取被加壳PE文件的重定位表指针
PIMAGE_DOS_HEADER pPeDos = (PIMAGE_DOS_HEADER)PeBuf;
PIMAGE_NT_HEADERS pPeNt = (PIMAGE_NT_HEADERS)(pPeDos->e_lfanew + PeBuf);
PIMAGE_DATA_DIRECTORY pPeRelocDir = &(pPeNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
//获取壳文件的重定位表指针
PIMAGE_DOS_HEADER pStuDos = (PIMAGE_DOS_HEADER)StubBuf;
PIMAGE_NT_HEADERS pStuNt = (PIMAGE_NT_HEADERS)(pStuDos->e_lfanew + StubBuf);
PIMAGE_DATA_DIRECTORY pStuRelocDir = pStuNt->OptionalHeader.DataDirectory;
pStuRelocDir = &(pStuRelocDir[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
//获取重定位目录
PIMAGE_BASE_RELOCATION pStuReloc = (PIMAGE_BASE_RELOCATION)((DWORD)StubBuf + pStuRelocDir->VirtualAddress);
//定义一个存储TypeOffset的结构体
typedef struct {
WORD Offset : 12;
WORD Type : 4;
}TypeOffset, *PTypeOffset;
//修复重定位信息
PTypeOffset pTypeOffset = (PTypeOffset)(pStuReloc + 1);
DWORD dwCount = (pStuReloc->SizeOfBlock - 8) / 2;
while (pStuReloc->VirtualAddress)
{
for (DWORD i = 0; i < dwCount; i++)
{
if (*(PDWORD)(&pTypeOffset[i]) == NULL)
{
break;
}
//存储重定位地址的相对虚拟地址
DWORD dwRVA = pStuReloc->VirtualAddress + pTypeOffset[i].Offset; //RVA
//重定位的地址
DWORD pRelocAddr = *(PDWORD)((DWORD)StubBuf + dwRVA);
//改掉(PDWORD)((DWORD)StubBuf + dwRVA)存储的值,也就是已经重定位的将来需要重定位的地址
*(PDWORD)((DWORD)StubBuf + dwRVA) = pRelocAddr - pStuNt->OptionalHeader.ImageBase
+ pPeNt->OptionalHeader.ImageBase + pPeNt->OptionalHeader.SizeOfImage;
}
//修改壳重定表中需要重定位的页的起始位置的相对虚拟地址
pStuReloc->VirtualAddress += pPeNt->OptionalHeader.SizeOfImage;
pStuReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pStuReloc + pStuReloc->SizeOfBlock);
}
//修改PE文件的重定位表指针
pPeRelocDir->Size = pStuRelocDir->Size;
pPeRelocDir->VirtualAddress = pStuRelocDir->VirtualAddress + pPeNt->OptionalHeader.SizeOfImage;
return true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
接下来讲一下添加区段,这个主要要注意更改PE头文件的信息,文件才能正常运行,不用对齐,我是把整个展开的dll拷贝过来的,按0x1000进行对齐展开的代码,按0x200对齐肯定没问题。要注意将新区段的属性得是可读可写可执行(我踩的一个坑),至于为什么后面再说;
下面是实现代码:
void CPe::AddSection(char* pBuf, char*pNewSection, int nSize)
{
PEHEADERINFO HeaderInfo = { 0 };
DWORD NumOfSection = 0;
if (IsPeFile(pBuf) == false)
{
return;
}
//获得PE的头文件信息
GetPeHeaderinfo(pBuf,&HeaderInfo);
//修改区段数量
NumOfSection = HeaderInfo.pFileHeader->NumberOfSections;
HeaderInfo.pFileHeader->NumberOfSections += 1;
//新增区段信息
PIMAGE_SECTION_HEADER pLastHeader = HeaderInfo.pSectionHeader + NumOfSection - 1;
PIMAGE_SECTION_HEADER pNewSecHeader = HeaderInfo.pSectionHeader + NumOfSection;
memcpy(pNewSecHeader->Name, "aStub", 6);
pNewSecHeader->Misc.VirtualSize = nSize;
pNewSecHeader->VirtualAddress = HeaderInfo.pOptionHeader->SizeOfImage;
int a = sizeof(*pBuf);
pNewSecHeader->PointerToRawData = pLastHeader->PointerToRawData + pLastHeader->SizeOfRawData;
pNewSecHeader->SizeOfRawData = nSize;
pNewSecHeader->Characteristics = 0xE0000020;
//修改镜像大小
HeaderInfo.pOptionHeader->SizeOfImage += nSize;
//把区段添加到PE文件中
memcpy(pBuf + pNewSecHeader->PointerToRawData, pNewSection, nSize);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
大家应该已经注意到我获取头文件信息 很多地方都是直接用的一个函数GetPeHeaderinfo,我是把获取头文件信息的操作封装成了函数;信息保存在一个自定义结构体类型的变量里;
typedef struct PEHEADERINFO
{
PIMAGE_FILE_HEADER pFileHeader;
PIMAGE_OPTIONAL_HEADER pOptionHeader;
PIMAGE_SECTION_HEADER pSectionHeader;
}PEHEADERINFO, *PPEHEADERINFO;
- 1
- 2
- 3
- 4
- 5
- 6
下面就说一下壳的实现:
既然前面对宿主PE的代码段进行加密,自然要在壳的代码里进行解密;在执行之前我们还要人工对原PE文件进行一次重定位;要让程序执行原PE文件;自然要跳转到原始的入口点,前面已经将原始入口点的相对虚拟地址保存到了壳里面。只要根据加载基址算出这个入口点的地址,到这个地址去执行就可以了。那么问题来了,我们的壳利用利用入口点直接开始执行了我们的代码,既没加载模块,也没加载资源什么的,无论是人工的进行重定位,还是获得当前的加载基址,都需要函数。所所以我们要人工的获取以下这些函数的地址。
首先,要先将可能要用到的函数定义好:
//Stub部分用到的函数的类型定义
typedef DWORD(WINAPI *fnGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName);
typedef HMODULE(WINAPI *fnLoadLibraryA)(_In_ LPCSTR lpLibFileName);
typedef HMODULE(WINAPI *fnGetModuleHandleA)(_In_opt_ LPCSTR lpModuleName);
typedef BOOL(WINAPI *fnVirtualProtect)(_In_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flNewProtect, _Out_ PDWORD lpflOldProtect);
typedef LPVOID(WINAPI *fnVirtualAlloc)(_In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect);
typedef void(WINAPI *fnExitProcess)(_In_ UINT uExitCode);
typedef int(WINAPI *fnMessageBox)(HWND hWnd, LPSTR lpText, LPSTR lpCaption, UINT uType);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
定义函数指针,保存函数地址
//需要获取的函数
fnGetProcAddress pfnGetProcAddress = NULL;
fnLoadLibraryA pfnLoadLibraryA = NULL;
fnVirtualProtect pfnVirtualProtect = NULL;
fnVirtualAlloc pfnVirtualAlloc = NULL;
fnGetModuleHandleA pfnGetModuleHandleA = NULL;
fnMessageBox pfnMessageBox = NULL;
fnExitProcess pfnExitProcess = NULL;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
好了该说一下,我上面提到的那个坑了,我开始没有把壳的这个区段保存成可读可写可执行的属性,导致获得的下面这些变量的值都无法保存;后来意识到这个问题,改了属性后果然正常。
下面是获取Kernel32.dll的代码
DWORD GetKernel32Addr()
{
DWORD dwKernel32Addr = 0;
_asm
{
push eax
mov eax, dword ptr fs : [0x30] //eax=PEB的地址
mov eax, [eax + 0x0C] //eax=PEB_LDR_DATA的指针
mov eax, [eax + 0x1C] //eax=模块初始化链表的指针,InInitializationOrderModuleList
mov eax, [eax] //eax=列表中的第二个条目
mov eax, [eax] //eax=列表中的第二个条目
mov eax, [eax + 0x08] //eax=获取到的Kernel32.dll基址(win7下获取的是KernelBase.dll
mov dwKernel32Addr, eax
pop eax
}
return dwKernel32Addr;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
然后用名称遍历导出表得到函数GetProcAddress的地址;
然后我们就可以利用GetProcAddress获得所需函数的地址:
void Init()
{
HMODULE hKernel32 = (HMODULE)GetKernel32Addr();
pfnGetProcAddress = (fnGetProcAddress)MyGetProcessAddress();
pfnLoadLibraryA = (fnLoadLibraryA)pfnGetProcAddress(hKernel32, "LoadLibraryA");
pfnVirtualProtect = (fnVirtualProtect)pfnGetProcAddress(hKernel32, "VirtualProtect");
pfnVirtualAlloc = (fnVirtualAlloc)pfnGetProcAddress(hKernel32, "VirtualAlloc");
pfnGetModuleHandleA = (fnGetModuleHandleA)pfnGetProcAddress(hKernel32, "GetModuleHandleA");
pfnExitProcess = (fnExitProcess)pfnGetProcAddress(hKernel32, "ExitProcess");
//获取messagebox
HMODULE hUser32 = pfnLoadLibraryA("user32.dll");
pfnMessageBox = (fnMessageBox)pfnGetProcAddress(hUser32, "MessageBoxA");
//加载基址
LoadBase = (DWORD)pfnGetModuleHandleA(NULL);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
下面就要利用得到的函数,对原宿主PE进行人工的重定位了,就是把需要重定位的虚拟地址改成真正的地址;用保存的地址-默认加载基址(加壳的过程中已保存)+实际的加载基址;在重定位的过程中要修改保护属性。
bool SelfReloc()
{
//获取重定位目录
PIMAGE_BASE_RELOCATION pStuReloc = (PIMAGE_BASE_RELOCATION)(LoadBase + g_PackInfo.stcPeRelocDir.VirtualAddress);
//定义一个存储TypeOffset的结构体
typedef struct {
WORD Offset : 12;
WORD Type : 4;
}TypeOffset, *PTypeOffset;
DWORD dwOldProtect = 0;
//修复重定位信息
while (pStuReloc->VirtualAddress)
{
//修改保护属性
//按道理来修改属性大小设置为0x1000就可以了,调试能力有限,暂时不知道为什么设置成0x2000才可以
pfnVirtualProtect((LPVOID)(LoadBase + pStuReloc->VirtualAddress), 0X2000, PAGE_EXECUTE_READWRITE, &dwOldProtect);
PTypeOffset pTypeOffset = (PTypeOffset)(pStuReloc + 1);
DWORD dwCount = (pStuReloc->SizeOfBlock - 8) / 2;
for (DWORD i = 0; i < dwCount; i++)
{
if (*(PDWORD)(&pTypeOffset[i]) == NULL)
{
break;
}
DWORD dwRVA = pStuReloc->VirtualAddress + pTypeOffset[i].Offset;
DWORD pRelocAddr = *(PDWORD)(LoadBase + dwRVA);
*(PDWORD)(LoadBase + dwRVA) = pRelocAddr - g_PackInfo.dwImageBase
+ LoadBase;
}
//恢复保护属性
pfnVirtualProtect((LPVOID)(LoadBase + pStuReloc->VirtualAddress), 0X2000, dwOldProtect, &dwOldProtect);
//获取描述下一个页重定位信息的结构体变量
pStuReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pStuReloc + pStuReloc->SizeOfBlock);
}
return true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
加壳的时候已经把原PE文件OEP相对虚拟地址保存下来,加上加载基址就是原PE入口点现在的地址,然后从那个地址执行就可以了:
void _declspec(naked) start()
{
Init();
DeXorCode();
SelfReloc();
_asm
{
push eax;
mov eax, LoadBase;
add eax, g_PackInfo.dwOriStartPoint;
mov dword ptr[esp], eax;
ret;
}
}
以上是关于软件加壳的原理及实现的主要内容,如果未能解决你的问题,请参考以下文章