Bentley MicroStation /J v07.00.01.11 - Tutorial

http://www.bentley.com - Webpage.
Installation Files: (80Mb+).

I'm back once again revisiting an old friend. About 9mths ago I first encountered Bentley MicroStation's serial # protection, it gave me a pretty serious head-ache back then and eventually I settled for a pretty rudimentary patch - the less said about that the better :-). My return was necessitated by the requirement to crack several add-ons, this time however I was more determined to break the protection. Launching the program brings up an all too familiar dialog inviting you to enter your name / organization & serial #. This dialog is slightly different to those which you may have encountered before, the Continue button will only be activated when a valid serial # is detected.

No problem you might think, "we'll just bpx Hmemcpy", but theres a problem, it won't break and neither will anything else you might care to think of (try your luck), even hwnd / bmsg won't hook. I tried some other (what I hoped would be quick-fix approaches, even bpm's on the 8xxxxxxx memory areas), but nothing seemed to lead anywhere. I also tried disassembling looking for StringRef's, some things of interest are visible but nothing I bpx'd for broke. This was a tough call, however I did know one thing, MicroStation's readme helpfully says that the basic licensing is controlled by a series of license files (msj.lic in fact), this looked like an enticing entry point but I just found myself lost in ustation.dll, working out what jump(s) to reverse would have been immensely tedious and not really worthy of a tutorial (I would be repeating exactly what I did before).

What I needed was the right API to bpx for.....the answer of course is probably sat on all reversers hard drives, but how many of us really use it :).....If your anything like me you just bpx via trial and error and hack through code patterns. Good old BoundsChecker however has the answer, enable all the reporting features and monitor the API's, after about 200,000 events (my god!) you'll hit the Registration Information screen, type a few things into the serial # field and hit the arrow keys or backspace, then flick to BoundsChecker. If you've done that last bit right you'll be looking at the API name I wanted at the very start, GetTabbedTextExtentA - I'll wager a fair amount many of you never knew this function existed.

This solves our problem but tracing can be tricky, after the break don't trace too much, instead search for your serial # in memory (you may find 4 occurences) bpm them all and hope one of them hits, I couldn't find any reliable way of doing this right every time but persevere and you will succeed, fortunately or perhaps unfortunately :) the first serial # is unrelated to the name / organization. If you follow the code closely (that involves bpm'ing copies of your serial #) you'll get to the real dll doing the work - baseman.dll.

The serial # is validated using mathematical operations which use a single table to get at the key XOR values, here are the main snippets:

:623F27F4 CMP ECX, 0Eh <-- Length check.
:623F2853 CALL 623F58D0 <-- This is where the maths will be done.

