SecuROM

Context

Wikipedia Article.

  • CD/DVD copy protection
  • Digital rights management (DRM)
  • Part of Sony DADC
  • Lots of controversies including Tron: Evolution :(

TL;DR: SecuROM was supposed to be a copy-protection for software. However the idea and the implementation was flawed as it made it harder or even impossible (!!) to use the protected software, resulting in users to download a protection-free version of their game through torrents.

Launcher

Before GridGameLauncher.exe starts GridGame.exe in a sub-process it will check for the game's activation. Since this check depends on your hardware id a single change in your hardware components will affect your activation. As it turns out you can plug in any external storage device such as an HDD and the game will not be able to launch after activation!! This has to be one of the most brain-dead ideas in history of software engineering. Thanks again for nothing Sony DADC!

When the game process starts it does the following:

  • Copies 1732 bytes from the memory mapped file -=[SMS_GridGame.exe_SMS]=- into a buffer
    • Bytes at offset 4-7 represent the launcher handle which is used to trigger "spot checks" through SendMessageW
  • Checks if the game was launched from GridGameLauncher.exe
  • Does a CRC of GridGameLauncher.exe
    • Access registry path HKEY_LOCAL_MACHINE\\Software\\Disney Interactive Studios\\tr2npc
      • Gets registry key InstallPath
      • Gets registry key Language
    • Checks if the file EN/patch.dat does not exist
      • Skips the rest if it does exist lol
    • Opens GridGameLauncher.exe
      • Reads 0x6D1558 bytes (the whole file) into a buffer
      • XORs 4 bytes at a time with 0xAE19EDA3 from (0x6D1558 - 40) / 4 to 0x6D1558
      • Checks if hash matches with 0x6A85B570 for EN language
        • RU = 0xD91791D4
        • CZ and PL = 0xBCD55594
      • Triggers a "spot check" if the hash does not match

This basically means we cannot simply modify the game binary as it does a CRC self-check, unless you create a patch.dat file which seems to be a backdoor implemented by the game devs. However, even if we want to modify the file statically we will not be able to progress without GFWL's signature check. The game is not meant to be playable without GFWL since the engine would just shutdown and crash because of a null pointer dereference somewhere deep inside the online subsystem code which requires GFWL to be initialized.

The launcher can simply be replaced by modifying multiple code locations to get it working without GFWL or by simply writing your own one:

// NOTE: Make sure that the launcher process has the same name
//       as the original one "GridGameLauncher.exe"

#define GAME_EXE L"GridGame.exe"
#define SECUROM_BUFFER_SIZE 1723
#define TR2NPC_PATH L"Software\\Disney Interactive Studios\\tr2npc"

auto main() -> int
{
    // I wonder what SMS means. SecuROM's Mapping Signature?
    auto file = std::format(L"-=[SMS_{}_SMS]=-", GAME_EXE);

    auto handle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, SECUROM_BUFFER_SIZE, file.c_str());
    if (!handle) {
        println("Could not create file mapping object {}", GetLastError());
        return 1;
    }

    auto buffer = MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, SECUROM_BUFFER_SIZE);
    if (!buffer) {
        println("Could not map view of file {}", GetLastError());
        CloseHandle(handle);
        return 1;
    }

    println("Created file mapping");

    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    WCHAR install_path[MAX_PATH] = {};
    auto install_path_size = DWORD(sizeof(install_path));

    auto result = RegGetValueW(
        HKEY_LOCAL_MACHINE, TR2NPC_PATH, L"InstallPath", RRF_RT_ANY, nullptr, &install_path, &install_path_size);

    if (result != ERROR_SUCCESS) {
        println("Failed to find installation path");
        return 1;
    }

    auto cmd = std::format(L"{}\\{}", install_path, GAME_EXE);
    CreateProcessW(NULL, (LPWSTR)cmd.c_str(), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

    wprintln(L"Launched {}", cmd);

    println("hProcess = {:x}", uintptr_t(pi.hProcess));
    println("hThread = {:x}", uintptr_t(pi.hThread));
    println("dwProcessId = {:x}", uintptr_t(pi.dwProcessId));
    println("dwThreadId = {:x}", uintptr_t(pi.dwThreadId));

    WaitForSingleObject(pi.hProcess, INFINITE);

    println("exited");

    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    UnmapViewOfFile(LPCVOID(buffer));
    CloseHandle(handle);

    return 0;
}

Todo

Figure out how messages are handled.

Layout of copied buffer at launch:

OffsetLength in bytesDescription
0x00044Launcher handle for spot checks.
0x00442Spot check message buffer.

Data File Encryption (DFE)

The launcher creates 20 encrypted files in AppData\Local\Temp\GridGameLauncher_Data_DFE.

Example filename: data_dfe_b93b5ee4f6834d365af674dbed527de5.

Other multiple file are located at installation path folder:

Todo

Check what SecuROM does with these files. They could just be useless garbage as they also seem to appear in different games with the exact same size.

FilenameSize in bytes
Layer06.Arch0118955
aliasdiffanhl3.bin884
const_ovr.bin22596
coord_abstr5.hal16643
cyc_nan.bik11772
dest_fix2.bin7423
dfe300460
dith_l3.bin9417
engined2.bin14905
f10.kea11649
jay_mix.bin25009
krn_snd.bin14368
lght_jmp_mod.hal25369
patpon.pat22272
srh50.bin30604
starter.bin24894
story_opl.dia4884
vorbis_sub.sub21992
w1n_000.nfs1961
wrk_sdl.bin929

Spot Check

Used to trigger checks by communicating with the SecuROM launcher by sending signals through SendMessageW. Each check does some calculations which might call some kernel32 functions. This only seems to be done once for each type if the check succeeded. A global map keeps track of the type check by key. The game might behave differently if one check fails or gets skipped because the results will be checked in different functions.

// This is a simplified version.
// All names are obviously made up.
// Also worth to notice is that the checks are implemented by the game devs.
// SecuROM most likely only provides the obfuscated part which verifies the end result.

enum class SpotChecks {
	OnlineSystem = 0,
	Bindings = 1,
	NoXp = 2,
	// ...
}

enum class SpotCheck {
	Ok = 0,
	Invalid = 1 ,
}

struct SpotCheckResult {
	SpotCheck check;
}

static TMap<SpotChecks, SpotCheckResult> global_map;

auto result = SpotCheckResult();
auto index = global_map.find_by_key(SpotChecks::NoXp, &result);

if (index == INVALID_INDEX) {
	// Trigger a check.
	// Might also be called multiple times,
	// passing down the result of the former call.
	SendMessageW(securom_hwnd, securom_buffer, 2u, 0);

	// Do some calculations.
	// Calls to kernel32 functions are actually obfuscated.
	auto data = GetClassNameA();
	auto tick_count_result = data.split('_').at(3);

	// Verify result.
	auto new_result = SpotCheckResult();

	if (is_valid_tick_count(tick_count_result)) {
		new_result.check = SpotCheck::Ok;
	} else {
		new_result.check = SpotCheck::Invalid;
	}

	global_map.insert(SpotChecks::NoXp, new_result);
}

// Somewhere else...

auto result = SpotCheckResult();
auto index = global_map.find_by_key(SpotChecks::NoXp, &result);

if (index != INVALID_INDEX && result.check == SpotCheck::Invalid) {
	// Execute code to troll the player. Thanks :>
}
KeySpot LocationTroll Code
0Online system functionWeird version change from 1.01 to 1.01.1
1PgPlayerInput / deadzone binding functionBuffers and delays all inputs
2UPgOnline::Init / checked in lobbyRemoves weapon from player
3PgPlayerController / triggered when spawningDisables targeting enemies
4Save load managerMesses with the save file by zero-ing the save data buffer which causes a crash when loading the save1
5Validates save game manager
Checks result in UPgOnlineGameManager::SetNextMap
Cannot go past 3rd map
6Pause Menu
Verifies game signature hash
Disables unpause when the game pauses
7PgGameInfo::CreateTeamsXP counter will not go up
8Main Menu???
9Result of manual check if CRC fails at launchGame will not launch
10UnusedUnused

1 The crash is super weird. Not sure if intended. TODO: Might want to investigate.

Kernel32 function GetClassNameA has been observed multiple times in spot checks. It provides a string with four different parts.

Example: GridGameLauncher.exe_7910_4E63862_FBC446.

PartDescription
GridGameLauncher.exeLauncher name
7910
4E63862GetTickCount
FBC446

Strings

Lots of useless string obfuscation for calling kernel32 functions.

Often encrypted with Rot13.

Compression

Uses zlib deflate.

