Closing DOS's Backdoor

Accessing DOS without going through the INT21 function handler can have serious repercussions. John shows how to close the vulnerable backdoors.


October 01, 1990
URL:http://www.drdobbs.com/architecture-and-design/closing-doss-backdoor/184408425

OCT90: CLOSING DOS'S BACKDOOR

Gaining access to DOS without going through INT21

John is a technical writer for Cummunications Machinery Corporation and can be reached at 340 Mathilda Dr., #3, Goleta, CA 93117.


Although totally protecting any IBM PC or compatible from intrusion is impossible, MS-DOS complicates matters by having two "backdoors" that allow access to the DOS function handler INT 21h. These backdoors are poorly documented but still present a huge gap in a PC's security. To have a secure system, it is essential to close and lock these backdoors.

These backdoors allow access to INT 21h through two far pointers that are easily accessible by any program. The first of these pointers is in low memory, at the address reserved for interrupts 30h and 31h (0:00C0 through 0:00C7). Normally an entry in the interrupt vector table contains a dword pointer to the interrupt's handler; however, INT30 and INT31 are in the form of a JMP FAR instruction that points to the alternative DOS function dispatcher (in my version of DOS 3.30: JMP FAR 0274:1446). This allows direct access to DOS without having to go through INT21.

This alternative DOS handler, however, has different entry requirements than a normal INT21 call. Its use requires some special handling and an understanding of the functions that it allows. Example 1 shows the alternative entry point as it exists in MS-DOS 3.30, with some changes for clarity.

Example 1: An alternative entry point into MS-DOS 3.30.

  ALT_DOS_ENTRY:
         POP AX          ; get rid of flags
         POP AX          ; save caller's segment
         POP CS:TEMP     ; save caller's offset
         PUSHF           ; save flags
         CLI             ; kill interrupts
         PUSH AX         ; save caller's segment
         PUSH CS:TEMP    ; save caller's offset
         CMP CL, 24h     ; is CL < max #?
         JA REFUSE RQST  ; no, so invalid
         MOV AH, CL      ; yes, AH=function #
         JMP CONT_INT21  ; and continue INT21

First, the handler expects the return address to be on the stack in an unusual order. Normally, when an interrupt occurs, the CPU pushes the flags onto the stack first, followed by the segment and offset of the caller's return address. However, this entry point apparently expects the flags to be pushed last, after the offset and segment of the return address. Because this routine eventually transfers control to the normal INT21h handler, the handler's first job is to translate the stack into an acceptable form for the eventual IRET. Second, this handler allows only functions 0 through 24h to be executed. Also, since the AX register is destroyed immediately upon entry, the function number is passed through CL and not through AH. Function 0Ch (CLEAR KEYBOARD BUFFER AND GET STDIN) is thus unavailable, as it uses AL for a subfunction value. These limitations may be familiar to former CP/M programmers -- they result from the original MS-DOS designers' desire for CP/M compatibility.

To use this call, therefore, the caller must manually set up the stack with the flags and a proper far return address. With the function number in CL, a far jump to 0:00C0 executes the call. After completion, the INT21 dispatcher then does an IRET to the return address on the stack as normal. Example 2 demonstrates this technique.

Example 2: A far jump executes the call and the dispatcher returns to the stack.

          MOV AX, offset RETURN    ; get return address' offset
          PUSH AX                  ; push flags and return address
          PUSH CS                  ; onto stack in reverse order
          PUSHF                    ; save flags for IRET
          MOV CL, 9                ; display DOS string
          MOV DX, offset MSG       ; this is the message
          PUSH CS
          POP DS                   ; verify that DS = local code
          JMP dword ptr ALT_DOS_PTR; and execute the function

  RETURN:
          MOV AH, 4Ch              ; terminate a process
          INT 21h                  ; via DOS

  ALT_DOS_PTR DW 00C0h,0000        ; entry point for alternative
                                   ; DOS handler (0:00C0h)

  MSG       DB 0Dh, 0Ah, "Example of backdoor MS-DOS"
            DB "function call.",0Dh, 0Ah, 7, "$"

CP/M programmers, however, used a near CALL to execute their DOS function calls. The second backdoor into MS-DOS exists precisely to duplicate this procedure: At offset 5 in every program's PSP (program segment prefix) is a far call instruction that theoretically allows access to DOS by doing a CALL 0005, as CP/M allowed. However, this offset usually shows an instruction similar to: CALL FAR F5C2:A496.

