DESKey DK2 API Guide
v8.x / v9.x Analysis
A few days back I undertook a very serious analysis of this target which is protected by a DESKey DK2 dongle. Prior to this, several crackers had told me the program was 'uncrackable', or 'impracticle to break'. Ordinarily when I'm told something can't be cracked I move on, my list of projects needs shortening. However, somewhat foolishly I'd remembered the appalling DigiSHOW.vld as being a DESKey, it couldn't be that hard surely :-). As it turns out, this target isn't impossible to break its just very difficult, however what surprised me was how far I could break down the interface between the application and the dongle.
Reading through the installation instructions it seems we'd really like to have a 'Dealer' or yellow dongle present, all other install types require some sort of password file which sounds like more work. We install the DESKey drivers and install VISI, at the end of the installation we get a series of "Dongle not present" message boxes and a few "Registration failed!" for good measure. None of these are catastrophic, just hit Cancel/OK as required.
The main program in VISI is cad3d.exe, run it and you'll get our familiar message box and an option to continue in DEMO mode, this results in being unable to save, the menu item is grayed. At 5.23Mb's we won't disassemble cad3d.exe we'll just do a PEDUMP and search the import table for DESKey names, its not hard to find our culprit, dk2win32.dll. 5 functions are imported (all of which are used, but only 4 of which we will be concerned with) :-
FindDK2() - is dongle present.
DK2WriteMemory() - write to dongle memory.
DK2ReadMemory() - read from dongle memory.
DK2DriverInstalled() - hopefully you installed the drivers.
DK2ReadRandomNumbers() - generates 'random numbers' from the dongle.
Loading the exports into SoftICE is the next step, the bpx of course goes on FindDK2(), lets take out the API guide :-
:006FCEFF PUSH 008D6244 <-- Pkey (0x79058954).
:006FCF04 PUSH 008D6240 <-- Id (0x4863).
:006FCF09 CALL DK2win32!FindDK2
:006FCF0E AND EAX,0000FFFF <-- Low word only.
:006FCF13 MOV [00925CA0],EAX <-- Store parallel port (for future API's).
:006FCF18 NEG EAX
:006FCF1A SBB EAX,EAX
:006FCF1C AND AL,F9
:006FCF1E ADD EAX,07 <-- Status in EAX (7=No dongle).
Tracing inside FindDK2() we uncover a familiar pattern of events, some of these are analogous between dongle manufacturers. Initially when I reversed VISI I was using Windows 98 and I didn't pay much attention to the first few lines of FindDK2() which check a dword value and make according jumps, in fact these are Windows version related and control how the dongle is accessed via the DeviceIOControl interface. I didn't verify this but I'm assuming (as HASP/Sentinel is much the same) that this flag is determined using something like GetVersion(). The only effect of this is that we have to make dual patches to each of our API interfaces for 9x & NT users. Here's our first patch (CALL 10003545 under 9x) :-
:10002458 XOR EAX,EAX <-- Clear EAX.
:1000245A INC EAX <-- EAX=1.
:1000245B MOV [EBP-04],EAX <-- Store it where required.
:1000245E JMP 200024FE <-- Function End.
:10002463 NOP <-- Aesthetic.
Now we always have a DESKey DK2 connected. Next up is DK2ReadMemory(), like Sentinel we will emulate the dongle read function, lets examine the application interface and emulation routine :-
:006FD6AC PUSH EAX <-- ByteCount.
:006FD6AD MOVZX AX,BYTE PTR [008D6249] <-- 0xDF.
:006FD6B5 PUSH ECX <-- Buffer.
:006FD6B6 MOV CX,[00925CA0] <-- Parallel port.
:006FD6BD PUSH EDX <-- Address.
:006FD6BE PUSH EAX <-- Seed.
:006FD6BF PUSH 008D6240 <-- Id.
:006FD6C4 PUSH ECX <-- DataReg.
:006FD6C5 CALL DK2win32!DK2ReadMemory
Emulation (CALL 100036B4 under 9x, with minor adjustments) :-
:10002693 PUSH ECX
:10002694 PUSH ESI
:10002695 PUSH EDI <-- Save some registers (as we have the space).
:10002696 PUSH EBP
:10002697 CALL 2000269C
:1000269C POP EBP <-- Set up delta offset.
:1000269D MOV EAX,[ESP+0000009C] <-- Address.
:100026A4 MOV ECX,[ESP+000000A4] <-- ByteCount.
:100026AB LEA ESI,[EBP+29] <-- Simulated dongle memory.
:100026AE ADD ESI,EAX <-- Locate Address.
:100026B0 MOV EDI,[ESP+000000A0] <-- Buffer.
:100026B7 REPZ MOVSB <-- Place in buffer.
:100026B9 XOR EAX,EAX
:100026BB INC EAX <-- Set EAX=1 for no good reason.
:100026BC POP EBP
:100026BD POP EDI
:100026BE POP ESI
:100026BF POP ECX <-- Restore registers.
:100026C0 JMP 20002749
With this routine in place we can now log the checks to the simulated memory. What you find however is many checks and difficulties working out what should be inside our simulated memory, however we can get some help from VISI itself and emulate DK2WriteMemory() in the process, its easy to see that the first 6 bytes read from address 10 are our dongle serial number, theres a very obvious MOV EAX, 7 as I recall if you don't pass this check. I tried guessing then at the remaining checks and got a list of question marks, but most of them revolved around the word read from address 3. This word is explicitly checked against the value 0xAA55 and leads to no meaningful results.
I didn't know whether word 3 should be 0xAA55 or not and there were too many other dongle reads related too it, I was grasping at straws, in search of some inspiration I wen't back to the installer and here I got some luck. Placing a bpx on DK2WriteMemory() I didn't expect anything to happen, but it did, the installer tries to write to word 3, guess what value :-). Without further ado I rewrote DK2WriteMemory(), the code is pretty much based on the read memory code above so I won't display it here, we patch in word 3 as 0xAA55, word 5 as 0xC914 (current date) & byte 8 as 0x01 and now start logging the checks from cad3d.exe.
Now DK2ReadMemory() reads the following :-
Address 16 (1 byte) :- looks like it should be 0xFF.
Address 2 (1 byte) :- looks like it should be 0xFF.
Address 0 (1 word) :- controls expiry date (see further on).
Address 7 (1 byte) :- looks like it should be 0x01.
Address 9 (1 word) :- no visible effect.
When you run VISI now you'll get one of 2 messages, either "Sales dongle expired" or "Sales dongle expired on <some date>". If your simulated dongle memory has 00 00 at address 0 you'll get the first message, if theres any value, the second. The low byte i.e. address 1 controls the expiry day and the high byte i.e. address 2 controls the expiry year counted in 6 month blocks from 1900. If you want a useful expiry date then set this word to 0xFF01. Launching cad3d.exe now gives us the final piece of the puzzle and the most difficult part to solve (under NT after selecting OK you'll get some more information in another message box that will prove very useful) :-
Interestingly you'll get this message under both 9x & NT, but exactly what is DESlock?, well in VISI this message is related to 2 DESKey API's, DK2ThroughEncryption() & DK2ReadRandomNumbers(). Both of these functions are capable of encrypting data using the DESKey and a seed byte, the algorithm in either case (if they aren't indeed the same) is unrecoverable without the original dongle, so we've got to hope for implementation mistakes. One might think by just looking at the names that DK2ThroughEncryption() is likely to be the culprit, in fact in VISI it only encrypts 4 bytes and the response is verified application side :-
:5000E9B4 ADD ESP,0C <-- Balance stack after DK2ThroughEncryption().
:5000E9B7 CMP DWORD PTR [EBP-04],1366A32D <-- Check response (0xAA8A77D3).
:5000E9BE JNZ 5000E9CE <-- Need I explain this.
:5000E9C4 MOV EAX, 1
:5000E9C9 JMP 5000E9D0
:5000E9CE XOR EAX,EAX
You can trivially rewrite DK2ThroughEncryption() to return this result (CALL 100025C4 NT, 10003636 9x), its not used for anything meaningful other than a sanity check before we get to the real action. DK2ReadRandomNumbers() is our real culprit, it uses the DESKey to generate 0x2000 bytes of random data, of course this is inappropriately named because a fixed response to a seed byte can hardly be called random. What does this random data do?, well as you've already figured, it decrypts file3d.dll which performs the save functionality. Lets look at some choice code snippets :-
:5000E1BB MOVSX EAX,BYTE PTR [ECX+EAX] <-- DESKey
:5000E1BF MOV ECX,[EBP+08] <-- File3d.dll .text section.
:5000E1C2 XOR AL,[ECX] <-- Decrypt.
:5000E1C4 MOV ECX,[EBP+08]
:5000E1C7 MOV [ECX],AL <-- Place decrypted byte.
:5000E1C9 MOV EAX,[EBP+08]
:5000E1CC MOVSX EAX,BYTE PTR [EAX] <-- Get decrypted byte.
:5000E1CF ADD [EBP-04],EAX <-- Add to store.
Checksum bytes :-
:5000E40D ADD ESP,08 <-- Correct stack.
:5000E410 MOV [EBP-04],EAX <-- Store checksum value.
:5000E413 MOV EAX,[EBP-04] <-- In EAX.
:5000E416 CMP [5000F068],EAX <-- Compare with good checksum.
:5000E41C JZ 5000E43B <-- Final sanity check.
This is the part that really cannot be cracked without access to the original dongle and believe me I tried very hard indeed to reconstruct the missing bytes, one could probably work out the first 4 bytes, no prizes for PUSH EBP, MOV EBP, ESP and something ESP as is usual when setting up a stack frame, but I'm pretty sceptical anyone could work out the entire 8k. Interesting though isn't it that DESlock's entire security depends entirely on a trite XOR encryption and the intelligence of the developers, data of insufficient length one feels is vulnerable to plaintext attack.
In the end, a registered user dumped the missing 8k of data and I merely added it to the end of dk2win32.dll (in a section I named .zen), all one needs do then is rewrite DK2ReadRandomNumbers() to return this data (modifying the relevant parts of the PE header). So whats the conclusion?, well without access to the dongle I consider VISI to be secure from cracking, however the idea behind all of these dongles is to prevent software piracy or make it difficult for any user (legitimate or otherwise) to pirate the software. In this respect one can only conclude that the DESKey is yet another miserable dongle failure, it took my legal owning friend less than 45 minutes to install his debugger and dump the missing data (and he's a complete novice), thats really how much your security is worth Vero :-).
I have recently received very lucid feedback from VISI's developers regarding this tutorial, you might like to read it here :-), in accordance with their legitimate request I have removed the pre-cracked dk2win32.dll that was available from this site. I do not wish to support dishonest users and without a legitimate dongle you won't be able to crack VISI using this document anyhow.
Quite often I tend to revisit targets (especially those where the developer provided feedback). Since the widespread distribution of my v7.2 dll, v8.x attempted to break it fairly simplisticly, I didn't verify this as such, but it seems likely (bearing in mind the nature of the break) that all VISI's developers did, was modify the query parameters which do the decryption of file3d.dll, hence the data returned by my v7.2 dll would not be correct. Unfortunately the scene found an easy way around this, simply distribute the v7.2 file3d.dll as part of the v8.x crack and all is well.
v9.1 which I am looking at now breaks this possibility for good, the developers have made several improvements, chief of these is a call to DK2SendAlgorithmString(), this is a 16 byte block encryption function, a random number generator based on the system time generates 2 pieces of data, a word which will be used as the Iteration1 parameter and a dword which will be used to generate the 16 bytes of command data. The DK2SendAlgorithmString() function sends back a modified 'algorithm string', the order of events is shown below.
DK2ReadRandomNumbers() (400 bytes of data).
DK2ReadRandomNumbers() (35 bytes of data).
The algorithm return string has a critical effect upon the decryption bytes from DK2ReadRandomNumbers(), if the program is satisfied with the decryption checksum the second call to DK2ReadRandomNumbers() will have its data decrypted too and the string 'cad3d.exe' will be passed to LoadLibraryA(). This new improvement really doesn't represent any great increase in security, v7.2 was uncrackable without access to the original dongle because of the unknown query data, v9.1 remains uncrackable because (well you guessed it), the same reason. The only changes one must make are a patch to DK2SendAlgorithmString(), since the Iteration1 parameter is passed through the stack we can retrieve it directly from there and modify it to a fixed response (actually one needs to retrieve both the stack reference and the saved copy (fortunately offset from EBP)), since there are also 2 calls to both functions we need a way to distinguish between each call, for DK2SendAlgorithmString() a simple flag will do, for DK2ReadRandomNumbers() we'll just check the ByteCount parameter.
Instead of patching dk2win32.dll directly I decided instead to write my own replacement dll, this is much easier than fiddling with delta offsets and also solves a nasty problem I found in the installer, the dll needs export stubs for several other (unused) DK2 functions since they are GetProcAddress()'d from file3d.dll and failure means "out you go". Once again within 1 hour we have a fully functional VISI.
In the course of my discussions with various end users, someone tested my v9.1 dk2win32.dll with v9.0, and the decryption failed. This was interesting for me since I had immediate thoughts the developers had done something rather clever, i.e. supplying custom file3d.dll's to each customer (with different decryption data), this would mean if a crack showed up, simple analysis of file3d.dll would tell them where the program had leaked from, though this doesn't seem to be the case as I've had 2 customers confirm the same file3d.dll. In fact all the increment from v9.0 to v9.1 seems to have taught VISI's developers is to change the encryption parameters slightly (this is hardly security), the dll's differ only by encrypted data and therefore (although not verified) v9.1's file3d.dll and dk2win32.dll will probably work with v9.0.