一个简单的外壳程序

原文链接:https://bbs.pediy.com/thread-206804.htm


最近一直在15PB学习,现阶段学的主要是关于壳的知识,正好现在也在做壳这个阶段项目,用了2天的时间实现了一个基础版的C++写的壳,拿出来跟大家分享一下,代码量不多,但知识点不少,适合新手学习提高~


        壳的流程看上去并不复杂,但需要的是你对PE文件有一定的了解,在了解了一些关于导入表、导出表、重定位表、IAT等基础知识以后方可写出一个具有基本功能的壳。但如果想要写一个加密、压缩或者兼容性很强的壳的话,绝对不是一件容易的事,此贴只是简单实现了壳的基本加载流程,展现的壳都做了哪些基础操作,关于更高技术的加密、反调试、压缩等技术暂不讨论,可能过后我会发一个讲述如何在此版本上进行一些拓展,实现如IAT加密、IAT-Hook、花指令、反调试等功能的壳。
        此项目的参考书目:《黑客免杀攻防》(任晓珲)、《加密与解密 第三版》(段钢)
        
        基础版壳所实现的功能:
        1.在原程序中添加一块区段,将壳部分的代码移植进去。
        2.在程序启动前优先获得控制权,执行完自己的代码以后再将控制权交还给原程序。
        3.对代码段进行简单的亦或加密。
        4.对原程序的导入表(IAT)进行修复。
        5.如果原程序开启了随机基址,则对源程序进行重定位修复。

        项目分为两部分,第一部分为加壳程序(Pack),第二部分为外壳程序(Shell.dll)。
        其中涉及到的重点是修复导入表和重定位表,这是加壳后的程序能够正常运行的基础,下面我就针对每个部分单独展开来说。
        为缩减篇幅,详细的代码就不在此贴出,只贴一些比较重要的代码,具体每个功能所用到的代码我会标注出在源码的哪个文件。

第一部分:加壳部分的编写

        先说一下文件加壳前后的变化:
   
        上图简单的示意了加壳前后的PE文件变化,首先是多出了一个区段,用于存放Shell部分的代码,再就是入口点变为了Shell部分的入口点,这样保证能够先运行我们壳部分的代码,执行我们想要的操作,最后就是跳回到原始程序的OEP,开始执行原始程序。
        流程看着简单,但如果能够让一般的PE文件正常运行,需要注意的细节还是很多的,下面我就来详细道来。

        框架:在一个普通的MFC新建工程基础上,自己添加了两个类,一个为Pack类,另一个为PE类。 MFC自带工程仅仅负责界面,加壳的主要流程是在Pack类里,在加壳过程中需要对PE文件操作是,就调用PE类中的函数来实现。
        加壳部分的流程(此流程在Pack类中的Pack()函数中实现):
        1.读取文件PE文件信息并保存 
        2.加密代码段操作
        3.将必要的信息保存到Shell (Pack部分和Shell部分的数据交换)
        4.将Shell部分附加到PE文件
        5.保存文件,完成加壳
        6.释放资源

 具体实现:
 1.读取文件PE文件信息并保存 
        要为一个PE文件加壳,首先就是要了解这个PE文件。那么就需要把这个文件读到内存中,加载到内存中的方式有两种,一种是以文件对齐的方式读到内存,也就是直接读取文件的二进制数据,另一种方式是以内存对齐的方式读到内存,或者其实就在模拟程序运行时的内存分部,我选择的是第二种,以内存对齐的方式读到内存,这样的好处就是在对PE文件进行操作的时候,不需要将相对虚拟地址(RVA)转换为文件偏移(ROffset),操作起来也更直观,直接就是内存中的偏移地址。读取文件的代码详见源码PE类中的InitPE(CString strFilePath)函数,或者参考《加密与解密 第三版》第443页内容。
        将文件读取到内存以后,下一步就是获取我们关注的信息并保存了,信息保存在PE类中的成员变量中,这样在Pack类中只需要定义一个PE类的对象,即可调用这些信息。
        我保存的关键信息有:保存PE文件的缓冲区的指针,PE文件的NT头指针、镜像大小、镜像基址、OEP地址、区段数量以及重定位表、导入表指针信息。这里保存的信息其实越详细越好,方便以后拓展功能的时候能够用到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//PE .h
