The cracking of XVision



Solving my own problems.

Browse by Tags

The problems

Visionware's XVision is actually pretty nice. It has a good X server that runs on Windows 3, Windows 9x, and Windows NT. It also provides decent VT320 terminal emulation. All in all, I enjoy using it, and I would like to continue using it--I even bought the disks for XVision 5.10 from eBay.

However, it requires a license. The versions that are floating around on sites like winworldpc have a demo license which allows you to use XVision for 30 minutes at a time. This gets mildly irritating after a while.

What can we do about this? Well, technically XVision is still a product, owned and sold by a company called MKS1, but we're talking about a version of that product that is over 30 years old at this point, for an operating system that was marked as end of life in 2001. I doubt MKS could sell me a license for XVision 5.x even if they wanted to.

XVision started out as a product from a British company called Visionware. Eventually Visionware was obtained by SCO (back when SCO was a good company) and evolved into a product called XVision Eclipse for 32-bit Windows. At this point, SCO changed the way the product is licensed, so it is doubtful that MKS have the means to generate old licenses. This is a common "feature" of older software.

I feel that as I'm making old software usable for historical and exploratory purposes, then ethically there's no real consequence of cracking the license for XVision 5 and 6 for 16-bit Windows. At this stage we're performing digital archaeology rather than piracy.

XVision Eclipse is a different story, however. I won't be touching that. I also don't need to. I obtained a license for XVision Eclipse from SCO a few decades ago.

So with that out of the way, let's have some fun.

The license format

I found an article on the scoscales site that documented the format of the XVision 5.x and 6.x serial number format--which is also how I found out that the license I originally had was a demonstration license. The format is interesting, as it points out that certain letter combinations were special. There must be some sort of check going on for this, right?

For XVision 6.01 and earlier (pre-XVision 7.0 Eclipse releases), XVision requires both a serial number and an activation key for installation. The serial numbers contain 15 digits (numbers), for example, "040006010054321". The "04" indicates that this is a serial number for XVision. The "601" towards the middle of the serial number indicates that this is XVision version 6.01. If there is a capital "D" inserted midway into the serial number, for example, "040006010D54321". The software is a "Demonstration" or "Evaluation" copy which will time out 30 days after installation. If there is a capital "S" inserted midway into the serial number, for example, "040006010S54321", then the license is for a Site license. (See below for more details on Site Licenses.) A serial number must have 15 digits or it will not install. Any letters in the serial number must be entered as upper case. XVision 6.01 and earlier needs both a serial number and activation key for installation.

Activation Keys contain 12 characters, a combination of numbers and upper case letters, for example, "2CD579DFEE5A". There are no "O"s in XVision Activation keys, only "Zeroes". An activation key must have 12 characters, and all letters must be upper case and entered correctly or it will not install. Activation keys and serial number are matched sets. An activation key will not work with the wrong serial number.

In addition to a serial number and activation key, during installation a Site License will also prompt for an accurate entry of the company name, complete with spelling, case and punctuation. For example, if the customer has determined that the Company's name entry for XVision Site License installation is "Computers-R-Us, Inc.", it must be entered exactly as such. Typing in "Computers R Us, Inc" will not work.

The documentation suggests there are "magic" characters in the serial. That implies validation logic somewhere in the codebase.

Time to go hunting.

Reverse engineering

I copied some of the DLLs over to a GNU/Linux system and did some objdump and strings splunking without much success. I didn't find much other than references to functions with names like nl_checkserial. We also have the problem that most tooling on modern systems for reverse engineering expect 32-bit PE binaries, not 16-bit NE binaries, so I decided to dig out the old rec decompiler on a Windows 95 VM. This is when things got interesting.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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) {  /* <- Comparison against 'D' */
            return(3);
          }
          esp = esp + 424;
          return(4);
        }
        if(*(A35c + 9) == 68) {    /* <- Comparison against 'D' */
          esp = esp + 424;
          return(1);
        }
        esp = esp + 424;
        return(0);
      }
    }
  }
}
 

Let's ignore the meat of that function and focus on the comparisons. The variable A35c seems to be coming from the call to L00461330. By itself this doesn't tell us much, but there are calls to GetPrivateProfileString, OpenFile, lseek, lread et al just prior to this which strongly suggests that the code flow in question:

  • Reads XVision's main ini file to get a location,
  • Use that to build a filename (using sprintf and strcat)
  • Opens that file with OpenFile
  • Reads the file's contents

Also those comparisons against D also piqued my interest, so I decided to spend some time looking closer at the function. What does function L00461330 do? Are my assumptions correct? Am I just hallucinating? Are these my feet?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
L00461330(A4, A8)
  /* unknown */ void  A4;
  /* unknown */ 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);
}
 

Wow, would you look at that. XOR encryption against a fixed string with byte swapping obfuscation for added measure so the data in memory isn't instantly recognisable as a XOR'd string. Yes, this is the exact code we need to tear apart.

We can clearly see that the key is "Visionware PC-Connect 1990." There is also a larger string used for validating site licenses, but we are not going to be doing anything with that here.