Production Servers

  • pa01.sonyvfactory.com:443/SecuROM_pa_web/activation
  • pa02.sonyvfactory.com:443/SecuROM_pa_web/activation
  • pa02.sonyvfactory.com:80/SecuROM_pa_web/activation
  • pa01.sonyvfactory.com:443/SecuROM_pa_web/activation
  • pa02.sonyvfactory.com:443/SecuROM_pa_web/activation
  • pa03.sonyvfactory.com:443/SecuROM_pa_web/activation

UI

Module paul.dll is used for activation.

It's made with MFC and ATL.

Activation Error Codes

Bug

Somebody at Sony DADC could not spell occurred lol.

CodeDescription
0an internal error occured
2API command not implemented
3API command is invalid
4out of memory
5a buffer is too small
6a buffer is too large
7unlock code has invalid format
8unlock code has invalid CPA
9unlock code is invalid
10unlock code is not available
11unlock code is expired
12unlock code was revoked
13API call is not allowed
14userdata request failed
15wrong version, API update needed
16timestamp expired
17activation failed
18a SSL error occured
19a connection error occured
20unlock convert error
21a XML error occured
22error during sending HTTP request
23error during receiving HTTP response
24USER has canceled transaction
25evaluation of the unlock code failed
26verification of unlock code failed
27userdata commit failed
28unlock code is invalid but within grace period
29unlock code is invalid and grace period ended
30invalid parameter passed to api
31unlock code is expired but within grace period
32unlock code is blacklisted (already used)
33unlock code is empty (not set)
34server error - global verify error
35server error - purchase error
36server error - error during verification
37server error - activation check error
38server error - error during creating activation
39server error - can not update statistics
40server error - registration required
41server error - license expired
42server error - project (CPA) not active
43server error - purchase not found
44server error - too many activations within timeframe
45server error - too many total activations
46server error - wrong/invalid serial
47server error - application not found
48grace period is undefined
49expiry of unlock code is undefined
50logfile does not exist, log something first
51unlock code is valid
52system time is corrupt
53server error - internal error
54server error - start date not reached
55server error - too many activations on same PC
56server error - too many activation on different PC's
57server error - unknown server error
58evaluation of unlockcode failed (hw changed?)
59server error - serial revoked too often
60server error - serial revoked too often within timeframe
61server error - license end date reached
62server error - invalid geographical region
otherunknown error

XLive (GFWL)

Call to XLiveSetSponsorToken (xlive_5026) with:

  • Token = product code (most likely)
  • ID = 0x425607F3 | 1112934387

Hardware ID (HWID)

Consists of five parts which will be hashed in each step with MD5 and XOR operations.

Layout

struct hwid_t {
	byte unk0;
	byte version_hash;
	WORD cpu_hash;
	byte gpu_hash;
	byte unk1;
	byte network_hash;
	WORD unk2;
	byte unk3;
	WORD disk_hash;
	WORD unk4;
	byte terminator;
};

// As string representation
auto hwid_to_string(hwid_t hwid) -> std::string {
	return std::format("{:02X}", hwid.unk0)
		+ std::format("{:02X}", hwid.version_hash)
		+ std::format("{:04X}", _byteswap_ushort(hwid.cpu_hash))
		+ std::format("{:02X}", hwid.gpu_hash)
		+ std::format("{:02X}", hwid.unk1)
		+ std::format("{:02X}", hwid.network_hash)
		+ std::format("{:04X}", hwid.unk2)
		+ std::format("{:02X}", hwid.unk3)
		+ std::format("{:04X}", _byteswap_ushort(hwid.disk_hash))
		+ std::format("{:04X}", hwid.unk4);
}

OS Version

Struct: OSVERSIONINFO

MD5(Nl)MD5(Nh)MD5(num)MD5(data[0])MD5(data[1])MD5(data[2])MD5(data[3])
128016dwProcessorTypedwMajorVersiondwBuildNumberdwPlatformId

CPU Info

Struct: SYSTEM_INFO

MD5(Nl)MD5(Nh)MD5(num)MD5(data[0])MD5(data[1])MD5(data[2])MD5(data[3])
128016dwProcessorTypedwAllocationGranularitywProcessorLevelwProcessorRevision

GPU Info

Struct: D3DADAPTER_IDENTIFIER9

MD5(Nl)MD5(Nh)MD5(num)MD5(data[0])MD5(data[1])MD5(data[2])MD5(data[3])
128016VendorIdDeviceIdSubSysIdRevision