This appears to reference a location in what seems to be either the BIOS or an impossibly high RAM memory area, and the code at this address is usually garbage. So, though this pointer in the PSP has been documented since the beginnings of MS-DOS, most programmers have ignored it.

The problem is that the address shown in the PSP is usually not accurate and before use should be rounded up to the nearest paragraph. Using the previous example, this results in: CALL FAR F5C2:A4A0.

Looking at the code at this address reveals the same instruction seen at the INT30 vector (JMP FAR 0274:1446). Both backdoors, therefore, jump to the alternate DOS function dispatcher. By setting up the stack and registers as described for the first backdoor, the corrected dword pointer can be inserted into the program's data area. A JMP FAR instruction can then be used to execute a limited number of DOS functions.

By modifying the pointer in the PSP, however, the simpler CP/M approach could also be used to access DOS functions. First, the PSP is modified (if necessary) to round the pointer up to the nearest paragraph. The caller can then use a CALL 0005 instruction to execute the DOS function, as it was done in CP/M. This pushes the caller's correct return address onto the stack and executes the far CALL in the PSP. The far CALL pushes the program's code segment onto the stack, as well as another return address; however, the second return address is pointing to offset 0Ah in the PSP. This is not a problem, because the first thing the DOS dispatcher does is eliminate the second return address with a POP AX instruction (see Example 1). It then rebuilds the stack so that the IRET correctly returns to the caller's program. So, with some work, the original designer's goal of CP/M compatibility is achieved, for whatever it is worth.

Now that the alternative DOS handler is understood, its use must be prevented. Most security programs do an admirable job of closing the door to normal DOS calls using INT 21h, but many ignore these alternative entry points into DOS. A secure system must close these backdoors. For the backdoor at 0:00C0, this is trivial. Simply replace the JMP FAR instruction at 0:00C0 with a pointer to a new handler that can refuse or execute the function as appropriate.

The second backdoor, however, seems to be more difficult. Because any number of PSPs can exist in memory at one time, patching each one with a new vector could be difficult. Fortunately, this is not necessary. Doing some calculations on the modified address given in the PSP (F5C2:A4A0, for example) shows that it translates into 0:00C0 because of the quirks of the "wrap-around" memory addressing found in the real-mode of the Intel IBM processors: Offset A4A0 = segment 0A4A, so segment F5C2 plus segment A4A = segment 1000C. Segment 1000C wraps around to segment 000C which translates into address 0:00C0. Thus, changing the PSP pointers is unnecessary, since both backdoors are different pointers to the same memory location. Changing the vector at 0:00C0 adequately protects against both.

Both of these backdoors have existed in PC-DOS since version 1.0 and in most versions of MS-DOS. Given that they have existed for almost nine years without causing any apparent problems, and that the alternative DOS handler is limited in its scope, how serious a danger do these backdoors present? Only the standard input/output and FCB functions are allowed, and although FCBs can delete and rewrite files, they can be used only on files in the current directory. Although a Trojan Horse program could use these backdoor approaches to do some damage, it would not seem to pose a major problem.

This would be true, except that one FCB call, function 13h (DELETE AN FCB), has a special case that could destroy all files on a hard disk. The special case requires that an extended FCB use a filename of "????????????" and an attribute of 1Fh. Seeing this specific combination, function 13h deletes all files in the current directory, including files marked with the read-only, volume, and subdirectory attributes. To make matters worse, this function replaces the first character of the deleted filenames with a 0, not the usual OE5h. This prevents most "undelete" utilities from being able to undo this call's severe damage.

Consider the potential damage of this call. If executed at the root directory, it effectively deletes all files on the disk. As subdirectories are only special files that contain directory information, these are also deleted. This therefore prevents any access to the files that were in those subdirectories, including any deeper subdirectories. Note that the files in the subdirectories are not deleted, and their space remains allocated; only the directory information about them has been erased. CHKDSK will therefore report these orphaned files as being unallocated clusters. It is possible to recover these files and the original tree structure, but only with painstaking work with a disk editor.

This behavior of MS-DOS is truly bizarre. Normally, only MS-DOS's internal routines can update or delete the files marked with the subdirectory attribute. That an FCB function is allowed to delete these files is an unbelievable quirk of MS-DOS. Example 3 shows the use of this special case, using the first DOS backdoor. Warning! If you experiment with this call, please do so only on a floppy disk and not on a hard disk. Calling this function while at the root directory will obliterate all files on the disk, requiring tedious work with your favorite disk editor to restore them. (This warning, by the way, is from my own personal experience!)

