0%

软件安全——第5章

Chapter 5 Windows PE病毒

5.1 基本概念

PE病毒:以Windows PE程序为载体,能寄生于PE文件,或Windows系统的病毒程序。
感染:在尽量不影响目标程序(系统)正常功能的前提下,使其具有病毒自己的功能(感染模块、触发模块、破坏模块等)。

5.2 分类

按照感染目标的类型分类:

  • 文件感染:将代码寄生在PE文件中。(传统感染型和捆绑释放型感染)
    • 传统感染型:在PE文件中添加病毒代码段与数据段,修改节表等控制结构使程序能够首先执行病毒代码。主体是目标程序。优点:被感染后的程序主体依然是目标程序,不影响目标程序图标,隐蔽性稍好。缺点:对病毒代码的编写要求较高,通常是汇编语言编写,难以成功感染自校验程序。
    • 捆绑释放型:将目标程序和病毒程序捆在一起,将目标程序作为数据存储在病毒体内。主体是病毒程序。编写较容易,可使用高级语言编写。
  • 系统感染:将代码或程序寄生在Windows操作系统,不针对特定的PE文件。感染途径有:
    • 即时通信软件
    • U盘和光盘
    • 电子邮件
    • 网络共享等

5.3 传统文件感染

使用技术

  • 重定位:病毒代码目标寄生位置不固定
  • API函数自获取:在没有引入函数表的情况下获取需要使用的API函数内存地址
  • 目标程序遍历搜索:全盘查找,或者部分盘符查找以感染其他文件
  • 感染模块:病毒代码插入位置选择与写入、病毒执行完毕后将控制权移交给正常的程序执行流程

重定位

  • 在编译时,有些基于Image Base的指令会将地址固定写死在指令之中,如push 0x401215,这时修改Image Base会使得这条指令的意义丢失,因此需要重定位。在病毒代码编译后而没有植入时,其起始地址很可能不是我们想要病毒代码在HOST文件中的起始地址,需要进行移动。
  • 其本质是修正实际地址与预期地址的差异
  • 解决方案:
    • 逐一硬编码(较为繁琐)
    • 病毒代码运行过程中自我重定位
      • call指令可以将下一条要执行的指令的地址压入栈,配合pop即可得到下一条指令的地址,以此病毒就可以知道自己的地址是什么。

