FlexiSIGN PRO and it's family of products from Amiable - by Goatass

"When I read this good tutorial by goatass, I sat back with a cold beer and pondered, why do perfectly capable developers keep on entrusting their protection strategies to the dongle sellers?, it is completely baffling. Look around my site just a little, you can only conclude that dongles are simply a total waste of money, I wonder if any dongle customer has ever thought to bring legal action against these lying marketeers?, there is surely a good case that dongles are unfit for their purpose". "When you re-write sproRead() remember that my example emulation code does initially depend on the stack (to get the read address), goatass noted some problems doing this, probably because of compilation intracacies, sproRead() doesn't always seem to preserve EBX as you can see here. Of course if you have access to my private page you'll know that there are ways and means of emulating all the Sentinel routines in just 1 place, goatass even shows some of the key code :-), say that word 0A copied to packet record structure+30h looks incriminating". "Tutorial slightly edited by CrackZ".

Cracking the Installation Password

At the first look, this protection seems to be very hard, however, after digging into it for a while I discovered that it's fairly easy. So let's get started. Launching the Autorun.exe file gets you to a screen where you need to enter a user code and a 20 digit password. So we put a few breakpoints and trace a bit and soon find out that it calls some function from Atilib.dll, with our Password as a parameter. OK, so we load the symbols for Atilib in SoftICE, the function it calls is named GetInfoByMainPkgPasswd. Let's trace in it :-

After some useless stuff we get to this place :-

:1000C443 PUSH EAX ; Point to a buffer
:1000C444 PUSH ECX ; Point to our password
:1000C445 CALL DecryptInfoCode3

Wee see that DecryptInfoCode3 decrypts an 8 byte string from our password. So we need to find out what it does with the decrypted string. Luckily we have an example serial, it's not for FlexiSign but it should decrypt to some logical values, so we put it in and continue.

:1000C44A MOV EDX, [ESP+88h+var_6F]      |
:1000C44E MOV ECX, [ESP+88h+var_6F+1]    |
:1000C452 XOR EAX, EAX                   |
:1000C454 AND EDX, 0FFh                  |
:1000C45A MOV AH, [ESP+88h+var_70]       |
:1000C45E AND ECX, 0FFh                  | This code takes the first 4
:1000C464 OR EAX, EDX                    | bytes of the decrypted 
:1000C466 MOV EDX, [ESP+88h+var_6F+2]    | string and reverse their order
:1000C46A SHL EAX, 8                     |
:1000C46D OR EAX, ECX                    |
:1000C46F AND EDX, 0FFh                  |
:1000C475 SHL EAX, 8                     |
:1000C478 OR EAX, EDX                    <- EAX contains the reverse ordered 4 bytes
:1000C47A MOV EDX, [ESP+88h+arg_0]       
:1000C481 MOV ECX, EAX                   <- mov it to ECX
:1000C483 ADD ESP, 8                     
:1000C486 SHR ECX, 2                     
:1000C489 MOV [EDX], ECX                 <- do "? ECX", it's our user code.

And the next part :-

:1000C48B MOV ECX, [ESP+80h+arg_4]       |
:1000C492 MOV EDX, [ESP+80h+var_6B]      |
:1000C496 MOV [ESP+80h+var_8], -1        |
:1000C49E SHL AL, 6                      |
:1000C4A1 MOV [ECX], AL                  |
:1000C4A3 MOV EAX, [ESP+80h+var_6F+3]    |
:1000C4A7 AND EAX, 0FFh                  |
:1000C4AC AND EDX, 0FFh                  |
:1000C4B2 SHL EAX, 8                     |
:1000C4B5 OR EAX, EDX                    | Takes the second part of the decrypted string.
:1000C4B7 MOV EDX, [ESP+80h+var_6B+1]    |
:1000C4BB SHL EAX, 8                     |
:1000C4BE AND EDX, 0FFh                  |
:1000C4C4 MOV BL, [ECX]                  |
:1000C4C6 OR EAX, EDX                    |
:1000C4C8 MOV EDX, [ESP+80h+var_6B+2]    |
:1000C4CC SHL EAX, 8                     |
:1000C4CF AND EDX, 0FFh                  |
:1000C4D5 OR EAX, EDX                    <- EAX contains the reverse ordered next 4 bytes
:1000C4D7 MOV EDX, EAX                   |
:1000C4D9 SHR EDX, 1Ah                   |
:1000C4DC OR BL, DL                      | Nothing important
:1000C4DE MOV EDX, [ESP+80h+arg_8]       |
:1000C4E5 MOV [ECX], BL                  |
:1000C4E7 MOV ECX, EAX                   <- mov it to ECX
:1000C4E9 SHR ECX, 0Ah                     
:1000C4F2 MOV [EDX], ECX                 <- yep, that's our product code.

