《黑客免杀攻防学习笔记》——C++设计一个简单的壳1

本文中的代码均来自《黑客免杀攻防》,如要转载,需写明来源,请勿用于非法用途,作者对此文章中的代码造成的任何后果不负法律责任。

看了一遍书本的第11章,感觉内容确实比较高级,之前虽然自认为是热衷于搞C/C++开发,但是运用windows API的开发真的是太少了。先来看看一个简单的壳的编写。

         这个加壳脱壳程序分为三部分,首先是用MFC写的界面程序,这个自然不用多说,主要功能就是调用加壳部分生成的dll文件中的导出函数对PE文件加壳;第二部分则是加壳程序,当然不仅仅是加壳,这其实上就是获得被加壳的PE文件的信息,然后根据这些信息将第三部分,也就是真正的壳加入到PE文件中去,并且修改一系列的值,使得这个壳能够发挥作用;第三部分自然就是真正的壳了,其中也涉及到一些对于PE文件的操作,但是相对于第二部分来说就简单一些。现在来看看具体的步骤,由于第三部分是基础,前两部分都是依靠第三部分来写的,因此按照书上的顺序,先说如何写这个壳。

1.壳的编写

         首先,为了减少壳的体积,这里的代码使用了以下处理:

#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步都很简单,所以这里只讲前两步。

1.1初始化所有API

这一块应该是内容最多的一块了,因为这里需要定义所有的函数变量以及初始化这些变量。首先来看变量的定义:

// 声明一个导出的全局变量,用以保存传递给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中相应的序号,最终获得地址:

《黑客免杀攻防学习笔记》——C++设计一个简单的壳1_第1张图片

既然已经得到了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地址,赋值给这些指针备用。至此初始化已经全部完成。

1.2解密宿主程序

         初始化一旦完成,就可以利用之前设计的算法对原PE文件的.text区段进行解密。由于加解密算法不是这里的重点,所以这里只使用了简单的异或等处理,代码如下:

void Decrypt()

{// 在导出的全局变量中读取需解密区域的起始于结束VA

         PBYTElpStart = g_stcParam.lpStartVA;

         PBYTElpEnd   = g_stcParam.lpEndVA;

         //循环解密

         while( lpStart

{

                   *lpStart-= 0x88;

                   *lpStart^= 0xA1;

                   lpStart++;

         }

}

         至此,壳的这一部分已经结束。说起来很容易,其实并非如此,至少对于我这样的菜鸟来说。首先需要很了解PE文件的格式,这里只用到了导出表,后面会用到各种结构。其次,寻找kernel32.dll的基址需要使用汇编语言,所以对汇编至少也要了解。最后,如果想让加解密算法复杂一些,那密码学的知识也要熟悉。

你可能感兴趣的:(免杀)