StruCad Drawing Viewer (spfview.exe) is much like the Microsoft file viewing programs which are given away to those people who don't possess the software used to create the original file. Even though StruCad have chosen to protect their viewer with a Sentinel dongle and machine dependant authorisation code I doubt the publishing of this document will damage them in any significant way. Our interest in this target is related only to aspects of the Sentinel implementation, specifically the read and dongle word algorithms.
Using IDA and techniques I describe elsewhere we can identify fairly easily the Sentinel packet record trademarks and begin to attempt labelling the various functions and their references, only the following 7 are of interest :-
sub_46A77C, sub_46A92C, sub_46A9C0, sub_46AAB4, sub_46ABF4, sub_46AD98 & sub_46AE24.
Still using IDA we'll adopt a theory verification approach, sub_46A77C takes one parameter (a PUSH EBX), just above this reference we can see sub_46A714 which takes EBX and 404h (1028 bytes), a quick look at the API guide confirms this must be sproFormatPacket(), we can with a reasonable degree of safety assume that EBX is a pointer to the packet record, this suggests that sub_46A77C is sproFindNextUnit(), whats strange here is that your supposed to call sproFindFirstUnit() with the developer ID you want to find before this, we will assume for now this does not need patching.
sub_46A92C is referenced at address 464F6A and takes 2 parameters via EAX & EDX, using SoftICE we'll set a bpx for this code and see what these are. As it happens EAX is the address of the packet record structure and EDX is 00483B5E, the function itself returns EAX=3 (unit not found) so this looks like sproFindFirstUnit(), the dongle ID is therefore assumed to be 3B5Eh, in most Sentinel's I've seen the ID is pushed either directly or the higher order bytes of the register are polished, e.g. AND EDX, 0FFFFh.
Using IDA we follow the code execution (this at a higher level) :-
:0046CBB2 CALL sproFindFirstUnit()
:0046CBB7 MOV [EBX], AX <-- Store the status code.
:0046CBBA CMP WORD PTR [EBX], 0 <-- Success?.
:0046CBBE JZ 46CBD1 <-- Good jump.
:0046CBC0 CMP WORD PTR [EBX], 2 <-- Invalid packet?.
If you patch sproFindFirstUnit() to return AX=0 StruCad will hang in a loop and you'll probably have to give the program a 3 finger salute. Lets gently relax in IDA and follow the execution path a little further :-
:0046DC72 MOV EAX, 16h <-- Will be pushed as a parameter.
:0046DC77 CALL 0046CC90 <-- Below here.
:00464F70 PUSH EBX <-- 0x7B.
:00464F71 PUSH ECX <-- Address where word will eventually be stored.
:00464F72 MOV EBX, ECX
:00464F74 PUSH ESP <-- Return address of word read.
:00464F75 PUSH EDX <-- Word address.
:00464F76 PUSH EAX <-- Packet record structure.
:00464F77 CALL 0046AAB4 <-- sproRead().
:00464F7C MOV DX, [ESP+0] <-- Word from dongle.
:00464F80 MOV [EBX], DX <-- Store it here.
As ever we will take this opportunity to rewrite sproRead(), heres how the new code might look :-
:0046AAD7 JZ 0046AAD9 (74 00).
:0046AAD9 PUSH EBP (55).
:0046AADA MOVZX EAX, WORD PTR [ESP+18] (0F B7 44 24 18).
:0046AADF SHL EAX, 1 (D1 E0).
:0046AAE1 CALL $+5 (E8 00 00 00 00).
:0046AAE6 POP EBP (5D) <-- Set up delta offset.
:0046AAE7 LEA EDI, [EBP+14] (8D 7D 14).
:0046AAEA MOVZX EAX, WORD PTR [EAX+EDI] (0F B7 04 38).
:0046AAEE MOV EDI, [ESP+1C] (8B 7C 24 1C).
:0046AAF2 MOV [EDI], AX (66 89 07).
:0046AAF5 XOR EAX, EAX (33 C0).
:0046AAF7 POP EBP (5D).
:0046AAF8 JMP 0046AB43 (EB 49).
This code might look a little daunting at first but in reality its very simple and can be pasted (with a few adjustments) into just about any sproRead() your ever likely to encounter. We start by pushing EBP to set up our call to retrieve the delta offset, in EAX we retrieve the word to read from the stack (MOVZX to extend the zero's), we multiply the word address by 2 as we are reading a word from our simulated dongle and next use the delta offset to point EDI at the start of our simulated dongle memory, before reading and storing to the return address (again from the stack) and finally clearing EAX (the status).
Its time now for some live SoftICE debugging. We'll set breakpoints at our new sproRead() function as well as at the other locations we didn't identify above. Here's the log of events :-
Words read :- 16, 17, 0, 16, 17, 8, 9, A, B, C, D, E, F, 10, 11, 12, 13, bpx @ 0046AE24 (system loop).
The final break is interesting for us, as are the manipulations performed in a loop through word 8-13 (we'll be back there I feel), this is really what dongle reverse engineering is about, following paths, as you proceed through the code woods new paths should open up before you, you'll only ever be stuck when all your paths lead to dead-ends, in which case you must retrace your steps. sub_46AE24 is referenced from sub_464FC0, the parameters pushed seem to indicate that this is sproQuery() :-
:00464FD3 PUSH EDX <-- Number of bytes in query string
:00464FD4 PUSH EAX <-- Packet record structure.
When we trace sproQuery() StruCad hangs, the real problem with the Sentinel SuperPro range of dongles is that the internal query algorithms from the original dongle are different for every customer, so there is no 'generic query' to reverse, only a specific customers algorithm, or at least thats what Sentinel would have you believe. You see to engineer an ASIC (let alone develop one) costs range anything from $30,000 to $100,000 so basic economics tells you that Sentinel definitely don't use an ASIC, they are using off-the-shelf components for sure, that means almost certainly factory burned PROM.
Of course Sentinel have done a 'trade off', in return for cheap production costs they have left their dongles vulnerable to hardware copying, virtually anyone could therefore copy a Sentinel in their possession using pretty rudimentary equipment (or at least equipment you would have access too in a school). Quick patching of the query displays the Viewer Registration dialog displaying the serial # (word 0), the identifier (unknown at the moment) and garbage written in the User Name box, a helpful (and badly spelt) tooltip informs you this cannot be changed, its very obviously the string constructed from words 8-13h read from the dongle. Lets look at how this works :-
:0046D13A MOV DI, 0ABCDh
:0046D13E MOV SI, 1h <-- Counter.
:0046D146 LEA EBP, [ESI+7h] <-- Start at word 8h.
:0046D149 MOVZX EAX, EBP <-- Extend zero's of dongle word to read in EAX.
:0046D14C CALL sproRead()
:0046D151 XOR AX, DI
:0046D154 MOV EDI, EAX
:0046D156 MOVZX EDX, AX
:0046D159 SHR EDX, 8h
:0046D15C MOV [EBX], DL <-- First byte of loop pass.
:0046D15E AND AX, 0FFh <-- Lower byte only.
:0046D162 MOV [EBX-1], AL <-- Second byte of loop pass.
:0046D165 INC ESI <-- Increment loop counter.
:0046D166 ADD EBX, 2h <-- Shift store.
:0046D169 CMP SI, 0Dh
:0046D16D JNZ 0046D146 <-- Loop dongle words.
sproRead() returns in EAX/EDX the value of the word read, in ECX is the word address. This loop produces a structure of 24 bytes which are firstly copied to a new location, before being fed through a byte-loop starting at 0046AFC7, this result makes up the user name. To completely reverse this we must write a function to produce the bytes we will need as results from the first loop shown above, only then can we reverse this loop to generate valid dongle contents for our desired user name.
I found very quickly that this is definitely not as easy as
it looks, the problem with the 2nd phase is that a range of adjacent
bytes will generate the same letter. For example, assume my real
name started with the letter 'P' or 50h, we use some trivial code
to find the first byte that would pass through phase 2 and give
0x50 at the end, i.e. 0xC4, however a little intuition (or trial
and error) tells you that 0xC5, 0xC6, 0xC7 would also work, the
difference being the value of ESI at the end, this is critical
because ESI will determine what values can be generated on subsequent
loop passes, if we assume that 0xC4 is the only possibility for
our example then the maximum value we can generate on the next
pass is 0x2E, no-one I know will want the name '
This means any key generator must incorporate code to handle this potential anomaly, its starting to sound easier to do it manually isn't it :-). What I actually discovered when replicating this scheme in assembly is that the phase 2 routine cannot generate any characters greater than 0x5A (i.e. 'Z'), this means lowercase User Names are an impossibility, note also that our user name is 32 characters long yet generated from only 24 bytes, how can this be so?, well the 3rd section of the loop (address 46B044 generates 2 bytes of the user name). I threw together eventually the source code to do this (including stage 1 which recovers the real dongle words, code shown above), you could probably make a million and one improvements. The program writes out the 24-byte file Strucad.dat which contains the required contents of dongle words 8 - 13h, just paste it over using a good HEX editor, I'm too lazy to write a completely automatic patcher, as we are here for learning about the algorithm and NOT downloading ready-made cracks.
StruCad Viewer v2.0 Name Generator (including source code).
Now only 2 hurdles remain, the mysterious Identifier string and the Action Code. We suspect the Identifier string is made from words 16 & 17 from the dongle (by simple process of elimination), using bpr we can easily verify this is the case (address 0046DF7B and sub_46BEB0 do the actual generation), below here there is some really serious maths going down, just take a look at sub_46B32C which does a lot of the work. I don't see there is really any requirement to reverse this, in fact the only approach that would be practicle would be brute-force and I suspect on that count the length of the maths would be prohibitive.
Lets bask a little more IDA. We can fish some very useful references, make sure you generate a .MAP file and use Msym from the /Util16 directory to generate a .sym file for use with SoftICE. Firstly we have the "This Product is not licensed !" at 0046D814, the deciding code retrieves a DWORD in EAX before checking a local value, this looks suspiciously like a checksum, we'll set a bpx here. We can also see references to error messages related to time-trial expiry, we can bpx here too. You might be wondering why I've quickly abandoned the quest to get a valid authorisation code, well I found the start of the routines that do this (sub_46B7CC) and let me assure you, this would be many hours work to reverse, remember too that there would be no real chance of brute-forcing a 16 byte code.
A summary of the further patchs required is given here (with some brief explanations) :-
:0046E103 TEST AL, AL <-- Patch this to MOV AL, 1
:0046E105 JZ 0046E10B <-- This is the result of the date validation.
:0046D80C CMP EAX, DS:DWORD_48A0C4 <-- Checksum.
:0046D812 JZ 0046D82B <-- Patch to JMP.
:0046AC0E CMP WORD PTR [EBX], 7242h <-- This is the
function CALL to sproOverWrite().
:0046AC13 JZ 0046AC1B <-- Patch so the function always returns AX=0.
:0047D9AC CALL 00403E58 <-- Returns EAX= -3 (you feel
it :-) ).
:0047D9B1 JZ 0047DA01 <-- Patch to JMP (EAX will be cleared safely shortly after).
This is a fairly strong protection, the quantity (not really quality) of the maths was enough to put me off reversing the authorisation algorithm, there was no requirement for me to break the Identifier although I think that would just be more tedious cut and paste..... So whats wrong with this protection?, well StruCad had the choice to use the Sentinel query and the Sentinel shell and didn't do so, almost certainly if they had done this program would be unbreakable without the original dongle (they could leave the scheme I describe above intact).
The problem though for StruCad, and I guess all dongle developers using these wrappers is that they offer no protection whatsoever for reversers with the original dongle (most warez groups have access too them these days), it would take a matter of 2 minutes to dump the decrypted program to disk and you'd be left with the program open to all attacks as per the status quo. There is no ready made crack here, I commend StruCad for such an intelligent protection.