public:
   HANDLE          m_hFile;       //PE 文件句柄
   LPBYTE          m_pFileBuf;       //PE 文件缓冲区
   DWORD          m_dwFileSize;     // 文件大小
   DWORD          m_dwImageSize;     // 镜像大小
   PIMAGE_DOS_HEADER    m_pDosHeader;     //Dos
   PIMAGE_NT_HEADERS    m_pNtHeader;     //NT
   PIMAGE_SECTION_HEADER  m_pSecHeader;     // 第一个SECTION结构体指针
   DWORD          m_dwImageBase;     // 镜像基址
   DWORD          m_dwCodeBase;     // 代码基址
   DWORD          m_dwCodeSize;     // 代码大小
   DWORD          m_dwPEOEP;       //OEP 地址
   DWORD          m_dwShellOEP;     // 新OEP地址
   DWORD          m_dwSizeOfHeader;   // 文件头大小
   DWORD          m_dwSectionNum;     // 区段数量
   DWORD          m_dwFileAlign;     // 文件对齐
   DWORD          m_dwMemAlign;     // 内存对齐
   DWORD          m_IATSectionBase;   //IAT 所在段基址
   DWORD          m_IATSectionSize;   //IAT 所在段大小
   IMAGE_DATA_DIRECTORY  m_PERelocDir;     // 重定位表信息
   IMAGE_DATA_DIRECTORY  m_PEImportDir;     // 导入表信息


        获取PE文件信息的函数 void GetPEInfo():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void CPE::GetPEInfo()
{
   m_pDosHeader  = (PIMAGE_DOS_HEADER)m_pFileBuf;
   m_pNtHeader    = (PIMAGE_NT_HEADERS)(m_pFileBuf + m_pDosHeader->e_lfanew);
   m_dwFileAlign  = m_pNtHeader->OptionalHeader.FileAlignment;
   m_dwMemAlign  = m_pNtHeader->OptionalHeader.SectionAlignment;
   m_dwImageBase  = m_pNtHeader->OptionalHeader.ImageBase;
   m_dwPEOEP    = m_pNtHeader->OptionalHeader.AddressOfEntryPoint;
   m_dwCodeBase  = m_pNtHeader->OptionalHeader.BaseOfCode;
   m_dwCodeSize  = m_pNtHeader->OptionalHeader.SizeOfCode;
   m_dwSizeOfHeader= m_pNtHeader->OptionalHeader.SizeOfHeaders;
   m_dwSectionNum  = m_pNtHeader->FileHeader.NumberOfSections;
   m_pSecHeader  = IMAGE_FIRST_SECTION(m_pNtHeader);
   m_pNtHeader->OptionalHeader.SizeOfImage = m_dwImageSize;
   // 保存重定位目录信息
   m_PERelocDir = 
     IMAGE_DATA_DIRECTORY(m_pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
   // 保存IAT信息目录信息
   m_PEImportDir =
     IMAGE_DATA_DIRECTORY(m_pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]);
}


2.加密代码段操作
        说道亦或加密可能大家会笑话我了哈哈,由于加密操作并不是此基础版壳的主要功能,所以我就用亦或操作象征性的意思意思,能防止静态分析就可以啦~
        加密代码段是需要用到的PE信息是:PE文件的缓冲区指针,代码段基址,代码段大小。这三个信息在上一步操作中已经获取:
1
2
3
4
5
6
7
8
9
DWORD CPE::XorCode(BYTE byXOR)
{
   PBYTE pCodeBase = (PBYTE)((DWORD)m_pFileBuf + m_dwCodeBase);
   for  (DWORD i = 0; i < m_dwCodeSize; i++)
   {
     pCodeBase[i] ^= byXOR;
   }
   return  m_dwCodeSize;
}


        该函数返回的是加密的长度,这个变量需要保存在Shell部分,以供Shell部分解密的时候用。

3.将必要的信息保存到Shell (Pack部分和Shell部分的数据交换)
        对PE文件进行操作是在两个时候,一个是加壳前的操作,由Pack部分实现;另一个是加完壳、程序运行的时候,由Shell部分实现,这两个部分的操作或多或少的都需要一些PE信息,Pack部分可以在主程序中调用函数获取PE信息,而Shell部分如果再去获取PE信息的话,就显得繁琐了许多,所以还不如直接让Pack部分把Shell所需要的信息告诉它,这时就涉及到两个部分之间的数据交换。
        我采用的是让Shell部分导出一个结构体供Pack使用,Pack将Shell加载到内存之后,获取这个结构体的地址,然后将要传递的信息保存进这个结构体,保存完毕、在生成文件的时候,Pack所传递的结构体数据也会一并保存在被加壳后的PE文件中,这样在壳运行的时候,Shell部分就可以像调用自己的内部变量一样调用这些数据了,方便了Shell部分的操作。
        Shell部分所导出的结构体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 导出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;


        Pack部分要做的就是载入Shell.dll这个文件,然后获取这个结构体信息,往里面保存数据:
