; This is an example of an active TSR that counts keyboard interrupts ; once activated. Every minute it writes the number of keyboard ; interrupts that occurred in the previous minute to an output file. ; This continues until the user removes the program from memory. ; ; ; Usage: ; KEYEVAL filename- Begins logging keystroke data to ; this file. ; ; KEYEVAL REMOVE - Removes the resident program from ; memory. ; ; ; This TSR checks to make sure there isn't a copy already active in ; memory. When doing disk I/O from the interrupts, it checks to make ; sure DOS isn't busy and it preserves application globals (PSP, DTA, ; and extended error info). When removing itself from memory, it ; makes sure there are no other interrupts chained into any of its ; interrupts before doing the remove. ; ; The resident segment definitions must come before everything else. ResidentSeg segment para public 'Resident' ResidentSeg ends EndResident segment para public 'EndRes' EndResident ends .xlist .286 include stdlib.a includelib stdlib.lib .list ; Resident segment that holds the TSR code: ResidentSeg segment para public 'Resident' assume cs:ResidentSeg, ds:nothing ; Int 2Fh ID number for this TSR: MyTSRID byte 0 ; The following variable counts the number of keyboard interrupts KeyIntCnt word 0 ; Counter counts off the number of milliseconds that pass, SecCounter ; counts off the number of seconds (up to 60). Counter word 0 SecCounter word 0 ; FileHandle is the handle for the log file: FileHandle word 0 ; NeedIO determines if we have a pending I/O opearation. NeedIO word 0 ; PSP is the psp address for this program. PSP word 0 ; Variables to tell us if DOS, INT 13h, or INT 16h are busy: InInt13 byte 0 InInt16 byte 0 InDOSFlag dword ? ; These variables contain the original values in the interrupt vectors ; we've patched. OldInt9 dword ? OldInt13 dword ? OldInt16 dword ? OldInt1C dword ? OldInt28 dword ? OldInt2F dword ? ; DOS data structures: ExtErr struct eeAX word ? eeBX word ? eeCX word ? eeDX word ? eeSI word ? eeDI word ? eeDS word ? eeES word ? word 3 dup (0) ExtErr ends XErr ExtErr {} ;Extended Error Status. AppPSP word ? ;Application PSP value. AppDTA dword ? ;Application DTA address. ; The following data is the output record. After storing this data ; to these variables, the TSR writes this data to disk. month byte 0 day byte 0 year word 0 hour byte 0 minute byte 0 second byte 0 Keystrokes word 0 RecSize = $-month ; MyInt9- The system calls this routine every time a keyboard ; interrupt occus. This routine increments the ; KeyIntCnt variable and then passes control on to the ; original Int9 handler. MyInt9 proc far inc ResidentSeg:KeyIntCnt jmp ResidentSeg:OldInt9 MyInt9 endp ; MyInt1C- Timer interrupt. This guy counts off 60 seconds and then ; attempts to write a record to the output file. Of course, ; this call has to jump through all sorts of hoops to keep ; from reentering DOS and other problematic code. MyInt1C proc far assume ds:ResidentSeg push ds push es pusha ;Save all the registers. mov ax, ResidentSeg mov ds, ax pushf call OldInt1C ; First things first, let's bump our interrupt counter so we can count ; off a minute. Since we're getting interrupted about every 54.92549 ; milliseconds, let's shoot for a little more accuracy than 18 times ; per second so the timings don't drift too much. add Counter, 549 ;54.9 msec per int 1C. cmp Counter, 10000 ;1 second. jb NotSecYet sub Counter, 10000 inc SecCounter NotSecYet: ; If NEEDIO is not zero, then there is an I/O operation in progress. ; Do not disturb the output values if this is the case. cli ;This is a critical region. cmp NeedIO, 0 jne SkipSetNIO ; Okay, no I/O in progress, see if a minute has passed since the last ; time we logged the keystrokes to the file. If so, it's time to start ; another I/O operation. cmp SecCounter, 60 ;One minute passed yet? jb Int1CDone mov NeedIO, 1 ;Flag need for I/O. mov ax, KeyIntCnt ;Copy this to the output shr ax, 1 ; buffer after computing mov KeyStrokes, ax ; # of keystrokes. mov KeyIntCnt, 0 ;Reset for next minute. mov SecCounter, 0 SkipSetNIO: cmp NeedIO, 1 ;Is the I/O already in jne Int1CDone ; progress? Or done? call ChkDOSStatus ;See if DOS/BIOS are free. jnc Int1CDone ;Branch if busy. call DoIO ;Do I/O if DOS is free. Int1CDone: popa ;Restore registers and quit. pop es pop ds iret MyInt1C endp assume ds:nothing ; MyInt28- Idle interrupt. If DOS is in a busy-wait loop waiting for ; I/O to complete, it executes an int 28h instruction each ; time through the loop. We can ignore the InDOS and CritErr ; flags at that time, and do the I/O if the other interrupts ; are free. MyInt28 proc far assume ds:ResidentSeg push ds push es pusha ;Save all the registers. mov ax, ResidentSeg mov ds, ax pushf ;Call the next INT 28h call OldInt28 ; ISR in the chain. cmp NeedIO, 1 ;Do we have a pending I/O? jne Int28Done mov al, InInt13 ;See if BIOS is busy. or al, InInt16 jne Int28Done call DoIO ;Go do I/O if BIOS is free. Int28Done: popa pop es pop ds iret MyInt28 endp assume ds:nothing ; MyInt16- This is just a wrapper for the INT 16h (keyboard trap) ; handler. MyInt16 proc far inc ResidentSeg:InInt16 ; Call original handler: pushf call ResidentSeg:OldInt16 ; For INT 16h we need to return the flags that come from the previous call. pushf dec ResidentSeg:InInt16 popf retf 2 ;Fake IRET to keep flags. MyInt16 endp ; MyInt13- This is just a wrapper for the INT 13h (disk I/O trap) ; handler. MyInt13 proc far inc ResidentSeg:InInt13 pushf call ResidentSeg:OldInt13 pushf dec ResidentSeg:InInt13 popf retf 2 ;Fake iret to keep flags. MyInt13 endp ; ChkDOSStatus- Returns with the carry clear if DOS or a BIOS routine ; is busy and we can't interrupt them. ChkDOSStatus proc near assume ds:ResidentSeg les bx, InDOSFlag mov al, es:[bx] ;Get InDOS flag. or al, es:[bx-1] ;OR with CritErr flag. or al, InInt16 ;OR with our wrapper or al, InInt13 ; values. je Okay2Call clc ret Okay2Call: clc ret ChkDOSStatus endp assume ds:nothing ; PreserveDOS- Gets a copy's of DOS' current PSP, DTA, and extended ; error information and saves this stuff. Then it sets ; the PSP to our local PSP and the DTA to PSP:80h. PreserveDOS proc near assume ds:ResidentSeg mov ah, 51h ;Get app's PSP. int 21h mov AppPSP, bx ;Save for later mov ah, 2Fh ;Get app's DTA. int 21h mov word ptr AppDTA, bx mov word ptr AppDTA+2, es push ds mov ah, 59h ;Get extended err info. xor bx, bx int 21h mov cs:XErr.eeDS, ds pop ds mov XErr.eeAX, ax mov XErr.eeBX, bx mov XErr.eeCX, cx mov XErr.eeDX, dx mov XErr.eeSI, si mov XErr.eeDI, di mov XErr.eeES, es ; Okay, point DOS's pointers at us: mov bx, PSP mov ah, 50h ;Set PSP. int 21h push ds ;Set the DTA to mov ds, PSP ; address PSP:80h mov dx, 80h mov ah, 1Ah ;Set DTA call. int 21h pop ds ret PreserveDOS endp assume ds:nothing ; RestoreDOS- Restores DOS' important global data values back to the ; application's values. RestoreDOS proc near assume ds:ResidentSeg mov bx, AppPSP mov ah, 50h ;Set PSP int 21h push ds lds dx, AppDTA mov ah, 1Ah ;Set DTA int 21h pop ds push ds mov si, offset XErr ;Saved extended error stuff. mov ax, 5D0Ah ;Restore XErr call. int 21h pop ds ret RestoreDOS endp assume ds:nothing ; DoIO- This routine processes each of the I/O operations ; required to write data to the file. DoIO proc near assume ds:ResidentSeg mov NeedIO, 0FFh ;A busy flag for us. ; The following Get Date DOS call may take a while, so turn the ; interrupts back on (we're clear of the critical section once we ; write 0FFh to NeedIO). sti call PreserveDOS ;Save DOS data. mov ah, 2Ah ;Get Date DOS call int 21h mov month, dh mov day, dl mov year, cx mov ah, 2Ch ;Get Time DOS call int 21h mov hour, ch mov minute, cl mov second, dh mov ah, 40h ;DOS Write call mov bx, FileHandle ;Write data to this file. mov cx, RecSize ;This many bytes. mov dx, offset month ;Starting at this address. int 21h ;Ignore return errors (!). mov ah, 68h ;DOS Commit call mov bx, FileHandle ;Write data to this file. int 21h ;Ignore return errors (!). mov NeedIO, 0 ;Ready to start over. call RestoreDOS PhasesDone: ret DoIO endp assume ds:nothing ; MyInt2F- Provides int 2Fh (multiplex interrupt) support for this ; TSR. The multiplex interrupt recognizes the following ; subfunctions (passed in AL): ; ; 00- Verify presence. Returns 0FFh in AL and a pointer ; to an ID string in es:di if the ; TSR ID (in AH) matches this ; particular TSR. ; ; 01- Remove. Removes the TSR from memory. ; Returns 0 in AL if successful, ; 1 in AL if failure. MyInt2F proc far assume ds:nothing cmp ah, MyTSRID ;Match our TSR identifier? je YepItsOurs jmp OldInt2F ; Okay, we know this is our ID, now check for a verify vs. remove call. YepItsOurs: cmp al, 0 ;Verify Call jne TryRmv mov al, 0ffh ;Return success. lesi IDString iret ;Return back to caller. IDString byte "Keypress Logger TSR",0 TryRmv: cmp al, 1 ;Remove call. jne IllegalOp call TstRmvable ;See if we can remove this guy. je CanRemove ;Branch if we can. mov ax, 1 ;Return failure for now. iret ; Okay, they want to remove this guy *and* we can remove it from memory. ; Take care of all that here. assume ds:ResidentSeg CanRemove: push ds push es pusha cli ;Turn off the interrupts while mov ax, 0 ; we mess with the interrupt mov es, ax ; vectors. mov ax, cs mov ds, ax mov ax, word ptr OldInt9 mov es:[9*4], ax mov ax, word ptr OldInt9+2 mov es:[9*4 + 2], ax mov ax, word ptr OldInt13 mov es:[13h*4], ax mov ax, word ptr OldInt13+2 mov es:[13h*4 + 2], ax mov ax, word ptr OldInt16 mov es:[16h*4], ax mov ax, word ptr OldInt16+2 mov es:[16h*4 + 2], ax mov ax, word ptr OldInt1C mov es:[1Ch*4], ax mov ax, word ptr OldInt1C+2 mov es:[1Ch*4 + 2], ax mov ax, word ptr OldInt28 mov es:[28h*4], ax mov ax, word ptr OldInt28+2 mov es:[28h*4 + 2], ax mov ax, word ptr OldInt2F mov es:[2Fh*4], ax mov ax, word ptr OldInt2F+2 mov es:[2Fh*4 + 2], ax ; Okay, with that out of the way, let's close the file. ; Note: INT 2F shouldn't have to deal with DOS busy because it's ; a passive TSR call. mov ah, 3Eh ;Close file command mov bx, FileHandle int 21h ; Okay, one last thing before we quit- Let's give the memory allocated ; to this TSR back to DOS. mov ds, PSP mov es, ds:[2Ch] ;Ptr to environment block. mov ah, 49h ;DOS release memory call. int 21h mov ax, ds ;Release program code space. mov es, ax mov ah, 49h int 21h popa pop es pop ds mov ax, 0 ;Return Success. iret ; They called us with an illegal subfunction value. Try to do as little ; damage as possible. IllegalOp: mov ax, 0 ;Who knows what they were thinking? iret MyInt2F endp assume ds:nothing ; TstRmvable- Checks to see if we can remove this TSR from memory. ; Returns the zero flag set if we can remove it, clear ; otherwise. TstRmvable proc near cli push ds mov ax, 0 mov ds, ax cmp word ptr ds:[9*4], offset MyInt9 jne TRDone cmp word ptr ds:[9*4 + 2], seg MyInt9 jne TRDone cmp word ptr ds:[13h*4], offset MyInt13 jne TRDone cmp word ptr ds:[13h*4 + 2], seg MyInt13 jne TRDone cmp word ptr ds:[16h*4], offset MyInt16 jne TRDone cmp word ptr ds:[16h*4 + 2], seg MyInt16 jne TRDone cmp word ptr ds:[1Ch*4], offset MyInt1C jne TRDone cmp word ptr ds:[1Ch*4 + 2], seg MyInt1C jne TRDone cmp word ptr ds:[28h*4], offset MyInt28 jne TRDone cmp word ptr ds:[28h*4 + 2], seg MyInt28 jne TRDone cmp word ptr ds:[2Fh*4], offset MyInt2F jne TRDone cmp word ptr ds:[2Fh*4 + 2], seg MyInt2F TRDone: pop ds sti ret TstRmvable endp ResidentSeg ends cseg segment para public 'code' assume cs:cseg, ds:ResidentSeg ; SeeIfPresent- Checks to see if our TSR is already present in memory. ; Sets the zero flag if it is, clears the zero flag if ; it is not. SeeIfPresent proc near push es push ds push di mov cx, 0ffh ;Start with ID 0FFh. IDLoop: mov ah, cl push cx mov al, 0 ;Verify presence call. int 2Fh pop cx cmp al, 0 ;Present in memory? je TryNext strcmpl byte "Keypress Logger TSR",0 je Success TryNext: dec cl ;Test USER IDs of 80h..FFh js IDLoop cmp cx, 0 ;Clear zero flag. Success: pop di pop ds pop es ret SeeIfPresent endp ; FindID- Determines the first (well, last actually) TSR ID available ; in the multiplex interrupt chain. Returns this value in ; the CL register. ; ; Returns the zero flag set if it locates an empty slot. ; Returns the zero flag clear if failure. FindID proc near push es push ds push di mov cx, 0ffh ;Start with ID 0FFh. IDLoop: mov ah, cl push cx mov al, 0 ;Verify presence call. int 2Fh pop cx cmp al, 0 ;Present in memory? je Success dec cl ;Test USER IDs of 80h..FFh js IDLoop xor cx, cx cmp cx, 1 ;Clear zero flag Success: pop di pop ds pop es ret FindID endp Main proc meminit mov ax, ResidentSeg mov ds, ax mov ah, 62h ;Get this program's PSP int 21h ; value. mov PSP, bx ; Before we do anything else, we need to check the command line ; parameters. We must have either a valid filename or the ; command "remove". If remove appears on the command line, then remove ; the resident copy from memory using the multiplex (2Fh) interrupt. ; If remove is not on the command line, we'd better have a filename and ; there had better not be a copy already loaded into memory. argc cmp cx, 1 ;Must have exactly 1 parm. je GoodParmCnt print byte "Usage:",cr,lf byte " KeyEval filename",cr,lf byte "or KeyEval REMOVE",cr,lf,0 ExitPgm ; Check for the REMOVE command. GoodParmCnt: mov ax, 1 argv stricmpl byte "REMOVE",0 jne TstPresent call SeeIfPresent je RemoveIt print byte "TSR is not present in memory, cannot remove" byte cr,lf,0 ExitPgm RemoveIt: mov MyTSRID, cl printf byte "Removing TSR (ID #%d) from memory...",0 dword MyTSRID mov ah, cl mov al, 1 ;Remove cmd, ah contains ID int 2Fh cmp al, 1 ;Succeed? je RmvFailure print byte "removed.",cr,lf,0 ExitPgm RmvFailure: print byte cr,lf byte "Could not remove TSR from memory.",cr,lf byte "Try removing other TSRs in the reverse order " byte "you installed them.",cr,lf,0 ExitPgm ; Okay, see if the TSR is already in memory. If so, abort the ; installation process. TstPresent: call SeeIfPresent jne GetTSRID print byte "TSR is already present in memory.",cr,lf byte "Aborting installation process",cr,lf,0 ExitPgm ; Get an ID for our TSR and save it away. GetTSRID: call FindID je GetFileName print byte "Too many resident TSRs, cannot install",cr,lf,0 ExitPgm ; Things look cool so far, check the filename and open the file. GetFileName: mov MyTSRID, cl printf byte "Keypress logger TSR program",cr,lf byte "TSR ID = %d",cr,lf byte "Processing file:",0 dword MyTSRID puts putcr mov ah, 3Ch ;Create file command. mov cx, 0 ;Normal file. push ds push es ;Point ds:dx at name pop ds mov dx, di int 21h ;Open the file jnc GoodOpen print byte "DOS error #",0 puti print byte " opening file.",cr,lf,0 ExitPgm GoodOpen: pop ds mov FileHandle, ax ;Save file handle. InstallInts: print byte "Installing interrupts...",0 ; Patch into the INT 9, 13h, 16h, 1Ch, 28h, and 2Fh interrupt vectors. ; Note that the statements above have made ResidentSeg the current data ; segment, so we can store the old values directly into ; the OldIntxx variables. cli ;Turn off interrupts! mov ax, 0 mov es, ax mov ax, es:[9*4] mov word ptr OldInt9, ax mov ax, es:[9*4 + 2] mov word ptr OldInt9+2, ax mov es:[9*4], offset MyInt9 mov es:[9*4+2], seg ResidentSeg mov ax, es:[13h*4] mov word ptr OldInt13, ax mov ax, es:[13h*4 + 2] mov word ptr OldInt13+2, ax mov es:[13h*4], offset MyInt13 mov es:[13h*4+2], seg ResidentSeg mov ax, es:[16h*4] mov word ptr OldInt16, ax mov ax, es:[16h*4 + 2] mov word ptr OldInt16+2, ax mov es:[16h*4], offset MyInt16 mov es:[16h*4+2], seg ResidentSeg mov ax, es:[1Ch*4] mov word ptr OldInt1C, ax mov ax, es:[1Ch*4 + 2] mov word ptr OldInt1C+2, ax mov es:[1Ch*4], offset MyInt1C mov es:[1Ch*4+2], seg ResidentSeg mov ax, es:[28h*4] mov word ptr OldInt28, ax mov ax, es:[28h*4 + 2] mov word ptr OldInt28+2, ax mov es:[28h*4], offset MyInt28 mov es:[28h*4+2], seg ResidentSeg mov ax, es:[2Fh*4] mov word ptr OldInt2F, ax mov ax, es:[2Fh*4 + 2] mov word ptr OldInt2F+2, ax mov es:[2Fh*4], offset MyInt2F mov es:[2Fh*4+2], seg ResidentSeg sti Okay, ints back on. ; We're hooked up, the only thing that remains is to terminate and ; stay resident. print byte "Installed.",cr,lf,0 mov dx, EndResident ;Compute size of program. sub dx, PSP mov ax, 3100h ;DOS TSR command. int 21h Main endp cseg ends sseg segment para stack 'stack' stk db 1024 dup ("stack ") sseg ends zzzzzzseg segment para public 'zzzzzz' LastBytes db 16 dup (?) zzzzzzseg ends end Main
The following is a short little application that reads the data file produced by the above program and produces a simple report of the date, time, and keystrokes:
; This program reads the file created by the KEYEVAL.EXE TSR program. ; It displays the log containing dates, times, and number of keystrokes. .xlist .286 include stdlib.a includelib stdlib.lib .list dseg segment para public 'data' FileHandle word ? month byte 0 day byte 0 year word 0 hour byte 0 minute byte 0 second byte 0 KeyStrokes word 0 RecSize = $-month dseg ends cseg segment para public 'code' assume cs:cseg, ds:dseg ; SeeIfPresent- Checks to see if our TSR is present in memory. ; Sets the zero flag if it is, clears the zero flag if ; it is not. SeeIfPresent proc near push es push ds pusha mov cx, 0ffh ;Start with ID 0FFh. IDLoop: mov ah, cl push cx mov al, 0 ;Verify presence call. int 2Fh pop cx cmp al, 0 ;Present in memory? je TryNext strcmpl byte "Keypress Logger TSR",0 je Success TryNext: dec cl ;Test USER IDs of 80h..FFh js IDLoop cmp cx, 0 ;Clear zero flag. Success: popa pop ds pop es ret SeeIfPresent endp Main proc meminit mov ax, dseg mov ds, ax argc cmp cx, 1 ;Must have exactly 1 parm. je GoodParmCnt print byte "Usage:",cr,lf byte " KEYRPT filename",cr,lf,0 ExitPgm GoodParmCnt: mov ax, 1 argv print byte "Keypress logger report program",cr,lf byte "Processing file:",0 puts putcr mov ah, 3Dh ;Open file command. mov al, 0 ;Open for reading. push ds push es ;Point ds:dx at name pop ds mov dx, di int 21h ;Open the file jnc GoodOpen print byte "DOS error #",0 puti print byte " opening file.",cr,lf,0 ExitPgm GoodOpen: pop ds mov FileHandle, ax ;Save file handle. ; Okay, read the data and display it: ReadLoop: mov ah, 3Fh ;Read file command mov bx, FileHandle mov cx, RecSize ;Number of bytes. mov dx, offset month ;Place to put data. int 21h jc ReadError test ax, ax ;EOF? je Quit mov cx, year mov dl, day mov dh, month dtoam puts free print byte ", ",0 mov ch, hour mov cl, minute mov dh, second mov dl, 0 ttoam puts free printf byte ", keystrokes = %d\n",0 dword KeyStrokes jmp ReadLoop ReadError: print byte "Error reading file",cr,lf,0 Quit: mov bx, FileHandle mov ah, 3Eh ;Close file int 21h ExitPgm Main endp cseg ends sseg segment para stack 'stack' stk db 1024 dup ("stack ") sseg ends zzzzzzseg segment para public 'zzzzzz' LastBytes db 16 dup (?) zzzzzzseg ends end Main