前言
本篇内容将讲解 PE 动态内容映射、PE 蠕虫(捆绑马)制作及其局限性。
这篇文章昨天发过一次,后面有人提醒文章中不能出现原代码,所以我把文章删了,今天重发,删了原代码,想看原文和原代码的可以在星球下载。
本文的内容均参考自 Windows APT Warfare 这本书,并对书中的内容做了补充和拓展,有需要的可以自行在京东购买:
一、PE 动态内容映射
一张图搞懂 PE 的动态内容映射:
左边是进程执行时 PE 被加载到内存中的结构,右边是没有被加载起来的 PE 静态文件结构。从 PE 静态文件到执行时加载到内存时步骤如下:
- 检查 NT Headers 中 Optional Header 记录的 ImageBase 地址,0x400000 这是应用程式动态加载时被存放的位置(注:如果同时启用 ASLR 防护与重定位功能则会是随机的 ImageBase 地址)。
- 检查 NT Headers 中 Optional Header 记录的 SizeOflmage,获取在 ImageBase 位置上需要 0xDEAD 字节才能存放该 PE 动态加载的内容,然后在 0x400000 上申请一块 0xDEAD 大小空间。
- 接着将 PE Header(DOS、NT、Section)拷贝到 ImageBase 地址上,这三个部分总共所占用的大小能从 Optional Header 的 SizeOfHeaders 得知为 0x400 的大小,然后系统会将 PE 静态内容的 offset=0 至 0x400 上的内容复制到 ImageBase 的 0 至 0x400 的空间上。
- 从 PE 静态内容中的 FileHeader 的 NumberOfSections 属性得知 Section Data 数量,而从 Section Headers 数组获取每一个 Section Data 内容地址 PointerToRawData 与这个 Section Data 希望被映射在动态内存的 RVA (Relative Virtual Address),即 VirtualAddress 属性。然后将每一个 Section Data 内容以复制到动态内存对应的位置上,即 ImageBase VirtualAddress 地址。
上面就是所有 PE 从静态到动态内存的映射流程,完成后 Main Thread 就会从 NDLL!LdrplnitializeProcess 开始执行入程式装载器函数将程式修正并跳至执行程式入口点(0×401234),整个程式成功执行起来。
二、PE 蠕虫感染(捆绑马)
聪明的你一定想到了,程序启动后是从 AddressOfEntryPoint 指向 .text Data 处开始执行的,而 .text Data 中保存了程序原代码经编译器编译后的机器码。如果我们自己增加一个 Section Header 和 Section Data,并将 AddressOfEntryPoint 地址修改成我们增加的 Section Data 地址不就能劫持该程序执行我们想要执行的任意代码了吗?没错,这就是 PE 蠕虫感染或者是我们常说的捆绑马的由来了。如下图,新增一个 .scdata Header 和 .scdata Data,假设 .scdata Data 大小为 0x1000:
1. 新增 Section Header
新增 Section Header 时需要注意的一个点,即 PE Header 剩余空间是否足够增加一个新的 Section Header,当不够时需要将 Section Data 整体往后移。由于 PE Header 的大小是经过 FileAlignment 对齐后的大小,所以 PE Header 的大小必定是 FileAlignment 的倍数,PE Header 大小不足一个 FileAlignment 的大小则按一个 FileAlignment 大小算,因此 Section Header 后面必定剩余有一个小于 FileAlignment 的大小可以使用:
这就产生了两种情况,即 PE Header 剩余空间足够和 PE Header 剩余空间不足。
有两种方式计算 PE Header 剩余空间:
- 通过 OptionalHeader 的 SizeOfHeaders 属性记录的 PE Header 大小减去 Section Headers 的末尾地址。
- 通过第一个 Section Header 记录的 PointerToRawData,即 Section Data 的开始地址减去 Section Headers 的末尾地址。
printf("[*] PE Headers 剩余大小:%dn", pe_content OptionalHeader->SizeOfHeaders - (DWORD_PTR)(pSectionHeaders NumberOfSections));
printf("[*] PE Headers 剩余大小:%dn", pe_content pSectionHeaders[0].PointerToRawData - (DWORD_PTR)(pSectionHeaders NumberOfSections));
通过这两种方法计算出来的 PE Header 剩余空间大小是一样的:
(1)PE Header 剩余空间足够
当 PE Header 剩余空间足够时直接在原 Section Headers 后面插入一个新的 Section Header 即可。
(2)PE Header 剩余空间不足
当 PE Header 剩余空间不足时,将 Section Data 往后移一个 FileAlignment 大小的位置,这样就为 PE Header 腾出了一个 FileAlignment 大小的空间。
最后,导出到文件之前别忘记修改的 Optional Header 的 SizeOfHeaders 属性记录的 PE Header 的大小。
这样系统才知道 PE Header 增长了一个 FileAlignment 大小的空间。
(3)创建一个新的 Section Header
新的 Section Header 需要设置的参数值如下:
一般 Characteristics 有可读、可执行权限就行了,但部分 shellcode 会有自解密或自修改的行为,需要有可写权限才能正常执行。
可以使用 P2ALIGNUP 宏获取对齐后的长度:
代码语言:javascript复制#define P2ALIGNUP(size, align) ((((size) / (align)) 1) * (align))
2. 修补 File Header 和 Optional Header
3. 按顺序写入二进制数据
计算输出大小,即旧 PE 文件的大小(即最后一个 Section Header 的 PointerToRawData SizeOfRawData 的大小) 新增的 Section Header 的大小,如果 PE Header 的剩余空间不足,还要加上偏移大小 Alignment。
申请内存,然后按顺序写入二进制数据:
- PE Header
- 新增的 Section Header
- 旧的 Section Data
- 新增的 Section Data,向新增的 Section Data 写入 Shellcode,即在新增的 Section Header 的 PointerToRawData 地址写入 Shellcode。
这样一个捆绑马就做好了,输出到文件。
使用如下 shellcode 进行一下捆绑测试,其功能是弹出一个错误警告框:
代码语言:javascript复制char payload[] =
"x31xd2xb2x30x64x8bx12x8bx52x0cx8bx52x1cx8bx42"
"x08x8bx72x20x8bx12x80x7ex0cx33x75xf2x89xc7x03"
"x78x3cx8bx57x78x01xc2x8bx7ax20x01xc7x31xedx8b"
"x34xafx01xc6x45x81x3ex46x61x74x61x75xf2x81x7e"
"x08x45x78x69x74x75xe9x8bx7ax24x01xc7x66x8bx2c"
"x6fx8bx7ax1cx01xc7x8bx7cxafxfcx01xc7x68x79x74"
"x65x01x68x6bx65x6ex42x68x20x42x72x6fx89xe1xfe"
"x49x0bx31xc0x51x50xffxd7";
使用如下应用进行捆绑:
捆绑成功:
点击执行生成的 EVCapture_v4.2.1_infected.exe 就会执行我们的 shellcode 弹出一个警告框:
三、免杀360捆绑马制作
先写一个免杀效果比较好的加载器,shellcode 异或加密 base64 加密 动态 key,使用反射执行,这样一个特征非常少的加载器就做好了:
然后将其转为 shellcode:
将其与如下应用进行捆绑:
生成:
使用360扫描成功通过静态扫描:
执行成功上线:
当然虽然免杀了360,但是VT查杀效果并不太好:
由于 exe 转成 shellcode 后特征比较明显,最好的办法是用汇编写过一个免杀的加载器再编译成 shellcode,这样捆绑的效果就比较好了。
最后
如果希望在捆绑 shellcode 之后还能执行原程序内容,可以在加载器中利用线程调用原程序调用原程序入口点,如下,假如原程序入口点是 0x1123:
但是不要使用 WaitForSingleObject,不然后面的加载器内容就无法执行了。
如果希望在捆绑 shellcode 之后还能进行签名,需要在捆绑之前先将 exe 原来的签名去除:
代码语言:javascript复制signtool.exe remove /s xxx.exe
需要注意的是,并不是所有 exe 都可以被捆绑,比如加了壳的,如果 exe 原程序内容加了在执行时的完整性校验,则不能在捆绑之后执行原程序内容。
参考文献
[1] Windows APT Warfare
长按-识别-关注
锦鲤安全
一个安全技术学习与工具分享平台
点分享
点收藏
点点赞
点在看