【Windows】从零开始写一个加密壳

本文详细介绍了如何从零开始编写一个Windows 32位程序的加密壳,内容涵盖PE文件结构、加壳思路、ShellCode的获取与集成、重定位表的处理,以及成品的检验。通过理解PE文件的各个组成部分,如DOS头、NT头、EP、ImageBase、重定位表等,作者展示了如何新增Section,更改EP,并加密.text段。同时,文章还提供了手动修复重定位的步骤,确保壳代码能在运行时正确执行。
摘要由CSDN通过智能技术生成


(本文仅为个人学习记录,不保证文章中没有学术性错误或笔误)

认识PE文件结构

既然要写壳,那么就不得不了解一下Windows的可执行文件格式——PE文件的内部结构了

这次要写的是32位程序的壳,用到的是PE32结构,64位的PE32+结构和PE32结构大同小异,只是部分字段变宽了而已

DOS头

DOS头是标志着一个DOS文件的开头,而PE文件是DOS文件的一个拓展,因此一个PE文件一定有DOS头

DOS头以magic字符“MZ”开头,是某个人名字的首字母,后面跟了很多内容,我们要关心的只有其中一个字段

其中的e_magic即是MZ刚好凑成的WORD,而e_lfanew记录着NT头的偏移地址,我们读取这个值就能获得NT头的起始地址

NT头

NT头是我们需要重点关注的对象,可以看到NT头是分为32和64版本的

不同位数的不同之处体现在OptionalHeader结构处

其中的Signature始终为“PE\0\0”

FileHeader结构中我们需要关注的只有NumberOfSections这个字段,待会儿会用到


OptionalHeader中,分为2个部分,一个是Standard fields,一个是NT additional fields

我们需要关注的字段为AddressOfEntryPoint、ImageBase、SizeOfImage、DataDirectory

其中,常量IMAGE_NUMBEROF_DIRECTORY_ENTRIES的值为16

代表着有至多16个DataDirectory,每个结构都记录了相应的RVA(相对虚拟地址)和大小

根据下标的不同,有不同的含义,我们需要关注的是IMAGE_DIRECTORY_ENTRY_BASERELOC,涉及到后面手动恢复重定位时定位重定位表

EP

EP即EntryPoint,也就是上面我们看到的AddressOfEntryPoint,它代表了一个程序的入口点,也就是这个程序第一句被执行的代码的地址

这个值不是我们写程序的时候能指定的,也不是main函数的第一行代码的地址,事实上,编译器会在编译的时候添加一些启动代码,在启动代码执行完以后才会调用main函数,而启动代码通常就被设置为EP

我们写壳就是要在程序执行之前做一些我们自己想做的事,因此通常思路就是将EP改到指向我们自己的代码,在我们的代码执行完以后再跳回原EP处执行

ImageBase

每一个程序都有自己在内存中装载的地址,这个值就是指定被运行的程序会被装载到内存的哪个位置的

但是由于各种pwn攻击的存在,诞生了一些对抗攻击的技术,地址随机化就是其中一个

多个DLL如果指定了相同的ImageBase也会导致撞车,因此也只能采用随机地址来装载DLL

因为上述原因,程序真正运行起来后被装载到的位置通常和ImageBase的值不一样,但是ImageBase后面同样是有用的

重定位表

在程序编译的时候,地址是按照ImageBase来计算的,而程序运行的时候地址却往往不同,因此需要有一个将这些地址转换的过程,编译器将需要重定位的代码的地址记录在重定位表里

重定位是发生在PE文件加载的时候,这个时候才刚刚确定程序真正的加载地址,EP的代码还没有运行,PE加载器自动帮我们把重定位表记录的地址的数据执行如下操作:

Data += BaseAddress - ImageBase

这个操作对于我们来说是透明的,绝大多数情况下不需要管它,但是写壳的时候涉及到对代码段的加密解密,因此后面会进行手动还原的操作

Section Table

段表是紧挨着NT头后面的一个结构数组,这个数组的长度可以由上面提到的NumberOfSections来得到

其中常量IMAGE_SIZEOF_SHORT_NAME的值为8

Section的Name是一个char[8],它不必以\0结尾,也就是最多可以有8个字符,以“.”开头也不是必须的

Misc联合体我们通常取VirtualSize,它代表着文件被映射到内存中以后这个Section在内存中的大小

VirtualAddress代表着文件被映射到内存中以后这个Section的起始地址的相对偏移

SizeOfRawData是文件中Section的实际大小

PointerToRawData是文件中Section的起始地址的相对偏移

Characteristics记录了这个Section的属性,比如是否可读可写可执行

因为原程序的代码已经被编译器编译好了,很难加入大量代码到已有的空间中,因此我们的代码也会以新Section的方式添加到PE文件中

PE结构总览


使用WinHex打开一个程序以后可以看到它的内部结构,MZ、PE字符可以很容易的被发现

还能看到一行不能以DOS模式运行的提示,这是为了兼容性,当PE文件在DOS中被运行时就会这样报错

