-----------Crusade in Europe----------- A 4am and san inc crack 2017-08-10 --------------------------------------- Name: Crusade in Europe Version: 2 Genre: simulation Year: 1985 Credits: by Sid Meier and Ed Bever, Apple II version by Jim Synoski Publisher: Microprose Software Platform: Apple ][+ or later (64K); double hi-res option requires an Apple //e or later (128K) Media: double-sided 5.25-inch floppy OS: Diversi-DOS C1983 Previous cracks: none (Asimov has uncracked .nib images) ~ Chapter 0 In Which We're Off And Running The disk is a standard 16-sector disk, except track $22 which is unreadable. Searching for "BD 8C C0" leads us to the track $0D sector $00, from the file named "\\". (There's another copy of this code on track $1E sector $08, but it's unused.) --v-- 0273 BD 8C C0 LDA $C08C,X 0276 BD 8E C0 LDA $C08E,X 0279 20 44 F9 JSR $F944 027C B0 10 BCS $028E 027E AD 2E 02 LDA $022E 0281 4A LSR 0282 C5 2E CMP $2E 0284 D0 16 BNE $029C 0286 A9 DB LDA #$DB 0288 8D 01 02 STA $0201 028B 4C 93 02 JMP $0293 028E A9 FF LDA #$FF 0290 8D 01 02 STA $0201 0293 BD 88 C0 LDA $C088,X 0296 AD 81 C0 LDA $C081 0299 4C A4 02 JMP $02A4 029C A9 DB LDA #$C0 029E 8D 01 02 STA $0201 02A1 4C 93 02 JMP $0293 --^-- We've found the protection routine! Looks like a soft target. The success path at $0286 puts #$DB in $0201. Let's patch $028F and $029D to #$DB, so $0201 ends up with the correct value even if the protection check fails. And we're done, right? Wrong. While the game starts nicely, it asks for a word from the manual before starting any scenario. That's annoying, and it's the second protection routine. Let's enter the proper password and see what happens. This is where everyone until now made a critical mistake. The game plays for a looong time and everything looks fine. That is, until it prints "Fatal error: nnn" (the number changes each time) and hangs. Okay, that leads to two possibilities: 1. there's another protection check like the first one, or 2. the protection check has a protection check of its own, i.e. an anti-tamper check Actually, there's a third possibility: 3. that both of those things are true Let's find out. ~ Chapter 1 In Which Our Fears Are Confirmed The problem is that the program is written in compiled Integer Basic, and the result is interpreted at run-time using a custom interpreter. The p-code language is very simple, composed primarily of comparisons, transfers of control, and a couple of arithmetic instructions and read/write primitives. The rest is I/O-related: fetching keyboard input, setting various display modes, cursor positioning, and character printing. It's faster than the original Basic, and far more compact than native code, but fast enough for the purpose. It looks like this (and I have no idea of the true names for the routines, I'm just describing the behaviour): --v-- .BYTE $12, $27, $00 ; jsr rel imm16 .BYTE $04, $AF, $61 ; push16 (imm16) .BYTE $F6, $34 ; push16 imm8 .BYTE $24 ; add16 stk, stk .BYTE $F6, $FF ; push16 imm8 .BYTE $5C ; push00xx (stk16) .BYTE $C8 ; pop8 (stk16) --^-- There's a dispatcher at $00AF (in zero page), which does a load/store/jump. Prior to that are several routines for adjusting the instruction pointer by popping from the stack, incrementing by one, or adjusting according to a passed parameter. If we replace the store/jump with an unconditional jump to spare memory, we can watch the dispatcher in action. In particular, we can see when it starts to print the "FATAL ERROR" message, and see who requested it. Once we find that point, we can backtrack until we find the start of that routine. If we don't find the comparison that triggers it, then we patch our redirector to watch for someone about to write the routine address in the dispatcher, then use that address and backtrack. Lather, rinse, repeat, until we find the comparison that sets off the whole chain. Time passes... Sure enough, there's a comparison of two 16-bit values, but not of the kind that we expect. Track $1F sector $0C, from the file named "B": --v-- .BYTE $04, $66, $53 ; push16 (imm16) .BYTE $04, $84, $53 ; push16 (imm16) .BYTE $2E ; cmpne16 stk, stk .BYTE $0E, $09, $00 ; btrue rel imm16 .BYTE $12, $FF, $11 ; jsr rel imm16 --^-- When the two values match, the string is printed. That looks like a timer. Now to find where those two values are set. More time passes... $5384 is incremented monotonically and reset periodically. It looks like a frame counter. $5366 is much more interesting. Here, on track $1F sector $04, also from the file named "B": .BYTE $04, $66, $53 ; push16 (imm16) .BYTE $F0 ; push0 .BYTE $32 ; cmplt16 stk, stk .BYTE $10, $04, $00 ; btrue rel imm16 .BYTE $B6 ; rts .BYTE $F0 ; push0 .BYTE $F6, $14 ; push16 imm8 .BYTE $AC, $68, $53 ; for loop .BYTE $04, $68, $53 ; push16 (imm16) .BYTE $06, $B6, $52 ; push16 (imm16+ ; pop16*2) .BYTE $F0 ; push0 .BYTE $2E ; cmpne16 stk, stk .BYTE $0E, $0C, $00 ; btrue rel imm16 .BYTE $F6, $48 ; push16 imm8 .BYTE $F6, $48 ; push16 imm8 .BYTE $5E ; rand stk, stk .BYTE $24 ; add16 stk, stk .BYTE $08, $66, $53 ; pop16 (imm16) .BYTE $B2, $68, $53 ; next .BYTE $B6 ; rts It turns out that $5366 is a flag to indicate that a protection check failed. It begins as $FFFF and is checked periodically. The cmplt16 checks if $5366 is a negative number. If so, then an array is parsed via cmpne16 to possibly find a zero. If one is found, then $5366 is replaced with a RND(72)+72. There's our timer, and we can find a zero in that array. But that array is scary. It means that there's a whole set of protection checks that might fail. At least we know where to look. If we instruct our dispatcher redirection to watch for writes to those addresses, we'll know when protection checks fail and can backtrack to the check itself. So, backtracking from there, and yes - the initial protection routine has a protection routine of its own, and yes, it's a checksum. We've found the third protection routine, and confirmed possibility #2 -- the protection check is itself protected by an anti-tamper check. ~ Chapter 2 In Which The Whole Is More (Or Less) Than The Sum Of Its Parts The checksum code is on track $0D sector $0B from the file named "A". It looks like this: --v-- .BYTE $AC, $E2, $52 ; for loop .BYTE $04, $83, $54 ; push16 (imm16) .BYTE $04, $E2, $52 ; push16 (imm16) .BYTE $5C ; push00xx (stk16) .BYTE $24 ; add16 stk stk, ; pop .BYTE $08, $83, $54 ; pop16 (imm16) .BYTE $04, $83, $54 ; push16 (imm16) .BYTE $02, $B8, $0B ; push imm16 .BYTE $32 ; cmplt16 stk stk, ; pop .BYTE $10, $0D, $00 ; btrue rel imm16 .BYTE $04, $83, $54 ; push16 (imm16) .BYTE $02, $B6, $0B ; push imm16 .BYTE $3E ; sub stk, stk .BYTE $08, $83, $54 ; pop16 (imm16) .BYTE $B2, $E2, $52 ; next --^-- It's literally a sum of the bytes in the buffer, with a subtraction when the value exceeds a threshold. Using a smaller return value in the initial protection routine (which we did to fake success) causes this checksum routine to behave a bit differently. The problem manifests itself in this code: --v-- .BYTE $F6, $2D ; push16 imm8 .BYTE $F0 ; push0 .BYTE $0A, $B6, $52 ; pop16 (imm16+ ; pop16*2) .BYTE $04, $83, $54 ; push16 (imm16) .BYTE $F6, $47 ; push16 imm8 .BYTE $2C ; cmpeq16 stk stk, ; pop .BYTE $10, $04, $00 ; bfalse rel imm16 --^-- The threshold isn't reached anymore on one pass because the sum is too low, so the resulting value is too large to compare against a 8-bit value. One solution is to adjust the threshold so that the subtraction happens again. If we lower the threshold, then the subtraction happens again, but then a different pass fails to trigger a subtraction because of the distribution of values in this buffer. If we raise the threshold, then everything works again. Given a different set of values, the opposite case could be true instead. We raise the threshold and then we run the game again. The game plays for a looong time and everything looks fine. That is, until it prints "Fatal error: nnn" (the number changes each time) and hangs. Damn. ~ Chapter 3 In Which We Are Getting Really Tired Of Having Our Fears Confirmed Backtracking again, we find a new timer setting. Track $1E sector $04, also from the file named "B": --v-- .BYTE $04, $AF, $51 ; push16 (imm16) .BYTE $04, $68, $53 ; push16 (imm16) .BYTE $24 ; add16 stk, stk .BYTE $5C ; push00xx (stk16) .BYTE $F0 ; push0 .BYTE $2E ; cmpne16 stk, stk .BYTE $0E, $0C, $00 ; btrue rel imm16 .BYTE $F6, $48 ; push16 imm8 .BYTE $F6, $48 ; push16 imm8 .BYTE $5E ; rand stk, stk .BYTE $24 ; add16 stk, stk .BYTE $08, $66, $53 ; pop16 (imm16) --^-- This one is also checking if a particular memory location is zero. If that zero is found, then $5366 is replaced with a RND(72)+72. A quick bit of math to get the address, and some backtracking to find it, reveals the routine. Track $1F sector $04, also from the file named "B": --v-- .BYTE $12, $27, $00 ; jsr rel imm16 .BYTE $04, $AF, $61 ; push16 (imm16) .BYTE $F6, $34 ; push16 imm8 .BYTE $24 ; add16 stk, stk .BYTE $F6, $FF ; push16 imm8 .BYTE $5C ; push00xx (stk16) .BYTE $C8 ; pop8 (stk16) --^-- The result of the JSR is stored to a calculated memory location. What does the JSR do? It calls a native function. What does the native function do? It checks the disk again like the first protection routine. We've found the fourth protection routine, and confirmed possibility #1 (and by extension, #3). The code for the fourth protection routine looks like this: --v-- $E251 AD 14 E1 LDA $E114 $E254 20 98 E3 JSR $E398 $E257 AD 14 E1 LDA $E114 $E25A 20 AA E2 JSR $E2AA $E25D 20 8D E2 JSR $E28D $E260 90 0B BCC $E26D $E262 CE F6 E2 DEC $E2F6 $E265 30 0A BMI $E271 $E267 20 35 E3 JSR $E335 $E26A 4C 51 E2 JMP $E251 $E26D A9 00 LDA #$00 $E26F F0 02 BEQ $E273 $E271 A9 FF LDA #$FF $E273 48 PHA ... It checks in a loop for the special track, then returns success or failure. Seems like a simple change. There's just one thing wrong, though: searching the disk doesn't find that code. The reason is that it's all byte- swapped. And relocated. Track $0B sector $0D, from the file named "SHR1": --v-- ; #$61 becomes #$E2 $E26B 4C A9 61 JMP $61A9 $E26E F0 00 BEQ $E270 $E270 A9 02 LDA #$02 $E272 48 PHA $E273 FF ??? --^-- Eeew. Still, replacing that #$FF should fix it. More time passes... It is getting dark. You are likely to be eaten by an anti- tamper grue. The game plays for a looong time and everything looks fine. That is, until it prints "Fatal error: nnn" (the number changes each time) and hangs. Sigh. Backtracking again, we find that the array has a different entry zeroed out. We've found the fifth protection routine. ~ Chapter 4 In Which Some Words Are Hard But Word Sums Are Harder But here's a new thing: the game plays for a looong time and everything looks fine. Really fine. It doesn't hang anymore. So we're done. Celebrate! Well, no. There's that pesky manual protection that needs to go away. We could cheat and make any answer work. Yes, that's one way to do it, and it's what I wanted to do to be done with it. But 4am said "no," so no. If you type nothing at the codeword lookup screen, the game enters demonstration mode. We want this mode to remain available, so the "type anything" option won't work. We choose to take a different path. The first thing is to reverse the logic so that typing nothing would enter the game proper, and typing the proper word would enter demonstration mode. I fix that. Except that the "fatal error" message came back. Yes, the p-code itself has a checksum. We've found the sixth protection routine. Another point of interest regarding the manual check is that if you type the wrong word, the game prints "You are an enemy spy" and then enters demo mode. I found the check that causes the "enemy spy" text to be printed. I change it to print the "demonstration mode" text instead. The "fatal error" message came back. We've found the seventh protection routine. Yes, that part of the p-code has a separate checksum. I fix the checksum for that. Great, but the message still asks for a word from the manual. We want to fix that. I spent some hours crafting the perfect wording for the prompt. It was crap and we threw it out. 4am spent some minutes crafting some wording for the prompt. It was perfect. That's a fine skill. I put the text in. Of course, the "fatal error" message came back. Yes, that part of the p-code also has a *separate* checksum. We've found the eighth and final protection routine... of side A. ~ Chapter 5 In Which We Flip The Disk And Immediately Regret This Decision So far, this 128K version has had similar protection to the 64K version (crack no. 1353). But this version has an option to use double hi-res graphics throughout the game, and those files are on side B. The second side protection is just like the first side, except that it isn't. Side B contains all the same protection check as side A, but even after we defeat those, the "fatal error" message still appears. When searching for the same checksum values to change, we run into a funny coincidence. --v-- 4C CB 9F JMP $9FCB 4C 93 B7 JMP $B793 4C F6 82 JMP $82F6 4C B7 0B JMP $0BB7 4C C1 0C JMP $0CC1 --^-- On first glance, this looks like an ordinary looking jump table, including the well-known address $B793 (standard high-level entry point for reading multiple sectors from disk). But the jump table also happens to have some of the same values as the checksum we're looking for. Coincidence? We might be suspicious that the game's DOS is in the language card, so $B793 doesn't point to anything meaningful. Oh, and that jump table? According to a track/sector map (thanks Copy II Plus), it's not part of any file. It's in an unallocated sector on the disk (track $0E sector $04). We're just full of coincidences today. Backtracking, we find this p-code: --v-- .BYTE $04, $A1, $61 ; push16 (imm16) .BYTE $04, $2B, $62 ; push16 (imm16) .BYTE $F6, $08 ; push16 imm8 .BYTE $24 ; add16 stk, stk .BYTE $5C ; push00xx (stk16) .BYTE $C8 ; pop8 (stk16) .BYTE $04, $9F, $61 ; push16 (imm16) .BYTE $04, $2B, $62 ; push16 (imm16) .BYTE $F6, $07 ; push16 imm8 .BYTE $24 ; add16 stk, stk .BYTE $5C ; push00xx (stk16) .BYTE $C8 ; pop8 (stk16) .BYTE $04, $00, $61 ; push16 (imm16) .BYTE $04, $84, $63 ; push16 (imm16) .BYTE $2C ; cmpeq16 stk, stk .BYTE $0E, $09, $00 ; bfalse rel imm16 --^-- And the value at $622B points to the start of the jump table in memory *that someone loaded*. And the add of #$08 in the first case, and #$07 in the second case, points to the $82F6, which is then fetched and stored. And that $82F6 happens to be one of the checksums of interest. There are no coincidences. We've found the ninth protection routine. Another p-code routine performs the same operations after adding #$05 and #$04 to fetch the $B793 instead, which makes it the... [counts furiously] TENTH PROTECTION ROUTINE. We patch both copies of the checksums, and, at long last, the game runs properly. And there was much rejoicing. Quod erat liberandum. ~ Acknowledgments Thanks to 4am for editing and reviewing drafts of this write-up. --------------------------------------- docs by qkumba No. 1358 ------------------EOF------------------