API函数自获取

  • 找到DLL文件的引入函数节,在其中进行遍历查找即可。
  • kernel32.dll中的关键API函数:GetProcAddress和LoadLibraryA
  • 需要首先获得kernel32.dll文件的基地址,可以硬编码但是很难兼容,主要通过kernel32模块中的相应结构和特征定位
  • 获取kernel32.dll中的地址的方法:
    • 程序入口代码执行时,栈顶存储的地址
      系统打开一个可执行文件时,它会调用Kernel32.dll中的CreateProcess函数,CreateProcess函数在完成应用程序装载后,会先将返回地址压入到堆栈顶端。当该应用程序结束后,会将返回地址弹出放到EIP中,继续执行。这个返回地址显然位于kernel32.dll之中。在此基础上按照内存对齐(一般为0x10000)的值向前遍历直至检测到kernel32.dll的文件头 (搜索较费时且容易产生异常情况)
    • SEH链末端处理函数
      SEH:Structured Exception Handler,异常处理模块,以链表形式存在。在链中查找prev成员等于0xFFFFFFFF(表示已经遍历到链表尾)EXCEPTION_REGISTER结构,该结构中handler值指向系统异常处理例程,它总是位于KERNEL32模块中。根据这一特性,然后进行向前搜索就可以查找KERNEL32.DLL在内存中的基地址。
    • PEB相关数据结构指向各模块地址
      TEB:Thread Environment Block,线程环境块,该结构体包含进程中运行线程的各种信息,进程中的每个线程都对应一个TEB结构体。
      PEB:Process Environment Block,进程环境块,存放进程信息,每个进程都有自己的PEB信息。位于用户地址空间。
      • fs:[0]指向TEB结构,TEB结构中偏移0x30位置保存的是PEB的地址,因此可以从fs:[30h]获得PEB地址。
      • 然后通过PEB[0x0c]获得PEB_LDR_DATA数据结构地址(即下面的VOID *DllList指针)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      		typedef struct _PEB { // Size: 0x1D8
      /*000*/ UCHAR InheritedAddressSpace;
      /*001*/ UCHAR ReadImageFileExecOptions;
      /*002*/ UCHAR BeingDebugged;
      /*003*/ UCHAR SpareBool; // Allocation size
      /*004*/ HANDLE Mutant;
      /*008*/ HINSTANCE ImageBaseAddress; // Instance
      /*00C*/ VOID *DllList;
      /*010*/ PPROCESS_PARAMETERS *ProcessParameters;
      ...
      • 然后通过从PEB_LDR_DATA[0x1c]获取InInitializationOrderModuleList.Flink地址
      1
      2
      3
      4
      5
      6
      7
      8
      9
      typedef struct _PEB_LDR_DATA
      {
       ULONG Length; // +0x00
       BOOLEAN Initialized; // +0x04
       PVOID SsHandle; // +0x08
       LIST_ENTRY InLoadOrderModuleList; // +0x0c
       LIST_ENTRY InMemoryOrderModuleList; // +0x14
       LIST_ENTRY InInitializationOrderModuleList;// +0x1c
      } PEB_LDR_DATA,*PPEB_LDR_DATA; // +0x24
      • 最后在Flink[0x08]中得到KERNEL32.DLL模块的基地址。
    • 栈区特定高端地址的数据
      • 这种方法只适用于Windows NT操作系统,但这种方法的代码量最小,只有25B。
      • 每个执行的线程都有它自己的TEB(线程环境块),该块中存储线程的栈顶的地址,从该地址向下偏移0X1C处的地址肯定位于Kernel32.dll中。则可以通过该地址向低地址以64KB为单位来查找Kernel32.dll的基地址。
  • 获取指定函数内存地址的方法
    • 通过Address of Names数组查找函数名,记录索引值
    • 在Address of Name Ordinals编号数组中找到这个索引值对应的编号
    • 在Address of Functions数组中以编号为索引即可找到指定函数的内存地址

目标程序遍历搜索

  • 通常以PE文件的格式(EXE、SCR、DLL等)作为感染目标
  • 对目标进行搜索通常使用FindFirstFile和FindNextFile两个API函数
  • 可进行递归或非递归遍历

文件感染

  • 感染的关键在于:
    • 病毒代码能够运行
      • 选择位置放入病毒代码并将控制权交由病毒代码
    • 原有的正常功能不能被破坏
      • 记录原始的程序控制点位置,当病毒代码执行完毕后交回控制权
      • 设置感染标记,避免重复感染
  • 代码插入位置选择
    • 添加新节:在新节中专门存放病毒代码,需要检查节表空间是否足够
      • 判断该文件是否是可执行文件(检查MZ和PE标识)
      • 判断该文件是否已经被感染(避免重复感染)
      • 获取数据目录的个数,经过计算得到节表的起始地址
      • 得到最后一个节表的偏移,并在其后写入新节的属性等控制信息
      • 在病毒节中写入病毒代码和数据
      • 修正文件头信息(节的数量等)
    • 碎片式感染:将病毒代码分散插入到节之间的填充部分
    • 插入式感染:将病毒代码插入到HOST代码节的中间或前后(可能会导致程序无法运行)
    • 伴随式感染:备份HOST程序并用自己的程序替换HOST程序,自己的代码执行完之后再去执行HOST备份程序

5.4 捆绑式感染

HOST作为数据存放在病毒程序中,执行病毒程序时还原并执行HOST文件。熊猫烧香即属于此类病毒。

优点:编写简单、效率高。可感染自校验程序。
缺点:被感染后的程序主体是病毒程序,易被发现(程序叠加+释放执行),程序图标问题。(需要处理好资源节,熊猫烧香就没有处理好导致暴露)

5.5 系统感染

此类病毒通常作为单独个体,不感染系统中的其他文件。

需要通过自启动获得控制权

  • 于计算机启动时启动:BIOS-MBR-DBR-系统内部
  • 于系统内部启动:修改注册表键值、于系统中特定位置启动、以配置文件形式启动、修改特定文件以启动
  • 利用系统自动播放机制(Autorun.inf)
    • inf文件是Winodws操作系统下用来描述设备或文件等数据信息的文件。autorun.inf是一个文本形式的配置文件,我们可以用文本编辑软件进行编辑,它只能位于驱动器的根目录下。这个文件包含了需要自动运行的命令,如改变的驱动器图标、运行的程序文件、可选快捷菜单等内容。相关资料
  • 在其他可执行文件中嵌入少量病毒代码
  • 替换DLL文件

传播方式:可移动磁盘存储与网络传播

5.6 实验内容

链接命令——设定代码节可写:

link /subsystem:windows /section:.text,rwe mype1.obj
其中的/section:.text,rwe表示.text节可读可写可执行。

手动修改入口点,使两个弹窗变成一个弹窗:

将Address of Entry Point进行修改,跳过弹出第一个弹窗的指令(在实验中应为+0x16)

代码重定位写法

1
2
3
4
call delta ;这条语句执行之后,堆栈顶端为delta在内存中的真正地址
delta:
pop ebp ;这条语句将delta在内存中的真正地址存放在ebp寄存器中
sub ebp,offset delta

1
2
3
4
call @F
@@:
pop ebp
sub ebp,offset @B

(这里的@F指的是前面最近的一个@@标号,@B指后面最近的一个@@标号)

kernel32.dll基地址获取代码理解

1
2
3
4
5
6
7
8
9
10
11
mov eax,[esp]  ;from stack
xor edx,edx
getK32Base:
dec eax
mov dx,word ptr[eax+IMAGE_DOS_HEADER.e_lfanew] ;3ch
test dx,0f000h ;check f0
jnz getK32Base
cmp eax,dword ptr[eax+edx+IMAGE_NT_HEADERS.OptionalHeader.ImageBase]
;double check ImageBase value with eax
jnz getK32Base
mov [ebp+k32Base],eax ;save ImageBase into k32Base

这里第一条语句为从栈中获取kernel32.dll中的地址保存到eax中。
之后将dx保存为PE文件中new EXE Header的偏移位置(0x3C),检查dx的值是否小于0x1000。
如果小于,再检查Image Base的值是否等于eax(如果eax指向dll文件头,那么Image Base的值应该等于eax)。若等于,则查找完毕,eax即为kernel32.dll的起始地址。
注意上面代码对eax是逐次减1比较,由于内存对齐机制,这里可以直接按照对齐去查找,能够减少很多循环的次数。

kernel32.dll中函数内存地址的获取

API’s Address = ( API’s Ordinal * 4 ) + AddressOfFunctions’ VA + Kernel32 imagebase