可以看到NT头之后紧跟着几个Section Table,其中最后的.Titvt段是我新增的壳代码段,在我的电脑上,一个由VS2019编译的32位C++程序会有.text .data .rdata .rsrc .reloc五个Section

在最后一个Section Table的后面会有大量用于对齐而填充的00,之后便是每个Section的真正的内容了

绝大多数情况下我们都可以认为最后一个Section Table之后还有大量空余地址可以用于存放新的Section Table

加壳思路

新增Section

我们读入一个程序的文件后,可以解析它的PE结构,我们在原本的基础上新增一个Section用于存放我们的壳代码

NumberOfSections记录了Section的数量,我们将它+1,然后在最后一个Section Table的后面增加一个新的Section Table

length是我们要添加的壳代码的长度,也就是新Section的大小

SizeOfImage也需要增加相应的长度,否则PE加载器会报错

VirtualAddress在内存中是0x1000对齐的,因此需要计算一下我们的新Section的虚拟地址应该是多少

同理,VirtualSize也一样需要对齐

由于这个Section的数据是需要当作代码执行的,因此需要添加可执行属性

更改EP


我们把新增的Section的RVA设置为EP,这样,程序运行的第一句代码就是我们的壳代码了

加密.text段


因为是第一次写壳,就不搞那么复杂了,只是简单的把整个.text段异或一下

在前面我们已经拿到第一个Section Table的指针了,从第一个开始依次遍历所有Section Table,找到名为.text的Section,获取.text段的范围(其实.text通常都是第一个),然后逐字节异或,加密就完成啦

保存ImageBase

因为程序在装载好以后ImageBase会被PE加载器覆写为真正的基址,原来的ImageBase将会丢失,我们不希望再去读文件来确定ImageBase,因此将它保存在壳代码中

在原文件的末尾就是我们需要添加的Section的内容该储存的位置了,我们写入jz+jnz的字节码,跳过后面的4个字节(也可以直接jmp,这里是为了迷惑IDA)

那么后面的4个字节将可以拿来存放东西,我们把ImageBase写入这里,到时候就可以直接读取了

然后写入shellcode,也就是真正的壳代码

最后用一个长jmp跳回原来的EP,开始真正的程序的运行

ShellCode

获取编译好的shellcode

我们另外创建一个项目,用来生成shellcode,在项目属性里关闭安全检查(GS)和编译器优化,否则会有security_cookie检查和常量优化等的干扰

先写一个func()函数,里面用来写壳代码,再在后面写一个main()函数,用来输出字符串形式的shellcode,方便硬编码进加壳器中

编写shellcode

由于是需要直接植入的代码,因此不能有常量区的数据和导入的函数供我们使用(比如字符串、cout、GetModuleHandleW()、printf()等)

我们需要先从当前的PEB(进程环境块)中获得Kernel32.dll的基址,我们手动读取fs:[30h]来得到PEB结构的地址,至于怎么通过PEB得到基址,网上有很多讲解,咱们用代码跟着手动实现一下

因为DLL也是PE文件,于是拿到基址后,我们来解析这个PE结构

有一个函数叫GetProcAddress(),是Kernel32.dll导出的,我们只要遍历它的导出表,即可获得这个函数的地址,我们也就获得了第一个能够调用的函数,而有了这个函数以后,我们就能获得任意函数的地址了

我们依次比较每个导出函数的名字,如果是GetProcAddress的话,则将对应下标的ordinal的值当作下标访问funcAddr,拿到该函数的地址

需要注意的是,如果直接用字符串比较的话,字符串会被保存在常量区,我们的shellcode换个地方就会失效,必须把所有操作和数据都硬编码在代码中,因此转换成short数组来比较

如果开了编译器优化的话,有的会把这种比较直接优化成常量比较,因此要关闭优化

获取所需函数


本次的壳一共需要以上3个函数,我们前面已经找到了第一个函数,接下来用它来获取另外2个函数的地址

由于GetProcAddress()需要传入字符串参数,我们需要想办法避开字符串

随手写了一个生成器,用来将字符串变成硬编码的数据,以为会用到很多次,没想到就只用了2次

用获取到地址的函数拿到主模块的基址

同样,解析PE拿到我们要的数据,然后把另一个函数的地址也获取了

手动修复重定位

先设置.text段为可写,等会再恢复

先讲一讲为什么需要下面的步骤

正常情况下PE加载器会帮我们进行重定位,让我们的代码中的地址与实际装载的地址相符合

但是我们将.text段全部异或了,那么这个时候它重定位的时候可不会管你有没有进行加密,全部执行重定位

这个时候如果直接再次异或的话,结果是不对的(举个例子,((A^C)+B)^C和A+B在绝大多数情况下肯定是不同的)

所以我们需要先把PE加载器重定向的位于.text段内的地址恢复原来的值,然后再进行解密,接着再重新手动重定向回去


(注:后续开发中发现一个错误,下图的 sizeof(PIMAGE_BASE_RELOCATION) 把字符 P 去掉,之前错误的写法没有导致运行出错所以没有及时发现)

