No CD cracking - Heroes of Might and Magic 3 (from binary, using OllyDbg)

This is a log which describes the process I followed to remove the CD check from Heroes of Might and Magic 3. To that end, I used OllyDbg version 2.00.

OllyDbg is an excellent debugger. The features I appreciated most were: visual representation of branches, modify (by assembling) in-place, and most of all the ability to save modified executable.

Heroes of Might and Magic 3 was the successor to my favourite in the series: Heroes of Might and Magic 2. I remember being frustrated by it requiring an inserted game CD as a copy protection mechanism.

As a child, "no CD cracks" were fascinating to me. It was almost magical how a tiny executable written by a hobbyist could "topple" barricades devised by larger teams of professional programmers.

Given my recent success with reverse engineering and modifying the Knights of the Round arcade game (see Warlock's Tower), I became interested in trying to hack Heroes of Might and Magic 3 myself.


Ran the game, paused main thread during "no CD" dialogue.

Stack contents while "no CD" dialogue was active:

0019BE7C 004D1EDB HEROES3.004D1EDB
; probably the address after call to "show dialog"
; for CD check failed dialog

0019BE80 000000FF
; some other parameter

0019BE84 02B39369 ASCII "The Heroes III CD-ROM was not found!"...
; pointer to string to be printed in the warning dialogue

Traced call to

004D1ED6 E8 F5010000 CALL HEROES3.004D20D0
; probably calls "show dialog inner"
; for CD check failed dialog

Then to

004D1CB0 ; likely start of "show dialog"
; invoked for both "are you sure you want to quit?" and
; "CD-ROM was not found"

Figured out ollydbg's "save executable" functionality and was able to save a modified binary in which the "CD missing" dialogue is misaligned and has no button - by modifying one of the arguments to what I believed to be the "show dialog" function.

I am onto the "show dialog" path. My plan is to trace it and see how the decision to show it or not is made.

Moving on to

004D6660 /. 55 PUSH EBP
; start of caller of 004D1CB0

004D66DC |. /75 4F JNE SHORT 004D672D
; jumps over call to "show dialog for CD error"


call HEROES3.004D6660

Investigate in function HEROES3.004D6660:

004D6684 /73 3D JNB SHORT 004D66C3
; effect: none

Investigate in function HEROES3.004D6660:

004D66D5 /74 56 JE SHORT 004D672D
; effect: NO CD dialogue no longer appears!

Found that byte at DS:[0019FCA0+48=0019FCE8] is 1 when CD not detected

004D6298 |. 8847 48 MOV BYTE PTR DS:[EDI+48],AL
; SHOW_CD_DIALOG := 0 or 1
; when New->Back is clicked, arriving on main menu again
; (sets to 1 when it dialog hasn't yet been shown, because we hardcoded
; 0 via ollydbg the first time it COULD have been shown)

; set to 1 if:
; BYTE PTR DS:[5FBE1C] = 0
; DWORD PTR DS:[5FB284] != 0

004CA1FE |. 891D 84B25F00 MOV DWORD PTR DS:[5FB284],EBX
; sets one of the above to 1 soon after startup
; ESI is 0 here, so if I replace EBX with ESI, it will not ask for CD anymore
; BUT! we can't be sure ESI will always be 0 here
; or that we're not missing something else by not modifying
; at a lower level...

DS:[5FBD28] is used in a comparison to EBX (which is hardcoded to 0)

Find out where DS:[5FBD28] is written to.

004C96FA |> \E8 B1AA0100 CALL 004E41B0
; performs CD check, returning result in EAX (0 = pass)
; this is part of a block of many calls to initialization functions
; tested that the value returned by this call differs when CD is inserted

004C96FF |. A3 28BD5F00 MOV DWORD PTR DS:[5FBD28],EAX
; if we modify value of EAX in debugger to 0, game works without CD

While we can perhaps modify to rely on ESI (likely 0 at this point) instead of EAX, I'd prefer digging in deeper and modifying where the CD device is actually checked.

The above call invokes function 004E41B0, which is the CD check function proper. It loops over all logical drives, checking whether each one contains a CD - the Heroes 3 CD.

004E41B0 /$ 55 PUSH EBP
; this is the start of the CD check function

When found, the following short-circuits out of the check loop. This branch is taken when the CD has been found.

004E449C /75 39 JNE SHORT 004E44D7

Changing it to the below will now succeed the CD check on the first logical drive, thus passing the CD check every time.

004E449C /EB 39 JMP SHORT 004E44D7

Upon further analysis, I realized that to get the above modification, at least one optical (CD/DVD) drive must be present.
My computer does have an optical (CD/DVD) drive, but I've disabled it to test this.
Sure enough, my modification doesn't work anymore, so I need a better way.

Final approach

Identified the following as the start of the "check many drives" loop:

004E4358 /8B45 F0 MOV EAX,DWORD PTR SS:[EBP-10]
004E435B |85C0 TEST EAX,EAX

Replaced with the following, to act as if we're successful before we even enter the "check many drives" loop:

004E4358 /E9 7A010000 JMP 004E44D7
; note, this DOES clobber the next (TEST EAX,EAX) instruction,
; but we cannot reach it anyway

As a side note, now I understand why the unmodified game takes so long to fail with a "no CD found" error: the "check many drives" loop sleeps for three seconds after each check:

004E44B0 |> \68 B80B0000 PUSH 0BB8
004E44B5 |. FF15 B0315B00 CALL DWORD PTR DS:[<&KERNEL32.Sleep>]

Tested that all of the game's features are available with or without a CD present.