So now we know what has to be in the decrypted string, but how the hell can we encrypt it?. Digging in the DecryptInfoCode3 function we quickly find out that reversing it won't be easy, so we'll try to examine it using our beloved IDA. Now wait a minute, what's that function in there :- "EncryptInfoCode3" wonder what that might be. The idiots left the encryption function in the dll and saved us all the trouble. Playing a bit with the function we find that the syntax is quite similar to DecryptInfoCode3 :-

push <length of string>
push <pointer to buffer>
push <pointer to string>
call EncryptInfoCode3

OK, so all that left now is to make a small program that shl's our product code with 0Ah and our user code with 2, puts them in the right places in the string and calls EncryptInfoCode3 to get the password.


First off, I want to say that I have purchased this product and I have an original dongle and license for FlexiSIGN PRO. Amiable is a company that makes high end software for the sign making industry. A tutorial written by CrackZ a while back about CASMate was the first paper on the protection used by this company. I have also cracked Inspire 1.6 by the same company but didn't have the time to write a paper on it. Anyway this application uses the Sentinel SuperPro dongle along with a user number/password which will decide which program you are allowed to install. In this tutorial I will show you how to find and emulate the Sentinel dongle and how to bypass any of the related checks.


http://www.woodmann.com/crackz/ - All the Sentinel tutorials (and everything else).
SoftICE, Hex Editor, IDA or W32Dasm.
13/9/1 : Note from CrackZ. This FTP is no longer anonymous access.

Lets Go!

OK, after installing the program we look at the files. Run the App.exe which is the main program, the App2.exe is the Product Manager (we will worry about that later). What do you see?, it loads up some stuff and then gives an error message saying :-

"This application requires that a Hardware key be installed, but none was found".

This tells us that the program searched our ports to see if there was a dongle attached and it failed. Having read the Sentinel manual (address above) we know that the program must call sproFindFirstUnit() after initializing the packet record with sproInitialize(). While we are talking about the packet record, what it is is a structure that holds and will hold dongle information, from the manual again we know that this packet record MUST be pushed on to the stack before any other dongle API can be called. That is a good thing for us because it provides us with a land mark so we can find all the API calls to the dongle.

We also know that when a dongle API fails it will return error code 3 in EAX. That is another helping point, we can look at the return values from calls and see if they return 3 in EAX, another thing to help us is the dongle lag, when the program tries to access the dongle and it's not there it will hang for a few seconds, when that happens you can pretty much say that the CALL was a dongle API call. Before the actual call to the dongle API the program MUST check if it has a valid packet record before proceeding to the dongle call, that is what we will look for to find the dongle calls. It looks something like this :-

JE packet_record_valid
MOV AX, 0002
RET 000C

7242 is the packet record signature and it must be checked, if it fails the error code that is returned in EAX is 2 which means Invalid Packet. OK, enough with the theory, lets get to business. After looking at which files are called by the program we will open each one of them in W32Dasm and look at what functions they import and export. We come to a file named Sx32w.dll, after opening it we see in the Export list that it exports all the functions that we saw in the Sentinel manual. Interesting, if you HEX edit the file you will see towards the end "SentinelSuperPro WIN32 DLL", this is the DLL that is provided by Rainbow to developers to use with their applications, and it's also helpful to us because we now have a central place calling the dongle and we know in order to call the dongle this DLL must be used.

What we will do is put our emulator in this file and make all the dongle modifications in one place. The key to cracking a dongle protected program is tracing different paths until you get to the correct one. OK, so how do we break on the dongle API calls?, what I did, since the APIs are called from a bunch of different DLLs, I opened Sx32w.dll and looked up the offset to sproFindFirstUnit() then I hex edited the file, placed the cursor at that offset and replaced the first byte with CCh which is INT 3. You must rememeber the byte that you replaced so we can replace it back eventually. In SoftICE place a breakpoint on INT 3 i.e. BPINT 3 and exit to windows. Now run App.exe and wait for the break. SoftICE breaks, now we do E EIP to edit the byte at the Instruction Pointer and change the CC back to what it was before and press ENTER. Now just BPX EIP and exit SoftICE and go back to the hex edit of the file and change back the CCh to the original byte. Now we run the App.exe again and we will have a break on sproFindFirstUnit(). When it breaks it looks like this :-