Example 3: An FCB function can delete files. Calling this function while at the root directory will obliterate all files on the disk, requiring tedious work with your favorite disk editor to restore them.

          MOV AX, offset RETURN    ; get return address' offset
          PUSH AX                  ; push flags and return address
          PUSH CS                  ; onto stack in reverse order
          PUSHF                    ;
          MOV CL, 13h              ; DELETE FCB function
          MOV DX, offset FCB       ; this is the special FCB
          PUSH CS
          POP DS                   ; verify that DS = local code
          JMP dword ptr ALT_DOS_PTR; and execute the function

   RETURN:
          MOV AH, 4Ch              ; terminate the process
          INT 21h                  ; via DOS
  ALT_DOS PTR DW 00C0h, 0000       ; entry point for alternative
                                   ; DOS handler (0:00C0h)

  FCB    DB 0FFh                   ; extended FCB
      DB 5 dup (0)                 ; reserved bytes
      DB 1Fh                       ; all attribute bits set
      DB 0                         ; default drive ID
      DB "????????????"            ; match all files
      DB 19h dup (0)               ; rest of FCB

This dangerous call, therefore, provides the answer to the question asked above: MS-DOS's backdoors present a severe threat to an unsecured system. Even though an anti-viral program may filter INT21h calls, if it doesn't change the vector at 0:00C0, it is easy to destroy all files on a hard disk.

Listing One, page 98, shows one approach with a device driver called BACKDOOR.SYS. By installing the device driver in the first line of your CONFIG.SYS file (DEVICE=BACKDOOR.SYS), you ensure that it is installed before any other programs can run. BACKDOOR.SYS simply replaces the vector at 0:00C0h with a pointer to a new handler and then installs itself as a character device. The new handler refuses any requests for DOS services through the alternative DOS function dispatcher. This effectively closes both of MS-DOS's backdoors. It also filters INT21h to specifically look for the special function 13h call and rejects the function request if it occurs.

No IBM PC or compatible running in real mode can be completely safe from destructive programs, whether intentional or not. However, it makes no sense to allow known dangers to continue to exist. Closing DOS's backdoors removes one of the more obscure dangers to your computer and its data.

_CLOSING DOS'S BACKDOOR_ by John Switzer

[LISTING ONE]



             TITLE - BACKDOOR.SYS - closes DOS's backdoors
             PAGE 60,132
             .RADIX 16

; BACKDOOR.SYS closes two "backdoors" into the MS-DOS INT 21h function
; dispatcher that could be used by a virus or trojan horse to cause damage.
; It also filters INT 21h directly to reject a special case of function 13h
; which could destroy all data on a disk.
; For use with MASM 5.1
; MASM BACKDOOR;
; LINK BACKDOOR;
; EXE2BIN BACKDOOR.EXE BACKDOOR.SYS
;
        ASSUME CS:CSEG, DS:CSEG
CSEG    SEGMENT PARA PUBLIC 'CODE'
             ORG 0000h                 ; device driver starts at 0

             DW 0FFFFh,0FFFFh          ; far pointer to next device
             DW 8000h                  ; character device driver
             DW offset DEV_STRAT_RTN   ; pointer to the strategy routine
             DW offset DEV_INT_RTN     ; pointer to the interrupt routine
             DB "B"+80h,"ACKDOOR"      ; device name with high bit set will
                                       ; avoid any filename conflicts
INSTALL_MSG  DB 0Dh,0Ah
             DB "BACKDOOR is installed at $"

DEV_HDR_BX   DW 0000                   ; pointer for ES:BX for device
DEV_HDR_ES   DW 0000                   ; request header

ORIG_INT21_OFF DW 0000                 ;
ORIG_INT21_SEG DW 0000                 ;

TEMP         DW 0000                   ; used for temporary storage

REFUSE_RQST  PROC FAR                  ;
             POP AX                    ; get rid of flags on stack
             POP AX                    ; get the return segment
             POP CS:TEMP               ; and save offset
             PUSH AX                   ; save the return address in proper
             PUSH CS:TEMP              ; order
             STC                       ; return STC for error
             MOV AX,0FFFFh             ; return AX=-1
             RET                       ; and do FAR RET back to caller
REFUSE_RQST  ENDP

