Code: Select all
http://echoofsoul.aeriagames.com/
Protected files can be seen having a header like this:
The first four bytes are used as a signature to determine if the file is protected or not. Inside of the game, we can see a check for this like this:
Code: Select all
v2 = (const CHAR *)sub_E2DF80(lpFileName);
v3 = CreateFileA(v2, 0x80000000, 1u, 0, 3u, 0x80u, 0);
if ( v3 == (HANDLE)-1
|| (NumberOfBytesRead = 0,
v9 = 0xD0B7A0CC,
ReadFile(v3, Buffer, 4u, &NumberOfBytesRead, 0),
CloseHandle(v3),
NumberOfBytesRead != 4) )
{
LABEL_8:
result = 0;
}
else
{
for ( i = 0; ; ++i )
{
v7 = i;
if ( i >= 4 )
break;
if ( Buffer[i] != *((_BYTE *)&v9 + i) )
goto LABEL_8;
}
result = 1;
}
return result;
Here the file is loaded and the first 4 bytes are read. Afterward, the code checks byte by byte against the value of v9 (or 0xD0B7A0CC). If it matches its considered protected; otherwise it is not.
Next the game makes use of the Crypto library functions provided by Microsoft. The game creates an MD5 hash object by using the following:
Code: Select all
if ( !CryptAcquireContextW((HCRYPTPROV *)(a2 + 1140), L"SBENCRYPTIONKEYCONTAINER10", L"Microsoft Enhanced Cryptographic Provider v1.0", 1u, 0) && GetLastError() == -2146893802 )
CryptAcquireContextW((HCRYPTPROV *)(a2 + 1140), L"SBENCRYPTIONKEYCONTAINER10", 0, 1u, 8u);
if ( !*(_DWORD *)(a2 + 1140) )
{
v6 = GetLastError();
sub_E4B130(L"CryptAcquireContext failed. (%d)(0x%08x)", v6);
}
v7 = (HCRYPTHASH *)(a2 + 1148);
if ( CryptCreateHash(*(_DWORD *)(a2 + 1140), 0x8003u, 0, 0, (HCRYPTHASH *)(a2 + 1148))
&& CryptHashData(*v7, &pbData, 8u, 0)
&& CryptDeriveKey(*(_DWORD *)(a2 + 1140), 0x6801u, *v7, (DWORD)&loc_800000, (HCRYPTKEY *)(a2 + 1144)) )
v11 = 0;
else
LOBYTE(v11) = 0;
Here the game is creating a hash provider context to use an MD5 crypto object. The pbData is added to the has object as a key for the encryption / decryption which in this case is:
Once initialized, the game makes use of the crypto provider with its encryption and decryption by the following two functions:
Code: Select all
int __usercall sub_409810@<eax>(HCRYPTKEY hKey@<ecx>, int a2@<esi>)
{
signed int v2; // ecx@1
int v3; // eax@2
int result; // eax@3
int v5; // [sp+0h] [bp-4h]@1
v5 = 8;
CryptDecrypt(hKey, 0, 1, 0, (BYTE *)a2, (DWORD *)&v5);
v2 = 0;
do
{
LOBYTE(v3) = *(_BYTE *)(v2 + a2);
if ( (_BYTE)v3 == 127 )
{
result = 0;
}
else if ( (_BYTE)v3 == -128 )
{
result = 255;
}
else
{
v3 = (unsigned __int8)v3;
if ( (unsigned __int8)v3 >= 0x80u )
result = v3 - 1;
else
result = v3 + 1;
}
*(_BYTE *)(v2++ + a2) = result;
}
while ( v2 < 8 );
*(_DWORD *)a2 ^= 0xA4A7FF88;
*(_DWORD *)(a2 + 4) ^= 0xA0447823;
return result;
}
BOOL __usercall sub_4097B0@<eax>(int a1@<edx>, DWORD a2@<ecx>, HCRYPTKEY hKey)
{
signed int v3; // ecx@1
unsigned __int8 v4; // al@2
char v5; // al@3
DWORD pdwDataLen; // [sp+0h] [bp-4h]@1
pdwDataLen = a2;
*(_DWORD *)a1 ^= 0xA4A7FF88;
*(_DWORD *)(a1 + 4) ^= 0xA0447823;
v3 = 0;
do
{
v4 = *(_BYTE *)(v3 + a1);
if ( v4 )
{
if ( v4 == -1 )
{
v5 = -128;
}
else if ( v4 >= 0x80u )
{
v5 = v4 + 1;
}
else
{
v5 = v4 - 1;
}
}
else
{
v5 = 127;
}
*(_BYTE *)(v3++ + a1) = v5;
}
while ( v3 < 8 );
pdwDataLen = 8;
return CryptEncrypt(hKey, 0, 1, 0, (BYTE *)a1, &pdwDataLen, 8u);
}
Not going to go into much detail on these, but we see that the data is processed in 8 byte chunks. Those 8 bytes are broken into 4 byte parts and xor'd with the keys: 0xA4A7FF88 and 0xA0447823
Some minor adjustments are made based on the byte data and the crypto provider is called to encrypt or decrypt the data.
I created two tools to deal with both of these functions.
eosdec - A tool to decrypt Echo of Souls files.
eosenc - A tool to encrypt Echo of Souls files.
A simple test to validate the encryption is being handled properly is to:
eosdec the EoS.ini file. Then to eosenc the resulting decrypted file.
The new encrypted file will match the original EoS.ini perfectly.
You can check out this project (and possibly other future tools for this game) here:
https://gitlab.com/atom0s/EoSTools