1
2
3
4
5
6
7
8
9
10
11
12
   HMODULE hShell = LoadLibrary(L "Shell.dll" );
   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文件 (此部分代码不一一贴出,详见Pack类中的Pack()函数)
        这一步操作就有点繁琐了,因为需要注意到很多细节,要不然很容易使得加壳后的程序变成一个“不是有效的Win32程序”,这样的话你练调试的机会都没有,最大的悲剧莫过于此,造成这个的原因大概就是你在添加区段的时候,没有适当修改PE文件目录信息表中的数据。

4.1.读取Shell代码
        之前已经通过LoadLibrary(L"Shell.dll")的方式加载了Shell模块,但为了操作方便我还是申请了一块空间专门存放Shell部分,同时获取一下Shell的镜像大小,为增加区段做准备。

4.2.设置Shell重定位信息
        由于我们的Shell部分是DLL,默认加载基址是0x10000000,而我们要将DLL文件移植到EXE文件,EXE文件大多默认加载基址是0x00400000,再加上有些程序会有随机基址,这时候基址就更加不定了,所以要想顺利执行Shell部分的代码,进行重定位是必须的!
        重定位的实现有两种,一种是系统的PE加载器通过重定位表的信息,在加载程序之前给你重定位好;另一种则是用代码进行手动重定位,模拟PE加载器所进行的重定位操作。由于我们的Shell部分是最先执行的,所以不妨让系统直接给我们的壳部分的代码进行重定位,而原程序部分的代码重定位,则只能通过我们在Shell部分用代码手动实现。
        简言之,让系统重定位Shell部分的代码,保证Shell部分的函数可以正常执行。然后我们在Shell部分手动重定位原程序代码,让原程序能够执行。
        那么问题来了,我们该如何重定位Shell部分的代码呢?下面可是重点哦!
        由于Shell部分的代码我是通过LoadLibrary(L"Shell.dll")的方式加载的,这说明加载到内存中之前,PE加载器以及帮我们修复过重定位了。而我们现在再去访问重定位表的信息的时候,是已经修复过的正确的重定位信息,而我们想要的是原始的重定位信息,把原始的重定位信息写入加壳后的文件,当PE加载器运行被加壳程序的时候,才能通过原始的重定位信息给我们的Shell部分进行正确的重定位,所以首先就是恢复重定位之前的原始信息。
        拿一条重定位数据举例来说:
        ①重定位原始地址=重定位后的地址-加载时的镜像基址
        重定位原始地址是一个内存相对偏移(RVA),需要把这个RVA加上PE文件默认的加载基址,然后再写回重定位表中的数据,才能让系统正确的进行重定位。
        ②新的重定位地址=重定位原始地址+新的镜像基址
        由①②可得:③新的重定位地址=重定位后的地址-加载时的镜像基址+新的镜像基址
        但由于我们的Shell部分是加载到PE文件的末尾,所以RVA地址还需要加上那个PE文件的镜像大小
        最终得出④新的重定位地址=重定位后的地址-加载时的镜像基址+新的镜像基址+代码基址(PE文件镜像大小)
        只需要把思所得出的地址信息,写到加壳后的的PE文件中,系统就可以帮你正确的进行重定位了,当然,由于要重定位的信息我们自己添加的,需要通过修改PE文件目录表中的重定位信息来告诉系统应该去哪找重定位表。
        这些操作必须对PE文件的重定位信息有一定的了解,如果不了解的话,建议大家还是先恶补一下这方面的知识,推荐参考资料《黑客免杀攻防》第七章 PE文件格式详解。当你对这些基本信息有一点了解之后,再阅读这段代码应该会很容易:
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
BOOL CPE::SetShellReloc(LPBYTE pShellBuf, DWORD hShell)
{
   typedef struct _TYPEOFFSET
   {
     WORD offset : 12;       // 偏移值
     WORD Type  : 4;       // 重定位属性(方式)
   }TYPEOFFSET, *PTYPEOFFSET;
   //1 .获取被加壳PE文件的重定位目录表指针信息
   PIMAGE_DATA_DIRECTORY pPERelocDir =
     &(m_pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
   
   //2 .获取Shell的重定位表指针信息
   PIMAGE_DOS_HEADER    pShellDosHeader = (PIMAGE_DOS_HEADER)pShellBuf;
   PIMAGE_NT_HEADERS    pShellNtHeader = (PIMAGE_NT_HEADERS)(pShellBuf + pShellDosHeader->e_lfanew);
   PIMAGE_DATA_DIRECTORY  pShellRelocDir =
     &(pShellNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
   PIMAGE_BASE_RELOCATION  pShellReloc = 
     (PIMAGE_BASE_RELOCATION)((DWORD)pShellBuf + pShellRelocDir->VirtualAddress);
   
   //3 .还原修复重定位信息
   // 由于Shell.dll是通过LoadLibrary加载的,所以系统会对其进行一次重定位
   // 我们需要把Shell.dll的重定位信息恢复到系统没加载前的样子,然后在写入被加壳文件的末尾
   PTYPEOFFSET pTypeOffset = (PTYPEOFFSET)(pShellReloc + 1);
   DWORD dwNumber = (pShellReloc->SizeOfBlock - 8) / 2;
   for  (DWORD i = 0; i < dwNumber; i++)
   {
     if  (*(PWORD)(&pTypeOffset[i]) == NULL)
       break ;
     //RVA
     DWORD dwRVA =pTypeOffset[i].offset + pShellReloc->VirtualAddress;
     //FAR 地址(LordPE中这样标注)
     // ***新的重定位地址=重定位后的地址-加载时的镜像基址+新的镜像基址+代码基址(PE文件镜像大小)
     DWORD AddrOfNeedReloc =  *(PDWORD)((DWORD)pShellBuf + dwRVA);
     *(PDWORD)((DWORD)pShellBuf + dwRVA) 
       = AddrOfNeedReloc - pShellNtHeader->OptionalHeader.ImageBase + m_dwImageBase + m_dwImageSize;
   }
   //3 .1修改Shell重定位表中.text的RVA
   pShellReloc->VirtualAddress += m_dwImageSize;
   //4 .修改PE重定位目录指针,指向Shell的重定位表信息
   pPERelocDir->Size = pShellRelocDir->Size;
   pPERelocDir->VirtualAddress = pShellRelocDir->VirtualAddress + m_dwImageSize;
   return  TRUE;
}


4.3.修改被加壳程序的OEP,指向Shell
        这样就可以让程序运行时从Shell部分开始执行,那么这个地址改如何确定呢?还记得之前我们的Shell部分所导出的那个结构体么?对,其第一个变量就是Shell部分启动函数的地址,让OEP执行这个地址即可。

4.4.合并PE文件和Shell的代码到新的缓冲区
        目前我们内存中有两个缓冲区,一个是原PE程序的缓冲区,另一个是Shell部分的缓冲区,我们要做的就是重新申请一块连续的空间,大小为这两个缓冲区的大小,然后那他们拷贝进去。内存中这样处理是没问题的,这时如果保存这个缓冲区的话就是两个文件的结合体,但逻辑上并没有如此简单,因为你从物理上多出来一个Shell区段,但逻辑上并没有变,也就是系统并不认识这个新加的Shell区段,这时候你就又需要去修改PE文件信息了,这次需要修改的是区段表中的信息,涉及到新增一个区段目录表信息,并正确设置该区段的起始地址、大小等信息,而且在设置这些信息的时候还要考虑到文件对齐的问题,源代码中则是MergeBuf( )函数实现了这两个缓冲区的合并,并添加区段信息:
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
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);
}


