恶意代码的亲密接触之病毒编程技术(3)
重定位
病毒自身的重定位是病毒代码在得以顺利运行前应解决的最基本问题。病毒代码在运行时同样也要引用一些数据,比如API 函数的名字、杀毒软件的黑名单、系统相关的特殊数据等,由于病毒代码在宿主进程中运行时的内存地址是在编译汇编代码时无法预知的,而病毒在感染不同的宿主时其位于宿主中的准确位置同样也无法提前预知,因此病毒就要在运行时动态确定其引用数据的地址,否则,引用数据时几乎肯定会发生错误。对于普通的PE文件比如动态链接库而言,在被加载到不同地址处时由加载器根据PE中一个被称为重定位表的特殊结构动态修正引用数据指令的地址,而重定位表是由编译器在编译阶段生成的,因此动态链接库本身无需为此做任何额外处理。病毒代码则不同,必须自己动态确定需引用数据的地址。比如一段病毒代码被加载在0x400000处,地址0x401000处的一条语句及其引用的数据定义如下所示,相关地址是编译器在编译时计算得到的,这里假设编译时预设的基地址也是0x400000:
如果病毒代码在宿主中也加载到基地址0x400000,显然是能够正常执行的,但如果这段代码被加载在基地址0x500000 运行时则出错,对病毒而言,这是大多数时候都会遇到的情况,因为指令中引用的仍然是0x402035这个地址。如果病毒代码不是在宿主进程中而是作为一个具有重定位表的独立PE文件运行,正常情况下由系统加载器根据重定位表表项将 mov eax,dword ptr [402035]中的0x402035修改为正确值0x502305,这样这句代码就变成了mov eax, dword ptr [5402035],程序也就能准确无误地运行了。不过很可惜,对在其它进程内运行病毒代码而言,必须采取额外的手段、付出额外的代价感染宿主PE文件时就及时加以解决,否则将导致宿主进程无法正常运行。 至少有两种方法可以解决重定位的问题: A)第一种方法就是利用上述PE 文件重定位表项的特殊作用构造相应的重定位表项。在感染目标PE文件时,将引用自身数据的需要被重定位的地址全部写入目标PE文件的重定位表中,如果目标PE 无任何重定位表项(如用MS linker 的/fixed)则创建重定位表节并插入新的重定位项;若已经存在重定位表项,则在修改已存在的重定位表节,在其中插入包含了这些地址的新表项。重定位的工作就完全由系统加载器在加载PE文件的时候自动进行了。重定位表项由PE 文件头的DataDirectory数据中的第6 个成员 IMAGE_DIRECTORY_ENTRY_BASERELOC 指向。该方法需要的代码稍多,实现起来也相对比较复杂,另外如果目标文件无重定位表项(为了减小代码体积,这种情况也不少见),处理起来就比较麻烦,只有用高级语言编写病毒才常用该种方法,在一般的PE 病毒中很少使用。 B)利用Intel X86体系结构的特殊指令,call 或fnstenv等指令动态获取当前指令的运行时地址,计算该地址与编译时预定义地址的差值(被称为delta offset),再将该差值加到原编译时预定的地址上,得到的就是运行时数据的正确地址。对于intel x86指令集而言,在书写代码时,通过将delta offset 放在某个寄存器中,然后通过变址寻址引用数据就可以解决引用数据重定位的难题。还以上例说明,假如上述指令块被操作系统映射在0x500000处那么代码及其在内存中的地址将变为:
显然,mov 指令引用的操作数地址是不正确的,如果我们知道了mov指令运行时地址是0x501000,那么计算该地址和编译时该指令预设地址的差值:0x501000-0x401000 = 0x100000。很显然指令引用的实际数据地址应该为0x402035+0x100000 = 0x502035。从上例可以看出,只要能够在运行时确定某条指令动态运行时的地址,而其编译时地址已知,我们就能够通过将delta offset 加到相应的地址上正确重定位任何代码或数据的运行时地址。原理如图3 所示:
通常只要在病毒代码的开始计算出delta offset,通过变址寻址的方式书写引用数据的汇编代码,即可保证病毒代码在运行时被正确重定位。假设ebp 包含了delta offset,使用如下变址寻址指令则可保证在运行时引用的数据地址是正确的:
在书写源程序时可以采用符号来代替硬编码的地址值,上述的例子中给出的不过是编译器对符号进行地址替换后的结果。现在的问题就转换成如何获取delta offset的值了,显然:
在运行时就动态计算出了delta offset 值,因为call要将其后的第一条指令的地址压入堆栈,因此pop ebp 执行完毕后ebp 中就是delta的运行时地址,减去delta的编译时地址“offset delta”就得到了delta offset 的值。除了用明显的call 指令外,还可以使用不那么明显的fstenv、fsave、fxsave、fnstenv等浮点环境保存指令进行,这些指令也都可以获取某条指令的运行时地址。以fnstenv 为例,该指令将最后执行的一条FPU 指令相关的协处理器的信息保存在指定的内存中,结构如下图4 所示:
该结构偏移12字节处就是最后执行的浮点指令的运行时地址,因此我们也可以用如下一段指令获取delta offset:
delta offset 也不一定非要放在ebp 中,只不过是ebp 作为栈帧指针一般过程都不将该寄存器用于其它用途,因此大部分病毒作者都习惯于将delta offset 保存在ebp 中,其实用其他寄存器也完全可以。 在优化过的病毒代码中并不经常直接使用上述直接计算delta offset 的代码,比如在Elkern开头写成了类似如下的代码:
为什么不采用第二种书写代码的方式?其原因在于尽管第一种格式在书写源码时显得比较罗嗦, 但是addrOpenProcess-_start_ip 是一个较小相对偏移值,一般不超过两个字节,因此生成的指令较短,而addrOpenProcess在32 Win32编译环境下一般是4 个字节的地址值,生成的指令也就较长。有时对病毒对大小要求很苛刻,更多时候也是为了显示其超俗的编程技巧,病毒作者大量采用这种优化,对这种优化原理感兴趣的读者请参阅Intel手册卷2中的指令格式说明。
API 函数地址的获取 |
恶意代码的亲密接触之病毒编程技术(3)
最新推荐文章于 2018-11-29 15:10:41 发布