最后再恢复.text段的保护属性

大功告成,这个时候就可以直接跳到原EP处开始执行了

集成shellcode

我们运行生成器,将输出的数据复制到加壳器的代码中

在这里,我将它们作为全局变量存放

输入与输出

我比较喜欢拖动进行加壳,所以使用命令行来传参

读入的文件我没有检验其合法性,按理来说是需要检查这是不是一个合法的PE文件的

完整的检查代码在我另一个项目里,也许要用到的时候可以直接copy过来

最后将加完壳的文件写入输出文件中,结束整个加壳过程

成品检验

ExeInfoPE



IDA 7.5

刚用IDA打开时,定位到了EP但是没有将其识别为一个函数,左侧的2个函数是没有任何意义的数据片段

我们在0x409008处按P后按F5

void __usercall sub_409008(char a1@<bh>, int a2@<edi>, int a3@<esi>)
{
   
  unsigned int v3; // eax
  unsigned int v4; // ecx
  int v5; // edx
  int v11; // edx
  unsigned int v12; // ecx
  int v13; // [esp-24h] [ebp-DCh]
  int v14; // [esp-20h] [ebp-D8h]
  int v15; // [esp-1Ch] [ebp-D4h]
  int v16; // [esp-18h] [ebp-D0h]
  int v17; // [esp-14h] [ebp-CCh]
  int v18; // [esp-10h] [ebp-C8h]
  int v19; // [esp-Ch] [ebp-C4h]
  int v20; // [esp-8h] [ebp-C0h]
  int v21; // [esp-4h] [ebp-BCh]
  char v22[4]; // [esp+0h] [ebp-B8h] BYREF
  char v23[20]; // [esp+4h] [ebp-B4h] BYREF
  char v24[16]; // [esp+18h] [ebp-A0h] BYREF
  int v25; // [esp+28h] [ebp-90h] BYREF
  unsigned int v26; // [esp+2Ch] [ebp-8Ch]
  unsigned int v27; // [esp+30h] [ebp-88h]
  int (__cdecl *v28)(_DWORD); // [esp+34h] [ebp-84h]
  int v29; // [esp+38h] [ebp-80h]
  int v30; // [esp+3Ch] [ebp-7Ch]
  int v31; // [esp+40h] [ebp-78h]
  unsigned int v32; // [esp+44h] [ebp-74h]
  void (__cdecl *v33)(unsigned int, int, int, int *); // [esp+48h] [ebp-70h]
  int v34; // [esp+4Ch] [ebp-6Ch]
  int v35; // [esp+50h] [ebp-68h]
  int v36; // [esp+54h] [ebp-64h]
  int v37; // [esp+58h] [ebp-60h]
  _DWORD *v38; // [esp+5Ch] [ebp-5Ch]
  _DWORD *v39; // [esp+60h] [ebp-58h]
  int v40; // [esp+64h] [ebp-54h]
  _DWORD *v41; // [esp+68h] [ebp-50h]
  _DWORD *v42; // [esp+6Ch] [ebp-4Ch]
  int v43; // [esp+70h] [ebp-48h]
  int (__cdecl *v44)(int, char *); // [esp+74h] [ebp-44h]
  _DWORD *v45; // [esp+78h] [ebp-40h]
  int v46; // [esp+7Ch] [ebp-3Ch]
  unsigned int v47; // [esp+80h] [ebp-38h]
  _BYTE *m; // [esp+84h] [ebp-34h]
  unsigned int v49; // [esp+88h] [ebp-30h]
  unsigned int i; // [esp+8Ch] [ebp-2Ch]
  unsigned int n; // [esp+90h] [ebp-28h]
  unsigned int l; // [esp+94h] [ebp-24h]
  _DWORD *j; // [esp+98h] [ebp-20h]
  int k; // [esp+9Ch] [ebp-1Ch]
  _DWORD *v55; // [esp+A0h] [ebp-18h]
  unsigned __int16 *v56; // [esp+A4h] [ebp-14h]
  unsigned int v57; // [esp+A8h] [ebp-10h]
  int v58; // [esp+ACh] [ebp-Ch]
  _DWORD *v59; // [esp+B0h] [ebp-8h]
  int v60; // [esp+B4h] [ebp-4h]
  int savedregs; // [esp+B8h] [ebp+0h]
  void *retaddr; // [esp+BCh] [ebp+4h] BYREF

  v60 = *(_DWORD *)(***((_DWORD ***)NtCurrentPeb()->ImageBaseAddress + 7) + 8);
  v43 = v60;
  v59 = (_DWORD *)(v60 + *(_DWORD *)(v60 + 60));
  v45 = (_DWORD *)(v60 + v59[30]);
  v32 = v45[6];
  v29 = v60 + v45[7];
  v31 = v60 + v45[8];
  v11 = v60 + v45[9];
  v30 = v11;
  v44 = 0;
  for ( i = 0; ; ++i )
  {
   
    v12 = i;
    if ( i >= v32 )
      break;
    v56 
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值