So, I copied some executables and DLLs over to a GNU/Linux machine and did some objdump
and strings
spelunking. I didn't really find much, other than reference to functions with names like nl_checkserial
. So I decided to copy the same files to my Windows 95 VM and run them past the rec
decompiler.
Then, after loading them up in an editor, I decided to look for both the decimal and hexadecimal literals for the character 'D
'. This got me some interesting results.
if(L00461330( & A258, A35c) >= 0) {
eax = A35c + 2;
ecx = 4962860;
if(*eax == *L004BBA2C) {
if(*(eax + 2) == *L004BBA2E) {
ax = *A35c;
if(ax == *L004BBA28) {
goto L00461551;
}
if(ax == *L004BBA24) {
goto L00461551;
}
if(ax == *L004BBA20) {
goto L00461551;
}
if(ax == *L004BBA1C) {
L00461551:
if(*A35c == *L004BBA1C) {
if(*(A35c + 9) == 68) {
return(3);
}
esp = esp + 424;
return(4);
}
if(*(A35c + 9) == 68) {
esp = esp + 424;
return(1);
}
esp = esp + 424;
return(0);
}
}
}
}
Ignoring the meat of that function and focusing in on the comparisons, the variable A35c
seems to be coming from the call to L00461330
. Given that earlier on, there's calls to GetPrivateProfileStringA
, OpenFileA
, lseek
, lread
et al, I figured that this could potentially be something that:
- Reads XVision's main ini to get program location,
- Use that to build a filename using a mix of
sprintf
and strcat
,
- Open that file with
OpenFileA
- Read its contents,
- etc.
So, this looks like it could be interesting to spend some time looking at. What does that mysterious function L00461330
do? Are my assumptions correct? Am I just making things up as I go along?
L00461330(A4, A8)
void A4;
void A8;
{
eax = 0;
dl = 0;
do {
dl = dl ^ *(A4 + eax);
eax = eax + 1;
} while(eax <= 19);
if(*(A4 + 20) != dl) {
return(-3);
}
eax = 0;
dl = 0;
do {
dl = dl ^ *(A4 + eax);
eax = eax + 1;
} while(eax <= 12);
if(*(A4 + 13) != dl) {
return(-2);
}
eax = 0;
dl = 0;
do {
dl = dl ^ *(A4 + eax);
eax = eax + 1;
} while(eax <= 5);
if(*(A4 + 6) != dl) {
return(-1);
}
eax = 0;
do {
*(A4 + eax) = *(A4 + eax) ^ *(eax + "Visionware PC-Connect 1990");
eax = eax + 1;
} while(eax <= 19);
*A8 = *(A4 + 3);
*(A8 + 1) = *(A4 + 17);
*(A8 + 2) = *(A4 + 14);
*(A8 + 3) = *(A4 + 8);
*(A8 + 4) = *A4;
*(A8 + 5) = *(A4 + 15);
*(A8 + 6) = *(A4 + 19);
*(A8 + 7) = *(A4 + 11);
*(A8 + 8) = *(A4 + 7);
*(A8 + 9) = *(A4 + 1);
*(A8 + 10) = *(A4 + 4);
*(A8 + 11) = *(A4 + 12);
*(A8 + 12) = *(A4 + 16);
*(A8 + 13) = *(A4 + 9);
*(A8 + 14) = *(A4 + 5);
*(A8 + 15) = 0;
return(0);
}
Woah woah woah woah, is that XOR encryption? Is it doing some byte swapping for added obfuscation? Yes, and yes. This is the exact code for taking serialised data from, say, c:\xvision\serial.no
and reconstructing it back to the serial number entered during initial installation. This is probably the code that gets used when you go to About and it prints out your serial number so that you feel good about spending a few hundred bucks on your software. You did do that, right? :)
There is also a similar function that uses a longer key, presumably that would be used for site licenses. We're going to ignore that for now, though.
Ok, so let's take that code and write something in C that can take the binary data and reconstruct the original serial.
int
serial_decode(char* serial, char* output)
{
char* str = NULL;
int idx = 0;
uint16 dl = 0;
if ((str = (char *)malloc(sizeof(char) * 32)) == NULL) {
MessageBox(NULL,
"Could not allocate memory!",
"Fatal Error",
MB_OK | MB_ICONEXCLAMATION);
exit(1);
}
memset(str, 0, 32);
do {
dl = dl ^ *(serial + idx);
idx++;
} while (idx <= 19);
if (*(serial + 20) != (char)dl) {
return 3;
}
idx = 0;
dl = 0;
do {
dl = dl ^ *(serial + idx);
idx++;
} while(idx <= 12);
if ((char)*(serial + 13) != (char)dl) {
return 2;
}
idx = 0;
dl = 0;
do {
dl = dl ^ *(serial + idx);
idx++;
} while(idx <= 5);
if (*(serial + 6) != dl) {
return 1;
}
idx = 0;
do {
char tmp = (char)*(str + idx) ^ (char)*(s_magic + idx);
*(str + idx) = tmp;
idx++;
} while (idx <= 19);
*output = *(str + 3);
*(output + 1) = *(str + 17);
*(output + 2) = *(str + 14);
*(output + 3) = *(str + 8);
*(output + 4) = *str;
*(output + 5) = *(str + 15);
*(output + 6) = *(str + 19);
*(output + 7) = *(str + 11);
*(output + 8) = *(str + 7);
*(output + 9) = *(str + 1);
*(output + 10) = *(str + 4);
*(output + 11) = *(str + 12);
*(output + 12) = *(str + 16);
*(output + 13) = *(str + 9);
*(output + 14) = *(str + 5);
*(output + 15) = 0;
return 0;
}
It's pretty simple how it works. There are some added obfuscation steps, like comparing the values of bytes 6, 13, and 20 against the results of XOR'ing the preceding bytes, but all in all it's pretty simple. We can just reverse the polarity of the neutron flow and build the serialised blob from an arbitrary serial number, right?
void
serial_generate(const char* str, char* output)
{
int idx = 0;
uint16 dl = 0;
char tmp = 0;
output[3] = *str;
output[17] = *(str + 1);
output[14] = *(str + 2);
output[8] = *(str + 3);
output[0] = *(str + 4);
output[15] = *(str + 5);
output[19] = *(str + 6);
output[11] = *(str + 7);
output[7] = *(str + 8);
output[1] = *(str + 9);
output[4] = *(str + 10);
output[12] = *(str + 11);
output[16] = *(str + 12);
output[9] = *(str + 13);
output[5] = *(str + 14);
idx = 0;
do {
tmp = (char)*(output + idx) ^ (char)*(s_magic + idx);
*(output + idx) = tmp;
idx++;
} while (idx <= 19);
idx = 0;
dl = 0;
do {
dl = dl ^ output[idx];
idx++;
} while(idx <= 5);
output[6] = dl;
idx = 0;
dl = 0;
do {
dl = dl ^ output[idx];
idx++;
} while(idx <= 12);
output[13] = dl;
idx = 0;
dl = 0;
do {
dl = dl ^ output[idx];
idx++;
} while (idx <= 19);
output[20] = dl;
}
Yes, of course we can. This is simply the same steps as the decoder, just in reverse. Ensuring that we spend time to insert the correct values at bytes 6, 13, and 20. This doesn't result in exact same values when converting from serialised data to serial number to serialised data, but the canary bytes work, so the that should be enough for XVision, right?