Our first step is to re-write the rec output into readable C.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// Attempt to decode a serialised blob into a serial.
//
// Returns:
//   0  =  Successful decode.
//   1  =  Byte 6 XOR'd value is invalid.
//   2  =  Byte 13 XOR'd value is invalid.
//   3  =  Byte 20 XOR'd value is invalid.
int
serial_decode(char *serial, char *output)
{
  char   *str = NULL;
  int     idx = 0;
  uint16  dl  = 0;

  if ((str = (char *)malloc(sizeof(char) * 32)) == NULL) {
    perror("serial_decode");
    exit(1);
  }
  memset(str, 0, 32);

  // Check the byte 20 XOR value.
  do {
    dl = dl ^ *(serial + idx);
    idx++;
  } while (idx <= 19);
  if (*(serial + 20) != (char)dl) {
    return 3;
  }

  // Check the byte 13 XOR value.
  idx = 0;
  dl  = 0;
  do {
    dl = dl ^ *(serial + idx);
    idx++;
  } while(idx <= 12);
  if ((char)*(serial + 13) != (char)dl) {
    return 2;
  }

  // Check the byte 6 XOR value.
  idx = 0;
  dl  = 0;
  do {
    dl = dl ^ *(serial + idx);
    idx++;
  } while(idx <= 5);
  if (*(serial + 6) != dl) {
    return 1;
  }

  // XOR the serial against the magic key.
  idx = 0;
  do {
    char tmp = (char)*(str + idx) ^ (char)*(s_magic + idx);
    *(str + idx) = tmp;
    idx++;
  } while (idx <= 19);

  // Swap bytes.
  *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 in how it works. There are some added obfuscation steps involving comparing the values of bytes 6, 13, and 20 against the results of XOR'ing the preceding bytes to provide a canary, but all in all it's pretty simple. We can simply reverse the polarity of the neutron flow and build the a value from an arbitrary serial.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// Generate a serialised serial number from the source serial in STR.
void
serial_generate(const char *str, char *output)
{
  int    idx = 0;
  uint16 dl  = 0;
  char   tmp = 0;

  // Start by swapping bytes.
  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);

  // XOR with the magic key.
  idx = 0;
  do {
    tmp = (char)*(output + idx) ^ (char)*(s_magic + idx);
    *(output + idx) = tmp;
    idx++;
  } while (idx <= 19);

  // Generate XOR value for byte 6.
  idx = 0;
  dl  = 0;
  do {
    dl = dl ^ output[idx];
    idx++;
  } while(idx <= 5);
  output[6] = dl;

  // Generate XOR value for byte 13.
  idx = 0;
  dl  = 0;
  do {
    dl = dl ^ output[idx];
    idx++;
  } while(idx <= 12);
  output[13] = dl;

  // Generate XOR value for byte 20.
  idx = 0;
  dl  = 0;
  do {
    dl = dl ^ output[idx];
    idx++;
  } while (idx <= 19);
  output[20] = dl;
}
 

This is simply the same steps as the decoder, just in reverse. Looking closely at the code, we need to ensure we spend time to insert the correct canary values at bytes 6, 13, and 20 or the result won't be valid. This should be enough to get something working. Let's test things.

Testing some presumptuous presumptions

After a short amount of time writing some C to write out a serial.no file, I decided to test the results with XVision.

First I wrote some code that took the following steps:

  1. Decode the demo serial.no contents,
  2. Encode the resulting value, and finally
  3. Decode that value.

If the final decoded value matches the original, both encoder and decoder are correct.

$ ./test
=========================================
= DECODING SERIALISED LICENSE.
= 'f-Y]W'
first: '040005100D21819'
=========================================
= ENCODING NEW SERIALISED LICENSE.
= '040005100D21819'
final: '0D'
=========================================
= DECODING SERIALISED LICENSE.
= 'f-sY]WkQBT `rusZVZeR'
second: '040005100D21819'
$
 

So far so good. Now let's modify the code so that I can encode arbitrary values into serial.no

$ ./test
=========================================
= ENCODING NEW SERIALISED LICENSE.
= '999999999999999'
final: '99'
$
 

This felt suspiciously easy. That's usually a bad sign.

Testing the serial file with XVision on Windows 3
Figure:Testing the serial file with XVision on Windows 3

Mischief managed.

But wait, there's more

I decided to fire up Borland C++ and write a tool that could do all this for me but with a neat GUI. Also I've not used ObjectWindows for a long, long time.

License generator written in Borland C++
Figure:License generator written in Borland C++

The result is a 16-bit app that can be ran on any version of Windows that supports 16-bit binaries. It also generates serials that look authentic.

Generating a license
Figure:Generating a license

The only caveat here is that one needs to install XVision first.

The future

The remaining logic appears to live inside what looks like a 16-bit virtual machine. Untangling that would take significantly more effort, so I've left it for another day.

I have done what I wanted to do: turn XVision into something usable that doesn't quit on me every 30 minutes. This will allow me to explore XVision deeper.

Some interesting notes

Firstly, the 04000 prefix is meaningless. I wonder if they're reusing another licensing schema where the 04 is a product id and the other bytes are number of users.

The product version is also meaningless. The demo license I used was for XVision 5.10, not 5.0 or 5.6. It also worked on 6.01.

It would be interesting to see if this stuff works on other Visionware products, such as PC-Connect. Chances are it will.

Maybe one day I'll locate a copy of PC-Connect.


 
Return to top
 
Created: