本文中的代码均来自《黑客免杀攻防》,如要转载,需写明来源,请勿用于非法用途,作者对此文章中的代码造成的任何后果不负法律责任。
看了一遍书本的第11章,感觉内容确实比较高级,之前虽然自认为是热衷于搞C/C++开发,但是运用windows API的开发真的是太少了。先来看看一个简单的壳的编写。
这个加壳脱壳程序分为三部分,首先是用MFC写的界面程序,这个自然不用多说,主要功能就是调用加壳部分生成的dll文件中的导出函数对PE文件加壳;第二部分则是加壳程序,当然不仅仅是加壳,这其实上就是获得被加壳的PE文件的信息,然后根据这些信息将第三部分,也就是真正的壳加入到PE文件中去,并且修改一系列的值,使得这个壳能够发挥作用;第三部分自然就是真正的壳了,其中也涉及到一些对于PE文件的操作,但是相对于第二部分来说就简单一些。现在来看看具体的步骤,由于第三部分是基础,前两部分都是依靠第三部分来写的,因此按照书上的顺序,先说如何写这个壳。
首先,为了减少壳的体积,这里的代码使用了以下处理:
#pragma comment(linker,"/entry:\"StubEntryPoint\"") // 指定程序入口函数为StubEntryPoint()
#pragma comment(linker,"/merge:.data=.text") //将.data合并到.text
#pragma comment(linker,"/merge:.rdata=.text") //将.rdata合并到.text
因为后面第二部分需要对text进行修改,写入一些参数,所以要修改.text的属性。
#pragma comment(linker,"/section:.text,RWE") // 将.text段的属性设置为可读、可写、可执行
如果按照原有的程序入口点的话,就会有很多初始化环境等代码,所以这里采用了自己指定程序入口点。同时加入了一些健壮性的代码:
__asm subesp,0x50; // 抬高栈顶,提高兼容性
start(); // 执行壳的主体部分
__asm addesp,0x50; // 平衡堆栈
// 主动调用ExitProcess函数退出进程可以解决一些兼容性问题
if ( g_funExitProcess)
{
g_funExitProcess(0);
}
__asm retn;
而其中的start函数则是真正壳的执行部分。下面来看看这个主函数:
void start()
{
// 1. 初始化所有API
if (!InitializationAPI() ) return;
// 2. 解密宿主程序
Decrypt();
// 3. 询问是否执行解密后的程序
if (g_stcParam.bShowMessage )
{
int nRet =g_funMessageBox(
NULL,L"解密完成,是否运行原程序?",L"解密完成",MB_OKCANCEL);
if (IDCANCEL== nRet) return;
}
// 4. 跳转到OEP
__asm jmpg_stcParam.dwOEP;
}
可以看到主函数非常简单,由四个步骤完成,但是3、4步都很简单,所以这里只讲前两步。
这一块应该是内容最多的一块了,因为这里需要定义所有的函数变量以及初始化这些变量。首先来看变量的定义:
// 声明一个导出的全局变量,用以保存传递给Stub部分的参数
typedef struct _GLOBAL_PARAM
{
BOOL bShowMessage; // 是否显示解密信息
DWORDdwOEP; // 程序入口点
PBYTElpStartVA; // 起始虚拟地址(被异或加密区)
PBYTElpEndVA; // 结束虚拟地址(被异或加密区)
}GLOBAL_PARAM,*PGLOBAL_PARAM;
extern "C"__declspec(dllexport) GLOBAL_PARAMg_stcParam;
这里采用的结构体定义的一些在脱壳的时候需要用到的参数,这些参数在Stub这个项目中并不会被初始化,而是在调用这个dll的加壳程序中初始化,这个等到后面再细说。注意最后一句是将这个结构体的一个变量导出,以便后面的初始化和调用。再来看看函数指针的调用:
// 基础API定义声明
Typedef DWORD (WINAPI *LPGETPROCADDRESS)(HMODULE,LPCSTR); //GetProcAddress
typedef HMODULE (WINAPI*LPLOADLIBRARYEX)(LPCTSTR,HANDLE,DWORD); // LoadLibaryEx
extern LPGETPROCADDRESSg_funGetProcAddress;
extern LPLOADLIBRARYEX g_funLoadLibraryEx;
从上面注释可以看到前两个分别是声明GetProcAddress和LoadLibaryEx两个函数的指针,接下来定义两个变量。至于为什么要声明这两个变量的指针呢?因为在使用后面代码脱壳时这个dll中的IAT表早已丢弃,因此要使程序可以自动获得API的地址,然后再进行调用,所以必须要用到这两个函数。
至于如何获得GetProcAddress的地址,相信看过0day这本书的同学都不会陌生,这里就贴出那里的图片,这里面需要首先获得GetProcAddress函数所在的kernel32.dll或是kernelBase.dll的基址,然后找到导出表的ENT,利用字符串匹配找到在EAT中相应的序号,最终获得地址:
既然已经得到了GetProcAddress的地址,那LoadLibaryEx的地址直接就出来了,因为它也是在同一个dll中的,不过为什么不用LoadLibary函数呢?因为LoadLibary函数在kernel32.dll中,win xp和之后的OS的dll链表是不一样的,在xp中第二项是kernel32.dll,win vista以及后面的系统第二项则是kernelBase.dll,第三项才是kernel32.dll,而这里的程序都是用链表的第二项,但kernelBase.dll并不包含LoadLibary,所以为了增加程序的兼容性,使用了LoadLibaryEx函数。
下面再来看其他函数指针的声明和定义:
// 其他API定义声明
typedef VOID (WINAPI *LPEXITPROCESS)(UINT); // ExitProcess
typedef int (WINAPI*LPMESSAGEBOX)(HWND,LPCTSTR,LPCTSTR,UINT); // MessageBox
typedef HMODULE (WINAPI *LPGETMODULEHANDLE)(LPCWSTR); //GetModuleHandle
typedef BOOL (WINAPI*LPVIRTUALPROTECT)(LPVOID,SIZE_T,DWORD,PDWORD);//VirtualProtect
extern LPEXITPROCESS g_funExitProcess;
extern LPMESSAGEBOX g_funMessageBox;
extern LPGETMODULEHANDLEg_funGetModuleHandle;
extern LPVIRTUALPROTECT g_funVirtualProtect;
有了上面的那两个函数,这几个函数的地址都很容易找到,先通过g_funLoadLibraryEx加载相应的dll文件到内存,然后使用g_funGetProcAddress获得API地址,赋值给这些指针备用。至此初始化已经全部完成。
初始化一旦完成,就可以利用之前设计的算法对原PE文件的.text区段进行解密。由于加解密算法不是这里的重点,所以这里只使用了简单的异或等处理,代码如下:
void Decrypt()
{// 在导出的全局变量中读取需解密区域的起始于结束VA
PBYTElpStart = g_stcParam.lpStartVA;
PBYTElpEnd = g_stcParam.lpEndVA;
//循环解密
while( lpStart { *lpStart-= 0x88; *lpStart^= 0xA1; lpStart++; } }