5.保存文件
        保存文件就是保存上个函数中所合并的缓冲区,由于我是直接从从内存中dump出来的,其分布也是以内存对齐大小所对齐的,所以保存成文件的时候我顺便修改了文件对齐大小,同内存对齐大小相同;同时将数据目录表中不必要的信息摸掉:
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
BOOL CPACK::SaveFinalFile(LPBYTE pFinalBuf, DWORD pFinalBufSize, CString strFilePath)
{
   // 修正区段信息中 文件对齐大小(文件对齐大小同内存对齐大小)
   PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFinalBuf;
   PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pFinalBuf + pDosHeader->e_lfanew);
   PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);
   for  (DWORD i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++, pSectionHeader++)
   {
     pSectionHeader->PointerToRawData = pSectionHeader->VirtualAddress;
   }
   // 清除不需要的目录表信息
   // 只留输出表,重定位表,资源表
   DWORD dwCount = 15;
   for  (DWORD i = 0; i < dwCount; i++)
   {
     if  (i != IMAGE_DIRECTORY_ENTRY_EXPORT && 
       i != IMAGE_DIRECTORY_ENTRY_RESOURCE &&
       i != IMAGE_DIRECTORY_ENTRY_BASERELOC )
     {
       pNtHeader->OptionalHeader.DataDirectory[i].VirtualAddress = 0;
       pNtHeader->OptionalHeader.DataDirectory[i].Size = 0;
     }
   }
   // 获取保存路径
   TCHAR strOutputPath[MAX_PATH] = { 0 };
   LPWSTR strSuffix = PathFindExtension(strFilePath);
   wcsncpy_s(strOutputPath, MAX_PATH, strFilePath, wcslen(strFilePath));
   PathRemoveExtension(strOutputPath);
   wcscat_s(strOutputPath, MAX_PATH, L "_cyxvc" );
   wcscat_s(strOutputPath, MAX_PATH, strSuffix);
   // 保存文件
   HANDLE hNewFile = CreateFile(
     strOutputPath,
     GENERIC_READ | GENERIC_WRITE,
     0,
     NULL,
     CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
   if  (hNewFile == INVALID_HANDLE_VALUE)
   {
     MessageBox(NULL, _T( "保存文件失败!" ), _T( "提示" ), MB_OK);
     return  FALSE;
   }
   DWORD WriteSize = 0;
   BOOL bRes = WriteFile(hNewFile, pFinalBuf, pFinalBufSize, &WriteSize, NULL);
   if  (bRes)
   {
     CloseHandle(hNewFile);
     return  TRUE;
   }
   else
   {
     CloseHandle(hNewFile);
     MessageBox(NULL, _T( "保存文件失败!" ), _T( "提示" ), MB_OK);
     return  FALSE;
   }
}


