Sentinel License Manager Cracking
Removing need for dongle in SentinelLM Wlscgen


by CyberHeg


( )Beginner (X)Intermediate (X)Advanced ( )Expert

The target audience for this essay is reasonably experienced crackers with some ASM coding experience who wish to generate keys for Sentinel License Manager protected products.

Removing need for dongle in SentinelLM Wlscgen
Written by CyberHeg


Wslcgen is a part of SentinelLM SDK v7.1.0 and it is the license generator that makes SentinelLM protected programs activated to a usable state. Reading the manuals of the SDK says that it needs a special "License meter key". Unless you have this you can not make any licenses and you will have to buy one from Rainbow Tech. Also when you have exhausted your key you will have to buy a new one. My initial guess was that it was a standard Sentinel SuperPro dongle with a counter and I was right. What makes this target interesting is to see how well Rainbow Tech. managed to use their own protection methods.

Tools required

Wslcgen.exe from SentinelLM SDK v7.1.0, IDA v4.04+, SoftICE v4.05, hex editor, SuperPro Developer's Guide from, api.txt from the C/C++ interface available from and Killer_3K's superpro flirt sig for IDA (from CrackZ site) and the SentinelLM sigs: w32mcdll.sig, w32mcst1.sig.

Target's URL/FTP

Program History

Uncertain - this appears to be a descendant of the earlier Sentinel License Manager and the Elan license manager. The models for licensing appear to come from the ancient "netls" package, but the key generation appears to be totally different.


Here's a little info on how the dongle and API works. This is by no means a replacement for the real manual which is really a must to read in order to understand the protection scheme. SuperPro has 128 bytes of memory. This is split into 64 WORDs (2 bytes long), also called cell's counted from 0-63. Since we are talking about WORDs the values of these go from 0x0000 (0) till 0xFFFF (65635). The first 8 cells are nonwriteable. Heres a little table of the use of those 8 :-

Cell no. | use
0 | Dongle serial number which is unique for every dongle (the number just gets incremented
  | in production of the dongles) (readonly)
1 | Developer ID (to ensure you get hold of a dongle from the right vendor) (readonly)
2 | Overwrite password 1 (no access)
3 | Overwrite password 2 (no access)
4 | Write password (no access)
5 | Reserved by Rainbow Tech. (no access)
6 | Reserved by Rainbow Tech. (no access)
7 | Reserved by Rainbow Tech. (no access) 8-63 | User read and writeable (full access)

Each cell has a value of it's access type. The following table shows the different types :-

No. | Type
0 | Read/writeable cell
1 | Readonly cell
2 | Counter cell which can only be decremented
3 | Locked and hidden/algorithm

On an API call, a return variable will hold the error code. At assembler level this always refers to EAX. What is important to know is that if it is 0 then dongle is found and 3 dongle is not found and everything else also means error of some kind. Here below is a list of the most important API's needed to emulate in general :-

SproFindFirstUnit() is the first API called when looking for the dongle. It will take the Developer ID as a parameter in order to look for the correct dongle.

SproRead() will read a WORD value from a specific cell. The cell number and store address of the read is specified as input parameters.

SproQuery() is a algorithm function. You put in a query value and get a response. Since the DWORD algorithm is access code 3 you can't read it out of the dongle. That is why emulation of it is different for every program. The query value and store addresses used are put in as input parameters.

SproDecrement() decrements a counter cell (access code 2) and takes cell address as parameter.

SproActivate() activates a algorithm which was deactivated before.

When running the program we get a error msg with a problem accessing the meter key. So first we disassemble wlscgen.exe in IDA and apply the sspro.sig. Be sure to make a file, and convert it so we use the map with the symbol loader making it more clear what we do when working in SoftICE. First we will find SproFindFirstUnit at 00423495. Using api.txt we can also identify the parameters :-

:0042348F push 0A870h ; Developer ID of the Meter Key
:00423494 push esi ; packet record
:00423495 call sproFindFirstUnit

and inside :-

:00435B00 arg_0 = dword ptr 0Ch, arg_4 = word ptr 10h
:00435B00 push ebx
:00435B01 push esi
:00435B02 mov eax, [esp+arg_0]
:00435B06 or eax, eax
:00435B08 jnz short loc_435B13
:00435B0A mov ax, 2
:00435B0E pop esi
:00435B0F pop ebx
:00435B10 retn 8

Replacing the code at address 00435B06 with a xor eax, eax and NOP'ing the jump and move right after returns SP_SUCCESS. First API done. Next we will look at SproRead. Putting a bpx on it in SoftICE makes it break at 00423079. By identifying the parameters we will get this :-

:0042306C lea eax, [ebp+var_4] ; prepare parameters
:0042306F lea ecx, [ebp+var_80C]
:00423075 push eax ; store address
:00423076 push 0Fh ; cell to read
:00423078 push ecx ; packet record
:00423079 call sproRead
:0042307E test ax, ax ; check return code
:00423081 jz short loc_423089 ; if its 0 then continue
:00423083 mov [ebp+var_4], 0

Having read Goatass's essay on FlexiSIGN along with CrackZ's on other Sentinel targets makes this a easy task. The following emulation made in Hiew showing that its almost the same as theirs :-

:00435CEB: 66813E4272 cmp w,[esi],07242        ; validate packet record
:00435CF0: 7400 je 000435CF2
:00435CF2: 55 push ebp                         ; save EBP
:00435CF3: E800000000 call 000435CF8           ; gets Delta offset
:00435CF8: 5D pop ebp                          ; the Delta offset will be placed in EBP now
:00435CF9: 8D551E lea edx,[ebp][0001E]         ; the displacement 0x1E will be added and 
                                                 the address in EDX will point to the 
                                                 entry of our memory array
:00435CFC: 5D pop ebp                          ; fix stack
:00435CFD: 0FB74C2410 movzx ecx,w,[esp][00010] ; get the cell address to read from from stack
:00435D02: D1E1 shl ecx,1                      ; Because a cell/WORD is 2 bytes long we need 
                                                 to multiply with 2 to get the correct entry
:00435D04: 0FB7040A movzx eax,w,[edx][ecx]     ; move the WORD (entry address + calculated 
                                                 displacement) into AX
:00435D08: 8B7C2414 mov edi,[esp][00014]       ; get the store address from the stack and put
                                                 it in EDI
:00435D0C: 668907 mov [edi],ax                 ; move WORD to the store address
:00435D0F: 33C0 xor eax,eax                    ; SP_SUCCESS
:00435D11: 5F pop edi                          ; clean_exit
:00435D12: 5E pop esi
:00435D13: C20C00 retn 0000C
:00435D16: 0000 add [eax],al                   ; our dongle memory will start from here
:00435D18: 0000 add [eax],al
:00435D1A: 0000 add [eax],al
:00435D1C: 0000 add [eax],al
:00435D1E: 0000 add [eax],al

The emulation is pretty much the same every time. The stack displacements differ a bit usually from program to program, but looking at the registers just before they get pushed on stack (like at 00423075) makes its easier to see what we are looking for. Finding the displacement value is done in SoftICE using "d esp" and looking how far ahead the data we need is from the current address. Our dongle memory cell's will start at 435D16 so be sure to clean the bytes from there and max 128 bytes ahead.

Once this is done and starting the program again we see a new error message "LM 7.x meter key not found." So far so good. Now it will be time for recovering the dongle WORDs needed in order to continue. By setting a bpx on SproRead we see many breaks on it. Starting with the first one I pasted above, reading from 0xFh and putting a bpm on the store address will sooner or later get us to this to the following code :-

:0041E1DA call sub_42303F
:0041E1DF mov eax, [esp+3E4h+var_3CC] ; get the WORD which is stored at 0xFh
:0041E1E3 add esp, 4 ; fix the stack from previous call
:0041E1E6 cmp eax, 700h ; is our WORD 0x700h?
:0041E1EB jz short loc_41E208 ; it must be
:0041E1ED push 0FFFFFFFFh
:0041E1EF call ds:MessageBeep ; else do bad stuff

Putting 0700 at the address matching 0x1E bytes ahead from our entry at 435D16 will make the conditional jump go in the right direction. Starting the program again we will get no more nags and a nice login screen will show instead. Even though there are more then just the address 0xFh getting called as address to read from it is not possible to find any real compares like the one showed above. So lets save them for later and continue. At this stage it will still take some time before the program starts because of the delay called Sentinel lag. This is a clear indication to us that we are not finished. The last and most tricky API is still needed to work on. Putting a bpx on SproQuery shows that it gets called plenty of times. The first instance of this is showed below :-

:00423668 push 4 ; length
:0042366A push ecx ; response32
:0042366B push eax ; response
:0042366C lea ecx, [ebp+var_4D4]
:00423672 push edx ; query data
:00423673 push 0Ch ; algorithm starting at 0Ch is the place to do query
:00423675 push ecx ; packet record
:00423676 call sproQuery
:0042367B lea ecx, [ebp+var_58]
:0042367E lea edx, [ebp+var_94]
:00423684 push ecx
:00423685 push 4
:00423687 push edx
:00423688 call sub_42394F
:0042368D add esp, 0Ch
:00423690 lea ecx, [ebp+var_58]
:00423693 movzx eax, [ebp+var_A]
:00423697 imul eax, 9
:0042369A push 4
:0042369C add eax, offset a3a70b51a ; "3A70B51A"
:004236A1 push eax
:004236A2 push ecx
:004236A3 call _strncmp ; string compare
:004236A8 add esp, 0Ch
:004236AB mov edi, eax
:004236AD test edi, edi
:004236AF jz short loc_4236C4

Thinking back on CrackZ essay on 3dsmax my first thought was that we had a situation just like this. After making a emulation like his it didn't work at all. Since this program is not crc protected like 3dsmax was patching the jump after the string compare should be enough. Unfortunately it would have had to be patched multiple times as the query on address 0Ch is called from multiple places. Besides, a emulation is much better.

So lets see what went wrong. In 3dsmax there was a plain string compare with the response value from the dongle. This doesn't seem to be the case here. The values compared are double DWORD's so what is important is to see how they get made. Notice the call after SproQuery. With a little debugging we will see that it converts a DWORD to ascii. A simple example of this is '1234' which will get converted to '31323334'. By putting in some recoqnizable DWORD's into response and response32 we will easily see that response32 is the store address which gets used for this conversion. After this it is easy to identify the parameters of this conversion routine :-

:00423676 call sproQuery
:0042367B lea ecx, [ebp+var_58] ; get the address to store the converted string
:0042367E lea edx, [ebp+var_94] ; get response32
:00423684 push ecx ; output, address where the converted string will be placed
:00423685 push 4
:00423687 push edx ; input, our response value placed in response32 
:00423688 call sub_42394F ; convert string
:0042368D add esp, 0Ch ; fix stack

Trying out the program a few times we see that the string compare will differ even though we are at the same code. Looking at the string compare parameters shows that the string which our response is compared with is taken from a array. The following shows the code which prepares for the string compare :-

:00423690 lea ecx, [ebp+var_58] ; our converted string
:00423693 movzx eax, [ebp+var_A] ; some number which differs from time to time
:00423697 imul eax, 9 ; use this number to calculate the displacement in the array
:0042369A push 4
:0042369C add eax, offset a3a70b51a ; "3A70B51A" ; add entry/base of the array
:004236A1 push eax ; string to compare with
:004236A2 push ecx ; our string
:004236A3 call _strncmp ; string compare

In order to make any of this info useful we need to find out how this number at 00423693 gets there. What will determine its value?. The answer to this is found just before the call to SproQuery.

00423611 xor ebx, ebx
00423613 push ebx
00423614 call _time ; time
00423619 add esp, 4
0042361C push eax
0042361D call _srand ; random
00423622 add esp, 4
00423625 call _rand ; random
0042362A cdq
0042362B mov ecx, 0Ah
00423630 idiv ecx
00423632 movzx eax, dx
00423635 imul eax, 9
00423638 mov [ebp+var_A], dx ; this is the number we was looking for

Calls to time and rand is a pure indication that the number we have is a totally random number. A summary of what the program does :-

1. find a random number.
2. save the random number on stack.
3. call SproQuery.
4. convert our response.
5. get the random number again.
6. calculate displacement.
7. add base of array (now we will have the actual string to compare with).
8. do compare.
9. validate if it was good.

Since there is a limited number of strings in the array, we could check out each queryvalue if we had a real dongle. But since we don't, we have no idea what the program wants, or do we?. Yes we have a way of knowing what the program wants. This is because we know the string which our response will be tested against!. So in order to get everything good and make the string compare zero we have to innovate a little. Here is our battle plan for a successful compare :-

1. get random number from stack.
2. calculate the displacement just like the program does.
3. add the base of the array (now we know which string will be compared with).
4. input this compare string to a inverted convertion rutine which will get us the DWORD we are looking for.
5. save this DWORD in response32.

Before we make our real emulation we must first make an inverted convert routine. This is one solution which was made and tested in a real assembler like masm or tasm :-

_todword proc near instr = dword ptr 8

push esi ; save stack
push edx ; save registers
push ecx
mov esi, [esp+instr] ; esi is ptr to start of string
xor eax, eax ; eax = 0
xor ecx, ecx ; edx = 0


movzx edx, byte ptr [esi+ecx] ; Grab first byte from string
shl eax, 4 ; multiply existing value by 16
cmp edx, 40h ; if it's A-F, subtract 0x7 first
jle short noncharval
sub edx, 7


lea eax, [eax+edx-30h] ; subtract 0x30 for regular numbers.
inc ecx
cmp ecx, 2 ; 2 total passes (1 time to loop point)
jl short loop_point
pop ecx ; restore registers
pop edx
pop esi ; restore stack

_todword endp

Here we will to push a pointer to the string onto the stack. The return value will be the first byte of the string placed in AL. We will have to increment our string and call this routine 4 times inorder to get the entire DWORD constructed. Since we don't know yet how much code will have to be written as emulating SproQuery we have to place this code somewhere else. Since SproFindFirstUnit was emulated trivially we can use up rest of the space which was used for the original FindFirstUnit. So here is how it will look when it gets implemented to our file.

:00435B00: 53 push ebx
:00435B01: 56 push esi
:00435B02: 8B44240C mov eax,[esp][0000C]
:00435B06: 33C0 xor eax,eax
:00435B08: 90 nop
:00435B09: 90 nop
:00435B0A: 90 nop
:00435B0B: 90 nop ; this is all our emulated SproFindFirstUnit
:00435B0C: 90 nop
:00435B0D: 90 nop
:00435B0E: 5E pop esi
:00435B0F: 5B pop ebx
:00435B10: C20800 retn 00008
:00435B13: 56 push esi ; here our convert routine starts
:00435B14: 52 push edx
:00435B15: 51 push ecx
:00435B16: 8B742410 mov esi,[esp][00010]
:00435B1A: 33C0 xor eax,eax
:00435B1C: 33C9 xor ecx,ecx
:00435B1E: 0FB6140E movzx edx,b,[esi][ecx]
:00435B22: C1E004 shl eax,004
:00435B25: 83FA40 cmp edx,040
:00435B28: 7E03 jle 000435B2D
:00435B2A: 83EA07 sub edx,007
:00435B2D: 8D4410D0 lea eax,[eax][edx][-0030]
:00435B31: 41 inc ecx
:00435B32: 83F902 cmp ecx,002
:00435B35: 7CE7 jl 000435B1E
:00435B37: 59 pop ecx
:00435B38: 5A pop edx
:00435B39: 5E pop esi
:00435B3A: C3 retn

This is the same routine once more but written into the file this time. Now that we have done the work needed in order to get our emulation working, lets continue on the real emulation. Because we will have to emulate more then one address we first have to check which address has been pushed onto the stack. After that we can do whats needed :-

:004360DD: 66813E4272 cmp w,[esi],07242
:004360E2: 7400 je 0004360E4 ; validate packet record
:004360E4: 66837C24140C cmp w,[esp][00014],00C ; is this SproQuery called with 0Ch as query 
:004360EA: 752E jne 00043611A ; no, jump to the next compare (which will be implemented next)
:004360EC: 0FB755F6 movzx edx,w,[ebp][-000A] ; yes it was! get the random number from stack
:004360F0: 6BD209 imul edx,edx,009 ; calculate the displacement just like the program does
:004360F3: 81C2A0CA5500 add edx,00055CAA0 ; add entry of array so we will have a pointer to 
                                            the correct string to compare with
:004360F9: 8B5C241C mov ebx,[esp][0001C] ; get the address of response32
:004360FD: 33FF xor edi,edi ; EDI = 0
:004360FF: 33C9 xor ecx,ecx ; ECX = 0
:00436101: 03D1 add edx,ecx ; ecx will be our counter so move pointer
:00436103: 52 push edx ; push the address to WORD for convert onto stack
:00436104: E80AFAFFFF call 000435B13 ; call our conversion routine
:00436109: 5A pop edx ; fix stack and get our string address back into edx
:0043610A: 88043B mov [ebx][edi],al ; put the converted byte into response32 with the 
                                      displacement of edi
:0043610D: 47 inc edi ; add 1 to the displacement of response32
:0043610E: B902000000 mov ecx,000000002 ; from now on we will move our double DWORD string 2 
                                          bytes ahead to make successful convertion
:00436113: 83FF04 cmp edi,004 ; have we converted all 4 bytes to recover the DWORD?
:00436116: 72E9 jb 000436101 ; no! lets convert ascii value to a byte
:00436118: EB7E jmps 000436198 ; yes! we are done now, jump to clean_exit

We need to add some space between our routine and the clean_exit since there will be more emulation to do. Heres the clean_exit code :-

:00436198: 33C0 xor eax,eax ; SP_SUCCESS
:0043619A: 5F pop edi ; fix stack as required
:0043619B: 5E pop esi
:0043619C: 5B pop ebx
:0043619D: C21800 retn 00018 ; return from caller with stack displacement

When emulating this address we first have to check if its the correct one. If it is then we must find out where to get the string from and where to place it, by finding response32 from the stack. Then we initialize 2 counters. One for the input string and a second for the output. We make a loop with 4 runs to recreate the DWORD. This is done by pushing the offset of the correct string into our routine. The byte located at the first 2 bytes in the string will be placed in AL. After that we move the value in AL to our response address and increment the counters/pointers for next loop.

At address 004360EA we have a jump to the next compare which will be implemented next. Since its not implemented now you might want to change it temporarily to the clean_exit code to avoid program crashes. OK so far so good. By breaking on SproQuery again we will see that the strncmp() will get good. But now it seems the program does not want to start anymore. We know that SproRead is involved as SproQuery works so far.

If you're lazy like me you get it to work the easy way: by trial and error. The WORDs which are missing all either have to do with the program startup or the meter count. 5 minutes time spent on experimenting will make you see that the program will start and the meter will be filled up by inserting a 0xFFFF in the following cells : 0x8h, 0x9h, 0xBh and 0x24h. However we need to finish the emulation of the SproQuery. By breaking on SproQuery in SoftICE again we will see that besides 0Ch then also 20h is called many times. This should be our next target to emulate. The first call to this cell occur at 00423CAE :-

:00423C51 push 0
:00423C53 call _time
:00423C58 add esp, 4
:00423C5B push eax
:00423C5C call _srand
:00423C61 add esp, 4
:00423C64 call _rand ; find random number
:00423C69 cdq
:00423C6A mov ecx, 0Ah
:00423C6F idiv ecx
:00423C71 movzx esi, dx ; put random number into ESI
:00423C74 imul esi, 9 ; calculate displacement
:00423C94 lea ecx, [ebp+var_4]
:00423C97 lea eax, [ebp+var_7C]
:00423C9A lea edx, [ebp+var_B8]
:00423CA0 push 4 ; length
:00423CA2 push ecx ; response32
:00423CA3 push eax ; response
:00423CA4 lea ecx, [ebp+var_4BC]
:00423CAA push edx ; query data
:00423CAB push 20h ; algorithm starting at 20h is the place to do query
:00423CAD push ecx ; packet record
:00423CAE call sproQuery
:00423CB3 lea ecx, [ebp+var_40] ; get the address to store the converted string
:00423CB6 lea edx, [ebp+var_7C] ; get response32
:00423CB9 push ecx ; output, address where the converted string will be placed
:00423CBA add esi, offset a5a8c98f9 ; "5A8C98F9" ; add entry of the array to the displacement
:00423CC0 push 4
:00423CC2 push edx ; input, our response value placed in response32
:00423CC3 call sub_42394F ; run conversion routine
:00423CC8 add esp, 0Ch ; fix stack
:00423CCB lea ecx, [ebp+var_40]
:00423CCE push 4
:00423CD0 push esi ; correct double DWORD
:00423CD1 push ecx ; converted response32 from the dongle
:00423CD2 call _strncmp

Looking at this we instantly see that very little has been changed to the 0Ch cell call. The method used for compare is still the same, and now that we have our inverted conversion routine this should be an easy task. However, the way the random number is saved has changed. There is no more multiplying going on after the SproQuery and the entry to the compare string array is added in the middle of the initialization of the string conversion rutine. At 00423C71 we see that the random number we need is copied into ESI. The instruction after calculates our real displacement. Tracing ESI in SoftICE from that address shows that no other instruction will touch the value of this. The first change happening to ESI will be when we push it onto the stack as a default operation of the original SproQuery routine :-

:004360C0 push ebx
:004360C1 push esi ; push the displacement onto stack
:004360C2 push edi
:004360C3 mov eax, [esp+arg_0]

All this will make it easier for us as the program takes care of the displacement calculation now. So lets revise our battle plan and start our emulation:-

1. get displacement from stack.
2. add the base of the array so we will get the string the strncmp will compare with.
3. input this compare string to our inverted convertion routine which will get us the DWORD we are looking for.
4. save this DWORD in response32.

This emulation will be placed right after the 0Ch emulation. So incase its not the 0Ch cell that will be called then we should find out if its 20h. Our cell 20h emulation :-

:0043611A: 66837C241420 cmp w,[esp][00014],020 ; is this SproQuery called with 20h as 
                                                 query address?
:00436120: 7576 jne 000436198 ; no! jump to clean_exit
:00436122: 8B5C241C mov ebx,[esp][0001C] ; yes it was! get the address of response32
:00436126: 0FB61424 movzx edx,b,[esp] ; get the displacement from stack
:0043612A: 81C260CB5500 add edx,00055CB60 ; add entry of array so we will have a pointer to 
                                            the correct string to compare with
:00436130: 33FF xor edi,edi ; EDI = 0
:00436132: 33C9 xor ecx,ecx ; ECX = 0
:00436134: 03D1 add edx,ecx ; ecx will be our counter so move pointer
:00436136: 52 push edx ; push the address to WORD for convert onto stack
:00436137: E8D7F9FFFF call 00435B13 ; call our conversion routine
:0043613C: 5A pop edx ; fix stack and get our string address back into edx
:0043613D: 88043B mov [ebx][edi],al ; put the converted byte into response32 with the 
                                      displacement of edi
:00436140: 47 inc edi ; add 1 to the displacement of response32
:00436141: B902000000 mov ecx,000000002 ; from now on we will move our double DWORD string 2 
                                          bytes ahead to make successful convertion
:00436146: 83FF04 cmp edi,004 ; have we converted all 4 bytes to recover the DWORD?
:00436149: 72E9 jb 000436134 ; no! lets convert another ascii value to a byte
:0043614B: EB4B jmps 000436198 ; yes! we are done now, jump to clean_exit

This emulation is almost like the other with the exception of the displacement which was kindly done for us allready. The clean_exit is of course the same where we set SP_SUCCESS and fix the stack as required. Now our 2nd cell compare is also working as required. Our program will start now, meter is filled up thanks to the WORD's which was inserted and used by SproRead. Having spent hours or even days looking in SoftICE at this program going in and out of calls, tracing response addresses and so on, we will notice strange call's which were not identified by Killer_3k's flirt sig. A example of this is the code just after the strncmp at the first SproQuery of the 20h cell :-

:00423CD2 call _strncmp ; the first 20h cell call
:00423CD7 add esp, 0Ch ; fix stack
:00423CDA mov esi, eax ; store return code from the string compare
:00423CDC lea eax, [ebp+var_4BC]
:00423CE2 push eax
:00423CE3 call sub_435BF0 ; unknown call
:00423CE8 test ax, ax ; what testing ?
:00423CEB jnz loc_423D7C

Looking at this and looking inside the call shows code that is unlike the other API's. What makes me stop at this is the return code in EAX which is 0x00120003. Does this mean return code 3, no dongle found?. This return code even gets tested if its zero so this should really turn on our warning lights. Applying the SentinelLM flirt signatures makes IDA identify this function :-

00423CD2 call _strncmp
00423CD7 add esp, 0Ch
00423CDA mov esi, eax
00423CDC lea eax, [ebp+var_4BC]
00423CE2 push eax
00423CE3 call _RNBOsproFindNextUnit@4
00423CE8 test ax, ax
00423CEB jnz loc_423D7C

Yes! it was a dongle check. Notice that the API differs alot from the other dongle API's which all starts with Spro*. This must be an API which only comes from SentinelLM and not the SuperPro library itself. Lets remove it while we're at it. Replacing the first code inside with a XOR EAX, EAX and RET should do the trick. SP_SUCCESS and no more bad stuff. Testing the other SentinelLM API's in SoftICE which starts with _RNBO* gives nothing so we will leave those. Making a random license and wanting to calculate the license code gives us another error.

Looking again at SproRead and SproQuery didnt give any results. The SproRead should have been fixed fully by now. Checking at what addresses sproQuery would be called from in IDA gives us a few more placed which ought to be emulated. However I could not get it to break any of these places like 0043C5D2. Lets go back to the start and try to get a overview of this mess. The meter key is a dongle. Its actually a Sentinel SuperPro. If there is a meter, it has to be decremented at some point in time when you have made a key. So actually our initial problem is our solution! Somewhere in time the dongle WORD's has to be changed.

Looking at our API list and seeing what API's are left makes us see two API's SproOverWrite and SproDecrement. First we take SproOverWrite and set a breakpoint on it in SoftICE but no breaks. Then SproDecrement and we will see that during the license generation it will be called several times. Here is a example :-

004242F9 push 9 ; address
004242FB lea eax, [ebp+var_410]
00424301 push 34C8h ; write password
00424306 push eax ; packet record
00424307 call sproDecrement
0042430C test ax, ax
0042430F jnz short loc_42433A ; if not SP_SUCCESS then do bad stuff

Return code 3 is no good to us! We can do better then that. Since we don't have any dongle and we don't want to decrement anything, emulating it trivially like with SproFindFirstUnit should be enough! Doing so makes the program generate a nice license string to us and we can finally conclude that we won over the program.

Final Notes

This program wasn't easy to reverse. However, it could have been alot harder then it is. A very big implementation weakness is the checking of SproQuery right after the call. SproQuery is the hardest API to reverse as it will differ for every program, so making direct compares like strncmp() is like doing a favour to the reverser. So all in all we must conclude that Rainbow Tech. failed at protecting this. The keygen makes keys now but there is more which I havent discussed here. Rainbow Tech. wisely did a protection against abuse by supplying every company which buys SentinelLM SDK with a specific serial. This is used while installing the SDK. The serial holds a unique key called a Vendor ID. This Vendor ID can be compared like FLEXlm's seeds which are unique for every vendor.

The SDK installer patches all files which has something to do with generation of code/keys so our Wlscgen.exe also gets patched. Making new licenses will use that Vendor ID, so at the current state you can't use this for more then your own programs which of course use the same Vendor ID as Wlscgen. The emulation makes the program able to make keys. This program was reversed under Windows 2000 SP0 so if you use another OS there might be another program flow. Looking at the api's and where they get called from shows that I did not "secure" every call. There are still addresses which might need more, especially SproQuery addresses.

I could not get any of these to get run but it might have something to do with special licensing options getting set before they get triggered. If you should experience that you should have a good chance of fixing them, as they use the same method of calling and comparing. The code in my emulation also could need a bit of optimizing to reduce size. This could be a challenge for you!.

Ob Duh

This essay discusses SentinelLM, and god forbid you should find any target protected with this scheme.