NEW_INT21    PROC NEAR
             PUSH AX                   ; save original registers first thing
             PUSH BX
             CMP AH,13h                ; is this the DELETE FCB function?
             JNZ CONT_ORIG_INT21       ; no, so continue on
             MOV BX,DX                 ; point BX to the FCB
             CMP byte ptr DS:[BX],0FFh ; got an extended FCB?
             JNZ CONT_ORIG_INT21       ; no, so continue on
             CMP byte ptr DS:[BX+6],1Fh; yes, so got the special attribute?
             JNZ CONT_ORIG_INT21       ; no, so continue on
             CMP word ptr DS:[BX+8],"??"; yes, so filename starts with "??" ?
             JNZ CONT_ORIG_INT21       ; no, so continue on
             CMP word ptr DS:[BX+0Ah],"??"; yes, so filename = "??" ?
             JNZ CONT_ORIG_INT21       ; no, so continue on
             CMP word ptr DS:[BX+0Ch],"??"; yes, so filename = "??" ?
             JNZ CONT_ORIG_INT21       ; no, so continue on
             CMP word ptr DS:[BX+0Eh],"??"; yes, so filename = "??" ?
             JNZ CONT_ORIG_INT21       ; no, so continue on
             CMP word ptr DS:[BX+10h],"??"; yes, so filename = "??" ?
             JNZ CONT_ORIG_INT21       ; no, so continue on
             CMP byte ptr DS:[BX+12h],"?"; yes, so filename ends with "??" ?
             JNZ CONT_ORIG_INT21       ; no, so continue on
             POP BX                    ; yes, so reject it altogether
             POP AX                    ;
             MOV AL,0FFh               ; return match not found
             STC                       ; STC just for the heck of it
             RETF 0002                 ; and IRET with new flags

CONT_ORIG_INT21:
             POP BX                    ; restore original registers
             POP AX                    ;
             JMP dword ptr CS:ORIG_INT21_OFF; continue with original handler
NEW_INT21    ENDP

DEV_STRAT_RTN PROC FAR                 ;
             MOV CS:DEV_HDR_BX,BX      ; save the ES:BX pointer to the
             MOV CS:DEV_HDR_ES,ES      ; device request header
             RET                       ;
DEV_STRAT_RTN ENDP

DEV_INT_RTN  PROC FAR                  ;
             PUSH AX                   ; save all registers
             PUSH BX                   ;
             PUSH CX                   ;
             PUSH DX                   ;
             PUSH DS                   ;
             PUSH ES                   ;
             PUSH DI                   ;
             PUSH SI                   ;
             PUSH BP                   ;
             PUSH CS                   ;
             POP DS                    ; point DS to local code
             LES DI,dword ptr DEV_HDR_BX; ES:DI=device request header
             MOV BL,ES:[DI+02]         ; get the command code
             XOR BH,BH                 ; clear out high byte
             CMP BX,00h                ; doing an INSTALL?
             JNZ DEV_IGNORE            ; no, so just ignore the call then
             CALL INSTALL_BACKDOOR     ; yes, so install code in memory

DEV_IGNORE:                            ;
             MOV AX,0100h              ; return STATUS of DONE
             LDS BX,dword ptr CS:DEV_HDR_BX; DS:BX=device request header
             MOV [BX+03],AX            ; return STATUS in the header
             POP BP                    ; restore original registers
             POP SI                    ;
             POP DI                    ;
             POP ES                    ;
             POP DS                    ;
             POP DX                    ;
             POP CX                    ;
             POP BX                    ;
             POP AX                    ;
             RET                       ; and RETF to DOS
DEV_INT_RTN  ENDP

INSTALL_BACKDOOR PROC NEAR             ;
             CALL CLOSE_BACK_DOOR      ; install new handler to close back
                                       ; door
             CALL HOOK_INT21           ; and hook INT21 filter
             MOV AH,09h                ; DOS display string
             MOV DX,offset INSTALL_MSG ; show installation message
             INT 21h                   ; via DOS
             MOV AX,CS                 ; display current code segment
             CALL OUTPUT_AX_AS_HEX     ; output AX as two HEX digits
             MOV AL,3Ah                ; now output a colon
             CALL DISPLAY_TTY          ; to the screen
             MOV AX,offset REFUSE_RQST ; show new handler's offset
             CALL OUTPUT_AX_AS_HEX     ; output AX as two HEX digits
             CALL DISPLAY_NEWLINE      ; output a newline to finish display
             LES DI,dword ptr DEV_HDR_BX; ES:DI=device request header
             MOV Word Ptr ES:[DI+0Eh],offset INSTALL_BACKDOOR; this is the
             MOV ES:[DI+10h],CS        ; end of resident code
             RET                       ;
