壳代码
上一节写到了,壳代码是写在shell.dll里的(看雪的大佬写的)。所以分析dll文件就很有必要。
一.Shell.dll的源码和二进制文件信息
1.1 壳代码
这里就直接粘贴了,涉及到的细节下面讨论。觉得太长了可以直接跳过
shell.h
#ifdef SHELL_EXPORTS
#define SHELL_API __declspec(dllexport)
#else
#define SHELL_API __declspec(dllimport)
#endif
#include<Windows.h>
//导出ShellData结构体
extern"C" typedef struct _SHELL_DATA
{
DWORD dwStartFun; //启动函数
DWORD dwPEOEP; //程序入口点
DWORD dwXorKey; //解密KEY
DWORD dwCodeBase; //代码段起始地址
DWORD dwXorSize; //代码段加密大小
DWORD dwPEImageBase; //PE文件映像基址
IMAGE_DATA_DIRECTORY stcPERelocDir; //重定位表信息
IMAGE_DATA_DIRECTORY stcPEImportDir; //导入表信息
DWORD dwIATSectionBase; //IAT所在段基址
DWORD dwIATSectionSize; //IAT所在段大小
BOOL bIsShowMesBox; //是否显示MessageBox
}SHELL_DATA, *PSHELL_DATA;
//导出ShellData结构体变量
extern"C" SHELL_API SHELL_DATA g_stcShellData;
//Shell部分用到的函数的类型定义,均用函数指针类型表示
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);
shell.h中导出了变量g_stcShellData
。fnGetProcAddress
,fnLoadLibraryA
等相当于自定义的类型。
shell.cpp
这段有点长,是完整代码。觉得又臭又长的话可以先跳到下面一个部分。
#include"Shell.h"
#pragma comment(linker, "/merge:.data=.text")
#pragma comment(linker, "/merge:.rdata=.text")
#pragma comment(linker, "/section:.text,RWE")
//函数和变量的声明
DWORD MyGetProcAddress(); //自定义GetProcAddress
HMODULE GetKernel32Addr(); //获取Kernel32加载基址
void Start(); //启动函数(Shell部分的入口函数)
void InitFun(); //初始化函数指针和变量
void DeXorCode(); //解密操作
void RecReloc(); //修复重定位操作
void RecIAT(); //修复IAT操作
SHELL_DATA g_stcShellData = { (DWORD)Start }; // 注意这里,首先将g_stcShellData ->dwStartFun设为start()函数的地址
//Shell用到的全局变量结构体
DWORD dwImageBase = 0; //整个程序的镜像基址
DWORD dwPEOEP = 0; //PE文件的OEP
//Shell部分用到的函数指针定义
fnGetProcAddress g_pfnGetProcAddress = NULL;
fnLoadLibraryA g_pfnLoadLibraryA = NULL;
fnGetModuleHandleA g_pfnGetModuleHandleA = NULL;
fnVirtualProtect g_pfnVirtualProtect = NULL;
fnVirtualAlloc g_pfnVirtualAlloc = NULL;
fnExitProcess g_pfnExitProcess = NULL;
fnMessageBox g_pfnMessageBoxA = NULL;
//************************************************************
// 函数名称: Start
// 函数说明: 启动函数(Shell部分的入口函数)
// 作 者: cyxvc
// 时 间: 2015/12/28
// 返 回 值: void
//************************************************************
//__declspec(naked)是编译器直接拿来用的汇编函数代码,所以一定要记得在开始的// 时候保存上下文标志位(压栈),在结束的时候要记得恢复上下文(出栈)。
__declspec(naked) void Start()
{
__asm pushad
InitFun();
DeXorCode();
if (g_stcShellData.stcPERelocDir.VirtualAddress)
{
RecReloc();
}
RecIAT();
if (g_stcShellData.bIsShowMesBox)
{
g_pfnMessageBoxA(0, "欢迎使用CyxvcProtect, by 15PB !", "Hello!", 0);
}
__asm popad
//获取OEP信息
dwPEOEP = g_stcShellData.dwPEOEP + dwImageBase;
__asm jmp dwPEOEP
g_pfnExitProcess(0); //实际不会执行此条指令
}
//************************************************************
// 函数名称: RecIAT
// 函数说明: 修复IAT操作
// 作 者: cyxvc
// 时 间: 2015/12/28
// 返 回 值: void
//************************************************************
void RecIAT()
{
//1.获取导入表结构体指针
PIMAGE_IMPORT_DESCRIPTOR pPEImport =
(PIMAGE_IMPORT_DESCRIPTOR)(dwImageBase + g_stcShellData.stcPEImportDir.VirtualAddress);
//2.修改内存属性为可写
DWORD dwOldProtect = 0;
g_pfnVirtualProtect(
(LPBYTE)(dwImageBase + g_stcShellData.dwIATSectionBase), g_stcShellData.dwIATSectionSize,
PAGE_EXECUTE_READWRITE, &dwOldProtect);
//3.开始修复IAT
while (pPEImport->Name)
{
//获取模块名
DWORD dwModNameRVA = pPEImport->Name;
char* pModName = (char*)(dwImageBase + dwModNameRVA);
HMODULE hMod = g_pfnLoadLibraryA(pModName);
//获取IAT信息(有些PE文件INT是空的,最好用IAT解析,也可两个都解析作对比)
PIMAGE_THUNK_DATA pIAT = (PIMAGE_THUNK_DATA)(dwImageBase + pPEImport->FirstThunk);
//获取INT信息(同IAT一样,可将INT看作是IAT的一个备份)
//PIMAGE_THUNK_DATA pINT = (PIMAGE_THUNK_DATA)(dwImageBase + pPEImport->OriginalFirstThunk);
//通过IAT循环获取该模块下的所有函数信息(这里之获取了函数名)
while (pIAT->u1.AddressOfData)
{
//判断是输出函数名还是序号
if (IMAGE_SNAP_BY_ORDINAL(pIAT->u1.Ordinal))
{
//输出序号
DWORD dwFunOrdinal = (pIAT->u1.Ordinal) & 0x7FFFFFFF;
DWORD dwFunAddr = g_pfnGetProcAddress(hMod, (char*)dwFunOrdinal);
*(DWORD*)pIAT = (DWORD)dwFunAddr;
}
else
{
//输出函数名
DWORD dwFunNameRVA = pIAT->u1.AddressOfData;
PIMAGE_IMPORT_BY_NAME pstcFunName = (PIMAGE_IMPORT_BY_NAME)(dwImageBase + dwFunNameRVA);
DWORD dwFunAddr = g_pfnGetProcAddress(hMod, pstcFunName->Name);
*(DWORD*)pIAT = (DWORD)dwFunAddr;
}
pIAT++;
}
//遍历下一个模块
pPEImport++;
}
//4.恢复内存属性
g_pfnVirtualProtect(
(LPBYTE)(dwImageBase + g_stcShellData.dwIATSectionBase), g_stcShellData.dwIATSectionSize,
dwOldProtect, &dwOldProtect);
}
//************************************************************
// 函数名称: RecReloc
// 函数说明: 修复重定位操作
// 作 者: cyxvc
// 时 间: 2015/12/28
// 返 回 值: void
//************************************************************
void RecReloc()
{
typedef struct _TYPEOFFSET
{
WORD offset : 12; //偏移值
WORD Type : 4; //重定位属性(方式)
}TYPEOFFSET, *PTYPEOFFSET;
//1.获取重定位表结构体指针
PIMAGE_BASE_RELOCATION pPEReloc =
(PIMAGE_BASE_RELOCATION)(dwImageBase + g_stcShellData.stcPERelocDir.VirtualAddress);
//2.开始修复重定位
while (pPEReloc->VirtualAddress)
{
//2.1修改内存属性为可写
DWORD dwOldProtect = 0;
g_pfnVirtualProtect((PBYTE)dwImageBase + pPEReloc->VirtualAddress,
0x1000, PAGE_EXECUTE_READWRITE, &dwOldProtect);
//2.2修复重定位
PTYPEOFFSET pTypeOffset = (PTYPEOFFSET)(pPEReloc + 1);
DWORD dwNumber = (pPEReloc->SizeOfBlock - 8) / 2;
for (DWORD i = 0; i < dwNumber; i++)
{
if (*(PWORD)(&pTypeOffset[i]) == NULL)
break;
//RVA
DWORD dwRVA = pTypeOffset[i].offset + pPEReloc->VirtualAddress;
//FAR地址
DWORD AddrOfNeedReloc = *(PDWORD)((DWORD)dwImageBase + dwRVA);
*(PDWORD)((DWORD)dwImageBase + dwRVA) =
AddrOfNeedReloc - g_stcShellData.dwPEImageBase + dwImageBase;
}
//2.3恢复内存属性
g_pfnVirtualProtect((PBYTE)dwImageBase + pPEReloc->VirtualAddress,
0x1000, dwOldProtect, &dwOldProtect);
//2.4修复下一个区段
pPEReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pPEReloc + pPEReloc->SizeOfBlock);
}
}
//************************************************************
// 函数名称: DeXorCode
// 函数说明: 解密操作
// 作 者: cyxvc
// 时 间: 2015/12/28
// 返 回 值: void
//************************************************************
void DeXorCode()
{
PBYTE pCodeBase = (PBYTE)g_stcShellData.dwCodeBase + dwImageBase;
DWORD dwOldProtect = 0;
g_pfnVirtualProtect(pCodeBase, g_stcShellData.dwXorSize, PAGE_EXECUTE_READWRITE, &dwOldProtect);
for (DWORD i = 0; i < g_stcShellData.dwXorSize; i++)
{
pCodeBase[i] ^= i;
}
g_pfnVirtualProtect(pCodeBase, g_stcShellData.dwXorSize, dwOldProtect, &dwOldProtect);
}
//************************************************************
// 函数名称: InitFun
// 函数说明: 初始化函数指针和变量
// 作 者: cyxvc
// 时 间: 2015/12/28
// 返 回 值: void
//************************************************************
void InitFun()
{
//从Kenel32中获取函数
HMODULE hKernel32 = GetKernel32Addr();
g_pfnGetProcAddress = (fnGetProcAddress)MyGetProcAddress();
g_pfnLoadLibraryA = (fnLoadLibraryA)g_pfnGetProcAddress(hKernel32, "LoadLibraryA");
g_pfnGetModuleHandleA = (fnGetModuleHandleA)g_pfnGetProcAddress(hKernel32, "GetModuleHandleA");
g_pfnVirtualProtect = (fnVirtualProtect)g_pfnGetProcAddress(hKernel32, "VirtualProtect");
g_pfnExitProcess = (fnExitProcess)g_pfnGetProcAddress(hKernel32, "ExitProcess");
g_pfnVirtualAlloc = (fnVirtualAlloc)g_pfnGetProcAddress(hKernel32, "VirtualAlloc");
//从user32中获取函数
HMODULE hUser32 = g_pfnLoadLibraryA("user32.dll");
g_pfnMessageBoxA = (fnMessageBox)g_pfnGetProcAddress(hUser32, "MessageBoxA");
//初始化镜像基址
dwImageBase = (DWORD)g_pfnGetModuleHandleA(NULL);
}
//************************************************************
// 函数名称: GetKernel32Addr
// 函数说明: 获取Kernel32加载基址
// 作 者: cyxvc
// 时 间: 2015/12/28
// 返 回 值: HMODULE
//************************************************************
HMODULE GetKernel32Addr()
{
HMODULE 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下第三个条目是Kernel32.dll)
mov dwKernel32Addr, eax
pop eax
}
return dwKernel32Addr;
}
//************************************************************
// 函数名称: MyGetProcAddress
// 函数说明: 自定义GetProcAddress
// 作 者: cyxvc
// 时 间: 2015/12/28
// 返 回 值: DWORD
//************************************************************
DWORD MyGetProcAddress()
{
HMODULE hModule = GetKernel32Addr();
//1.获取DOS头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(PBYTE)hModule;
//2.获取NT头
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((PBYTE)hModule + pDosHeader->e_lfanew);
//3.获取导出表的结构体指针
PIMAGE_DATA_DIRECTORY pExportDir =
&(pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);
PIMAGE_EXPORT_DIRECTORY pExport =
(PIMAGE_EXPORT_DIRECTORY)((PBYTE)hModule + pExportDir->VirtualAddress);
//EAT
PDWORD pEAT = (PDWORD)((DWORD)hModule + pExport->AddressOfFunctions);
//ENT
PDWORD pENT = (PDWORD)((DWORD)hModule + pExport->AddressOfNames);
//EIT
PWORD pEIT = (PWORD)((DWORD)hModule + pExport->AddressOfNameOrdinals);
//4.遍历导出表,获取GetProcAddress()函数地址
DWORD dwNumofFun = pExport->NumberOfFunctions;
DWORD dwNumofName = pExport->NumberOfNames;
for (DWORD i = 0; i < dwNumofFun; i++)
{
//如果为无效函数,跳过
if (pEAT[i] == NULL)
continue;
//判断是以函数名导出还是以序号导出
DWORD j = 0;
for (; j < dwNumofName; j++)
{
if (i == pEIT[j])
{
break;
}
}
if (j != dwNumofName)
{
//如果是函数名方式导出的
//函数名
char* ExpFunName = (CHAR*)((PBYTE)hModule + pENT[j]);
//进行对比,如果正确返回地址
if (!strcmp(ExpFunName, "GetProcAddress"))
{
return pEAT[i] + pNtHeader->OptionalHeader.ImageBase;
}
}
else
{
//序号
}
}
return 0;
}
start函数
关于shell.cpp,首先注意最上面几行中SHELL_DATA g_stcShellData = { (DWORD)Start };
这里将导出变量g_stcShellData
的dwStartFun
成员变量设为start()
函数的地址。
其次是start
函数
//************************************************************
// 函数名称: Start
// 函数说明: 启动函数(Shell部分的入口函数)
// 作 者: cyxvc
// 时 间: 2015/12/28
// 返 回 值: void
//************************************************************
//__declspec(naked)是编译器直接拿来用的汇编函数代码,所以一定要记得在开始的// 时候保存上下文标志位(压栈),在结束的时候要记得恢复上下文(出栈)。
__declspec(naked) void Start()
{
__asm pushad
InitFun();
DeXorCode();
if (g_stcShellData.stcPERelocDir.VirtualAddress)
{
RecReloc();
}
RecIAT();
if (g_stcShellData.bIsShowMesBox)
{
g_pfnMessageBoxA(0, "欢迎使用CyxvcProtect, by 15PB !", "Hello!", 0);
}
__asm popad
//获取OEP信息
dwPEOEP = g_stcShellData.dwPEOEP + dwImageBase;
__asm jmp dwPEOEP
g_pfnExitProcess(0); //实际不会执行此条指令
}
start
函数是壳代码的入口函数,那它什么时候执行呢?就是把目标程序(Test.exe)被加壳之后。双击Test.exe,首先执行start函数,再执行原Test.exe的入口程序。
InitFun();
为初始化函数,初始化些变量,有的已经被加壳器设置好了。DoXorCode
定义如下:
void DeXorCode()
{
PBYTE pCodeBase = (PBYTE)g_stcShellData.dwCodeBase + dwImageBase; //获取被加壳代码代码段地址,示例代码中的计算出来为 0x401000
DWORD dwOldProtect = 0;
// g_pfnVirtualProtect为kernel32.dll的VirtualProtect函数
g_pfnVirtualProtect(pCodeBase, g_stcShellData.dwXorSize, PAGE_EXECUTE_READWRITE, &dwOldProtect);
for (DWORD i = 0; i < g_stcShellData.dwXorSize; i++)
{
pCodeBase[i] ^= i;
}
g_pfnVirtualProtect(pCodeBase, g_stcShellData.dwXorSize, dwOldProtect, &dwOldProtect);
}
执行shell中的代码时,原始代码已经被加壳了,所以需要先脱壳。g_stcShellData.dwXorSize
为加壳器设置好的变量,即代码段长度,示例中的为0xA00。PAGE_EXECUTE_READWRITE(0x40)
表示修改脱壳后的代码段可读可写可执行。
RecReloc();
表示修复重定位表,有个if说明不一定执行,这个不太明白,先放着。
RecIAT
为修复IAT表,执行过程如下
void RecIAT()
{
//1.获取导入表结构体指针
PIMAGE_IMPORT_DESCRIPTOR pPEImport =
(PIMAGE_IMPORT_DESCRIPTOR)(dwImageBase + g_stcShellData.stcPEImportDir.VirtualAddress); // 0x402244
//2.修改内存属性为可写(IAT所在区段)
DWORD dwOldProtect = 0;
g_pfnVirtualProtect(
(LPBYTE)(dwImageBase + g_stcShellData.dwIATSectionBase), g_stcShellData.dwIATSectionSize,
PAGE_EXECUTE_READWRITE, &dwOldProtect);
//3.开始修复IAT
while (pPEImport->Name)
{
//获取模块名
DWORD dwModNameRVA = pPEImport->Name;
char* pModName = (char*)(dwImageBase + dwModNameRVA);
HMODULE hMod = g_pfnLoadLibraryA(pModName);
//获取IAT信息(有些PE文件INT是空的,最好用IAT解析,也可两个都解析作对比)
PIMAGE_THUNK_DATA pIAT = (PIMAGE_THUNK_DATA)(dwImageBase + pPEImport->FirstThunk);
//获取INT信息(同IAT一样,可将INT看作是IAT的一个备份)
//PIMAGE_THUNK_DATA pINT = (PIMAGE_THUNK_DATA)(dwImageBase + pPEImport->OriginalFirstThunk);
//通过IAT循环获取该模块下的所有函数信息(这里之获取了函数名)
while (pIAT->u1.AddressOfData)
{
//判断是输出函数名还是序号
if (IMAGE_SNAP_BY_ORDINAL(pIAT->u1.Ordinal))
{
//输出序号
DWORD dwFunOrdinal = (pIAT->u1.Ordinal) & 0x7FFFFFFF;
DWORD dwFunAddr = g_pfnGetProcAddress(hMod, (char*)dwFunOrdinal);
*(DWORD*)pIAT = (DWORD)dwFunAddr;
}
else
{
//输出函数名
DWORD dwFunNameRVA = pIAT->u1.AddressOfData;
PIMAGE_IMPORT_BY_NAME pstcFunName = (PIMAGE_IMPORT_BY_NAME)(dwImageBase + dwFunNameRVA);
DWORD dwFunAddr = g_pfnGetProcAddress(hMod, pstcFunName->Name);
*(DWORD*)pIAT = (DWORD)dwFunAddr;
}
pIAT++;
}
//遍历下一个模块
pPEImport++;
}
//4.恢复内存属性
g_pfnVirtualProtect(
(LPBYTE)(dwImageBase + g_stcShellData.dwIATSectionBase), g_stcShellData.dwIATSectionSize,
dwOldProtect, &dwOldProtect);
}
示例代码IAT表结构体信息如下,其VirtualAddress为0x2244
其中,g_stcShellData.dwIATSectionBase
为IAT所在区段的起始,示例代码有.text, .rdata, .data等5个区段,IAT base为0x2244,.rdata为0x2000, .data为0x3000。所以IAT在.rdata段。经过一系列操作(中间还没太弄懂),最后需要恢复内存属性不可执行。
然后start函数输出"欢迎使用CyxvcProtect, by 15PB !", "Hello!"
表示执行了壳代码,之后便用__asm jmp dwPEOEP
跳到原先示例代码的OEP。
1.2 加壳器代码
加壳器代码太多了,就选几个重点的
入口函数
BOOL CPACK::Pack(CString strFilePath, BOOL bIsShowMesBox)
{
//1.读取PE文件信息并保存
CPE objPE;
if (objPE.InitPE(strFilePath) == FALSE)
return FALSE;
//2.加密代码段操作
DWORD dwXorSize = 0;
dwXorSize=objPE.XorCode(0x15);
//3.将必要的信息保存到Shell
HMODULE hShell = LoadLibrary(L"Shell.dll");
if (hShell == NULL)
{
MessageBox(NULL, _T("加载Shell.dll模块失败,请确保程序的完整性!"), _T("提示"), MB_OK);
//释放资源
delete[] objPE.m_pFileBuf;
return FALSE;
}
PSHELL_DATA pstcShellData = (PSHELL_DATA)GetProcAddress(hShell, "g_stcShellData");
pstcShellData->dwXorKey = 0x15;
pstcShellData->dwCodeBase = objPE.m_dwCodeBase;
pstcShellData->dwXorSize = dwXorSize;
pstcShellData->dwPEOEP = objPE.m_dwPEOEP;
pstcShellData->dwPEImageBase = objPE.m_dwImageBase;
pstcShellData->stcPERelocDir = objPE.m_PERelocDir;
pstcShellData->stcPEImportDir = objPE.m_PEImportDir;
pstcShellData->dwIATSectionBase = objPE.m_IATSectionBase;
pstcShellData->dwIATSectionSize = objPE.m_IATSectionSize;
pstcShellData->bIsShowMesBox = bIsShowMesBox;
//4.将Shell附加到PE文件
//4.1.读取Shell代码
MODULEINFO modinfo = { 0 };
GetModuleInformation(GetCurrentProcess(), hShell, &modinfo, sizeof(MODULEINFO));
PBYTE pShellBuf = new BYTE[modinfo.SizeOfImage];
memcpy_s(pShellBuf, modinfo.SizeOfImage, hShell, modinfo.SizeOfImage);
//4.2.设置Shell重定位信息
objPE.SetShellReloc(pShellBuf, (DWORD)hShell);
//4.3.修改被加壳程序的OEP,指向Shell
DWORD dwShellOEP = pstcShellData->dwStartFun - (DWORD)hShell;
objPE.SetNewOEP(dwShellOEP);
//4.4.合并PE文件和Shell的代码到新的缓冲区
LPBYTE pFinalBuf = NULL;
DWORD dwFinalBufSize = 0;
objPE.MergeBuf(objPE.m_pFileBuf, objPE.m_dwImageSize,
pShellBuf, modinfo.SizeOfImage,
pFinalBuf, dwFinalBufSize);
//5.保存文件(处理完成的缓冲区)
SaveFinalFile(pFinalBuf, dwFinalBufSize, strFilePath);
//6.释放资源
delete[] objPE.m_pFileBuf;
delete[] pShellBuf;
delete[] pFinalBuf;
objPE.InitValue();
return TRUE;
}
InitPE
负责读取PE基础信息。XorCode
从代码段起始加密代码。之后就是从shell.dll加载壳代码,首先就是获取shell.dll中导出的结构体变量pstcShellData
,并对其进行设置。之后就是一些设置和设置重定向信息(这部分不是很懂)。
InitPE
这里InitPE
并不只是简单读取文件,而是将PE文件以内存映像的方式读取,也就是会创建更大的缓冲区来模拟内存分布,所以加密代码段时并不需要考虑磁盘文件分布。
BOOL CPE::InitPE(CString strFilePath)
{
//打开文件
if (OpenPEFile(strFilePath) == FALSE)
return FALSE;
//将PE以文件分布格式读取到内存
m_dwFileSize = GetFileSize(m_hFile, NULL);
m_pFileBuf = new BYTE[m_dwFileSize];
DWORD ReadSize = 0;
ReadFile(m_hFile, m_pFileBuf, m_dwFileSize, &ReadSize, NULL);
CloseHandle(m_hFile);
m_hFile = NULL;
//判断是否为PE文件
if (IsPE() == FALSE)
return FALSE;
//将PE以内存分布格式读取到内存
//修正没镜像大小没有对齐的情况
m_dwImageSize = m_pNtHeader->OptionalHeader.SizeOfImage;
m_dwMemAlign = m_pNtHeader->OptionalHeader.SectionAlignment;
m_dwSizeOfHeader = m_pNtHeader->OptionalHeader.SizeOfHeaders;
m_dwSectionNum = m_pNtHeader->FileHeader.NumberOfSections;
if (m_dwImageSize % m_dwMemAlign)
m_dwImageSize = (m_dwImageSize / m_dwMemAlign + 1) * m_dwMemAlign;
LPBYTE pFileBuf_New = new BYTE[m_dwImageSize];
memset(pFileBuf_New, 0, m_dwImageSize);
//拷贝文件头
memcpy_s(pFileBuf_New, m_dwSizeOfHeader, m_pFileBuf, m_dwSizeOfHeader);
//拷贝区段
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(m_pNtHeader);
for (DWORD i = 0; i < m_dwSectionNum; i++, pSectionHeader++)
{
memcpy_s(pFileBuf_New + pSectionHeader->VirtualAddress,
pSectionHeader->SizeOfRawData,
m_pFileBuf+pSectionHeader->PointerToRawData,
pSectionHeader->SizeOfRawData);
}
delete[] m_pFileBuf;
m_pFileBuf = pFileBuf_New;
pFileBuf_New = NULL;
//获取PE信息
GetPEInfo();
return TRUE;
}
拷贝shell.dll中的壳代码
下面这段代码可以看出这是将整个shell.dll的代码拷贝出来
MODULEINFO modinfo = { 0 };
GetModuleInformation(GetCurrentProcess(), hShell, &modinfo, sizeof(MODULEINFO));
PBYTE pShellBuf = new BYTE[modinfo.SizeOfImage];
memcpy_s(pShellBuf, modinfo.SizeOfImage, hShell, modinfo.SizeOfImage);
之后就是将被加壳后程序的OEP设置成壳代码start
函数相对于shell.dll的OEP。这里注意,这个objPE.SetNewOEP
展开如下:
void CPE::SetNewOEP(DWORD dwShellOEP)
{
m_dwShellOEP = dwShellOEP + m_dwImageSize;
m_pNtHeader->OptionalHeader.AddressOfEntryPoint = m_dwShellOEP;
}
也就是加上了原PE文件的Image Size。
MergeBuf
然后就是MergeBuf
,pShellBuf
为shell.dll内容的首地址,pShellBufSize
为shellBuf
的大小。
void CPE::MergeBuf(LPBYTE pFileBuf, DWORD pFileBufSize,
LPBYTE pShellBuf, DWORD pShellBufSize,
LPBYTE& pFinalBuf, DWORD& pFinalBufSize)
{
//获取最后一个区段的信息
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuf;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pFileBuf + pDosHeader->e_lfanew);
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);
PIMAGE_SECTION_HEADER pLastSection =
&pSectionHeader[pNtHeader->FileHeader.NumberOfSections - 1];
//1.修改区段数量
pNtHeader->FileHeader.NumberOfSections += 1;
//2.编辑区段表头结构体信息
PIMAGE_SECTION_HEADER AddSectionHeader =
&pSectionHeader[pNtHeader->FileHeader.NumberOfSections - 1];
memcpy_s(AddSectionHeader->Name, 8, ".cyxvc", 7);
//VOffset(1000对齐)
DWORD dwTemp = 0;
dwTemp = (pLastSection->Misc.VirtualSize / m_dwMemAlign) * m_dwMemAlign;
if (pLastSection->Misc.VirtualSize % m_dwMemAlign)
{
dwTemp += 0x1000;
}
AddSectionHeader->VirtualAddress = pLastSection->VirtualAddress + dwTemp;
//Vsize(实际添加的大小)
AddSectionHeader->Misc.VirtualSize = pShellBufSize;
//ROffset(旧文件的末尾)
AddSectionHeader->PointerToRawData = pFileBufSize;
//RSize(200对齐)
dwTemp = (pShellBufSize / m_dwFileAlign) * m_dwFileAlign;
if (pShellBufSize % m_dwFileAlign)
{
dwTemp += m_dwFileAlign;
}
AddSectionHeader->SizeOfRawData = dwTemp;
//标志
AddSectionHeader->Characteristics = 0XE0000040;
//3.修改PE头文件大小属性,增加文件大小
dwTemp = (pShellBufSize / m_dwMemAlign) * m_dwMemAlign;
if (pShellBufSize % m_dwMemAlign)
{
dwTemp += m_dwMemAlign;
}
pNtHeader->OptionalHeader.SizeOfImage += dwTemp;
//4.申请合并所需要的空间
pFinalBuf = new BYTE[pFileBufSize + dwTemp];
pFinalBufSize = pFileBufSize + dwTemp;
memset(pFinalBuf, 0, pFileBufSize + dwTemp);
memcpy_s(pFinalBuf, pFileBufSize, pFileBuf, pFileBufSize);
memcpy_s(pFinalBuf + pFileBufSize, dwTemp, pShellBuf, dwTemp);
}
这里面包括几个步骤
- 在原先PE中将节的数量+1
pNtHeader->FileHeader.NumberOfSections += 1;
- 设置好新的节数据区
AddSectionHeader
,名字叫.cyxvc
- 将原PE文件头中文件大小加大,
pNtHeader->OptionalHeader.SizeOfImage += dwTemp;
- 将壳代码插入到PE文件尾部。
二.DLL基础
2.1 DllMain简介
跟exe有个main
或者WinMain
入口函数一样,DLL也有一个入口函数,就是DllMain
。以“DllMain”为关键字。在MSDN的介绍中。The DllMain function is an optional method of entry into a dynamic-link library (DLL) 。也就是说,DllMain
是可有可无的。
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
return TRUE;
}
2.2 何时调用DllMain
系统是在什么时候调用DllMain
函数的呢?静态链接时,或动态链接时调用LoadLibrary
和FreeLibrary
都会调用DllMain
函数。DllMain
的第二个参数ul_reason_for_call
指明了系统调用Dll的原因,它可能有以下取值
DLL_PROCESS_ATTACH
:1,当Dll被进程第一次调用时,导致DllMain
函数被调用, 如果同一个进程后来再次调用此Dll时,操作系统只会增加Dll的使用次数,不会再用DLL_PROCESS_ATTACH
调用DLL的DllMain
函数。DLL_PROCESS_DETACH
: 0,当Dll被从进程的地址空间解除映射时,系统调用了它的DllMain
,传递的ul_reason_for_call
值是DLL_PROCESS_DETACH
。如果进程的终结是因为调用了TerminateProcess
,系统就不会用DLL_PROCESS_DETACH
来调用DLL
的DllMain
函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。DLL_THREAD_ATTACH
:2,当进程创建一线程时,系统查看当前映射到进程地址空间中的所有Dll文件映像,并用值DLL_THREAD_ATTACH
调用Dll的DllMain
函数。DLL_THREAD_DETACH
:3,如果线程调用了ExitThread
来结束线程(线程函数返回时,系统也会自动调用ExitThread
),系统查看当前映射到进程空间中的所有Dll文件映像,并用DLL_THREAD_DETACH
来调用DllMain
函数。
2.3 Dll导出函数
这里用到了__declspec(dllexport)
修饰符,但是shell.cpp里并没有导出函数,而是导出了一个结构体变量g_stcShellData
。
extern"C" SHELL_API SHELL_DATA g_stcShellData;
三.加壳后的示例代码
3.1 010Editor
-
NT可选头:
可以看到AddressEntryPoint从0x1159变成了0x7230。SizeOfImage变成了0xC000,以前是0x6000。其它重要信息并无变化 -
节表
可以看到多了一个cyxvc节。其它数据并无变化
cyxvc的磁盘偏移,RVA,Size均为0x6000。
-
代码段
可以看到代码段数据全变0了。 -
节区
跟之前相比多了一个cyxvc节,并且长度为0x6000(一个Image的长度)
3.2 IDA
start函数
这个start就是shell代码的start,也就是先执行壳代码
4441的16进制为0x1159,也就是赋值操作是重新设置OEP,jump即跳到OEP。
执行效果和加壳前一模一样。下一节,我会来调试以下加壳程序和加壳后的test程序运行过程。