Vellum Solids 99 & WIBU-KEY - Tutorial by CrackZ

In this tutorial I'm looking at the WIBU-KEY dongle from German supplier WIBU. The target program Vellum Solids 99 has already been cracked (rather badly I might add) and distributed several months ago to any loser who cares to search the web, so this essay will not damage the aforementioned authors. What we'll look at here are specific characteristics of the WIBU implementation including a little API awareness, maybe a brief foray into the encryption. I must thank the kind individual (you know who you are) who passed me the information upon which this document is based, find for yourselves the file wkapi.hlp or download it from the link at the end of this tutorial.

The main program file VellumSolids.exe is an awe inspiring 18.7Mb's, as it turns out most of our interesting functions can be found using IDA (you won't need the disassembly to be completed), like DESKey all of the functions are called into using the WIBU supplied dll (wkwin32.dll), this isn't encrypted and exports a range of clearly defined functions (as it turns out most of these are obselete). Using IDA we can easily locate the WIBU import names.

WkbOpen2, WkbUnselect2, WkbCrypt2, WkbClose2, WkbSelect2, WkbRelease2, WkbAccess2, WkbGetLastError, WkbIsComplete.

I couldn't find in the API guide any indication as to a specific calling order of these functions, I just assumed the WIBU dongle had to be initialised before some functions (like WkbCrypt2) could be used. However launching Vellum presents us with an initial dilemma, a serial # / Authorization Code dialog, fortunately this is unrelated to the WIBU dongle (the checks come after). The scheme itself seems pretty easy to crack (changing the JZ at 005A31DD to JMP looks like an obvious answer) but I did some brief analysis work anyhow.

At this point I'll confess to having some problems, I started analysing the scheme and preparing the text I would write here when I encountered a section of code that just didn't work how I was expecting, I'll just show you a very brief snippet :-

:005CB9D3 PUSH ECX <-- 00's ?.
:005CB9D4 CALL sub_13499E0 <-- Generate EAX.
:005CB9D9 MOV ECX, EAX <-- Store EAX (result from function) in ECX.
:005CB9DE TEST ECX, ECX <-- ECX=0?.
:005CB9E0 JNZ 005CB9E6 <-- Must jump to continue checking routines.
:005CB9E2 XOR ESI, ESI <-- Clear ESI and its all over.