6.释放资源
        万事大吉以后不要忘记擦屁股,养成良好的习惯,释放掉资源,避免资源泄露(虽然这个小程序也用不了多少内存吧哈哈!)
1
2
3
4
   delete[] objPE.m_pFileBuf;
   delete[] pShellBuf;
   delete[] pFinalBuf;
   objPE.InitValue();


至此,第一部分(Pack)的功能已经完成,剩下的就是Shell部分的编写了!

第二部分:外壳程序(Shell.dll)的编写

        壳的Shell部分,网上很多人在用汇编编写,汇编的优势是不需要进行重定位,移植性好,用C++的话需要重定位,而这个问题在第一部分已经解决了,再配合上两部分的数据交换,Shell部分用C++写起来也变得如鱼得水,代码的可读性有了很大的提高(好吧,我承认,其实是我不会用汇编写...)。
        外壳程序部分的流程(此流程在Shell工程中的Start()函数中实现):
        1.获取Shell部分所用到的函数
        2.解密代码段
        3.修复原程序的重定位信息
        4.修复原程序的导入表(IAT)信息
        5.跳到程序入口点,将控制权交还给程序

具体实现:
1.获取Shell部分所用到的函数
        有很多壳为了隐藏自己的行为,不让别人看出它用到了哪些函数,直接没有导入表,不让PE加载器为其导入函数。而是直接自己获取所用到的函数,此项目也是用的这种方法,通过代码实现导入Shell部分所需要的函数。
        无论一个PE文件是否有导入表,系统都会为其加载两个模块,ntdll.dll和Kernel32.dll,那么我们就从这两个模块入手,目标是获取GetProcAddress()这个函数,而这个函数位于Kernel32.dll中,那么我们首先要做的就是获取Kernel32.dll的加载基址,常用的方法有三种:①通过特征匹配的暴力搜索。②利用系统的SEH机制找到Kernel32.dll的加载基址。③通过线程环境块TEB的信息逐步找到Kernel32.dll的加载基址。我在这里用的是第三种,详细的代码请见Shell部分的MyGetProcAddress()函数,第三种方法代码简介,但存在兼容性问题,因为XP和Win7下获取基址时的代码略有出入,所以如果你为了兼容性考虑,请改用其他两种方法。
        在获取了Kernel32.dll的加载基址以后,就可以通过遍历Kernel32.dll的导出表来搜索GetProcAddress(),从而获取GetProcAddress()函数的函数地址:
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
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;
}


        以上这段代码就是遍历Kernel32.dll的导出表并返回GetProcAddress()的地址,有了该函数的地址,我们就可以自己定义一个和GetProcAddress()函数原型一模一样的函数指针来调用GetProcAddress()函数了,有了这个函数指针,你想用什么函数自己获取就行了,然后在顺便获取一下LoadLibraryA()的函数地址,这样就可以任意得加载模块了。