INSTALL_BACKDOOR ENDP

CLOSE_BACK_DOOR PROC NEAR              ;
             PUSH ES                   ; save original registers
             PUSH AX                   ;
             PUSH BX                   ;
             XOR AX,AX                 ; point ES to the interrupt vector
             MOV ES,AX                 ; table
             MOV BX,00C1h              ; install new handler at INT30 + 1
             MOV AX,offset REFUSE_RQST ; get new offset for the handler
             MOV ES:[BX],AX            ; save it in interrupt vector table
             MOV AX,CS                 ; get the segment for the handler
             MOV ES:[BX+02],AX         ; and save it, too
             POP BX                    ; restore original registers
             POP AX                    ;
             POP ES                    ;
             RET                       ; and RET to caller
CLOSE_BACK_DOOR ENDP

HOOK_INT21   PROC NEAR
             PUSH AX
             PUSH BX
             PUSH ES
             MOV AX,3521h              ; get current INT21 vector
             INT 21h                   ; via DOS
             MOV CS:ORIG_INT21_OFF,BX  ; save the offset
             MOV BX,ES                 ;
             MOV CS:ORIG_INT21_SEG,BX  ; and the segment
             PUSH CS
             POP DS                    ; make sure DS=local code
             MOV DX,offset NEW_INT21   ; point to new handler
             MOV AX,2521h              ; install new handler
             INT 21h                   ; via DOS
             POP ES                    ; and restore original registers
             POP BX
             POP AX
             RET                       ; and RET to caller
HOOK_INT21   ENDP

OUTPUT_AX_AS_HEX PROC NEAR             ;
             PUSH AX                   ; save original registers
             PUSH BX                   ;
             PUSH CX                   ;
             PUSH AX                   ; save number for output
             MOV AL,AH                 ; output high byte first
             CALL OUTPUT_AL_AS_HEX     ; output AL as two HEX digits
             POP AX                    ; output low byte next
             CALL OUTPUT_AL_AS_HEX     ; output AL as two HEX digits
             POP CX                    ; restore original registers
             POP BX                    ;
             POP AX                    ;
             RET                       ; and RET to caller
OUTPUT_AX_AS_HEX ENDP

OUTPUT_AL_AS_HEX PROC NEAR             ;
             PUSH AX                   ; save original registers
             PUSH BX                   ;
             PUSH CX                   ;

             PUSH AX                   ; save the number for output (in AL)
             MOV CL,04h                ; first output high nibble
             SHR AL,CL                 ; get digit into low nibble
             ADD AL,30h                ; convert to ASCII
             CMP AL,39h                ; got a decimal digit?
             JBE OUTPUT_FIRST_DIGIT    ; yes, so continue
             ADD AL,07h                ; no, so convert to HEX ASCII

OUTPUT_FIRST_DIGIT:                    ;
             CALL DISPLAY_TTY          ; output it via BIOS
             POP AX                    ; get number back
             AND AL,0Fh                ; keep only low digit now
             ADD AL,30h                ; convert to ASCII
             CMP AL,39h                ; got a decimal digit?
             JBE OUTPUT_SECOND_DIGIT   ; yes, so continue
             ADD AL,07h                ; no, so convert to HEX ASCII

OUTPUT_SECOND_DIGIT:
             CALL DISPLAY_TTY          ; output it via BIOS
             POP CX                    ; restore original registers
             POP BX                    ;
             POP AX                    ;
             RET                       ; and RET to caller
OUTPUT_AL_AS_HEX ENDP

DISPLAY_NEWLINE PROC NEAR              ;
             PUSH AX                   ; save original AX
             MOV AL,0Dh                ; first do CR
             CALL DISPLAY_TTY          ; output it via the BIOS
             MOV AL,0Ah                ; do LF next
             CALL DISPLAY_TTY          ; output it via the BIOS
             POP AX                    ; restore original AX
             RET                       ; and RET to caller
DISPLAY_NEWLINE ENDP

DISPLAY_TTY  PROC NEAR                 ;
             PUSH AX                   ;
             PUSH BX                   ;
             MOV AH,0Eh                ; display TTY
             MOV BX,0007h              ; on page 0, normal attribute
             INT 10h                   ; via BIOS
             POP BX                    ;
             POP AX                    ;
             RET                       ;
DISPLAY_TTY  ENDP

CSEG    ENDS
        END

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.