Exported fn(): RNBOsproFindFirstUnit - Ord:000Bh
:004072B0 PUSH EBX
:004072B1 PUSH ESI
:004072B6 OR EAX, EAX <-- checks if the program PUSHed the packet record.
:004072B8 JNE 004072C3
:004072BA MOV AX, 2
:004072BE POP ESI
:004072BF POP EBX
:004072C0 RET 8

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

:004072C3 PUSH EAX
:004072C4 CALL 004010D0
:004072C9 MOV ESI, EAX
:004072CB CMP WORD PTR [ESI], 7242 <-- this is the trademark check for the
:004072D0 JE 004072E0 <-- validity of the packet record.
:004072D2 MOV AX, 2
:004072D6 POP ESI
:004072D7 POP EBX
:004072D8 RET 8

The code that follows is the actual call to the dongle but we don't really care about it because at this point there is no return value from the dongle only an error code of 3 if not present or 0 if present. What we will do is, NOP out the JNE instruction at 004072B8 and change the MOV AX, 2 to MOV AX, 0, like so :-

:004072B8 90 NOP <-- do away with the useless JNE.
:004072B9 90 NOP
:004072BA 66B80000 MOV AX, 0 <-- force return to be 0 - successful.

That is it for taking care of the first dongle check. Pretty simple eh?. Well don't get too happy there is more to do. After we hard code this patch using a Hex Editor, and I'm going to assume you know how to do that, we will continue to the next dongle call which will be sproRead(). This is where most of the work will take place, but it's not too bad. Lets start by setting up a BPINT on INT 3 like we did before with sproFindFirstUnit() so we can get to the code in SoftICE. Once we do that we break here :-

Exported fn(): RNBOsproRead - Ord:0002h
:00407480 PUSH ESI
:00407481 PUSH EDI
:00407482 MOV EAX, DWORD PTR [ESP+0C]
:00407486 OR EAX, EAX
:00407488 JNE 00407493
:0040748A MOV AX, 2
:0040748E POP EDI
:0040748F POP ESI
:00407490 RET 0C

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

:00407493 PUSH EAX
:00407494 CALL 004010D0
:00407499 MOV ESI, EAX
:0040749B CMP WORD PTR [ESI], 7242 <-- trademark check again.
:004074A0 JE 004074B0
:004074A2 MOV AX, 2
:004074A6 POP EDI
:004074A7 POP ESI
:004074A8 RET 0C

:004074C5 MOV EDI, DWORD PTR [ESP+14] <-- EDI is holding the buffer to store
:004074C9 OR EDI, EDI <-- the returned word form the dongle.

:004074E0 MOV [ESI+30], 000A
:004074E6 MOV AX, WORD PTR [ESP+10]
:004074EB MOV WORD PTR [ESI+34], AX
:004074EF PUSH ESI
:004074F0 CALL 00405E10 <-- this leads to the sproRead() call, we will emulate it.
:004074F5 OR AL, AL <-- is AL = 0 ?.
:004074F7 JNE 00407510 <-- if not bye bye.
:004074F9 MOV AX, WORD PTR [ESI+36] <-- return WORD from the dongle.
:004074FD MOV WORD PTR [EDI], AX <-- put that WORD into the buffer.

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

:00407500 MOV AX, WORD PTR [ESI+06] <-- holds the error code.
:00407504 PUSH EAX
:00407505 CALL 00406AC0 <-- checks error code, must return 0 in EAX to succeed.
:0040750A POP EDI
:0040750B POP ESI
:0040750C RET 0C

The code below comes from cm32.dll which is the DLL in charge of the main protection. It talks to the dongle and figures out the return values. Here is some explanation :-

:10002F60 CMP DWORD PTR [EBP-04], 17 <-- check if we reached CELL 17.
:10002F64 JGE 10002FB0
:10002F66 MOV ECX, DWORD PTR [EBP-04] <-- current CELL number.
:10002F69 MOV EDX, DWORD PTR [EBP+0C] <-- return buffer.
:10002F6C LEA EAX, DWORD PTR [EDX+2*ECX] <-- make room for the next WORD.
:10002F6F PUSH EAX
:10002F70 MOV ECX, DWORD PTR [EBP-04] <-- CELL to read from.
:10002F73 ADD ECX, 8 <-- skips the CELLs 1-7.
:10002F76 PUSH ECX
:10002F77 MOV EDX, DWORD PTR [EBP+08] <-- packet record.
:10002F7A PUSH EDX

* Reference To: SX32W.RNBOsproRead, Ord:0001h

:10002F7B CALL 100281C7 <-- call sproRead().
:10002F80 AND EAX, 0000FFFF <-- polish the error code.
:10002F85 MOV DWORD PTR [EBP-08], EAX
:10002F88 CMP DWORD PTR [EBP-08], 0 <-- was it successful?.
:10002F8C JE 10002FAE <-- good cracker.
:10002F8E CMP DWORD PTR [EBP-08], 7
:10002F92 JNE 10002FA0
:10002F97 MOV [EAX+04], 8
:10002F9E JMP 10002FAA

