Barubary
04-19-2001, 08:43 AM
I cracked the VMU file encryption system, at least for downloadable quests. The save file encryption system should be nearly identical to this code.
To run this, get the VMS file of a quest. Normally, quests have 2 files, script data and text/language-specific data. The Easter quest has a 3rd which merely contains that advertising logo bitmap. Since that's not encrypted, don't try to decrypt it. It's the 99 file so it's easily recognized.
Using a hex editor, extract from offset 0x280 in the VMS to the end into a file named vmu.bin in the directory of the compiled EXE.
Run the EXE, and you'll see a new file, outdata.bin. This is the decrypted version.
Below is the source code. It should work on any x86-based 32 bit environment. I certainly hope it's portable, but I kind of doubt it.
-- Barubary
#include <stdio.h>
#include <string.h>
typedef unsigned long DWORD;
typedef unsigned char BYTE;
inline DWORD read_DWORD(BYTE *input)
{
return ((DWORD) input[0]) | (((DWORD) input[1]) << 8) |
(((DWORD) input[2]) << 16) | (((DWORD) input[3]) << 24);
}
// This greatly simplifies the structure of
struct compress_input
{
const BYTE *ptr;
BYTE currbyte;
int currbits;
compress_input(const BYTE *input)
{
ptr = input;
currbits = 0;
}
inline BYTE getbit(void)
{
BYTE retval;
if (!currbits)
{
currbyte = *ptr++;
currbits = 8;
}
currbits--;
retval = currbyte & 1;
currbyte >>= 1;
return retval;
}
inline BYTE getbyte(void)
{
return *ptr++;
}
};
// Forceful forward copy; this is required because forward copying is
// expected when overlapping occurs.
void memfwdcpy(BYTE *dest, BYTE *src, DWORD count)
{
for (; count; count--)
*dest++ = *src++;
}
// The original function was a nightmare, so I simply figured out the
// algorithm and rewrote it.
DWORD pso_decompress(const BYTE *input, BYTE *output)
{
compress_input x = input;
BYTE *oldoutput = output;
int offset;
DWORD temp, count;
for (;;)
{
if (x.getbit())
{
*output++ = x.getbyte();
}
else
{
if (x.getbit())
{
temp = x.getbyte();
offset = x.getbyte() << 8;
offset |= temp;
if (!offset)
return output - oldoutput;
count = offset & 7;
if (count)
count += 2;
else
count = x.getbyte() + 1;
offset >>= 3;
memfwdcpy(output, &output[(int) offset - 8192], count);
output += count;
}
else
{
count = x.getbit() << 1;
count |= x.getbit();
count += 2;
offset = x.getbyte();
memfwdcpy(output, &output[(int) offset - 256], count);
output += count;
}
}
}
}
void build_table_sub(DWORD *stack)
{
DWORD *r5, *r6, *r7, r14;
r7 = &stack[1];
r5 = &stack[32];
for (r14 = 1; r14 < 25; r14++)
r7[r14] -= r5[r14];
// The -0x5C is a bit weird, but whatever...
r6 = &stack[1];
stack = &stack[-23];
// r14 is used as the counter, not r7
for (r14 = 25; r14 < 56; r14++)
r6[r14] -= stack[r14];
}
// Decrypt/encrypt quest data
void pso_decrypt(DWORD *stack, DWORD *input, DWORD size)
{
// r11 is stack; r12 is input; r13 is size
DWORD r14;
// There is a bug in the original that isn't fixed: it writes to the
// last DWORD whether it uses it entirely or not.
size = (size + 3) / sizeof(DWORD);
for (r14 = 0; r14 < size; r14++)
{
// Prefix ++ is the simplest way to represent this
if (++(stack[0]) > 55)
{
build_table_sub(stack);
stack[0] = 1;
}
input[r14] ^= stack[stack[0] + 1];
}
}
void build_decrypt_table(DWORD *stack, DWORD unknown)
{
// r11 is stack and r12 is unknown
DWORD r0, r13, r14;
r13 = r14 = 1;
stack[57] = unknown;
stack[56] = unknown;
// for() loop
for (; r13 < 55; r13++)
{
r0 = (r13 * 21) % 55;
stack[r0 + 1] = r14;
unknown -= r14;
r14 = unknown;
unknown = stack[r0 + 1];
}
// Repeated 4 times
build_table_sub(stack);
build_table_sub(stack);
build_table_sub(stack);
build_table_sub(stack);
stack[0] = 55;
}
// Source disassembly not included because it's so obvious
DWORD *init_decrypt_table(DWORD *stack)
{
build_decrypt_table(stack, 0);
return stack;
}
// Decompresses/decrypts a VMU quest
DWORD decrypt_vmu_quest(BYTE *input, DWORD insize, BYTE *output, DWORD
outsize)
{
// Stack storage (not including saved registers)
DWORD stack[58];
// Registers
// r11 is input, r12 is size, r13 is output
DWORD r10, r14;
// Why is this not ever used by the decryptor???
stack[1] = 0;
// Checks if enough space was allocated (?)
r14 = read_DWORD(input);
if (r14 > outsize)
return 0;
// This is in a delay slot, but that doesn't affect it, since the branch
// not taken case doesn't need it.
r10 = read_DWORD(&input[4]);
// Set the table to its defaults
init_decrypt_table(stack);
// Build the rest of the decryption table
build_decrypt_table(stack, r10);
// Decrypt the file
pso_decrypt(stack, (DWORD *) &input[8], insize - 8);
// Decompress the file
pso_decompress(&input[8], output);
return r14;
}
int main(void)
{
FILE *input, *output;
BYTE *indata = new BYTE[0x20000];
BYTE *outdata = new BYTE[0x20000];
long length;
input = fopen("vmu.bin", "rb");
output = fopen("outdata.bin", "wb");
fseek(input, 0, SEEK_END);
length = ftell(input);
fseek(input, 0, SEEK_SET);
fread(indata, length, 1, input);
length = decrypt_vmu_quest(indata, length, outdata, 0x20000);
fwrite(outdata, length, 1, output);
return 0;
}
To run this, get the VMS file of a quest. Normally, quests have 2 files, script data and text/language-specific data. The Easter quest has a 3rd which merely contains that advertising logo bitmap. Since that's not encrypted, don't try to decrypt it. It's the 99 file so it's easily recognized.
Using a hex editor, extract from offset 0x280 in the VMS to the end into a file named vmu.bin in the directory of the compiled EXE.
Run the EXE, and you'll see a new file, outdata.bin. This is the decrypted version.
Below is the source code. It should work on any x86-based 32 bit environment. I certainly hope it's portable, but I kind of doubt it.
-- Barubary
#include <stdio.h>
#include <string.h>
typedef unsigned long DWORD;
typedef unsigned char BYTE;
inline DWORD read_DWORD(BYTE *input)
{
return ((DWORD) input[0]) | (((DWORD) input[1]) << 8) |
(((DWORD) input[2]) << 16) | (((DWORD) input[3]) << 24);
}
// This greatly simplifies the structure of
struct compress_input
{
const BYTE *ptr;
BYTE currbyte;
int currbits;
compress_input(const BYTE *input)
{
ptr = input;
currbits = 0;
}
inline BYTE getbit(void)
{
BYTE retval;
if (!currbits)
{
currbyte = *ptr++;
currbits = 8;
}
currbits--;
retval = currbyte & 1;
currbyte >>= 1;
return retval;
}
inline BYTE getbyte(void)
{
return *ptr++;
}
};
// Forceful forward copy; this is required because forward copying is
// expected when overlapping occurs.
void memfwdcpy(BYTE *dest, BYTE *src, DWORD count)
{
for (; count; count--)
*dest++ = *src++;
}
// The original function was a nightmare, so I simply figured out the
// algorithm and rewrote it.
DWORD pso_decompress(const BYTE *input, BYTE *output)
{
compress_input x = input;
BYTE *oldoutput = output;
int offset;
DWORD temp, count;
for (;;)
{
if (x.getbit())
{
*output++ = x.getbyte();
}
else
{
if (x.getbit())
{
temp = x.getbyte();
offset = x.getbyte() << 8;
offset |= temp;
if (!offset)
return output - oldoutput;
count = offset & 7;
if (count)
count += 2;
else
count = x.getbyte() + 1;
offset >>= 3;
memfwdcpy(output, &output[(int) offset - 8192], count);
output += count;
}
else
{
count = x.getbit() << 1;
count |= x.getbit();
count += 2;
offset = x.getbyte();
memfwdcpy(output, &output[(int) offset - 256], count);
output += count;
}
}
}
}
void build_table_sub(DWORD *stack)
{
DWORD *r5, *r6, *r7, r14;
r7 = &stack[1];
r5 = &stack[32];
for (r14 = 1; r14 < 25; r14++)
r7[r14] -= r5[r14];
// The -0x5C is a bit weird, but whatever...
r6 = &stack[1];
stack = &stack[-23];
// r14 is used as the counter, not r7
for (r14 = 25; r14 < 56; r14++)
r6[r14] -= stack[r14];
}
// Decrypt/encrypt quest data
void pso_decrypt(DWORD *stack, DWORD *input, DWORD size)
{
// r11 is stack; r12 is input; r13 is size
DWORD r14;
// There is a bug in the original that isn't fixed: it writes to the
// last DWORD whether it uses it entirely or not.
size = (size + 3) / sizeof(DWORD);
for (r14 = 0; r14 < size; r14++)
{
// Prefix ++ is the simplest way to represent this
if (++(stack[0]) > 55)
{
build_table_sub(stack);
stack[0] = 1;
}
input[r14] ^= stack[stack[0] + 1];
}
}
void build_decrypt_table(DWORD *stack, DWORD unknown)
{
// r11 is stack and r12 is unknown
DWORD r0, r13, r14;
r13 = r14 = 1;
stack[57] = unknown;
stack[56] = unknown;
// for() loop
for (; r13 < 55; r13++)
{
r0 = (r13 * 21) % 55;
stack[r0 + 1] = r14;
unknown -= r14;
r14 = unknown;
unknown = stack[r0 + 1];
}
// Repeated 4 times
build_table_sub(stack);
build_table_sub(stack);
build_table_sub(stack);
build_table_sub(stack);
stack[0] = 55;
}
// Source disassembly not included because it's so obvious
DWORD *init_decrypt_table(DWORD *stack)
{
build_decrypt_table(stack, 0);
return stack;
}
// Decompresses/decrypts a VMU quest
DWORD decrypt_vmu_quest(BYTE *input, DWORD insize, BYTE *output, DWORD
outsize)
{
// Stack storage (not including saved registers)
DWORD stack[58];
// Registers
// r11 is input, r12 is size, r13 is output
DWORD r10, r14;
// Why is this not ever used by the decryptor???
stack[1] = 0;
// Checks if enough space was allocated (?)
r14 = read_DWORD(input);
if (r14 > outsize)
return 0;
// This is in a delay slot, but that doesn't affect it, since the branch
// not taken case doesn't need it.
r10 = read_DWORD(&input[4]);
// Set the table to its defaults
init_decrypt_table(stack);
// Build the rest of the decryption table
build_decrypt_table(stack, r10);
// Decrypt the file
pso_decrypt(stack, (DWORD *) &input[8], insize - 8);
// Decompress the file
pso_decompress(&input[8], output);
return r14;
}
int main(void)
{
FILE *input, *output;
BYTE *indata = new BYTE[0x20000];
BYTE *outdata = new BYTE[0x20000];
long length;
input = fopen("vmu.bin", "rb");
output = fopen("outdata.bin", "wb");
fseek(input, 0, SEEK_END);
length = ftell(input);
fseek(input, 0, SEEK_SET);
fread(indata, length, 1, input);
length = decrypt_vmu_quest(indata, length, outdata, 0x20000);
fwrite(outdata, length, 1, output);
return 0;
}