krystalgamer - Blog

Reversing: Spiderman 2000 - Breaking CD-ROM protection

Prologue

Spiderman was acting wonky with DxWnd after a long time working just fine (it happened to me before but it magically got solved, I think it might be Win10 related since the Win7 VM works perfectly). Not knowing the causes I decided to delete the game and reinstall it. Here comes the fuck up, I had my idb in the game folder. I wasn't able to recover it.

Now I had an uncracked game.

Diving into the problem

Starting the game I was prompted with the message Please insert the Spider-Man CD-ROM, select OK, and restart the game.. Not bad, time to xref on IDA.

xref of the string

What the..? Why there's two of them? The WinMain has you can already guess is the one that prompts when I start the game. The other is from the window handler function.

graph window handler

As you can see IDA could understand that all of those locs are part of a jumptable which indicates me it's switch case and almost all of them lead to DefWindowProcA. If you have done any WinAPI programming you'd know that you call that when you don't want any special handling of certain window interactions and let the OS do it for you.

But which kind of message would make it prompt that message? I left it for now.

Disabling first CD-ROM protection

graph winmain

Whatever the function is doing it must return 0. My approach to this problem was to simply NOP the function call and turn the jz to a jmp. It worked! The game started, I pressed New Game and... black screen, I start to see my desktop and game closes. What the hell was that?

Couple more tries later I find it actually is showing a message box, somewhere in the code it was sending the message code to show that box.

Disabling the second CD-ROM protection

Not knowing what was sending the message I decided to xref the function that I nop'd earlier(let's call it CheckCD) and it was indeed called somewhere else on the program.

int sub_515D80()
{
	int result; // eax
	signed int v1; // ecx
	signed int v2; // edx
	BOOL v3; // eax
	struct tagMSG Msg; // [esp+0h] [ebp-1Ch]

	result = CheckCD(0);// <-- checks for CD here
	v1 = 0;

	//Useless loop to clog the assembly
	do
	{
		v2 = v1;
		v1 += 2;
	}
	while ( v1 < 30 );

	//if-of-death
	if ( (_BYTE)result )
	{
		PostMessageA(hWnd, 0x10u, 0, 0);
		byte_2E098E8 = 1;
		while ( PeekMessageA(&Msg, hWnd, 0, 0, 0) )
		{
			v3 = GetMessageA(&Msg, hWnd, 0, 0);
			if ( !v3 || v3 == -1 )
				break;
			if ( Msg.message != 260 && Msg.message != 261 )
			{
				TranslateMessage(&Msg);
				DispatchMessageA(&Msg);
			}
		}
		exit(1);
	}

	if ( v2 != 28 )//Prevents tampering with the loop
		exit(1);
	return result;
}

Now we're looking at IDA pseudo, it really helps to understand what's going on. Although it might be unreliable some times it also helps a lot.

Let's discuss what's going on from easier to harder. The loop is used to fill up the assembly view with a bunch of useless instructions so the reverser has an harder time to know what's going on). If someone accidentally deletes messes the loop while patching some instructions v2 won't be 28 so it'll leave.

In my opinion it's kinda clever, back then there was no pseudo-code generation so looking at x86 assembly was the only option which would have made this a lot harder.

CheckCD again, must return 0 or else it will go on the if statement. The first thing about the if statement is the PostMessageA which sends the message 0x10 to the handler, which is WM_CLOSE. Then it processes the messages to force the closure, if it fails then it calls exit. They really want to make sure the game was closed.

By the way, the 260 and 261 are WM_SYSKEY(UP/DOWN) messages, the game is filtering them so the game wouldn't hang if someone as smashing the keyboard.

CheckCD exploration

cdrom check

As you can see there's a call to sub_516250 which does something with texture.dat. What does it do? No idea, it opens it, gets its size, reads the contents and performs some wonky stuff with it. If any of those tasks fails then it returns 0 which makes CheckCD return 2, else it will return a position of the texture.dat buffer.

cdrom check

There's some string copy and finally sub_516470 is called and the result negated and AND'd with 3, which is not a problem since the negation of zero is still zero and 3 AND 0 is still 0.

To end this I'll post some pseudo to make it less boring.

signed int __cdecl sub_516470(_DWORD *a1)
{

	if ( !a1 )
		return 0x124;
	v2 = 0;

	for ( i = FindWindowA(ClassName, 0); i; i = FindWindowA(ClassName, 0) )
	{
		if ( v2 >= 100 )
		break;
		SendMessageA(i, 0x10u, 0, 0);
		++v2;
	}
	v19 = 0;
	while ( 1 )
	{
		RootPathName = v19 + 'A';
		v32 = 58;
		v33 = 92;
		v34 = 0;
		if ( GetDriveTypeA(&RootPathName) == 5 )
		{
			//Lots of mciSendCommandA
			//breaks somewhere
		}
		v14 = __OFSUB__(v19 + 1, 26);
		v13 = v19++ - 25 < 0;
		if ( !(v13 ^ v14) )
			return 351;
	}
	return 0;
}

A lot has been cleared and altered so it's easier to understand. First it searches for ClassName windows which are SJE_CdPlayerClass. I'm not sure what they are but I think it's the one from the CD-ROM. You can start the game from there so I guess it makes sense to close them if someone goes into a level. Also it just closes until the 100 window.

After that it searches for CD-ROM drives starting on A. If does find the drive then the thing read from texture.dat is compared with something from the disc, I'm not really sure since I didn't care too much about mci commands, my best bet is that it's checking if the thing read from texture.dat is the same, if it does it breaks and returns 0! Else it returns 351. As before with some bytepatching you can ignore the if statement and always return 0.

Those final weird lines are just IDA Pro """"failed"""" decompilation, I put a lot of quotes since it's correct but the decompiler wasn't able to understand the logic in way to simplify it.

v14 = __OFSUB__(v19 + 1, 26);
v13 = v19++ - 25 < 0;
if ( !(v13 ^ v14) )
	return 351;

Comes from here

.text:005166F1                 mov     eax, [VARIABLE]
.text:005166F5                 inc     eax
.text:005166F6                 cmp     eax, 1Ah; 26
.text:005166F9                 mov     [VARIABLE], eax
.text:005166FD                 jl      loc_5164D1

Concluding, the loop instead of while(1) should be while(v19 < 26) because otherwise it would go beyond the letter Z, the last letter of the alphabet. And that's why pseudocode should always be taken as a grain of salt.