Network Info

Struct: PIP_ADAPTER_INFO

Todo

Verify if this is correct: Skip hashing when adapter type is ethernet (MIB_IF_TYPE_ETHERNET).

MD5(Nl)MD5(Nh)MD5(num)MD5(data[0])
4806Address

Volume Info

MD5(Nl)MD5(Nh)MD5(num)MD5(data[0])
12804_byteswap_ulong(lpVolumeSerialNumber)

XOR Operation

auto xor_op(byte* dest, byte* src, DWORD size) -> void {
	for (auto i = 1ul; i <= sizeof(MD5_LONG) * 4ul; ++i) {
		for (auto j = 0ul; j < size; ++j) {
			*(dest + j) ^= *(src++);
		}
	}
}

// Each part will xor its calculated MD5 hash
xor_op(&hwid.version_hash, (byte*)&data[0], sizeof(hwid_t::version_hash));

DES Encryption

SecuROM uses DES Cipher Feedback Encryption for the product activation process. The OpenSSL version that is used for this is from 2005.

cfb_image
auto encrypt_with_des(
	const_DES_cblock* cblock,
	unsigned char* input,
	unsigned char* output,
	size_t length) -> void
{
	DES_key_schedule key_schedule = {};
	DES_cblock result =  {};

	DES_set_key_unchecked(cblock, &key_schedule);

	DES_cfb_encrypt(
		input,
		output,
		8,
		length,
		&key_schedule,
		&result,
		DES_ENCRYPT
	);
}

Unlock Code

After a fresh game installation, the product has to be activated once through a activation process which roughly works like this:

  • SecuROM generates unlock request code which contains the users's HWID and serial and sends this to the activation servers
  • Server sends back the generated unlock code
  • SecuROM unpacks the binary and saves the result in the registry

SecuROM provides a manual activation in case the automatic one fails. This means that the user has to enter the unlock request code + serial manually on SecuROM's web page to request the unlock code. When SecuROM decides to shutdown their service, like in the case for Tron: Evolution, users would not be able to progress further. The product simply cannot be activated, or can it?

Fortunately, the unlock code can be generated locally without the need of SecuROM's activation servers in the following manner:

Note

The complete code can be found in the unlocker folder as this is only a summary.

Generate the user's hardware ID, see chapter HWID.

// Example: "010BE04D5A009B00000037210000"
auto hwid = generate_user_hwid();

Calculate the RSA plaintext:

\( c = m^e \mod n \)

VariableDescription
cPlaintext
mHWID
e0xB4109B85B0CAFBD73EDDAB05A9881
n0x1CF9DFF37F133D15D21CC4F5ADE91F
// NOTE: All Numbers are "huge" integers. A magic library should handle the maths :)
auto m = huge_integer_from_hex(hwid);
auto e = huge_integer_from_hex("B4109B85B0CAFBD73EDDAB05A9881");
auto n = huge_integer_from_hex("1CF9DFF37F133D15D21CC4F5ADE91F");
auto c = mod_exp(m, e, n);

Calculate plaintext length. The total length should be 30 but the number can be lower than that. Simply fill the rest with 'f' characters. Then append the original length at the end of the buffer.

auto plaintext = huge_integer_to_hex_string(c);

const auto max_plaintext_length = 30;

auto plaintext_length = plaintext.length();
auto offset = max_plaintext_length - plaintext_length;

if ((offset & 0x80000000) != 0) {
	println("[-] invalid plaintext length :(");
	return false;
}

if (max_plaintext_length != plaintext_length) {
	do {
		plaintext += "f";
		--offset;
	} while (offset);
}

plaintext += std::format("{:02x}", plaintext_length);

Convert hex string back to a byte buffer and XOR everything with the game's appid signature.

// Game signature aka appid (48 bytes)
// Found in spot check 6
unsigned char appid[] = {
	// 1st part (16 bytes)
	0xF9, 0x83, 0x7A, 0x1D, 0x22, 0x2F, 0x64, 0x74,
	0x28, 0xCB, 0x13, 0x30, 0x32, 0xD0, 0xD0, 0x0C,
	// 2nd part (32 bytes)
	0xE8, 0x96, 0xD4, 0xE1, 0xBD, 0xFC, 0x0E, 0x37,
	0x8C, 0x8D, 0x17, 0x74, 0x27, 0x27, 0xBC, 0xE0,
	0xE8, 0x96, 0xD4, 0xE1, 0xBD, 0xFC, 0x0E, 0x37,
	0xE8, 0x96, 0xD4, 0xE1, 0xBD, 0xFC, 0x0E, 0x37,
};

