九歌 发表于 2018-7-8 02:11

申请会员ID:cyxvc【冒充他人作品申请】

申请标题:申请会员ID:cyxvc

1、申 请 I D:cyxvc
2、个人邮箱:a206809643@163.com

3、原创技术文章:
看雪ID:cyxvc

         用C++实现的壳
         壳所实现的功能:
      1.在原程序中添加一块区段,将壳部分的代码移植进去。
      2.在程序启动前优先获得控制权,执行完自己的代码以后再将控制权交还给原程序。
      3.对代码段进行简单的亦或加密。
      4.对原程序的导入表(IAT)进行修复。
      5.如果原程序开启了随机基址,则对源程序进行重定位修复。

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

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

      先说一下文件加壳前后的变化:https://bbs.pediy.com/upload/attach/201512/662999_tzpel8bts6o3lsv.jpg
上图简单的示意了加壳前后的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_HEADERm_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_DIRECTORYm_PERelocDir;    //重定位表信息
IMAGE_DATA_DIRECTORYm_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);
//保存IAT信息目录信息
m_PEImportDir =
    IMAGE_DATA_DIRECTORY(m_pNtHeader->OptionalHeader.DataDirectory);
}


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 ^= 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_DIRECTORYstcPERelocDir;    //重定位表信息
IMAGE_DATA_DIRECTORYstcPEImportDir;    //导入表信息
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);
   
//2.获取Shell的重定位表指针信息
PIMAGE_DOS_HEADER    pShellDosHeader = (PIMAGE_DOS_HEADER)pShellBuf;
PIMAGE_NT_HEADERS    pShellNtHeader = (PIMAGE_NT_HEADERS)(pShellBuf + pShellDosHeader->e_lfanew);
PIMAGE_DATA_DIRECTORYpShellRelocDir =
    &(pShellNtHeader->OptionalHeader.DataDirectory);
PIMAGE_BASE_RELOCATIONpShellReloc =
    (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) == NULL)
      break;
    //RVA
    DWORD dwRVA =pTypeOffset.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;