:623F58E7 MOV DL, BYTE PTR [ECX] <-- Pointer into serial #.
:623F58E9 INC ECX <-- Increment said pointer.
:623F58EA MOV BYTE PTR [ESP+10], DL <-- Ready copy of serial #.
:623F58EE MOV EDX, EAX
:623F58F0 MOV EDI, DWORD PTR [ESP+10] <-- So we can access it here.
:623F58F4 AND EDX, 0FFh
:623F58FA AND EDI, 0FFh
:623F5900 XOR EDX, EDI <-- First XOR.
:623F5902 SHR EAX, 8
:623F5905 MOV EDX, DWORD PTR [4*EDX+623FF740] <-- Use first XOR result to hook the table.
:623F590C XOR EAX, EDX <-- Then XOR it (again).
:623F590E DEC ESI <-- Decrement loop counter.
:623F590F JNZ 623F58E7 <-- for (i=0, i<12, i++) {.....

:623F2866 CALL MSVCRT.strtoul <-- Your going to see this again.

I won't bore you too much with vast amounts of code, the serial # must be of length 14, the program then nulls the 7th & 8th positions before joining the 2 remaining sets of 6 together (String). The maths starts at 623F58E7 (code above) using an XOR encryption cycling through the 12 digits as well as the table - the maths isn't really worth reversing although letters are NOT liked by the program so don't use them, the end result of this XOR loop is inverted using a logic NOT, that result will then be divided by 64h (100 dec), the remainder being checksummed against positions 7 & 8 that were stripped away earlier.

You could try writing a program to generate these remainder values for you (I did this and then realised there wasn't much point) because you can bpx this code easily enough, a checksum will logically always exist regardless of the selection of the 1st 6 & last 6 digits. Sometimes its best not to simulate an encryption unless you really have to, in this instance you don't. Dump the table (which should be 1k using something like SoftDump - I've included it in a zip below if you feel so inclined).

e.g.

>123456xx789012 <-- 2 lots of 6 digits selected.
>xx positions nulled, 123456789012 is passed to XOR loop.
>BPX for 623F2879
>Note remainder value in EDX.
>Fix checksum as decimal value of EDX.

In my case 123456xx789012 generates a remainder of 46h, the xx is thus 70 (46h = 70 decimal).

Phase 1 is now reversed, but don't get too excited yet, we are just warming up and Phase 2 will certainly throw a spanner into the works. Another screen pops up asking for a License No. - the quick way to reach this next phase looks to be bpx baseman.dll (623F2272), the idea of this function would seem to be to return EAX=0 (if it does MSJ will start) but its a lot easier to say than do. Our efforts will focus in on the code range 623F56A0 --> 623F5708 and all the sub-functions in between, we'll need also to keep a careful eye on the stack variables.

We start firstly by pushing our Serial to MSVCRT.strtoul, this takes a single 32-bit argument, anything larger will therefore fail and return FFFFFFFFh or 4294967295 dec. This fact gives us some semblance of a range for acceptable values. The serial number you enter will then be moved to [ESP+8] as a single DWORD so 1234567890 becomes D2 02 96 49 or 499602D2h.

Immediately this happens you should bpr on the DWORD, please note the following terminology as I refer to it:

D2	02	96	49

Pos1	Pos2	Pos3	Pos4

:623F56C7 MOV BYTE PTR [ESP+7], AL <-- Pos4.
:623F56D8 MOV BYTE PTR [ESP+6], AL <-- Pos3.
:623F56E9 MOV BYTE PTR [ESP+5], AL <-- Pos2.
:623F56FA MOV BYTE PTR [ESP+4], AL <-- Pos1.
:623F5701 MOV EAX, DWORD PTR [ESP] <-- This is very important.

Here I'll make a confession, when I started out reversing this I didn't really watch this code very well, in fact I just traced over it (big mistake!), what follows next are elementary maths operations which use the first 6 & last 6 positions of the serial # we generated earlier. My big problem with this maths was that I could see the following code:

:623F22CA TEST EDX,EDX <-- Check EDX=0.
:623F22CC JZ 623F22F0 <-- Would seem good.
:623F22CE CMP EDX, 1 <-- Check EDX=1.
:623F22D1 JNZ 623F2422 <-- MOV EAX,1 bad.

I was looking crudely at this and could see that EDX got its value by dividing the 'generated number' by 2 to the power 27, SHR, 12 & SHR, 9, this value would need to be 0 or 1, meaning before the SHR,9 EDX would have to be 200h (512dec) - forget the fact that you have some margin of error. The problem was that EDX was never getting anywhere near this, I was always out by a factor of at least 10 (even if I maximised the serial number this was still the case), I'd convinced myself therefore that a good License never got here, it was a fake routine.

Thus I started to backtrace this call and found myself inside licnsmgr.dll (validateLicense()), I haven't linked the work I did backtracing trying to get just validateLicense() to do the job for me, needless to say nothing I could fix here appeared to work though it would prove to be an interesting foray (thats the benefit of hindsight :) ).

End diversion for now. The next move is obviously to try and work out how AL gets its value, as we can virtually fix the serial # at will the main objective is to get our MagicVal at least close to a manageable size, this might just make the arithmetic easier. There is a lot of maths about but the interesting CALL is 623F5980, what happens is this. Firstly each CALL to 623F5980 generates an XOR byte which is XOR'd with the bytes from our DWORD license number.

So we have (example License No. 1234567890):

XOR bytes:		A9h	97h	00h	94h
XOR'd with:		Pos4	Pos3	Pos2	Pos1
			49h	96h	02h	D2h
			----	----	----	----
Result:			E0h	01h	02h	46h

MagicVal (in EAX):	E0010246h <-- far too big.

Now that we can see how the end value of EAX is generated we'll do the maths (a lot harder than it sounds), then we'll work out what would be the desirable MagicVal. I know now the best way is to code a brute-force program, you need to obviously rip the validity criteria from the code:

:623F2295 LEA EDX, DWORD PTR [ECX+8*ECX] <-- 9 * last 6 of serial #.
:623F2298 LEA ECX, DWORD PTR [ECX+4*EDX] <-- 37 * last 6 of serial #.
:623F229B SUB EAX, ECX <-- Subtract from MagicVal.
:623F229D SUB EAX, ESI <-- Subtract first 6 of serial # from MagicVal.
:623F229F MOV EDI, EAX <-- Store MagicVal.
:623F22A1 SHR EAX, 12 <-- MagicVal / 262,144.
:623F22A4 MOV ECX, EAX <-- Store result.
:623F22A6 MOV ESI, EAX <-- Store again.
:623F22A8 MOV EDX, EAX <-- Store yet again.
:623F22AA SHR ECX, 5 <-- Divide by 32.
:623F22AD SHR ESI, 1 <-- Divide by 2.
:623F22AF SHR EDX, 9 <-- Divide by 512.
:623F22B2 AND ECX, 0F <-- Polish ECX.
:623F22B5 AND ESI, 0F <-- Polish ESI.

Reach here if EDX = 0 or 1, if EDX=1 ECX must be 0,5 or 7.

:623F22F0 CALL 623F2220 <-- Really just MOV EAX, 1.
:623F22F5 TEST ESI, ESI <-- Is ESI 0.
:623F22F7 JZ 623F2301 <-- Good jump.
:623F22F9 CMP ESI, EAX <-- Is ESI = EAX i.e. "is ESI 1".
:623F22FB JNZ 623F2422 <-- Bad jump.

:623F2301 CALL baseman.baseman11 <-- Really just XOR EAX,EAX.
:623F2306 TEST EAX, EAX <-- Is EAX=0.
:623F2308 MOV ESI, EDI
:623F230A JZ 623F2327 <-- This must jump because of baseman11.

:623F2327 AND ESI, 00030000
:623F232D CMP ESI, 00020000 <-- Check result.
:623F2333 JZ 623F2422 <-- Bad jump.
:623F2339 CMP ESI, 00010000 <-- Check AND result here also.
:623F233F JNZ 623F2361 <-- Decide route 1 good, route 2 good.

Route 1:

:623F2341 CALL 623F5560 <-- Set EAX=0.
:623F2346 AND EDI, 0000FFFF <-- Polish EDI.
:623F234C CMP EDI, EAX <-- Check.
:623F234E JZ 623F2418 <-- Jump route 1 good.

I'll pause here because when we reach the JNZ at 623F233F there are 2 possible ways to proceed, the first I've labelled as Route 1 (see above). We'll also need to consider Route 2 which has 2 forked routes to the end as well, this means a total of 3 possible outcomes. As it turns out Route 1 (the simplest) will prove to be a trial license (expiring 1st June 1999 **see end for more about this**), the AND result in ESI determines which of the routes is taken by Route 2.

:623F236E CMP ESI, 00030000 <-- AND result of MagicVal.
:623D2374 JNZ 623F23F3 <-- if ESI!=30000h a simpler route will be taken.

Naturally this isn't as simple as it looks. Firstly (the main problem is this), the XOR bytes I showed above are not constant values although the first XOR byte of A9h IS. Secondly (perhaps more interestingly) our User Name & Organization will be pushed as parameters to CALL 623F5780 which then performs a checksum.

To solve our first problem its back to the code to try and work out how the XOR bytes are determined, CALL 623F5FA0 seems to be interesting. Lets study the first pass:

:623F5FA8 MOV EDX, DWORD PTR [ECX] <-- This is a constant (A9C5FB55h).
:623F5FAD SHR EDX, 18 <-- This result will always be the same.
:623F5FB0 MOV BYTE PTR [EAX], DL <-- Therefore so will this.

The first pass of this call generates 8 bytes which are always the same regardless of the license number you enter (A9h is thus dependable as the first part of the key), however subsequent passes generate different bytes. We need to find out how.

Pass1:	A9  C5  FB  55  2C  20  83  1F  <-- These are constant.
Pass2:  97  XX  XX  XX  XX  XX  XX  XX
Pass3:  00  XX  XX  XX  XX  XX  XX  XX
Pass4:  94  XX  XX  XX  XX  XX  XX  XX  <-- 4 XOR bytes (1234567890 License Code).

This XOR scheme is long and full of maths, essentially it involves laying out all the variables on the stack, another separate location and even a few more tables, the loop I'm describing can really be broken down into 3 main functions:

:623F5EFD CALL 623F5F30 <-- The "location" copier.
:623F5F0F CALL 623F5FF0 <-- The "maths".
:623F5F21 CALL 623F5FA0 <-- The "manipulator".

On the 2nd pass the XOR result byte generated is copied via the "location" copier into 2 new DWORDS (these are based loosely on the String 12345678 - as would be seen in memory), this is passed to the "maths" which is truly horrid - look at it if you don't believe me :). The resulting 2 DWORDS are then stored as they represent loop Pass2's values, the first byte of which is the key :). The use of the "license number byte" if that makes sense is the reason why the XOR bytes change on each loop pass with different license numbers.

I don't doubt that this can be reversed if you chose to code a brute-forcer, I think however its a long and very tedious process indeed - perhaps its an idea to force past this check and examine the CALL I highlighted earlier which takes the User Name & Organization as parameters - this might save some work if you choose to take up the challenge :), naturally patching is left as an exercise.

Part 2 - "the continuation"

You've seen the scheme so its now a matter of logical procedure. I decided my first priority would be to brute-force a valid MagicVal initially not accounting for the name/organization checksum. I couldn't find any differences in function by forcing either of Route 2's path to the end so I took the easiest option, ESI!=30000h/20000h/10000h. The brute-forcer for this stage is linked below, the first valid value I got was 01BF5724h but there are many, many more, which should of course be obvious if you think about it.

Passing to the name/organization call I saw the checksum value for my real name & organization was 437h, this is elementary when you "go all the way" with reversing the scheme. I quickly calculated 01BF5724h+437h = 01BF5B5Bh, this is of course is the MagicVal I would need from the XOR scheme. As we know that the first XOR byte is always A9h and that 01h is required our validity range is instantly narrowed.

A9h (default) 169 dec = 1 0 1 0 1 0 0 1
01h (required)  1 dec = 0 0 0 0 0 0 0 1
			---------------
XOR ----------------> = 1 0 1 0 1 0 0 0 = 168 dec (A8h).

Our range is now A8000000h - A8FFFFFFh (16 million numbers or so). Evidently this is still too many to play the "narrow the range game" with. My objective now was to avoid having to duplicate the XOR scheme at all costs. The seemingly obvious answer is baseman.dll itself:

:623F2266 MOV EDX, DWORD PTR [ESP+34] <-- License Number (view this location).
:623F227B PUSH 623FF159 <-- "navailable".
:623F2280 PUSH EAX <-- License Number.
:623F2281 CALL 623F56A0 <-- Call the "XOR scheme".

These 4 lines hold the key. At the end of the XOR scheme our MagicVal from earlier must be in EAX, the stack also needs to be monitored (I believe it doesn't change) as do the registers. Now what if an evil reverser was to CALL the XOR scheme repeatedly with different License No's and use it to find a known valid MagicVal. This is 1 way out, there is still an easier way.

We can easily bpx for 623F2286 and see the value generated by the XOR loop for any License Code we choose, we know from the brute-forcer and checksummer the MagicVal which must equal this License Code XOR loop. However the MagicVal at 623F2286 is polished by subtracting parts of the serial #, this means that for any License Code we choose the serial # gives us a pretty large potential range to fix the MagicVal with.

So lets select a License Code: 2833872290.

This generates an XOR License Code of 01BD7337h (close enough).
However from the brute-forcer we need 01BF5B5Bh.

So the XOR License Code result is too small. However we can fix this by scaling the serial # accordingly (see the 2 key subtractions), this is a rudimentary process but I found these values below work the magic for me:

Last 6: BFC84h = 785540.
First 6: 1EFECh = 126956.

Now all thats needed is to recalculate the checksum assuming this new 1st serial #.

Therefore the validity criteria for me is as follows:

Serial # : 12695651785540
License : 2833872290

Of course this information is thoroughly useless to you because you don't know my real details :), however if you really think about it, working out your real checksum value and scaling the serial #'s accordingly is not exactly hard.

Reflections on the scheme & an addendum

I think this is a pretty good scheme but like every algorithm it has a weakness factor, by trivially subtracting the serial # to polish the License code the protection writer obviously thought he was improving the scheme by having the 2 codes check each other, however it is this fact that actually compromises the entire scheme by allowing a reverser some margin of error. Had the end of the License Code validation used something a little stronger than subtraction this would have been many more hours work, might I also suggest the name/organization checksum be strengthened a little as well :).

**Early on I suggested that there were 3 valid routes through baseman.dll (1 of the valid routes being a time-trial license), in fact ALL 3 routes are trial licenses (this fact wasn't immediately obvious when I was testing). I'll add a 2nd document discussing this at a later stage, impatient reversers can easily crack the trivial flag that controls this.


Miscellaneous Papers Return to Main Index


© 1998, 1999 CrackZ. 25th May 1999.