This code is looped from CELL 8 to CELL 17 (in HEX). After we have all the dongle data stored in memory we continue on, the code below is the CALL that called the mess above :-

:100029A5 CALL 10002F45 <-- called the above code (sproRead() loop).
:100029AA TEST EAX, EAX <-- return value from the CALL must be 1.
:100029AC JNE 100029B2 <-- good jump.
:100029AE XOR EAX, EAX
:100029B0 JMP 100029FA

:100029B2 MOV ECX, DWORD PTR [EBP-38] <-- gets one of the returned WORDs from the dongle.
:100029B8 MOV DWORD PTR [ECX+428], EDX
:100029BE MOV EAX, DWORD PTR [EBP-38] <-- buffer to the returned dongle data.
:100029C1 MOV ECX, DWORD PTR [EAX+428]
:100029C7 CMP ECX, DWORD PTR [EBP+0C] <-- compares the USER NUMBER from the program with the one returned from the dongle.

:100029CA JNE 100029D3
:100029CC MOV EAX, 1 <-- good flag.
:100029D1 JMP 100029FA

At this point ECX holds the returned USER NUMBER from the dongle and [EBP+0C] holds the correct USER NUMBER, well actually the one you used when you installed the program. So by simply doing D [EBP+0C] we can find out one of the return values. We write it down and also it's position from the first WORD from the dongle. After we return from this call we come to a check to see if this all suceeded :-

:10002428 CALL 10002974 <-- the call we just came back from.
:1000242D TEST EAX, EAX <-- EAX must be equal to 1.
:1000242F JE 100024DB <-- bad jump.
:10002435 MOV [EBP-14], 00000000 <-- good flag.
:10002491 MOV ECX, DWORD PTR [EBP-28]
:10002494 CMP DWORD PTR [ECX+04], 8 <-- check a return value from call.
:10002498 JNE 100024A3 <-- bad jump.
:1000249A MOV [EBP-30], 1 <-- good flag.
:100024A1 JMP 100024B2 <-- good jump.

The check above is not checking dongle data, it's some check from the call to see if it suceeded. Once the above compare is done and the JMP 100024B2 is executed we are done for this one part. Well it's time to look at the sproRead() function and how to emulate it. When looking at Sx32w.dll and scrolling to the sproRead() function we see all the check it does to make sure the packet record was initialized and that everything else is good before it gets to the actual reading. The actual reading begins at address 004074E0 :-

:004074E0 MOV [ESI+30], 000A
:004074E6 MOV AX, WORD PTR [ESP+10]
:004074EB MOV WORD PTR [ESI+34], AX <-- Developer ID - (note from CrackZ).
:004074EF PUSH ESI
:004074F0 CALL 00405E10 <-- does the actual read, not interesting.
:004074F5 OR AL, AL <-- AL must be 0, if it's 3 it failed.
:004074F7 JNE 00407510 <-- jump if reading failed.
:004074F9 MOV AX, WORD PTR [ESI+36] <-- return the dongle WORD.
:004074FD MOV WORD PTR [EDI], AX <-- save it to buffer.

:00407500 MOV AX, WORD PTR [ESI+06] <-- error code again.
:00407504 PUSH EAX
:00407505 CALL 00406AC0 <-- function to clean up the error code.
:0040750A POP EDI <-- EAX must be 0 now.
:0040750B POP ESI
:0040750C RET 0C

The above code does the reading from the dongle and stores the returned WORD into EDI and then checks the error code again and returns in EAX the error code to the calling function, so when we RET 0C from this function AX must be 0. OK on with the emulator. Looking at some of CrackZ's tutorials on Sentinel, we see that he uses pretty much the same emulator for all the applications he cracked with Super SentinelPro. (See here for the code). So I took it and implemented it as my sproRead() function but it kept crashing. So here is what I did :-

:004074E0 PUSH EBP <-- we save EBP.
:004074E1 CALL $+5 <-- gets the Delta Offset.
:004074E6 POP EBP <-- puts the Delta Offset into EBP.
:004074E7 LEA EDX, [EBP+4C1Ah] <-- this is where my dongle data is in the file.
:004074ED POP EBP <-- fix the stack again otherwise it crashes.
:004074EE SHL ECX, 1 <-- ECX holds the WORD we are reading.
:004074F0 MOVZX EAX, WORD PTR [ECX+EDX] <-- read the WORD from simulated memory.
:004074F4 MOV DX, 400h <-- hard code a good return code.
:004074F8 MOV [ESI+6], DX
:004074FC NOP
:004074FD MOV [EDI], AX <-- store dongle code in EDI (buffer).
:00407500 MOV AX, [ESI+6]
:00407504 PUSH EAX
:00407505 CALL sub_406AC0 <-- cleans up error code, EAX must be 0 when leaving.
:0040750A POP EDI
:0040750B POP ESI
:0040750C RETN 0Ch <-- return to the caller function.

