547 lines
26 KiB
Plaintext
547 lines
26 KiB
Plaintext
Following is an article and companion program source code that
|
|
describes two quirks of DOS:
|
|
|
|
1. The DOS INT21 function has an alternate entry point which could
|
|
be used by a virus to cause damage. This alternate entry
|
|
point is explained and the program source code removes it
|
|
from service.
|
|
|
|
2. Function 13h of INT21 (delete FCB) has a *SEVERE* quirk (it would
|
|
be a bug, except it seems to be deliberate) that allows it
|
|
to totally destroy a disk's directory structure, almost beyond
|
|
repair. The program source code also shows how to safeguard
|
|
against it.
|
|
|
|
I'm a little bit unsure about posting this because of the potential for
|
|
misuse, but if I can't put something like this in BIX, then
|
|
we're all in trouble, I think. If PC Tech Journal were still
|
|
alive, I might send it to them, but there isn't really anywhere
|
|
else it seems appropriate.
|
|
|
|
One warning... PLEASE do not try the examples given on a hard disk drive.
|
|
Use them only on a floppy. I managed to wipe out a goodly portion
|
|
of my hard disk playing around while writing this article, including
|
|
my only copy of the article! You've been warned.
|
|
|
|
And, of course, any comments, elaborations, questions, etc. would be
|
|
welcome in ibm.dos/secrets.2 or bixmail me.
|
|
|
|
Good luck - John Switzer (jswitzer)
|
|
|
|
|
|
|
|
|
|
|
|
Closing DOS's Backdoor
|
|
by John Switzer. (c) 1989, all rights reserved.
|
|
|
|
With each new story of a virus or worm infection, the issue of PC
|
|
security becomes more important. Although totally protecting any
|
|
IBM PC from infection is impossible, MS-DOS complicates matters
|
|
by having two "back doors" that allow access to INT 21h func-
|
|
tions. These back doors are poorly documented, but still present
|
|
a huge gap in a PC's security. To have a secure system, it is
|
|
essential to lock and close these back doors.
|
|
|
|
These backdoors allow access to the DOS function handler (INT
|
|
21h) through two far pointers 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 thru 0:00C7). Nor-
|
|
mally, 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 alterna-
|
|
tive 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 re-
|
|
quirements than a normal INT21 call. Its use requires some
|
|
special handling and an understanding of the functions that it
|
|
allows. In example 1 is the alternative entry point as it exists
|
|
in MS-DOS 3.30, with some changes for clarity:
|
|
|
|
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
|
|
|
|
(end example 1)
|
|
|
|
The first thing to notice is that 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 ad-
|
|
dress. However, this entry point expects the flags to be pushed
|
|
last, after the offset and segment of the return address. Since
|
|
this routine eventually transfers control to the normal INT21
|
|
handler, the handler's first job is to translate the stack into
|
|
an acceptable form for the eventual IRET.
|
|
|
|
The second thing of importance is that 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 AH. Therefore, function 0Ch (CLEAR
|
|
KEYBOARD BUFFER AND GET STDIN) is also unavailable as it uses AL
|
|
for a subfunction value. These limitations may be familiar to
|
|
former CP/M programmers as they are result of the original MS-
|
|
DOS designers' desire for CP/M compatibility.
|
|
|
|
To use this call, therefore, the caller must manually setup 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 INT 21 dispatcher will then IRET to the
|
|
return address on the stack as normal.
|
|
|
|
Example 2 demonstrates this technique:
|
|
|
|
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,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,"$"
|
|
|
|
(end example 2)
|
|
|
|
Note that CP/M programmers, however, used a near jump to execute
|
|
their DOS function calls. The second back door into MS-DOS
|
|
exists precisely to emulate this procedure. At offset 5 in every
|
|
program's PSP (program segment prefix), is a far call that theo-
|
|
retically allows access to DOS by doing a JMP 0005, as CP/M
|
|
allowed. However, looking at offset usually shows an instruction
|
|
similar to the following:
|
|
|
|
CALL FAR F5C2:A496
|
|
|
|
This seems 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 program-
|
|
mers have ignored it.
|
|
|
|
The problem is that the address shown in the PSP is not accurate
|
|
and needs to be rounded up to the nearest paragraph before being
|
|
used. Using the above 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). By setting up the
|
|
stack and registers as described for the first back door, the
|
|
dword pointer above can be inserted into the program's data area.
|
|
A JMP FAR instruction can then be used to execute a basic DOS
|
|
function. Ironically, the simpler JMP 0005 approach that was
|
|
used in CP/M cannot be used here as that would then execute the
|
|
CALL FAR instruction. This would cause another (and invalid)
|
|
return address to be pushed onto the stack, crashing the program
|
|
when the function returned.
|
|
|
|
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 back doors. For the back door at 0:00C0, this
|
|
is trivial. Simply replace the JMP FAR instruction at 0:00C0
|
|
with a JMP FAR into a new handler that can refuse or execute the
|
|
function, as appropriate.
|
|
|
|
The second back door, however, seems to be more difficult. Since
|
|
any number of PSPs can exist in memory at one time, patching each
|
|
one with a new vector could be very difficult. Fortunately, this
|
|
is not necessary. Doing some calculations on the modified ad-
|
|
dress given in the PSP (F5C2:A4A0) shows that it translates into
|
|
0:00C0, because of the quirks of "wrap-around" memory addressing
|
|
found in the real-mode of the Intel IBM processors:
|
|
|
|
offset A4A0 translates into 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 back
|
|
doors are different pointers to the same memory location. Chang-
|
|
ing the vector at 0:00C0 will adequately protect against both.
|
|
|
|
Both of these back doors have existed in PC-DOS since version
|
|
1.0, and in most versions of MS-DOS. Given that they have exist-
|
|
ed for over eight 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 back door 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 the FCB use
|
|
a filename of "???????????" and an attribute of 1Fh. Seeing this
|
|
specific combination, function 13h will delete all files in the
|
|
current directory, including files marked with the read-only and
|
|
subdirectory attributes. To make matters worse, this function
|
|
replaces the first character of the deleted filenames with a 0,
|
|
not the usual 0E5h. 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 would effectively delete all files on the
|
|
disk. Since subdirectories are simply special files that contain
|
|
directory information, these also would be deleted. This would
|
|
prevent any access to the files that were in those directories,
|
|
including any deeper subdirectories. Note that the files in the
|
|
subdirectories are not deleted; only the directory information
|
|
about them has been erased. CHKDSK will therefore report these
|
|
orphaned files as being unallocated clusters.
|
|
|
|
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
|
|
back door:
|
|
|
|
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
|
|
|
|
(end example 3)
|
|
|
|
WARNING! If you experiment with this call, please do so only on a
|
|
floppy and not a hard disk. Calling this function while at the
|
|
root directory will obliterate all files on the disk, requiring
|
|
very tedious work with a disk editor to restore them.
|
|
|
|
This dangerous call, therefore, provides the answer to the ques-
|
|
tion asked above: MS-DOS's back doors present a severe threat to
|
|
an unsecure system. Even though an anti-viral program may filter
|
|
INT 21h calls, if it doesn't change the vector at 0:00C0 destroy-
|
|
ing all files on the hard disk would be trivial.
|
|
|
|
Example 4 shows one approach with a device driver called BACK-
|
|
DOOR.SYS. Install the device driver in the first line of your
|
|
CONFIG.SYS file as "DEVICE=BACKDOOR.SYS" and you will insure 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 alter-
|
|
native DOS function dispatcher. This effectively closes both of
|
|
MS-DOS's back doors. It also filters INT 21h to specifically
|
|
look for this function 13h call, and rejects the function request
|
|
if it occurs.
|
|
|
|
(see BACKDOOR.ASM for example 4)
|
|
|
|
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 MS-DOS's back doors removes one of the more
|
|
obscure dangers to your computer and its data.
|
|
|
|
end of article. John Switzer 10/15/89
|
|
|
|
BACKDOOR.ASM source code follows:
|
|
|
|
|
|
TITLE - BACKDOOR.SYS - closes DOS's back doors
|
|
PAGE 60,132
|
|
.RADIX 16
|
|
|
|
; BACKDOOR.SYS closes two "back doors" 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.
|
|
|
|
|
|
; Written October, 1989 by John Switzer (jswitzer).
|
|
; Assembled using MASM 5.1
|
|
|
|
|
|
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
|
|
|
|
|