Logo Pending

SCI decompilation and Weird Loops

They’re not really that weird on the face of it, but that depends on who’s looking.

The decompiler in SCI Companion is a work of art. You can tell because I didn’t write it. (I only fixed a thing or two.) But there are some things that it can’t figure out, and when that happens the function or method body is replaced with a raw asm block. For example, the copy protection in Laura Bow 2 – The Dagger of Amon Ra has loop in it that SCI Companion can’t hack.

It’s a bit much to take in but the important bits are as follows: this code block (rm18::init) has four discrete segments. The first isn’t shown here and sets up a few simple things. The second (code_0087) is a regular loop, where temp0 counts up from zero to eleven. When it hits twelve, the loop is broken and we go to section three, code_00ad. Section three is a weird loop. If you look at the check at the top we see this:

pushi #size
pushi 0
lofsa tempList
send 4
bnt code_0116

Which basically means that when (tempList size?) returns zero/is false, we skip to section four. At the bottom of the section, right before the label for code_0116, there’s the command that makes section three a loop; jmp code_00ad.

So that means that section three keeps repeating until tempList is out of items. Section two put a bunch of values in it, and section three then takes items out at random and puts them into goodList, effectively randomizing the order. The items, incidentally, are the tiles depicting the various Egyptian gods that the copy protection is all about, clones of egyptProp given increasing cel values. Section three positions them as they’re added to goodList. It’s a good routine, Brent.

The problem that trips up SCI Companion and makes it spit out the stuff in those two pictures is that it doesn’t recognize the second loop for what it is. Counting from one value to another by a given increment? Easy. Iterating over a collection? It can figure those out. But picking items from a bag until it’s empty? That’s not on the menu.

To make this decompile, then, we first need to break the loop by commenting out that last jmp command. A single ; suffices. Compile the script resource, then go back and re-decompile it. A conditional loop, of course, consists of a check and a jump. We removed the jump so now it’s just the check:

(method (init &tmp i theTile theX theY)
  (LoadMany rsVIEW 18) ; load the tiles
  (super init:)
  (gGame handsOn:)
  (gIconBar disable: 0 1 3 4 5 6 7)
  (goodList add:)
  (tempList add:)
  (= theX -32)
  (= theY 46)
  (= i 0)
  ; Instantiate twelve tiles, with increasing cel numbers.
  (while (< i 12)
    (tempList add: ((egyptProp new:) cel: i yourself:))
    (++ i)
  ; This should be "(while (tempList size?)" but we removed the jump, remember?
  (if (tempList size?)
    ; Pick a tile number.
    (= i (Random 0 (- (tempList size?) 1)))
    ; Get the i-th tile.
    (= theTile (tempList at: i))
    ; Set up the tile's position on the grid and add it to goodList.
    (goodList add:
         x: (= theX (+ theX 48))
         y: theY
    ; Once we're halfway through, CRLF to the next row.
    (if (== (goodList size?) 6)
      (= theX -32)
      (= theY 111)
    ; Actually remove the tile from tempList so we won't pick it again.
    (tempList delete: theTile)
  ; Section four
  (gGame handsOff:)
  (self setScript: sInitEm)

The cool part is that once we replace that if with a while and compile the script, the result is effectively the same as the original. Only some of the specific opcode choices are different. For example, the original uses the two-byte pushi 1 throughout (also 0 and 2), but SCI Companion’s script compiler prefers to use the one-byte push1 there. The same values are pushed regardless.

[ , , , ] Leave a Comment

Combining PQ2 and KQ4’s copy protection scripts

I said I would, right?

  ; Correct answers' hashes, in original order.
  ; Determined by https://helmet.kafuka.org/sci/kq4_cp.html
  [answers 8] = [666 393 526 377 365 453 383 441]
(instance CopyProtection of Room
  (method (doit &tmp i ch hash myPick [yourAnswer 40])
    ; Just like in PQ2, we grab the current time, then mask out
    ; the lower bits to limit the range to a number from 0 to 7.
    (= myPick (& (GetTime gtTIME_OF_DAY) 7))
    ; Clear out the first character of our answer to effectively
    ; make it blank.
    (= yourAnswer 0)
    ; Request our input as before...
      "TO: Detective Bonds\nFROM: Captain Hall\nSUBJECT: ID of evidence photo\n\nPlease provide the LAST name of the person pictured in the attached evidence photo for homicide case 186751.\n\nPlease respond in box below, ASAP!\n"
      #icon 701 0 myPick
      #edit @yourAnswer 20
    ; Now we use some trickery from KQ4, but different.
    (= hash 0)
    (= i 0)
    ; While the character at position i is nonzero...
    (while (= ch (StrAt @yourAnswer i))
      ; Anything between 'a' and 'z' gets turned to uppercase.
      ; We don't bother putting it *back* in yourAnswer though.
      (if (and (>= ch 97) (<= ch 122)) (= ch (- ch 32)))
      ; Add this value to our running total.
      (= hash (+ hash ch))
      (++ i)
    ; Either the hash we calculated is the correct one, or
    ; we entered "bobalu".
    (if (or
          (== hash [answers myPick])
          (== hash 437)
      (gRoom newRoom: 1) ; or wherever your game starts.
      (Print "Sorry Bonds, you'll need to do better than that!")
      (= gQuitGame true)

And presto! I’d talk about some of the other games’ copy protection schemes but for example KQ5’s doesn’t pass the decompiler. Probably because of a difficulty involving endless loops. Still, feel free to suggest something.

[ , , ] Leave a Comment

Sorry, Bonds – Police Quest 2 Copy Protection

Last for now in the set on copy protection is Police Quest 2. I might go into some others, I dunno, and I have something planned where I optimize the hell out of the PQ2 copy protection script by means of KQ4. But let’s get down to it.

  [yourAnswer 40]
(procedure (ToUpper &tmp i ch)
  (= i 0)
  (while (= ch (StrAt @yourAnswer i))
    ; If ch is between 'a' and 'z'...
    (if (and (>= ch 97) (<= ch 122))
      ; ...change it to uppercase.
      (StrAt @yourAnswer i (- ch 32))
    (++ i)
(instance rm701 of Rm
  (method (doit &tmp myPick)
    (= myPick (& (GetTime gtTIME_OF_DAY) 7))
    (= yourAnswer 0)
    (Print "TO: Detective Bonds\n
            FROM: Captain Hall\n
            SUBJECT: ID of evidence photo\n
            Please provide the LAST name of the person pictured in
            the attached evidence photo for homicide case 186751.\n
            Please respond in box below, ASAP!\n"
            #icon 701 0 myPick
            #edit @yourAnswer 20
    (= gQuit true)
    ; Like in C, StrCmp returns zero if the strings are the same.
    ; Zero is false, so we use a not to make equal be true.
    (switch myPick
      (0    (if (not (StrCmp @yourAnswer "GRANANDEZ")) (= gQuit false)))
      (1    (if (not (StrCmp @yourAnswer "SIMMS"))     (= gQuit false)))
      (2    (if (not (StrCmp @yourAnswer "TASELLI"))   (= gQuit false)))
      (3    (if (not (StrCmp @yourAnswer "COLBY"))     (= gQuit false)))
      (4    (if (not (StrCmp @yourAnswer "BAINS"))     (= gQuit false)))
      (5    (if (not (StrCmp @yourAnswer "SNIDER"))    (= gQuit false)))
      (6    (if (not (StrCmp @yourAnswer "JONES"))     (= gQuit false)))
      (else (if (not (StrCmp @yourAnswer "DICKEY"))    (= gQuit false)))
    (if gQuit
      (Print "Sorry Bonds, you'll need to do better than that!")
      ; With gQuit set, we'll exit at the end of this doit cycle.
      (gGame restart:)
      ; The main game object can tell if we're restarting, so it'll
      ; put us in the first playable scene instead of the title screen.

Gee, I can think of a way to improve this already. Let’s turn yourAnswer from a local to a temp, and inline ToUpper:

(method (doit &tmp myPick i ch [yourAnswer 40])
  ; ...
  (= gQuit true)
  (= i 0)
  (while (= ch (StrAt @yourAnswer i))
    (if (and (>= ch 97) (<= ch 122))
      (StrAt @yourAnswer i (- ch 32))
    (++ i)
  ; ...

And that’s just one improvement. It really helps that, unlike the subtitle typing in Larry 5, we only uppercase one thing once. Tune in next time to see what could be done.

[ , , , ] 2 Comments on Sorry, Bonds – Police Quest 2 Copy Protection

Little Black Book – Larry 2 Copy Protection

It’s been a while now since I last explained a Sierra game’s copy protection, so why not go at it again?

Compared to KQ4, LSL2 has only a few different options, and it involves no math.

  phoneNum     ; The correct answer.
  [input 6]    ; Our guess. 11 characters max, but 16-bit values so 12/2=6
  ; The photo parts are put in hunk space instead of heap or something.
(instance rm10 of Rm
    picture 10
  (method (init &tmp dress face hair earring)
    ; Preload the photo and logo parts
    (Load rsVIEW 60)
    (super init:)
    ; For each part of the logo, create a new view in hunk space, and addToPic it.
    ; Adding a view to the pic causes it to be disposed, so in the end none of the logo parts take space.
    ((View new:) view: 60 loop: 4 cel: 0 posn:  72  52 setPri: 1 addToPic:) ; Logo
    ((View new:) view: 60 loop: 4 cel: 1 posn: 122  36 setPri: 0 addToPic:) ; S
    ((View new:) view: 60 loop: 4 cel: 2 posn: 141  35 setPri: 0 addToPic:) ; I
    ((View new:) view: 60 loop: 4 cel: 3 posn: 161  35 setPri: 0 addToPic:) ; E
    ((View new:) view: 60 loop: 4 cel: 4 posn: 190  35 setPri: 0 addToPic:) ; R
    ((View new:) view: 60 loop: 4 cel: 4 posn: 221  35 setPri: 0 addToPic:) ; R
    ((View new:) view: 60 loop: 4 cel: 5 posn: 252  35 setPri: 0 addToPic:) ; A
    ((View new:) view: 60 loop: 5 cel: 0 posn:  13 113 setPri: 7 addToPic:) ; Left girl
    ((View new:) view: 60 loop: 6 cel: 0 posn: 306 113 setPri: 7 addToPic:) ; Right girl
    ; Now create views for the photo parts.
    ((= dressView   (View new:)) view: 60 loop: 0 cel: 0 setPri: 1 posn: 154 981 init:)
    ((= faceView    (View new:)) view: 60 loop: 1 cel: 5 setPri: 2 posn: 154 981 init:)
    ((= hairView    (View new:)) view: 60 loop: 2 cel: 1 setPri: 3 posn: 154 981 init:)
    ((= earringView (View new:)) view: 60 loop: 3 cel: 4 setPri: 4 posn: 154 981 init:)
    ; I left out most of the answers in the interest of fairplay and conciseness.
    (switch (Random 1 16)
      (1 (= dress 0) (= face 5) (= hair 1) (= earring 4) (= phoneNum "555-7448"))
      (2 (= dress 1) (= face 5) (= hair 2) (= earring 4) (= phoneNum "555-5968"))
      ; ...
      (15 (= dress 2) (= face 4) (= hair 0) (= earring 2) (= phoneNum "555-5633"))
      (16 (= dress 1) (= face 1) (= hair 3) (= earring 2) (= phoneNum "555-5834"))
    ; Now apply the traits chosen above.
    (dressView   posn: 154 81 setLoop: 0 setCel: dress)
    (faceView    posn: 154 81 setLoop: 1 setCel: face)
    (hairView    posn: 154 81 setLoop: 2 setCel: hair)
    (earringView posn: 154 81 setLoop: 3 setCel: earring)
    (Display "© 1988 by Sierra On-Line, Inc." dsCOORD 60 176 dsCOLOR 1 dsBACKGROUND 3)
  (method (doit)
    (Format @input "555-")
      "Please find this girl's picture in your little black book, then type her telephone number here:"
      #at -1 144
      #width 248
      #font gFont
      #edit @input 11 ; add a textbox, max length 11.
    ; Reminder: like in C, StrCmp returns zero if the inputs are equal, and zero is false. Hence the not.
    (if (not (StrCmp @input phoneNum))
      (gRoom newRoom: 90)
      (Print "Sorry, but you need to spend more time staring at beautiful women!
              In order to play this game, you must have the original documentation.
              If you've lost your little black book, please telephone Sierra's
              Customer Support Department at the number printed on your disks.")
      (= gQuit true)

I can think of a few ways to improve this, and in fact I already did in some places — this is not the original LSL2 copy protection! Can you think of more ways to improve this script?

[ , , , ] Leave a Comment

King’s Quest 4 Copy Protection

King’s Quest 4 – The Perils of Rosella starts with a copy protection challenge right off the bat.

If you were playing the original 1988 version you could just enter the magic word “bobalu” and be done with it, but the 1989 version removed this.

It’s a pretty simple challenge-reply system, but the interesting bit is how your answers are considered. If someone were to somehow find the challenges they would also find the answers in the same order, but there’s a catch: the answers are hashed.

Very simply so, but they are. And of course the script code containing the challenges and answer hashes is compressed in the RESOURCE.001 file, and the whole thing is script code and nobody outside of Sierra could be expected to be able to read that stuff back in 1988. Sure, maybe some people could but still good luck figuring out how this worked. Even the backdoor phrase was compressed.

But now it’s 2018, nearly 2019, and I for one have made happy use of the tools now available to us, mostly to slake my own thirst for knowledge. So here’s how it works.

The copy protection script has several local variables: a random number from 1 to 79, the challenge text, the correct answer’s hash, a buffer for the user’s input, the hash for said input, and some work variables.

On startup, the random number is chosen. Then, in a big ol’ switch statement, the correct hash is decided on:

(switch (= randomPick (Random 1 79))
  (1 (= requestSum 431))
  (2 (= requestSum 521))
  (3 (= requestSum 535))
  (79 (= requestSum 686))

In another big ol’ switch (instead of doing it at once?), the matching challenge is set up:

(switch randomPick
  (1 (= requestText "On page 2, what is the fourth word of the first sentence?"))
  (2 (= requestText "On page 2, what is the fourth word of the second paragraph?"))
  (3 (= requestText "On page 3, what is the fourth word in the first paragraph?"))
  (79 (= requestText "In the section TIPS FOR NEW ADVENTURE PLAYERS, what is the eighth word in the first paragraph of tip #2 (STAY OUT OF DANGER)?"))

Incidentally, I said our guess would be stored in a buffer variable, that is an array in memory large enough to contain it, but I did not say any such thing about the challenge text. That’s because it’s stored as a pointer to the text, in the place it was loaded to as part of the script. From then on these challenges don’t mutate in any way. Our input can be literally anything.

Anyway, after displaying the challenge, we have our input in a buffer. This is where the magic happens:

(= i 0)
(while (< i (StrLen @userInput))
  (= ch (& (= ch (StrAt @userInput i)) $005f))
  (StrAt @userInput i ch)
  (= inputSum (+ inputSum ch))
  (++ i)

Iterating through the user’s input, we read the next line inside-out. Using the StrAt function we fetch the next character and store its value in our work variable. Then we use some binary magic on that same value to turn it into UPPERCASE, and assign that to our work var. Now, as I write this I feel like this can be simplified a little bit…

(= ch (& (StrAt @userInput i) $005f))

…Yeah, that seems nice. I don’t think it’d hurt functionality to do this. Anyway, the next line shows how SCI function and kernel calls can be variadic as all get out — given three arguments, StrAt will set the character on the given spot. In the third line, we add the character’s value to our running sum.

And that’s it! We can now compare our input to the expected answer, and either continue on to the title screen or display an error and quit.

But that’s not all there is to it. First, for some reason, the input is uppercased and then stored again, character by character. This is so the 1988 release can compare it against the magic backdoor word, which is also in uppercase. This seems like an awful waste when you could compare it against a number instead. Not to mention, the 1989 release doesn’t even have the backdoor and still does all this. (For the record, that would be 437.)

Second, this is such a simple method that there are guaranteed to be words with the same hashes. For example, “voice” and “licks” are both 374.

But yeah, that’s just about all there is to know about the copy protection in King’s Quest 4 – The Perils of Rosella.

[ , , , ] 7 Comments on King’s Quest 4 Copy Protection