BYTE data_buffer[56] = { 0x00, 0x01, 0xC7, 0x22 };
auto data_buffer_ptr = data_buffer + 4;

convert_hex_to_bytes(data_buffer_ptr + 5, (BYTE*)plaintext.c_str(), 32);

for (auto i = 0; i < 16; ++i) {
	*(data_buffer_ptr + 5 + i) ^= *(appid + i);
}

Calculate MD5 of game's appid and XOR it with the buffer.

MD5_CTX ctx = {};
MD5_Init(&ctx);
MD5_Update(&ctx, appid, sizeof(appid));
BYTE data[32] = {};
MD5_Final(data, &ctx);

xor_data(data, 16, data_buffer_ptr, 2u);

Encrypt the game's signature with lots of XOR operations and DES CBF.

BYTE cblock[19] = {};
for (auto i = 0; i < 16; ++i) {
	*(cblock + i) = *(appid + i) ^ *(appid + i + (16 * 1)) ^ *(appid + i + (16 * 2));
}

memcpy(cblock + 16, (BYTE*)plaintext.c_str(), 3);

BYTE des_buffer[21] = {};
memcpy(des_buffer, data_buffer_ptr + 2, sizeof(des_buffer));

*(cblock + 0) ^= *(cblock + 8);
*(cblock + 1) ^= *(cblock + 9);
*(cblock + 2) ^= *(cblock + 10);
*(cblock + 3) ^= *(cblock + 11);
*(cblock + 4) ^= *(cblock + 12);
*(cblock + 5) ^= *(cblock + 13);
*(cblock + 6) ^= *(cblock + 14);
*(cblock + 7) ^= *(cblock + 15);
*(cblock + 8) ^= *(cblock + 16);

encrypt_with_des(cblock, data_buffer_ptr + 2, des_buffer, sizeof(des_buffer) - 2);

Now comes the longest process: S-box DES encryption times 221. This will take a few seconds depending on the CPU's speed lol.

auto rounds = 0x20000;

BYTE output_buffer[64] = { *(data_buffer_ptr + 0), *(data_buffer_ptr + 1) };
memcpy(output_buffer + 2, des_buffer, 19);

do {
	for (auto i = 0; i < 8; ++i) {
		des_encrypt_with_sbox(output_buffer, data_buffer_ptr, 21);
		++des_calls;
		des_encrypt_with_sbox(data_buffer_ptr, output_buffer, 21);
		++des_calls;
	}
	--rounds;
} while (rounds);

Calculate CRC and XOR it with the buffer.

auto crc = get_crc(data_buffer + 1, 24);
xor_data((BYTE*)&crc, 4, data_buffer, 1);

Final DES CFB encryption with random seed.

BYTE cblock_seed[17] = {};
get_random_hash(cblock_seed, sizeof(cblock_seed) - 1);

memcpy(cblock_seed + 16, cblock, 1);

*(cblock_seed + 0) ^= *(cblock_seed + 8);
*(cblock_seed + 1) ^= *(cblock_seed + 9);
*(cblock_seed + 2) ^= *(cblock_seed + 10);
*(cblock_seed + 3) ^= *(cblock_seed + 11);
*(cblock_seed + 4) ^= *(cblock_seed + 12);
*(cblock_seed + 5) ^= *(cblock_seed + 13);
*(cblock_seed + 6) ^= *(cblock_seed + 14);
*(cblock_seed + 7) ^= *(cblock_seed + 15);
*(cblock_seed + 8) ^= *(cblock_seed + 16);

BYTE unlock_code_des_buffer[32] = {};
encrypt_with_des(cblock_seed, data_buffer, unlock_code_des_buffer, 25);

BYTE unlock_code_hex_buffer[40] = {};
decode_des_buffer(unlock_code_des_buffer, 25, unlock_code_hex_buffer, sizeof(unlock_code_hex_buffer));

Encode rest of the buffer to ASCII and insert hyphens etc.

decode_to_ascii(unlock_code_hex_buffer, output_buffer, sizeof(unlock_code_hex_buffer));

char unlock_code_buffer[64] = {};
insert_hyphens((char*)output_buffer, unlock_code_buffer, 40, 5);

Credits to 80_PA.