2.解密代码段
        解密代码段的操作其实就是在做与Pack部分加密的逆向操作,由于亦或操作是可逆的(两次亦或相同的值还是等于原来的值),所以Pack部分亦或什么,这里解密也就亦或什么就可以了。代码过于简单就不贴了...

3.修复原程序的重定位信息
        由于加壳程序的重定位指针指向了Shell部分的重定位,PE加载器在加载PE文件的时候对Shell部分的代码进行了重定位,所以本应给原程序进行的重定位就需要我们在Shell部分实现了,此项目原程序的重定位表并没有遭到破坏(有些壳会对重定位表进行破坏或加密),所以我们只要在Pack部分加壳的时候保存一下原程序的重定位表指针,然后在Shell部分对这个指针所指向的重定位表进行重定位就可以了,其实就是在模拟PE加载器的重定位操作。
        原程序的重定位表指针在Pack的时候有保存过,这里直接拿来用就可以了。
        重定位表最终指向的是一个需要重定位的地址,这个地址是基于原PE文件默认基址(一般为0x00400000)的地址,原PE文件的默认基址我们也有保存过,所以修复起来还是比较方便的,只需要遍历原PE文件的重定位表,然后通过一个公式计算出重定位后的地址再填充回去就可以了。
        计算公式:重定位后的地址=需要重定位的地址-默认加载基址+当前真实的加载基址。
        还有一点需要注意的是,在修复的时候你所修复的地址的内存属性不一定是可写的,所以最好在修复之前用VirtualProtect()修改内存属性为可写,修复完以后再将原来的属性设置回去。
        遍历重定位表并修复:
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
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);
   }
}


4.修复原程序的导入表(IAT)信息
        这又是一个很重要的知识点!导入表的用途是保存该PE文件用到的API函数的信息,由于每次启动程序时这些API函数的加载地址可能会不一样,所以PE文件中无法直接保存一个函数地址,而是保存这些函数的信息,当启动程序时,PE加载器会通过导入表信息为其加载所用到的模块,并获取需要调用的API函数的地址,再将该地址填充到IAT,这样程序才能正常的调用API。由于修复导入表最终是为了填充IAT(只要IAT中的函数地址正确,没有导入表信息也可以),所以又叫做修复IAT,这项技术不仅用再壳来修复原程序的IAT,在脱壳的时候也会用到IAT修复,只有正确修复了IAT才能让程序正常运行,所以很多壳会在IAT加密上做文章,来防止脱壳成功。
        此项目中没有破换原有PE文件的导入表信息,所以在Pack的时候保存导入表指针就可以找到导入表信息。有人会问,那为什么不直接让PE加载器来修复呢,其实这样也是可以的,只要你填写正确的导入表指针信息,系统PE加载器就可以帮你修复,但壳很少会这么做,我也是为了给IAT加密做基础,所以也没让PE加载器来替我修复原PE文件的导入表。
        那我就来简单说一下修复IAT的过程,那就是通过导入表指针遍历导入表信息,里面保存着需要导入的函数的名称和所在模块,我们所要做的就是加载这些模块,并从中获取函数地址,然后舔到正确的IAT位置即可。
        加载模块和获取函数地址这两个函数之前我们通过自定义函数指针的方式已经获取到了,那么剩下的就是在遍历导入表了,具体代码实现如下:
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
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);
}


5.跳到程序入口点,将控制权交还给程序
        经过了修复重定位和修复IAT的操作以后,原PE文件就已经可以正常执行了,在跳回到原程序入口点之前把你想做的事做完吧!

第三部分:总结

        至此,这个基础版的壳就完成了,虽然并没有什么加密、压缩、反调试的功能,但在这个框架的基础上进行拓展也会容易很多,毕竟都是用C++写的嘛。
        为了演示,我在壳的Shell部分增加了一个弹出MessageBox的操作,表示成功执行了Shell部分的代码。
        界面压根就没做,看着很挫,这个基础版就没有在意这些,为了演示大家将就看:
   

        加壳以后,运行程序弹出MessageBox:
   

        加壳测试在 Win7 x64 系统下,对大多数程序加壳后都可以正常运行,不支持dll加壳。
        有兴趣的朋友可以下载源码看一下,由于本人才疏学浅、能力有限,有错的第地方还请大牛指出!
        注:此壳不以加密为目的,被脱秒秒种的事...   

猜你喜欢

转载自blog.csdn.net/Na2Co3_Ren/article/details/79529496
今日推荐