sub_13499E0 takes as its last parameter a pointer to a string (in the previous 4 instances I'd seen it used this was the Authorization Code or Serial Number), however on this final pass, the structure is just an array of 00 bytes and therefore because of the table hooking mechanism being used EAX was always being returned as 0 (see further on for what this function really is!). I just couldn't for the life of me see how this could be correct. As you might expect, like all problems there is and was a very obvious solution. My mistake was that I'd seen code which appeared at first sight to filter OUT 2Dh bytes when in fact it is the presence of 2Dh bytes in a result generated from the Authorization Code which ultimately results in bytes being copied to this mysterious area.

There would seem to be 2 distinct phases, the first sub_5CC010 takes individual bytes from the Authorization code, appearing to reject any bytes not between 30-39h & 41-5Ah, (numbers and letters), hence the reason for my assumption above. Each byte has -32 added too it before hooking a table of bytes, the table entry will then be used in a switch procedure which jumps relative to various routines which perform some basic arithmetic (the loop counter is used here too), the resulting byte will be retained, at this stage there is no indication of a length check so the result is the same length as the Authorization code although we have an idea from above what it should contain. Lets pick out the key sections :-

:005CC02C MOVSX EAX, AL <-- Extend sign AL through EAX.
:005CC02F ADD EAX, -32h
:005CC032 CMP EAX, 28h
:005CC035 JA 005CC1A9 <-- Jump if we had '1'.
:005CC03B XOR ECX, ECX <-- Clear ECX.
:005CC03D MOV CL, BYTE PTR [EAX+5CC228] <-- Get table value.
:005CC043 JMP [5CC1B8+ECX*4] <-- Jump to routine.

The value of the table entry determines where the JMP resumes, but lets examine how the compiler fixes this up, look as an example at the structure of all the JMP locations :-

MOV CL, [ESP+4] <-- Loop position.
MOV DL, 30h
MOV AL, xx <-- Here.
SUB DL, CL
ADD AL, DL

The value I've marked as xx is the only variable in all 27 instances. Lets look at the table :-

00 01 02 03 04 05 06 07 1B 1B 1B 1B 1B 1B 1B 1B
1B 1B 1B 1B 1B
08 09 0A 0B 0C 0D 0E 0F 1B 10 11
12 13 14 15 16 17 18 19 1A

The 1B table value is used when our input range is 3A-46h and also when we use 4Fh or 'O', we could assume that these are bad, an Authorization Code would be unlikely to contain colon's and semi-colon's (who knows though), a '1' is filtered out by the JA hence the table index 00 corresponds to 32h. To actually replicate this scheme I decided to construct a table of my own which would contain the values used in the xx routine above, this is as follows :-

19 14 0F 12 0D 09 04 00 22 22 22 22 22 22 22 22
22 22 22 22 22 17 18 1A 13 15 16 0B 11 22 0C 0E
08 0A 10 05 02 07 01 06 03

Now all we do is hook this new table directly, I've attached the code I wrote in a zip at the end of this document (note the complete lack of any error checking, check out the parser program I wrote on the Coding page too). Merely having this however doesn't solve our problem, as sub_13499E0 needs to be completely reversed as well. Lets break the second part down into manageable snippets :-

:005CB97B CALL 005CBFC0 <-- Generate Authorization Code result.
:005CB99E PUSH EAX <-- Serial Number (will be overwritten).
:005CB99F PUSH ECX <-- Authorization Code result.
:005CB9A0 CALL 005CC260 <-- Important.

sub_005CC260 takes 2 parameters, the address of the Serial Number is overwritten, it loops through the Authorization Code result looking for 2Dh bytes, when 1 is found the following bytes of the Authorization Code result are copied to a new location (this is actually the 00's area I discussed at the start), the bytes are copied to this location until a 3rd 2Dh byte is found or the loop ends. At the end of this function we have 2 results, the bytes before the 2Dh and the bytes after, this is important.

sub_13499E0 is really just an ASCII string to number converter and takes 1 parameter, the pointer to the string to convert, it returns in EAX the hex value, so string '11111' becomes 0x2B67. There are 3 call's to it after sub_5CC260, each takes a different parameter :-

:005CB9AF PUSH EDX <-- String result (1st part of Authorization Code).
:005CB9B0 CALL sub_13499E0
:005CB9B8 MOV EDI, EAX <-- Save result in EDI.
.....

Further analysis reveals that the Serial Number is also 'parsed' for 2Dh bytes (just before the Authorization Code in fact) and checks we'll look at shortly confirm that the 2nd part of the Serial Number is 6 digits long. Using all this information, we have now a hint of the format of each of our required inputs :-

aaaaaa-bbbbbb (Serial Number).
CCCCC-DDDDD-EEEEE-FF (Result from the Authorization Code).

Editing the memory manually (the Auth. Code result) ensures we at least have non-zero values for the next phase of checking :-

:005CB9E6 MOV EAX, ESI <-- EAX=DDDDD (see key above).
:005CB9E8 CDQ <-- Clears EDX.
:005CB9E9 AND EDX, 0FFh <-- Low byte only.
:005CB9EF ADD EAX, EDX <-- Compiler inefficient?.
:005CB9F1 MOV ESI, EAX <-- ESI=DDDDD.
:005CB9F3 SAR ESI, 8h <-- / 256.
:005CB9F6 IMUL ESI, ECX <-- Multiply by EEEEE.
:005CB9F9 SUB ESI, EDI <-- Sub from CCCCC.
:005CB9FB PUSH EBP <-- bbbbbb.
:005CB9FC CALL 005CC310 <-- Check 2nd part of Serial Number.
:005CBA18 CMP ESI, EBP <-- Check result.

The 2nd part of the Serial Number is checked against quite a long list of hard-coded defaults, I won't list them (the returned value in EAX is what we are interested in). As the big picture with this entire function seems to be the return value of EAX I opted for the easy way out, we can see from the code above that the final value of ESI is obtained by dividing the 2nd part of the Authorization Code by 256, multiplying by the 3rd component and finally substracting the 1st, this suggests the FF was not a correct assumption as its never used, the real format is more likely CCCCCC-DDDDDD-EEEEEE.

Our task therefore is to work out a set of values that would work and reverse out the Authorization Code that would generate them. We'll have sub_5CC310 return EAX=4 by fixing the 2nd part of the Serial Number to 186A4 or 100004. One such set of values that would work are as follows (you can literally think up your own) :-

2nd part of Auth. Code = 612,352 (0x95800).
Divide by 256 = 2,392 (0x958).
Multiply by 50 = 119,600 (0x1D330).
Subtract 19,596 (0x4C8C) = 100,004 as required.
Required Authorization Code (from my generator) = XSWPS-PRSP46-5Q

Of course Vellum did not make this so easy, EAX=4 actually corresponds to an expired beta copy, we therefore can't avoid trying all the possible return values of EAX from sub_5CC310, my nagging fear was always that I would end up having to reverse the 2nd part 5CBA2A - 5CBBBB, lets see though.

EAX = 1 --> Invalid.
EAX = 3 --> Vellum Solids 99 (searches for the dongle).
EAX = 4, 8, 0Ch --> Beta/expired.
EAX = 5 --> Vellum Solids 99.
EAX = 6, 0Ah or 0Eh --> do second phase.
EAX = 7 --> Vellum Solids 99 AeroPack (searches for the dongle).
EAX = 9 --> Vellum Solids 99 AeroPack.
EAX = 0Bh, 0Dh --> Aero-CADD.

As we are here for reverse engineering we won't take what looks like a very easy way out (EAX=5), but I'll give those that have seen enough a valid combination (Serial Number :- 000000-700000, Auth. Code. XRRZ8-PRSP46-TQ4). We'll quickly generate EAX=3 by fixing the last 6 digits of the Serial Number to 700700, then doing the easy maths I show above we can feed 16900-612352-300 to my generator, for clarity just note :-

300 (612,352 / 256) - 16,900 = 700,700 --> Authorization Code --> XWMZ8-PRSP46-TQ4.

Now Vellum Solids 99 looks for the dongle and we can have our first look at WIBU. Load the wkwin32.dll exports into SoftICE and set some likely breakpoints (look at the names above), your first break will be WkbAccess2() at 005CBEC5 :-

:005CBEC2 PUSH EAX <-- pvCtrl.
:005CBEC3 PUSH 3 <-- flCtrl.
:005CBEC5 CALL WkbAccess

The operation of WkbAccess is actually pretty simple, EAX holds the return value which is a handle to the WIBU-BOX sub-system, obviously this can't be null. pvCtrl is actually a structure of WKBACCESS type :-

typedef struct {
ULONG flCtrl;
LONG lFirmCode;
LONG lUserCode;
ULONG flStdCtrl;
BYTE bVersionLow;
BYTE bVersionHigh;
.....some omissions
} WKBACCESS;

00 00 00 00 8A 02 00 00 59 14 AC 00 00 00 02 00
20 01 <-- From SoftICE.

IDA gives us just one reference for WkbAccess2(), patching the code is left to you. Tracing back the CALL in IDA shows us the next CALL will be to WkbOpen2() at 005CB609 :-

:005CB5F6 MOV ECX, DWORD PTR [15F7AEC] <-- 0xAC1459.
:005CB5FC PUSH 0
:005CB5FE PUSH ECX <-- lUserCode.
:005CB5FF PUSH 28A <-- lFirmCode.
:005CB604 PUSH 0 <-- pszPort.
:005CB606 PUSH 1 <-- flCtrl.
:005CB608 PUSH EAX <-- Handle to WIBU (hwkbsys).
:005CB609 CALL WkbOpen2()

Once again all WkbOpen2() does is return a handle to the WIBU-BOX entry in EAX or null if one isn't connected, the values of lUserCode & lFirmCode could well be of interest as they are probably used in the encryption functions. Basking in IDA next up is a CALL to WkbIsComplete() at 005CBF3D, once again this is a trivial EAX=1 type of patch and we move swiftly on to WkbSelect2() where things get more interesting :-

:005CB6B5 PUSH 0 <-- pvCtrl.
:005CB6B7 PUSH EAX <-- ulSelectCode.
:005CB6BD PUSH 0 <-- flCtrl (0x0000 - direct encryption/decryption via the WIBU-BOX).
:005CB6BF PUSH EAX <-- Handle to WIBU (hwkbsys).
:005CB6C1 CALL WkbSelect2()

Once again WIBU's well structured API is its main weakness, a status return always in EAX but maybe the forthcoming CALL to WkbCrypt2() can rescue it.

:005CB73D PUSH ECX <-- 0xA cbSrc.
:005CB748 PUSH EDX <-- pvCtrl '0123456789'.
:005CB749 PUSH EAX <-- pvDest.
:005CB74A PUSH 01 <-- flCtrl.
:005CB74C PUSH ECX <-- hwkbe.
:005CB755 CALL WkbCrypt2()

You try your best to help these developers and then they go and use encryption strings like 0123456789 and to top that off they don't even verify the return data, only the status, I mean come on!. The final CALL's are to WkbQueryEntry2() & WkbQuerySystem2(), address 005CB7C9, it might of course just be a co-incidence that our dongle serial # is 2 parts and currently 0-0 and these 2 functions are called twice. Lets look at the code anyhow :-

:005CB7C6 PUSH EAX <-- pvDest (Destination buffer).
:005CB7C7 PUSH EDX <-- flCtrl 0 (Flag).
:005CB7C8 PUSH ECX <-- hwkbe.
:005CB7C9 CALL WkbQueryEntry2()

In fact WkbQueryEntry2() only seems to check the status and WkbQuerySystem2() does not need patching, it merely populates a buffer which contains other unimportant system related information. Here is the relevant dongle ID code :-

:005CB898 MOV AX, WORD PTR [ESP+104+var_80] <-- High word of dongle ID.
:005CB8A0 MOV ECX, DWORD PTR [ESP+104+var_84] <-- Low dword of dongle ID.

These are converted from HEX to DEC so patch in something prominent.

In conclusion then, we can see that there is really nothing to fear from the WIBU key, its functions are all provided in 1 very convenient dll and not encrypted, the query algorithms would only pose a problem if portions of the code segment were decrypted using responses from those algorithms. It sounds simple I guess, but if your next dongle target isn't encrypted i.e. you can get a deadlisting, then in all likelihood you can reverse it.

Just how many times must I demonstrate to developers that using a dongle will NEVER prevent the illegal use of your software. The Serial Number / Authorization Code system used by Vellum is stronger than the dongle protection (although obfuscated by the way the compiler chose to handle what is really a pretty simple scheme). We didn't get any chance to examine WIBU's query, the developer didn't use it, in fact using the string '0123456789' one wonders whether he could really be bothered.

WIBU API Guide


Dongles Return to Main Index


© 1998, 1999, 2000 CrackZ. 26th February 2000.