看雪论坛
发新帖
3

[原创][开源]用C++实现的壳(基础版)

cyxvc 2015-12-29 19:41 17680
【扩展版】传送门:[URL="http://bbs.pediy.com/showthread.php?t=206873"]http://bbs.pediy.com/showthread.php?t=206873[/URL]


        最近一直在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地址、区段数量以及重定位表、导入表指针信息。这里保存的信息其实越详细越好,方便以后拓展功能的时候能够用到。
//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():
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文件的缓冲区指针,代码段基址,代码段大小。这三个信息在上一步操作中已经获取:
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部分所导出的结构体如下:
//导出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这个文件,然后获取这个结构体信息,往里面保存数据:
  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文件格式详解。当你对这些基本信息有一点了解之后,再阅读这段代码应该会很容易:
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( )函数实现了这两个缓冲区的合并,并添加区段信息:
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出来的,其分布也是以内存对齐大小所对齐的,所以保存成文件的时候我顺便修改了文件对齐大小,同内存对齐大小相同;同时将数据目录表中不必要的信息摸掉:
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.释放资源
        万事大吉以后不要忘记擦屁股,养成良好的习惯,释放掉资源,避免资源泄露(虽然这个小程序也用不了多少内存吧哈哈!)
  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()函数的函数地址:
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()修改内存属性为可写,修复完以后再将原来的属性设置回去。
        遍历重定位表并修复:
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位置即可。
        加载模块和获取函数地址这两个函数之前我们通过自定义函数指针的方式已经获取到了,那么剩下的就是在遍历导入表了,具体代码实现如下:
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加壳。
        有兴趣的朋友可以下载源码看一下,由于本人才疏学浅、能力有限,有错的第地方还请大牛指出!
        注:此壳不以加密为目的,被脱秒秒种的事...  

   源码下载: CyxvcProtect@15PB.rar
上传的附件:
本主题帖已收到 0 次赞赏,累计¥0.00
最新回复 (45)
5
Tennn 2015-12-29 20:01
2
感谢分享 前排来赞~
asdli 2015-12-29 20:08
3
赞 111111
rrrfff 2015-12-29 20:42
4
扔到github上
3
cyxvc 2015-12-29 20:47
5
还真没去那里看过,多谢提醒,以后常去逛逛
11
Netfairy 2015-12-29 21:04
6
感谢分享
zjjhszs 2015-12-29 22:03
7
唉  我只能说 15PB  广告打的真多  一开始真有期待 最后你发现 都是基础 的基础   发些内部视频出来啊
mykgd 2015-12-29 22:45
8
感谢分享!
2
littlewisp 2015-12-29 23:38
9
可以结合bambam,就不用loadlibrary了
3
cyxvc 2015-12-29 23:52
10
请教一下,什么是bambam 啊
Fido 2015-12-30 11:27
11
感谢楼主分享.........................
thxiaobo 2015-12-30 12:04
12
学到的知识分享,非常感谢。我为15PB匠人精神点赞
1
zfdyq 2015-12-30 12:54
13
NICE~
3
cyxvc 2015-12-30 13:35
14
小疯疯
Sollage 2015-12-30 14:42
15
感谢分享,学习下
timer 2015-12-30 15:04
16
nice work
coolsnake 2015-12-30 15:38
17
感谢分享
2
littlewisp 2015-12-30 15:51
18
一个开源壳
luisinscut 2015-12-30 19:25
19
感谢分享
smurf 2015-12-31 21:08
20
马上毕业了,加油吧,小伙子
yunshouhu 2016-1-1 01:30
21
测试了下在win7 x64可以正常运行,但是在xp win32系统运行不成功
3
cyxvc 2016-1-1 09:40
22
对,我编程的环境就是Win7 64的。我觉得运行不了是因为Shell.dll没有静态编译,如果加壳系统缺少VS2013的环境的话就无法正常加载Shell.dll

如果在xp 或者 Win32 下用源代码重新编译一下应该可以...

暂时没考虑太多兼容性问题啊
jet_coder 2016-1-3 15:38
23
条理清晰,学得很扎实,继续努力吧
jxlmz 2016-1-4 09:33
24
这个不错  很详细  很细致
hbcld 2016-1-18 17:41
25
感谢分享
CutlerMao 2016-3-1 01:22
26
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;
}

仔细阅读了一下源码,对如上函数有疑问。具体代码如下:
if (m_dwImageSize % m_dwMemAlign)
                m_dwImageSize = (m_dwImageSize / m_dwMemAlign + 1) * m_dwMemAlign;
楼主注释为://修正没镜像大小没有对齐的情况

详细情况我来举例。
假设:
文件对齐:0x200
内存对齐:0x1000
PE头大小:0x400(多半如此)
某PE中节的数量,假设为8,且每个节的大小刚好都是0x200(这样比较容易分析问题)
也就是说,PE文件原始大小为0x200 * 9
加载到内存的时候,需要的内存应该为0x1000 * 9(因为每个节要按照SectionAlignment对齐)
但楼主的算法实际情况是:
(0x200 * 9 / 0x1000 + 1)* 0x1000 = 0x1000 * 3(比真实需要的内存小)
显然只是考虑了 文件整体在内存中的对齐,而没有按照每个节来计算对齐需要的空间。

所以,我觉得,楼主这个空间分配小了。
memcpy_s(pFileBuf_New + pSectionHeader->VirtualAddress,
                        pSectionHeader->SizeOfRawData,
                        m_pFileBuf+pSectionHeader->PointerToRawData,
                        pSectionHeader->SizeOfRawData);
拷贝的目标位置 pFileBuf_New + pSectionHeader->VirtualAddress, 很有可能会超过你分配的内存大小。

请楼主确认下哈。。。
3
cyxvc 2016-3-1 21:25
27
先回答你第一个问题:
if (m_dwImageSize % m_dwMemAlign)
    m_dwImageSize = (m_dwImageSize / m_dwMemAlign + 1) * m_dwMemAlign;
其中m_dwImageSize这个变量是PE文件的镜像【总】大小,是从PE文件中获取到的,不是某一个区段的

大小,这个值用LoadPE查看一下,就是在【基本PE头信息】中的第三项【镜像大小】这个值,这个镜像

大小一般都是0x1000对齐的,但由于我在平时测试时发现有些PE文件的镜像大小并不是0x1000对齐,所

以我就在这里校验一下,如果没对齐就对齐一下,方便在后面添加区段。

再回答第二个问题
    目标位置的buf是每个区段0x1000对齐以后的,原位置的buf是每个区段默认的0x200对齐的。将后

置往前者拷贝,怎么可能会空间不够呢?

加过无数次一般的简单exe文件都没有任何问题。

你再仔细考虑考虑?
CutlerMao 2016-3-2 00:48
28
哈哈,确实是我搞错啦。
m_pNtHeader->OptionalHeader.SizeOfImage;已经是镜像在内存中对齐后所占用的内存空间了。我一直理解为是文件的在磁盘中的真实大小,这两个值确实不一样。要真是用文件在磁盘中的大小的话,那就真错了哈哈。感谢纠正~

最后,对误报表示歉意~请楼主收下鄙人的膝盖。
长发飘飘 2016-3-25 15:00
29
PTYPEOFFSET pTypeOffset = (PTYPEOFFSET)(pPEReloc + 1);//+1是什么意思啊??
    DWORD dwNumber = (pPEReloc->SizeOfBlock - 8) / 2;//为什么-8还除2????
可以说明清楚吗?
3
cyxvc 2016-3-30 16:46
30
首先回答第二个问题:
DWORD dwNumber = (pPEReloc->SizeOfBlock - 8) / 2;//为什么-8还除2????

其中 dwNumber 是这个Reloc区块需要重定位的地址的个数(也就是自己定义的TYPEOFFSET结构体的个数),这个数如何确定,就得分析 IMAGE_BASE_RELOCATION 这个结构体,其结构如图所示:

可见IMAGE_BASE_RELOCATION的前八个字节保存的是 VirtualAddress和SizeOfBlock,所以首先要-8,剩下的都是TypeOffset了,由上面我们自己定义的TYPEOFFSET结构体可知,TypeOffset 是一个数组,数组每项大小为两个字节(16位),它由高 4位和低 12位组成,高 4位代表重定位类型,低 12位是重定位地址,它与VirtualAddress 相加即是指向PE 映像中需要修改的那个代码的地址。

即每个TYPEOFFSET的结构体大小是2个字节。知道总大小,知道单个大小,求数量的话就是总大小除以每个大小,所以是(pPEReloc->SizeOfBlock - 8) / 2

再回答第一个问题:
PTYPEOFFSET pTypeOffset = (PTYPEOFFSET)(pPEReloc + 1);//+1是什么意思啊??

理解了第二问以后,这点也容易理解了,pTypeOffset指向的是TypeOffset数组的首地址,这个首地址相对于这个Reloc区块的偏移是8个字节(前八个字节保存的是VirtualAddress和SizeOfBlock)。(pPEReloc + 1)中的+1其实就是加了8个字节(pPEReloc指向的结构体IMAGE_BASE_RELOCATION的大小就是8个字节)。