//1.修改区段数量
pNtHeader->FileHeader.NumberOfSections += 1;
//2.编辑区段表头结构体信息
PIMAGE_SECTION_HEADER AddSectionHeader =
    &pSectionHeader;
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;
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.VirtualAddress = 0;
      pNtHeader->OptionalHeader.DataDirectory.Size = 0;
    }
}
//获取保存路径
TCHAR strOutputPath = { 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);
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 == NULL)
      continue;
    //判断是以函数名导出还是以序号导出
    DWORD j = 0;
    for (; j < dwNumofName; j++)
    {
      if (i == pEIT)
      {
      break;
      }
    }
    if (j != dwNumofName)
    {
      //如果是函数名方式导出的
      //函数名
      char* ExpFunName = (CHAR*)((PBYTE)hModule + pENT);
      //进行对比,如果正确返回地址
      if (!strcmp(ExpFunName, "GetProcAddress"))
      {
      return pEAT + 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_RELOCATIONpPEReloc=
    (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) == NULL)
      break;
      //RVA
      DWORD dwRVA = pTypeOffset.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部分的代码。       演示下,没有做界面,大家不介意看的:https://bbs.pediy.com/upload/attach/201512/662999_lrc7tl1i4441hn2.jpg
      加壳以后,运行程序弹出MessageBox: https://bbs.pediy.com/upload/attach/201512/662999_ywtqqh7ary1zvdk.jpg
      PS:加壳测试在 Win7 x64 系统下,对大多数程序加壳后都可以正常运行,不支持dll加壳。
      
         这就完成了非常纯洁的C++壳基础框架,
         而之后完善了一下,增加了IAT加密,在一个就是机器码绑定。并修复了一些bug,静态编译了Shell.dll,就可以在Win 7 x86和Win 7 x64位下都可以正常加壳了,如果加壳后的程序无法运行,请取消“IAT加密”选项,原因是我在IAT加密中使用了Hook-API,而            Hook以后全当Call来处理了,实际有的程序IAT位置所填写的地址指向的并不全是Call指令,有时候是Mov指令,所以“IAT加密”功能的兼容性不足。      本来想增加反调试功能,可我实在水平有限,无法干掉StrongOD的反反调试功能,所以就没在拓展版中假如反调试功能,如果大家有什么好的反调试技术可以添加到这个壳子里,我在这就不献丑了...
      拓展版在界面上有了变化,增加了一些可选选项:https://bbs.pediy.com/upload/attach/201601/662999_v5uzsw1zk4aitrl.jpg
         下面就分别介绍一下“IAT加密”和“机器码绑定”这两个功能的实现。

【IAT加密】      

      我对IAT进行加密的思路是:
      ①首先定义一个自己的导入表数据结构,在Pack部分加壳的时候,读取被加壳程序的导入表信息并保存到我自己的导入表数据结构中,然后抹去被加壳程序的导入表数据;
      ②在Shell部分对IAT进行解密的时候,直接从我自己定义的导入表数据结构中获取修复IAT所用的信息,当通过这些信息获取到真正的函数地址后,将该地址填入一个new出来的堆空间1内保存,在这个堆空间1内加入一些花指令,最终再调用真正的函数地址。
      如果直接将堆空间1的地址写入IAT,就完成了一个简单的IAT-Hook,但这样操作的话,真正的函数地址和要写入的IAT地址会同时出现,在反汇编中修改代码,直接将函数地址写入IAT地址的话,就会使得IAT-Hook失效,所以我没有直接将堆空间1写入IAT地址。
      ③而是将堆空间1写入了一段又new出来的堆空间2,然后再在别的函数中,将堆空间2的地址写入IAT地址,这就能保证真正的函数地址和IAT地址不同时出现,这样的话,如果脱壳者在不了解我自己定义的导入表数据结构的情况下,是很难修复IAT并成功脱壳的。
      我自己定义的IAT数据结构如下:


1
2
3
4
5
6
7
8
typedef struct _MYIMPORT
{
DWORDm_dwIATAddr;      //IAT地址
DWORDm_dwModNameRVA;      //模块名偏移
DWORDm_dwFunNameRVA;      //函数名偏移
BOOLm_bIsOrdinal;      //是否为序号导出函数
DWORDm_Ordinal;      //序号
}MYIMPORT, *PMYIMPORT;


      示意图如下:https://bbs.pediy.com/upload/attach/201601/662999_vaalg4momh0vbt6.jpg


      经过这样处理后的IAT,在你停在原程序OEP的情况下,用IAT修复工具是很难进行自动修复的,修复失败,也就意味着不能脱壳成功。      
      如果大家感兴趣可以用这个壳对一个程序进行“IAT加密”,然后忘掉这个导入表数据结构,再脱脱壳感受一下,嘿嘿...
      


         改进版:OEP处先保存一下各个寄存器的值,备用!对IAT加密解密程序,代码进行加密,
         然后申请一段内存,存入加密过的IAT,然后设置权限为无法读取,以及写入,然后注册一个异常回调函数,在准备进入原程序的OEP前,写一个有缓冲区溢出缺陷的函数,然后故意触发缓冲区溢出,在溢出的堆栈指向的地址中,我们写一个具有除0错误的汇编程序,然后            这个汇编程序就修正堆栈,让其返回正常的函数,嘿嘿,这样除0错误,会被异常回调函数处理,异常回调函数会取得先前保存的OEP的各个寄存器的值,然后恢复寄存器,恢复寄存器后,我们就可以恢复IAT了,存入加密过的IAT的那段新的内存给予读取权限,然后恢复                IAT,恢复以后,将模块名,全部指向一个系统存在,但是并没有加载的DLL,然后,将OEP地址恢复到EIP中,最后结束异常处理,然后卸载异常处理函数,当异常处理函数返回时,就跳到了原程序的OEP中了,而如果,在程序运行开始时有调试器试图附加,来调试你的            IAT解密过程的话,由于调试器处理了缓冲区溢出,以及除0错误,所以不会导致异常处理代码的执行,那么IAT也就没有机会解密,这样就会导致程序无法完全解密了


【机器码绑定】
      这个其实没有什么技术含量,只是在逻辑上很难破解(在你不知道这个程序所绑定的机器码的情况下)。
      原理就是,用机器码1同代码段进行亦或操作,这样加过壳的程序就只能在机器码1的电脑上运行了,而如果在其他机器上运行,Shell部分在对机器码绑定进行解密的时候,获取的机器码同机器码1不同,这样解密出来的代码就是垃圾代码,根本无法运行,这样双击的效果为:
   https://bbs.pediy.com/upload/attach/201601/662999_mjxzo6vzo96zvzx.jpg

      大家在测试的时候,通过我打包的文件中的“查看机器码”查看本机的机器码,然后将这个机器码输入到加壳选项中,所加壳后的程序就只能在你的机器上运行了,如果改动一位,加壳后的程序运行时就会出错。
      我这个获取机器码的代码是从网上找的,但貌似并不是真正的机器码,不过也无所谓,只要能保证每台机器所获取的信息都是不同且唯一的就行,原理一样。
      
【总结】
      给程序加壳其实就是拖慢破解者的进度,或者从数量上击垮对方,很难有脱不了的壳,除非是经过VM保护的代码才会很难分析。这就是一场加壳者和破解者之间的较量,再简单的壳也会有小菜脱不了,再难的壳也会有大牛能搞定。
      注:测试环境为win7系统,32位和64位都可以,但在xp上不行,原因是Shell部分获取Kernel32.dll基址的时候的代码只适用于win7,xp的话需要删除一句“mov eax, ”。如果你想要兼容xp的话,请从网上自行搜索获取Kernel32.dll基址的其他方法。




       附源代码:大于1M不能传{:1_890:}   
                           https://bbs.pediy.com/attach-download-102163.htm
                           https://bbs.pediy.com/attach-download-102243.htm

         
      

Hmily 发表于 2018-7-8 20:46

请在看雪论坛给hmilywen发一条短消息,确认本人申请,然后来这里回复提醒我查看。

发表于 2018-7-9 09:54

Hmily 发表于 2018-7-8 20:46
请在看雪论坛给hmilywen发一条短消息,确认本人申请,然后来这里回复提醒我查看。

提醒查看

Hmily 发表于 2018-7-9 11:28

游客 182.142.135.x 发表于 2018-7-9 09:54
提醒查看

你去看雪新注册一个“cyxvc一        ”来冒充cyxvc申请?脸租来的吗?

敛弈逆袭 发表于 2018-7-21 01:52

老大厉害了(ง •̀_•́)ง

cyxvc 发表于 2019-6-25 14:45

我擦。。居然有人冒充我,要不是我闲着没事搜我id我都不知道。。。

cyxvc 发表于 2019-6-25 17:44

管理员您好:
建议您删除此贴,
因为在百度搜我的id第一条就是这个,带有“cyxvc【冒充他人作品申请】”字样,让人一看以为我跟冒充别人作品有关,会存在歧义,影响有点不好。
我觉得我在贵站申请id,不能因为别人冒用我id就有可能产生不必要的歧义。希望管理员考虑一下。

cyxvc 发表于 2019-6-27 00:06

@Hmily麻烦管理员有时间处理一下可以么?

Hmily 发表于 2019-6-27 08:13

cyxvc 发表于 2019-6-25 17:44
管理员您好:
建议您删除此贴,
因为在百度搜我的id第一条就是这个,带有“cyxvc【冒充他人作品申请】” ...

别人如果连内容都不看,就妄下定论,你就可以让他睁大眼睛好好看看,你才是大牛,还被人冒充,我们不考虑对此类冒充贴进行删除,用于警示。

cyxvc 发表于 2019-6-28 23:43

真心点呗,我这么菜还有人来冒充,莫名躺枪,冒充谁不好非得冒充我。强迫症实在受不了,没办法谁让是你们的网站呢,管理员开心就好,呵呵
页: [1]
查看完整版本: 申请会员ID:cyxvc【冒充他人作品申请】