SentinelLM Investigation

SentinelLM SDK v7.1.0 (private friends only).
A generic SentinelLM target.
Source code and perhaps some SentinelLM 'pack' to follow.

This essay discusses in more detail the methods used by the SentinelLM licensing system. Initial research into the SentinelLM system (hereafter referred to as SLM) starts with the installation package. One is requested to input either a serial number or nothing for a demonstration, no checking function will be called if you enter no serial number. Inside SoftICE one can follow the technique below to land inside the checking code :

: hwnd ikernel - lets get some handles for our window
0CC0(2) 34A7 32 IKERNEL Button 178F:00001040 <-- Back button
0C90(2) 34A7 32 IKERNEL Button 178F:00001040 <-- Next button
0CBC(2) 34A7 32 IKERNEL Button 178F:00001040 <-- Cancel button

: bmsg 0C90 wm_lbuttondown - lets bmsg on our target button

After pressing the 'Next' button you should be fired back to SoftICE, some F12 work will probably land you back in isrt.dll and then iscript.dll, by using simple searching and bpm techniques you will find yourself quickly inside modbin.dll. Its a good idea to load modbin.dll into IDA if you want to follow along. The main checking function is checkLicense(), this is clearly exported, but is really nothing more than a glorified wrapper for the real function doing the important work, GetVendorId() and its main constituent sub_100014D0(), this returns -1 in the event of an invalid serial number or a value in the range 0 - 2^15 (as noted in Nolan Blenders' essay), achieved via an AND EAX, 7FFFh instruction on the final result.

GetVendorId() calls _scanf(), the %lu indicating that its expecting an unsigned long integer (a range from 0 to 4,294,967,295), this gives us a finite keyspace of 2^32. Due to the nature of the algorithm used to derive the eventual ID, a brute force attack is not impossible, although the loop counters guarantee at least some time will be spent bruting. The first function sub_10001310() performs a simple byte re-arrangement, it can be very effectively optimised and thought of like so :

mov eax, serial
xchg ah, al
rol eax, 16
xchg ah, al

This re-arrangement will be passed to sub_10001080(), the first of 2 loop functions, the result will then be split into 2 words and passed to sub_10001340() twice which swaps the low and high bytes of each word. Finally sub_10001000() (the 2nd loop) is used to derive a checksum word which must match the eventual Vendor Id. However, upon reading the manual we see that there is also an API option (also controlled incidentally) by the serial number, lets guess, isDevSrl() 'is Developer Serial' perhaps ;-), could be our culprit.

I modelled the complete function in assembly (mostly by ripping the code directly from IDA), however some small optimisations were made. Making a brute force attack on the first valid Vendor Id, a matter of 10 seconds elapsed before the valid combination (Vendor Id 0xAA0A / SN 8430250) dropped out of the loop, for any other Vendor Id's the loop would have to continue, my bet is the entire keyspace could be searched in around 2hrs on a reasonably fast PC.

This weak installation algorithm (during installation GetVendorId() will be called another 4 times) effectively compromises any 'watermarking' feature of developer licenses by Vendor Id, since anyone with a few hours to spare can easily generate valid serial numbers for any Vendor. Also, we can conclude that Vendor Id's 0x8000-0xFFFF were reserved for 'developers', and ID's 0x0-0x7FFF for everyone else, for our troubles of having a 'developer' Id we get some very useful libs in the /MsvcDev directory and also in the /Tools directory (old SLM/ELM libraries).

After installation I recommend you read all of the manuals in order to gain an overview of the SLM system, notice the 'rip off' of FLEXlm with 'feature names'. The target application I am using for this discussion offers both a standalone or network license, the integrated client dll lsapiw32.dll is shipped, along with the vendors own InstLicense.dll (possibly interesting), also present is the file Wcommute.exe, which is used for so called 'commuter licensing' on laptop computers, the application directly imports functions from lsapiw32.dll.

Starting out we select a standalone license and install the latest Sentinel drivers, result : dongle not found, a quick perusal of the main file locates us a Sentinel object for a dongle, it looks pretty basic though, a call to sproFindFirstUnit() as expected and a few sproRead()'s of the serial #, no problem patching those checks away in both the main program and InstLicense.dll, we select Standalone license and are confronted with a locking code and a please enter the license code dialog, this is where the real SentinelLM cracking begins.

The idea here is to get Wlscgen.exe to generate licenses for us and also dig out the Vendor Id of our target. Wlscgen is the application Vendor's are supposed to use to generate licenses for their customers, however as stated in CyberHeg's essay, this is protected by a Sentinel dongle (in fact what I'm assuming must be a universal dongle, ID 0xA870). By loading the SentinelLM signature into IDA we get a vast amount of recognition. By careful analysis in IDA we can see that there are references to the following Sentinel functions :

sproFindFirstUnit(sub_435B00).
sproRead(sub_435CD0).
sproWrite(sub_435E40), write passwords 0x26/0x34C8.
sproDecrement(sub_435F90), various words 0x9/0xB/0x24.
sproQuery(sub_4360C0).

I assumed that both sproWrite() & sproDecrement() would not be called on startup, since it seems logical they are only called into action when a license is generated. Setting breakpoints on the core 2 seems the way to go, i.e. sproRead(), sproQuery(), I assume you can trivially patch sproFindFirstUnit() for yourselves ;-). CyberHeg demonstrated in his essay one way to emulate sproQuery() since the query strings (and thus responses) are determined by a random number, a trivial patch is unreliable to make, however you'll notice that all the responses are hard-coded into the .data section and are _strncmp'd with the default 00000000. Therefore a much simpler patch is possible, we'll simply force sproQuery() to return 0 (the status) and modify the return strings in the .data section to 00000000 with our hex editor, this approach will work just as well as emulation code (see below for why it didn't! in the end) but will take less than 30 seconds and also (since there are 3 arrays of query result strings, work for any unexpected calls to sproQuery()).

This leaves us to focus on sproRead(), word 0xF is read first and reads are also performed upon words 0x8/0x9/0xB/0x24 & 0x26, the only conclusions I could actually reach were that word 0xF should be 0700h (a check late at the end before a MessageBeep, see code below) and that words 0x8 & 0xB should be equal (and non-zero), most of the other reads appeared to have their values saved in global variables.

:0041E1E6 cmp eax, 700h ; Word 0xF
:0041E1EB jz short loc_41E208
:0041E1ED push 0FFFFFFFFh ; uType
:0041E1EF call ds:MessageBeep ; Needless to say, this is bad

Now we can log in to Wlcsgen, don't forget to set your administrator password btw (just in case some person should abuse your license generator) ;-). Understanding the myriad of SentinelLM options is one of the first chores, looking at the API guide and target we see that the function responsible for 'checking out' licenses is LSRequest(), courtesy of IDA we also get the *publisherName (Lavenir Technology), and by using SoftICE we can easily get the remaining parameters, with merely a single xref to LSRequest() it seems likely the developers decided on a very simple 'Single Feature' license, the call to sprintf() just before the feature name also implies this is the case since the feature name is restricted to a maximum of 2 numeric digits with this option.

We know from CyberHeg and Nolan Blender's work that _computeVendorCode() returns in EAX the Vendor Id, more on this later, (note if you have a developer Id the return will have had its sign bit cleared), _computeVendorCode() is really nothing more than a wrapper for 2 other functions, the main being _DencVendId(), this is where it gets very interesting. _DencVendId() is yet another wrapper for calls to _VLM_deMorphId(), want to bet what this does ;-), read on. A quick look at _VLM_deMorphId() reveals it is nothing more than sub_10001080() which we saw, in modbin.dll (I didn't verify this, but its a good probability if we applied the SentinelLM signatures to modbin.dll this is what it would be, looking around some more, the entire system falls around us, the bit swapping function I highlighted is named _swap32bitOrder() .....).

It then struck me that the code that performs the 'deMorphing' is actually very unique and that the code, either dynamically (lsapiw32.dll) or statically linked (lsapiw32.lib) ought to look very similar across implementations, a quick disassembling of the lsapiw32.dll shipped with my target reveals this to be the case, (the error string "%s error: Illegal vendor identification." is exactly what we are looking for and thankyou Sentinel for leaving it in there with a neon sign saying "_computeVendorCode(sub_10018A8A) right here"). This means that we can simply breakpoint the code inside lsapiw32.dll and easily find the Vendor Id for any target using lsapiw32.dll (I bet now you are all thinking how to 'panzer' your copies of lsapiw32.dll, much like the early FLEXlm dll's).

Back to our target, we set breakpoints inside lsapiw32.dll to recover our Vendor Id and feature name, as I suspected the feature name is '12' (simple type), and the Vendor Id 0x1005, but notice, lsapiw32.dll does not distinguish between ordinary Vendor Id's and developer ones (however it seems likely Wlscgen.exe does), this means our Vendor Id may have been 0x9005 (we don't know, more of this later). We run Wlscgen.exe and attempt to generate a license code and discover to our dismay that we don't have enough licenses in the meter to generate a code, however we can see that word 24 (if you labelled your dongle memory something like me) is controlling the remaining trial license count (we'll quickly fix that to 0xFFFF). I had some difficulties figuring out why the meter for generating regular licenses was set to 0, I started backtracing and eventually found the culprit, a sproQuery() patch which didn't work as I'd expected, in fact when I clicked the generate license button I realised this wasn't the only instance of my foray into the .data section not working.

I checked the sproQuery() references and realised all of the query checking I'd seen was using _strncmp(), looking at the _strncmp() function code I realised its only effect on the registers was to return EAX/ECX=0 if the strings were equal or EAX/ECX=1 if they were not. I could simply add a small piece of code and redirect the _strncmp()'s I knew to be query checks there, this would also negate the need to patch the .data section strings, (and what better place to add the code than the redundant space inside sproQuery()). After doing this, and making a small patch to sproDecrement() we are able to generate licenses.

Getting back to my target, I ran the brute forcer on the Vendor Id (0x1005) and found the installation serial # 2918908432, at this point I merely re-installed the SDK and wrote a small program to re-apply the generic Sentinel patches. Now for the moment of truth, lets run Wlscgen.exe. After some experimentation (generating bad keys) I realised the License code type was custom, the code length Long and the feature version 7, at this point LSRequest() returns success. After further examination I saw exactly what the Vendor had done, they decided to lock the license manager to their own hardware key (specifically the dongle Id), this probably seemed like a good idea at the time, but in fact compromises the entire protection further since any cracker must disable the local hardware key protection and the dongle Id would inevitably be fixed, allowing the distribution of a single license code since the locking code is based on the local dongle (this can be verified by removing the lservrc license file and checking the same locking code appears).

Although my target is now effectively broken, there remain several assessments to make about the SentinelLM system, like the degree of customisation the Vendor Id provides and also (since Wlscgen's _computeVendorCode() does not care about 'developer licenses') whether there exists the possibility that a developer Id can generate the same codes as a corresponding ordinary Id, or whether (which seems crazy to me) that Rainbow provided futureproofing for their key licensing product of merely 32,767 developers. We will also delve inside the world of license generation to see what algorithmic strength is provided in the key generation (not that having the best encryption in the world would make much difference).

SentinelLM Customisation

I was hoping (of course) that 2 different Wlscgen's for different developer Id's would be very different, I was however set for disappointment when fc /b found a mere 3 bytes difference, I suspected immediately it was more likely to be a DWORD that formed the basis of this customisation (raw offset 0x15C8DC if you are interested), taking note of its value I decided to trace the one place I knew it would probably have to be used, _computeVendorCode().

At the start of _computeVendorCode() a copy is made of a 16 byte array (hard coded into the .data section, at address 55C8D0), the last DWORD contains the changes which fc /b found, the first 3 DWORD's (at least on 2 runs (the 0x1005 Vendor Id & the 0xAA0A Id) appear to be constant) :

:0043AE56 PUSH DWORD PTR [EBP-04] <-- last DWORD of Vendor Id array.
:0043AE59 CALL 43ADC9 <-- Really this is _swap32bitOrder().

At 43AE84 the Vendor Id is decoded. Therefore one potential solution to re-installing the SDK every time we need to generate licenses is to replicate this routine and brute force the 4th DWORD. Although I suspect this approach would work it belies the obvious, since the installation SDK has to compute this 'watermark' DWORD at some point there must be a routine somewhere during the install which works from the Vendor Id. After some bpm work we soon have a likely candidate, the function modbinFilelist().

modbinFilelist() is very interesting indeed, the watermarking system opens each of the files to be watermarked (there are many in all of the directories (including interestingly lsapiw32.lib & lsapiw32.dll themselves)) and searches for a 24 byte pattern (differs on each file), it assumes the following 16 bytes are our array, I advise you study slowly the pseudo code below :

xor ebx, ebx
call _myRand ; Generates a random # in EAX.
mov cl, al
xor cl, 5Bh
mov bl, cl
xor ecx, ecx
mov cl, al
xor edx, edx
shl ecx, 10h
or ebx, ecx
mov cl, al
shl ebx, 8
xor cl, 0A6h
mov dl, cl
xor al, 0A4h
shl edx, 10h
xor ecx, ecx
or ebx, edx
mov cl, al
or ebx, ecx
call _myRand
mov edi, eax
call _myRand
mov ebp, eax
push ebx
xor ebp, edi
mov SavedXor, eax
xor ebp, dword ptr [VendId] ; Plug in the Vendor Id here.
call _10001000 ; First array result.
add esp, 4
mov ebx, eax
push edi
call _10001000 ; Second array result.
add esp, 4
mov edi, eax
mov eax, SavedXor
push eax
call _10001000 ; Third array result.
add esp, 4
push ebp
call _10001000 ; Final array result.
add esp, 4

I omitted a small part of the end game here, the results are actually passed to the byte swapping function, but this is the real meat of the SentinelLM scheme. Effectively this means we can break the SentinelLM watermarking feature completely by generating our own array information for a fictional Vendor Id, what is more, with simple search and replace in the target binaries we can have _computeVendorCode() return our Vendor Id and Wlscgen generate licenses. Alternatively, we can recover the real Vendor Id from a target using any of the techniques described above and generate a new array table for Wlscgen.exe.

Signature Bytes for lsapiw32.dll

80 3B C0 78 A8 40 81 64
FF 51 14 39 6C 08 42 07
66 85 58 18 39 4D 07 7A

Ordinary Id vs Developer Id

Using the above technique I decided to generate a new array table for fictional Vendor Id 0x9005 (note that 0x9005 And 0x7FFF = 0x1005). The resulting table was placed inside both my target lsapiw32.dll and Wlscgen.exe. Wlscgen generated a different code to before (indicating that it at least recognises the difference in Vendor Id's) and my target accepted the code as valid, this demonstrates that SentinelLM does know the difference and that corresponding Developer Id's do not generate the same code as Ordinary Id's.

License Generation

Obviously we can take a look inside Wlscgen and see how SentinelLM generates its licenses, there are many functions in the binary prefixed by '_gen', this seems likely to mean 'generate', because of the vast array of options, these are all chained beneath sub_41EF72() which takes 6 parameters (I'm assuming these control option preferences), at 421187 just before _VLSencryptLicense() we can find the correct license key. The Developer Id is also used during the process as one might expect. Using either the sample programs provided with the SDK or your own code and the information here, it is now a fairly mundane task to rip portions of code for your own key generators, you might also have ideas about creating custom tools or replacement SentinelLM dll's ;-).

What do I think?

Well unfortunately for Rainbow, I fear (along with developers who use this) that they are going to learn the FLEXlm way, the watermarking feature is weak, and the Sentinel protection highlights glaring holes in the Sentinel dongle system. The entire system needs a pretty radical rethink ala FLEXlm else there will be many more versions where the warez scene will simply dig out the Vendor Id and make license generators.


Return to FLEXlm Return to Main Index


© 1998, 1999, 2000, 2001 CrackZ. September 2001.