这样经过(PTYPEOFFSET)(pPEReloc + 1),+1再强转为PTYPEOFFSET以后就确定TypeOffset数组的首地址了。
游鱼Andy 2016-5-26 12:18
31
看起来很高端啊  值得学习!感谢分享!
donself 2016-5-26 12:51
32
这年头哪有什么内部视频啊,外部视频都比内部视频有技术含量;
当兴趣或技术变成牟取利益的工具的时候,兴趣就已经不再是兴趣,技术也已经不再是技术了.
前进的小巨人 2016-5-29 14:43
33
楼主您好,感谢您的分享,对于被加壳程序的导入表修复我有一点点没弄明白

再修复导入表的时候,传入被加壳程序在内存中对齐后的导入表RVA,然后遍历这个结构,全部获取函数地址一次,然后放入IAT中。

但是IAT没有在PE头里面相关联,内存中的PE头部信息应该都没有关联,我不知道程序怎么使用这个函数地址,是通过什么?
以前我认为在使用这个函数的时候,首先通过函数名称,然后链接到地址,在跳转到该地址执行。

现在看您的程序是IAT没有与PE头相关联的,不知道有什么资料可以参考一下的?
qqsunqiang 2016-5-29 16:01
34
感谢楼主的分享,学习下。
痞子狗 2016-5-30 16:52
35
很不错,感谢楼主,适合学习
3
cyxvc 2016-6-1 01:07
36
你找个PE文件,用LordPE加载,然后点【目录】,其中有一项就是【IAT】,存着IAT的RVA和大小啊,这些信息通过遍历PE文件的目录表都可以遍历得到。
修复IAT的时候,所需要的信息都保存在PE文件的导入表中,从导入表中可以得到函数名,函数模块,然后从函数模块中得到函数地址,导入表中也保存着函数的填充地址(IAT),这样将你得到的函数地址写到IAT就可以了。
参考书籍的话,PE权威指南 里,导入表那一章应该有说明,希望能帮到你。
3
cyxvc 2016-6-1 01:09
37
IAT的填充是系统PE加载器完成的,在程序运行之初会一次性全部填充成真是的函数地址,程序用到某个函数的时候,直接去IAT里取函数地址就行,保存函数地址的这个地址是固定不变的。
nchxmoon 2016-7-5 18:06
38
感谢楼主无私的奉献,代码可编译,易阅读!
二十七秒 2016-7-22 16:23
39
感谢楼主,想不到一个壳如此复杂
shiphere 2016-9-21 19:48
40
观摩中。。。
我是谁! 2016-10-2 08:20
41
感谢楼猪分享技术,很感谢,特别对于我等菜鸟!!
Rookietp 2016-12-7 04:51
42
很好的文章,分析的很全面
oysjk 2016-12-7 22:39
43
我会认真学习的 谢谢版主~
1
黑手鱼 2017-4-24 18:26
44
匹配kenrel32名字不就通用了吗?
       _asm
       {
                       push        eax
                       push        ecx
                       push        edi
                       push        esi
                       xor                ecx,  ecx
                       mov                esi,  fs:[30h]                              //  Ptr32  _PEB
                       mov                esi,  [esi  +  0ch]                        //  Ptr32  _PEB_LDR_DATA
                       mov                esi,  [esi  +  1ch]                        //  Get  InInitializationOrderModuleList.Flink
               next_module  :
                       mov                eax,  [esi  +  8h]                          //  eax  =  kernel32.DLL地址
                       mov                edi,  [esi  +  20h]                        //  BaseDllName
                       mov                esi,  [esi]                                    //  下一个模块
                       cmp[edi  +  12  *  2],  cx                              //  模块结尾是0
                       jne                next_module                                  //继续循环
                       mov          dwKerenl32Addr,eax                    //找到的地址
                       pop                esi
                       pop                edi
                       pop                ecx
                       pop          eax
       }
冰雄 2017-9-5 08:17
45
感谢楼主的分享了。本壳还有地方需要修订就是重定位,因为测试发现电脑重启后,加了壳的程序就不能运行了。
1
瀚海云烟 2017-9-5 09:14
46
冰雄 感谢楼主的分享了。本壳还有地方需要修订就是重定位,因为测试发现电脑重启后,加了壳的程序就不能运行了。
我点进来看就是想看作者如何修正系统加载器对重定位操作的
返回



©2000-2017 看雪学院 | Based on Xiuno BBS | 域名 加速乐 保护 | SSL证书 又拍云 提供 | 微信公众号:ikanxue
Time: 0.016, SQL: 10 / 京ICP备10040895号-17