Since this file we are putting our emulator inside is a DLL that means it can be loaded to a different memory page every time the program runs. When running App.exe it gets loaded to 10000000 and when App2.exe (product manager) runs it gets loaded again but this time to 70000000. So we can't hard code the address of our dongle data in the file. If you don't follow me, I took the dongle data that I found and put it into the file right after the last byte of the .RELOC section. From 004074E0 to 004074E6 we get the Delta Offset which is our EIP + Loaded address of DLL, so no matter where the DLL is loaded the CALL $+5 will always calculate our correct position. At 4074E7 I take my current position and add 4C1Ah to it, this will put me right at the first WORD of my simulated dongle data that I hard coded to the file.

123A 223A 323A 423A 0000 0000 0000 0000 <-- end of .RELOC section.
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0000
FFFF FFFF 0100 0200 0100 0000 005E 1A00 <-- begining of my dongle data.

The second POP EBP is required here otherwise the stack gets screwed up and you will crash. After that we get to SHL ECX, 1, this takes the number that comes in as a parameter to the function which tells it which word in the dongle we need to read and we SHL it with 1 (same as multiplying by 2) to get the correct WORD, since a word is 2 bytes. Then we get to MOVZX, what this does is moves the WORD at [ECX+EDX] to EAX and extends the zeros meaning fill the rest with zeros. ECX is the WORD we need to read and EDX is the address to my dongle data, so this says get the first WORD from the dongle data.

After this we are done with the emulator, at address 4074F4 we put in the return code 0400 and then we take AX which holds our dongle WORD we got a second ago and place it into the buffer that was pushed as a parameter to this function. Then we get to the CALL at 407505 which PUSHes 0400 and cleans it up to return only AL which will be 00. If the read function was to fail the error code would be 0403 and after that cal1l, AL would be 03 and we know from the documentation that this means "Key not found".

We now have a completely emulated sproRead() function, that will run correctly no matter where the DLL is loaded. Next we have sproQuery(), this is very hard to make a generic emulator for so we just trace it with SoftICE and see what's going on. We look at cm32.dll and see what it does. Its really simple, all you have to do is make sure the sproQuery function returns 00 in AL and you are set, there are no checks of the returned values. cm32.dll file is very much like PMCore.dll which is the main DLL for the product manager so you will have to make some patches to this file as well to make sure the sproFindFirstUnit() and sproQuery() calls are fixed.

OK, so we did all this and we run App.exe but what is going on here, everything we changed on cm32.dll and PMCore.dll is gone!, OK it's probably some kind of file checker. What we do is set a BPX CreateFileA and run App.exe again and when it breaks we do one F10 and D *(ESP+08) and we see what file is being opened. There are alot of files opened before cm32.dll so we F5 until we see cm32.dll being opened and we F12 a few times until we are in Fscore.dll, we then F10 some more until you get to something like :-

JZ good

I can't remember the code exactly, but it's like this, if you do a D CL and D DL you will see it's comparing the file byte by byte with an image it has stored somewhere, so just patch the JZ good to JMP and it will not change your patched file. Now run App2.exe and do the same thing to fix the check it has on PMCore.dll. Run App.exe and it's working :-). Now when you do Rip n' Print everything works fine, except there is something there that forces the printer to print white lines on the images text. So we open PMCore.dll and look for some clues and we see something called IsDemoVersion() well we must make this fucntion succeed and we are good to go, the good flag here is 01 so check it out, it's really easy.


This protection was fun but it wasn't as hard as I expected it to be. Sentinel SuperPro is a decent dongle but it has too many fingerprints that allows us to find it with no problem. This program did not use the sproQuery() correctly at all and barely used sproRead() other than to check like 2 things in memory which made it very easy to emulate. The key with dongle reversing is to trace and follow paths, if you don't have the nerves to do this, dongles are not for you. I like dongles, they are fun to crack, so stick with it and you will see that most dongle protected applications are very easy to reverse.

Greets to my pals :- zip, CrackZ, GzA, int13h.


Return to Dongles Return to Main Index

© 1998, 1999, 2000 Hosted by CrackZ, goatass 16th June 2000.