When time stood still

Once upon a time, when few were online, a common copy protection scheme for DOS computer games was to ask the player to enter some word from the printed manual, or a code from a table. The assumption was that disks were easy to copy, but printed materials were not, which was proved fairly accurate.

Sometimes the challenge was easy to bypass. Some games chose from a small set of multiple choice questions which obviously could be defeated with trial-and-error. Others would store their secret words in plaintext in the binary, which a simple utility would reveal. Yet others would always ask the same question if you held down a key as soon as the game loaded, presumably because they incremented a counter that determined the question while waiting for the first keypress.

I’m proud that to have found an easy but less trivial workaround in my early teens. I noticed C tutorials often contained an example such as srand(time(0)), which seeded a pseudo-random number generator using the system clock.

I suspected that game developers might have adopted this practice, so I wrote a Terminate-and-Stay-Resident (TSR) program that intercepted the timer interrupt (INT 8h) and set the time to midnight. In other words, it would always be midnight for the computer.

Sure enough, I’d set the date to 01-01-1981 and run my utility, and several games would ask the same copy protection question every time!

Here’s the source to W.ASM:

cseg               segment
                   assume    cs:cseg
                   org       100h
first:             jmp       init

olint              label     WORD
old8               dd        ?

newint:
                   push      ax
                   push      bx
                   push      cx
                   push      dx
                   push      bp
                   push      si
                   push      di
                   push      ds
                   push      es
                   push      ss
                   mov       ah,1
                   xor       cx,cx
                   xor       dx,dx
                   int       1ah
                   pushf
                   call      old8
                   pop       ss
                   pop       es
                   pop       ds
                   pop       di
                   pop       si
                   pop       bp
                   pop       dx
                   pop       cx
                   pop       bx
                   pop       ax
                   iret
init:
                   mov       ax,3508h
                   int       21h
                   mov       olint,bx
                   mov       olint[2],es
                   mov       ax,2508h
                   lea       dx,newint
                   int       21h
                   mov       ax,ds:[2ch]
                   mov       es,ax
                   mov       ah,49h
                   int       21h
                   lea       dx,init
                   int       27h
cseg               ends
                   end       first

Hold still for a moment

Other games froze up if the clock failed to change, so I wrote a second TSR utility that also intercepted the keyboard interrupt (INT 9h). On detecting Alt-B, on the next 96 timer interrupts, it would reset the time to midnight. Since the timer interrupt went off 18.2 times a second, it would be midnight for about 5 seconds, then the clock would tick again.

A well-timed Alt-B was enough to force some of these games to issue the same challenge every time.

Here’s the source to TIMESTOP.ASM:

cseg     segment
         assume    cs:cseg
         org       100h
first:   jmp       init
hotkey   equ       30h                 ;Scan code for "B"
shftmsk  equ       00001000b           ;Shift mask
old8     label     word
oldloc8  dd        ?
old9     label     word
oldloc9  dd        ?
count    db        0

newint9  proc      near
         push      ax
         in        al,60h
         cmp       al,hotkey
         je        keygood
exit9:
         pop       ax
         jmp       oldloc9
keygood:
         mov       ah,2
         int       16h
         test      al,shftmsk
         jz        exit9
         inc       count
         jmp       exit9
newint9  endp

newint8  proc      near
         cmp       count,0
         je        exit8
         push      ax
         push      bx
         push      cx
         push      dx
         xor       cx,cx
         xor       dx,dx
         mov       ah,1
         int       1ah
         inc       count
         cmp       count,60h
         jne       all_done
         mov       count,0
all_done:pop       dx
         pop       cx
         pop       bx
         pop       ax
exit8:
         jmp      oldloc8
newint8  endp

init     proc      near
         mov       ax,3508h
         int       21h
         mov       old8,bx
         mov       old8[2],es
         mov       ax,3509h
         int       21h
         mov       old9,bx
         mov       old9[2],es
         mov       ax,2508h
         lea       dx,newint8
         int       21h
         mov       ax,2509h
         lea       dx,newint9
         int       21h
         mov       ax,ds:[2ch]
         mov       es,ax
         mov       ah,49h
         int       21h
         lea       dx,init
         int       27h
init     endp
cseg     ends
         end       first

Downloads:

Tempus Fugit

A decade later, I was a PhD student studying cryptography. Although the tiny tools I built in my youth may have lost their relevance, I was obliged to continue thinking about exploits involving pseudo-randomness. For example:


Ben Lynn blynn@cs.stanford.edu 💡