"lc_init() should only be used with license generators, and should not normally be used in applications shipped to clients. Use lc_new_job() instead, as it offers enhanced security." -- FLEXlm reference manual - v6.1.

FLEXlm v6.1 - lc_new_job()
Written by dan

Introduction

There are now quite a few essays written on FLEXlm. These essays should be read before reading this one. This essay might help you if you are trying to get encryption seeds for a target that uses FLEX v6.1 and you can't get them using the methods in previous essays. That was where I was before doing the things in this essay. If I would have read this essay back then, it would have helped me. I have tried to show not only what I found, but how I found it. The how is maybe less relevent to this subject.

The how is nothing special - these are common techniques. You probably could have found this stuff out on your own without having to know my methods. But maybe you will find some technique I used helpful for your cracking in general. I just tried to write something that I would have enjoyed reading.

Tools required

SoftICE, W32Dasm.
dumpbin / lib (Micro$oft - sorry!).
egrep (PC users - search cigwin).
perl (cigwin again).

Target's URL/FTP

A program that integrates FLEXlm v6.1 and calls lc_new_job(). Refer to past essays - those targets may have been updated.

Program History

FLEXlm has been broken before. They have since enhanced security.

Essay

Part 1 - learning curve and confusion

Initial interest in FLEXlm for me started a week or so ago. I read some essays, found some targets and applied the essays techniques to these targets. - nothing special. Then I ran into a problem: I had gotten the necessary "keys and seeds" needed to generate a license for a target (target A). There was a new version of target A (target B). I could not get the "keys and seeds" for target B using the same techniques for target A. (Note: target A used FLEXlm 6.0. Target B used FLEXlm 6.1).

So this sucked! I queried for answers and got some hints but no one said "you need to xor your answer with 0xff". I guessed I would have to either wait for an answer or investigate myself. About this time I replaced the target B seeds and keys with target A's. There was no logic in doing this - I was just flailing for answers. I generated a license for target B using target A's seeds and keys. This worked!! (after upgrading the "version" on the license entry).

At this point, if you understand what I wrote you might be asking - "You got the right license so time to move on right?" Maybe thats what I should of done. This was confusing to me. Target A and B definitely had different vendorcode structures - so how could the old target A info work on target B?. As another experiment, I took the "encryption_seeds" from target A and the VENDORKEYS from target B and generated a valid license for target B (again no logic in doing this). This was confusing also.

All through this process I was thinking that I am not really understanding the license generation process and what the SEEDS and VENDORKEYS are really used for.

Part 2 - a little less confusion but still - confusion

After reading more essays and some discussions with Nolan Blender, I started to realize that if you got the right license i.e. no error -8 that means you have the right "ENCRYPTION_SEEDS". The VENDORKEYS have nothing to do with this. I now understand that the encryption seeds are used as "keys" to a function that takes information on the license entry and makes an authenticator. That authenticator is compared to the one on the license entry line. It literally comes down to a character compare. If they don't match you get -8 error. This prevents someone from for example changing the expiration date in an otherwise valid license file.

Another essay should explore this authenticator function. I still don't quite understand the importance of the VENDORKEYS as far as a cracker generating a valid license. I think that as long as they follow certain checks, the actual values are not used to create the authenticator that you see in the license file. I did start to understand the importance of the ENCRYPTION_SEEDS in license generation though. So now I knew why my target A info worked. It was because target A was giving me the right ENCRYPTION_SEEDS. Note that there are no other right ENCRYPTION_SEEDS - these were the ones.

Therefore, target B was doing something funny with its vendorcode.data and hiding the ENCRYPTION_SEEDS somehow. So what this means is that it is perfectly valid to have the same ENCRYPTION_SEEDS with different VENDORKEYS - which is what target A and target B had. The problem then is one of two things :-

1. The vendorcode structure that I got from lc_init did not have the same encoding for the encryption seeds. That is es1 != vc.data[0]^vk5 and es2 != vc.data[1]^vk5.

2. The old encoding was true but the "real" vc.data was hiding somewhere else. Why? Because I KNEW the encryption seeds and KNEW vk5 and KNEW vc.data and this was TRUE : es1 != vc.data[0]^vk5 and es2 != vc.data[1]^vk5.

These things would have to be tested out. It should be noted at this point that having the right ENCRYPTION_SEEDS was a pure gift. This is rare that you have the answer before knowing where to find it. Another style of essay might not mention this fact that I had the right answer from the start. This makes this kind of attack 100% easier and should be considered less of a crack than someone who didn't have the answer. It is an important point. Another implication that the seeds worked is that FLEXlm itself was being configured correctly when I generated the license. This gave me confidence that my target was not using some of the flex-able features of FLEXlm.

I admit I don't quite understand this - it has something to do with lc_set_attr calls though. You on the other hand will probably not have the right encryption seeds (which might be why you're reading this). That's OK - follow the steps of the previous essays, which is what I did in the first place. You should have generated a license file with your features, however when ran you will get a -8 error from FLEXlm (bad seeds). You need this to be able to have the right code execute. You should note that FLEXlm is staticly linked to this target, and that the FLEXlm SDK contains the libraries that my target used. This is unusual for most of the cracking I have done. Having full documentation and source code, examples, libraries etc. is pretty overwhelming.

Part 3 - test some things out - Program flow

I wanted to see the flow of the program. Which routines were called and in what order. I put breaks on lc_init, lc_checkout, and l_sg (described below), then ran the program, watching the order things were called. Also observing return values. The API documents helped to interpret what to expect for return values. The way I originally found these routines in my target was partly by comparing the "signature" of the .dll code to my target. The flow went something like this :-

call lc_init { ...

:00440008 PUSH EAX <-- param.
:00440009 PUSH ESI <-- param.
:0044000A CALL 00441ECC <-- call l_sg(job,code,??).
:0044000F ADD ESP, 0C <-- restore.
:00440012 CMP D, [EBP-34], 87654321 <-- compare seed to default.
:00440019 JE 00440024
:0044001B CMP D, [EBP-30], 12345678 <-- compare other seed.
:00440022 JNE 0044004B

... }

call lc_checkout { ...

... call l_sg

... if(bad seeds) return -8

else return 0 }

Note that from reading other essays, I knew that the check for default values was a good place to look for seeds. I knew lc_checkout was another important location for getting "features". At this point, I decided to trace through the l_sg call inside lc_init. I printed out a copy of it and traced through it, writing notes on the copy. I should note at this point I was comparing the W32Dasm output of the program to my "annotated object dissassembly" (see appendix). The first part of l_sg was :-

l_sg(???,name,vendorcode)

:00441ECC PUSH EBP <-- after this push, 8 extra bytes on stack before params
:00441ECD MOV ECX, D, [ESP+08] <-- this is first param to l_sg.
:00441ED1 MOV EBP, ESP
:00441ED3 SUB ESP, 04 <-- we're going to use 4 bytes in this routine.
:00441ED6 MOV EAX, D, [ECX+50] <-- first param is a ptr.
...
:00441EDC TEST B, [EAX+00000141], 80 <-- OK, another dereference, confusing!.
:00441EE3 JE 00441F03 <-- test failed here on my trace.
:00441EE5 CMP D, [004F0274], 00000000 <-- this is a call vector.
:00441EEC JE 00441F03 <-- vector zero so don't call it.
:00441EEE PUSH [EBP+10] <-- set up params.
:00441EF1 PUSH [EBP+0C]
:00441EF4 PUSH ECX
:00441EF5 CALL, D, [004F0274] <-- vector call!.
:00441EFB ADD ESP, 0C
:00441EFE JMP 00441FA8
:00441F03 MOV EDI, D, [EBP+10] <-- jump here edi = vendorcode.
:00441F06 MOV ESI, D, [EBP+0C] <-- this is vendor name.
:00441F09 LEA EAX, D, [EDI+0C] <-- vendorcode+0c=vendorkeys
:00441F0C PUSH EAX
:00441F0D PUSH ESI
:00441F0E CALL 0044CC52 - this is l_key(name, keys).

Now, look at the first tests. It is testing a flag (which I failed), testing a vector for 0 and if not 0 will call the vector stored there. To me a call to a vector is suspect. Especially after that convoluted test!, I was glad that the program did not go to that vector. It looked like something scary was going to be there. I decided to ignore this part of l_sg for a while. I wanted to get more comfortable with l_sg. I traced through the remaining part and made notes on my printout. First I noted the parameters passed to it. I wasn't sure what the first one was but the other two were obvious. Then I wanted to know what the outputs were.

This code was generated from C so one output could be EAX, but the calling routines would trash eax. It basically came down to :-

:00441FA2 MOV D, [EDI+08], EDX <-- store something in vc.data[1] (seed 2).
:00441FA5 MOV D, [EDI+04], EAX <-- store something in vc.data[0] (seed 1).
restore and return

There are calls by value (value on stack) and calls by reference (pointer). This was a call by reference that modified the vendorcode that was passed to it. However, the values placed here were the same "wrong" seeds that I had gotten on my initial attempt to crack this target. After some more re-reading of essays, I realized that this is the "old style" of recovering the seeds - by xoring the vc.data with vendorcode5. By doing this I learned a lot about how the older versions of FLEXlm worked. But if you recall, I already knew these were the wrong seeds. I pretty much new that this is what l_sg would return because I had originally bpx'ed right after that call. This was because of previous essays. The only thing that I learned was that there was some funny flag/vector stuff going on in l_sg.

Part 4 - test some things out - Data Flow

I was tired of tracing the code and staring at disassembly. Time for a different approach :- Maybe the seeds were hidden somewhere else, or maybe the vendorcode.data still had something to do with the seeds. I knew one way to find out if vendorcode.data was still relevent : Using a valid license, poke bad values into vendorcode.data and see if the program fails or not. If vendorcode.data was not really used, the program would not fail. If vendorcode.data was used the program would fail.

It failed. OK, so vendorcode.data sill has some relevence to the authenticator and probably is still containing the seeds with some other function on top of it. This made me feel a little better. At least the seeds were still where I thought they were. I did a bpm on the vendorcode.data passed to lc_init. I followed the data around. It was copied several times so I bpm'ed all the copies. I ran the program several times. Trying to get a feel for what was going on. I could see that something wierd was happening - at one point the vendorcode.data appeared to get "randomized". That is, there were non-deterministic values placed there on sucessive runs - interesting and scary!.

Part 5 Program Flow again - Good Guy/Bad Guy

At this point I decided to target certain routines and observe execution of the program under two conditions : One condition is with a good license file. The other is with a bad licence file. The way I targeted the routines was to do a search in the library for references to l_sg (see appendix). I could have also searched W32Dasm for the l_sg address but I was starting to like this new strategy. I can't remember the order of things now, but I also traced down through lc_checkout as part of deciding which routines to single out. One of the routines that used l_sg was good_lic_key. By searching for references to good_lic_key and so on up the hierarchy I deduced that good_lic_key was called in lc_checkout. Ok this is getting somewhere because lc_checkout is the routine that returns -8 (bad seeds). By tracing "good" and "bad" execution of good_lic_key, I could see the following :-

good_lic_key { ...

l_sg()

extract_date()

l_ckout_crypt()

test eax from l_ckout_crypt()

if eax zero BAD GUY!!! jump to -8 !

good guy code ... }

Now if you have been following this, I already knew l_sg had some suspect code. But the bad guy test came from l_ckout_crypt(). What should I persue next - l_sg? - l_ckout_crypt ? I decided to trace l_ckout crypt. Wrong decision. What I found out from good guy/bad guy execution of l_ckout_crypt is that it forms a string of data based in part on a license file entry. Then, it forms an authenticator based on that string of data. Then it compares this authenticator to the one in the license file. Bad guy will fail this compare. So I could continue into this function to find the seeds - they have to be there somewhere. I decided to abandon this. It might have been around the corner but I felt that the l_sg should be followed some more.

Part 6 - Seeds are uncovered - the answer

OK, back to l_sg. I bpx'ed lc_init, lc_checkout, and l_sg. I knew that it would be called in both routines, but I wanted to see it when it got into lc_checkout. This time l_sg took the jump to the vector!. I should have known. It would have saved me a lot of time to just break on this jump in the first place!. The first part of the vector code went like this :-

store strlen(vendor name)

if parameter zero go somewhere else

job+offseta=gronk(time())

job+offsetb=gronk(time())

job+offsetc=gronk(time())

The "job" structure gets passed by reference to almost all FLEXlm functions. This is one way they communicate with each other. If you think about this, what function would gronk time() a bunch of times?. It will not give you the same results any time ran. It would give you RANDOM values. At this point things were making sense. The explanation for seeing random data in vc.data now was clear - the time() function was randomizing vc.data. As I traced through this function, I could see there were three main sections. 1. create random data in dword offsets +4 +8 +c +10 of job structure. 2. crunch vendor name into a 4 byte value. 3. process vendorcode.data using vendorkey1 vendorkey2, a constant, random data, and value from 2.

At some point I stopped tracing and reset the program, breaking after generation of the random data. I zeroed out the random data and set a break at the end of the routine. Pressed ctrl+d and there they were - the unencrypted correct seeds that I had gotten originally from target A!!. The code after the random number generation basically does :-

mask = vendorkey1 ^ vendorkey2 ^ dword const ^ vname hash ^ random

seed 1 = mask ^ vendorcode.data[0]

seed 2 = mask ^ vendorcode.data[1]

where :-

dword const = constant in the code

random = a 32-bit value composed of bytes at offsets +0xb +0x8 +0x13 and +0x9 of job

vname hash = a function of a vendor name checksum

OK. Now you can see that es1 != vc.d[0]^vk5. This was the answer. The seeds had been hidden by a different function. Further study of this routine shows that if the first parameter passed to it is 0 instead of a pointer to job, it will use 0 for random data and therefore will return the correct seeds. Since the random bytes are stored in the job, the real seeds can be used without ever seeing their true values in the CPU or memory as long as you have the vendorcode and job structures to work with.

Part 7 - Tracing flags

This was pretty much a done deal. I could get the seeds out of this target. A question was lingering. The l_sg checks a flag to see if it should jump to the "alternate" seed decrypter. Called in lc_init, this flag is clear. Called in good_lic_key, this flag was set. So somewhere this flag was being set. Time to bpm. I set a bpm on this byte. I found what was setting it. The code was something like :

:00444694 CALL 0043FA40 <-- call lc_init.
:00444699 ADD ESP, 10
:0044469C MOV ECX, D, [ESI] <-- ecx = *job.
:0044469E MOV EDX, D, [ECX+50] <-- OK this is familiar (see l_sg).
:004446A1 POP ESI
:004446A2 OR B, [EDX+00000141], 80 <-- set flag!.
:004446A9 MOV ESP, EBP
:004446AB POP EBP
:004446AC RET

Now let me digress. At this point I knew that this flag was going to be at the same address every time I ran the program. This makes the bpm that much easier. This implies that this variable is not dynamically declared. Or maybe it is dynamically declared but the process is deterministic. I don't know enough to say. But I do know that having this flag at the same location for each run is a great help. Well there it was, the same screwy dereferencing and the setting of the flag. Right after the call to lc_init. That made sense. Thats why l_sg behaved one way in lc_init and another way in lc_checkout. This was a gift having the flag set right after the call to lc_init. It implied that whatever this routine was, when called it set in motion the alternate seed decryption routine. This routine was pretty small. I searched for it in the library (by searching for references to lc_init, then examining these object files - see appendix). This routine turned out to be lc_new_job(). I then found out who was calling lc_new_job. The calls at that level looked like :-

lc_new_job()

lc_set_attr(0x38,"license file")  <-- set up where licence file is.

lc_checkout(feature a...)

...

lc_checkout(feature b...)

...

This code must have been part of what FLEXlm would call the "client code". I was at the top of the API chain. This code has vendor specific stuff in it whereas previous code was FLEXlm generic. This was good to know. After finding this I bpx'ed at lc_new_job, lc_init, l_sg, lc_checkout, the "real" l_sg and ran the program. I verified the program flow was how I thought it should be. All along I had no idea where the calls were coming from. I thought lc_init was the "init" routine called by the client. Now I knew that there was a level up and there was a high probability this was the top level.

Part 8 - the feature lc_new_job

This brings me to the title of this essay - the feature lc_new_job. Its pretty late in the essay to be finally discussing the title subject. But that's how I found it. There was no magic revelation that I should look for lc_new_job. I had to trace the program/data flow from an arbitrary starting point. I had remembered reading about lc_new_job in the documents. It was strange because they talked about something I felt was inappropriate to discuss in an API document. It stood out. I re-read the description. They made a point that you have to link with lm_new.obj. If you don't you will get a linker error for l_36_buf. (I already knew l_36_buf was the name of the l_sg "alternate seed" vector by the way).

Time to look at lm_new.obj. Anyway, a disassembly of lm_new.obj revealed the alternate seed decrypter that was calling time() to generate random data and xoring it with the real seed. This made sense. I knew that lc_new_job set that "alternate seed" flag. I knew that the documents said you needed lm_new.obj. These two facts confirmed that the "alternate seed" decrypter should be in lm_new.obj.

Part 9 - lm_new.c

When I discussed the information about lc_new_job and lm_new.obj, VoxQuietis informed me that he couldn't find lm_new.obj in his FLEXlm distribution. At this point, pilgrim noted this build rule in the FLEXlm makefile :-

lm_new.obj : ..\machind\lsvendor.c lmrand2.obj ..\machind\lm_code.h

   lmrand1 -i ..\machind\lsvendor.c

   $(CC) $(CFLAGS) $(INCS) /c lmcode.c

   $(LD) $(LFLAGS) /out:lmrand2.exe lmcode.obj lmrand2.obj \

   $(LIBS) $(CLIBS)

      del  lm_new.c  

      lmrand2  -o lm_new.c

      $(CC) $(CFLAGS) $(INCS) /c lm_new.c

What this is saying is that lm_new.obj doesn't come with the distribution - you make it from lm_new.c. Another thing this is saying is that lm_new.c doesn't come with the distribution - you GENERATE it. Time to look at lm_new.c. This module has two functions. Both functions are related to lc_new_job. One function is called directly by lc_new_job. This function "unpackages" the vendorcode and vendor name. The other function is the "alternate seed" decrypter and undoes the pre-encryption done on the seeds to form vc.data. The decrypter also conditionally masks the decrypted seeds with a random value and stores that mask in the job structure so the seeds can be recovered later. The "unpackager" was one of the remaining questions I had. That is, how was the original vendorcode stored in the executable?. Now I could see that it was obfuscated inside a function. So a recap of the relevant backtraces reveals :-

lc_new_job {

  call lm_new.unpackage(vname,&vc) (unpackage vendorcode,vendor name)

  call lm_new.unpackage(0,0)       (not sure why this call 0,0)

  call lc_init {

    ...

    call l_sg (alt seed flag not set - return old style vc.d^vk5)

    compare seeds to default 12345678 and 87654321

    ...

  }

  set alt seed flag

}

...

lc_checkout {

  ...

  good_lic_key {

    ...

    l_sg  {

      alt seed flag set so,

      lm_new.decrypt(job,name,vc)    (decrypt and RANDOMIZE!!)

    }

    extract_date

    l_ckout_crypt / -8 test

    ...

  }

  ...

}

Now back to lm_new.c. Another thing I noticed was that the "dword const" used in its decryption routine was not the same as the dword const used by my target. Refer back to part 6 for what the dword const is. This was maybe the most important observation made about lm_new.c. I decided to re-make lm_new.c. When I did, I got yet another different dword const. Not only that, other things were different about lm_new.c each time I generated it. What this is saying is that lm_new.c is a "personalized" version for each target. This means it is unique for each target. In the past, the encoding of the seeds was generic. They were encrypted with vendorkey5 and a generic library routine could be used to get vendorkey5. Now, a custom routine was used for each target.

Personalization is nothing new but is usually used to single out and verify a specific entity. For example, a private key is personalized. If a person signs something with a private key, you know it came from them. If private keys were not personalized, anyone could sign for anyone else. But then they wouldn't be "private" keys now would they?. They would be the same value for everyone. This method of personalization is very practical for globetrotter. They don't have to interact with their customers to give them a custom routine. It is all handled through automatic generation of lm_new.c.

Part 10 - The source code generator

I would like to illustrate what I think the source code generator does. The source code generator is everything used to make lm_new.c. The final source code generator executable is lmrand2. (I'm not sure how this ASCII art will look in your browser).

                ______________                     ______________________________
   
 clear seeds -->| encryption |--->encrypted seeds->|vendorcode.data[]=enc. seeds,|

                | routine    |                     |other vendorcode,vendorname  |

                -----^----^---                     -|-----------------------------

                     |    |                         |

                     |    |            _____________V__________

                     |    |            |Unpackager source code|--> unpackager source code

 _________________   |    |            |generator             |    in lm_new.c

 |PERSONALIZATION |  |    |            ------------------------

 |INFO = rand();  |--|    |

 ------------------  |    |-VENDORKEYS,VENDORNAME

                     |

                _____V____________________

                | Seed decryption source |-----> seed decryption source code in lm_new.c

                | code generator         |

                --------------------------

This basically says that the generator :- Creates some personalization info from rand(). Encrypts the seeds using personalization info, vendorkeys, and vendorname. Forms the vendorcode structure from the encrypted seeds and the rest of what vendorcode is. Feeds vendorcode and vendor name to the unpackager source code generator to generate the unpackager source code in lm_new.c. Feeds the personalization info to the seed decryption source code generator to generate the seed decryption source code in lm_new.c. Things to observe :- The encryption process and the obfuscation done by the unpackager are separate processes. It is unimportant what the seed decryption algorithm really is. The important thing is that it undoes what the encryption routine did. This is easy to do since the thing that encrypted the seeds and the thing that generates the decryption source code lie in the same entity. The encryption/decryption of the seeds is a personalized process.

Part 11 - Final analysis

We now have enough information to make a final analysis on what the "enhanced security" of lc_new_job is. (This might not be the "final" analysis because there still might be some things I missed). First is the obfuscation of the vendorcode and vendorname. The second is more complicated. Let me define some terms first : The two previous methods of breaking FLEXlm seeds are what I would call "pre-seed" attack and "post-seed" attack.

The pre-seed attack was possible because the methods of encrypting the seeds were generic. All you needed was the vendorcode structure to get the seeds. Calling l_svk with the vendorcode gives you vendorkey5. Xoring vendorkey5 with vendorcode.data gives you the clear seeds. The post-seed attack was possible because the seeds were in the clear after decrypting them. All you needed was to break at the compare to default values and write down the seeds. Both of these methods were un-obtrusive in the sense that the cracker only needed to set the right breakpoint and write down some values.

No modification of program execution was needed. So now we can see what the second part of the "enhanced security" is. It is trying to deal these two forms of attack. The personalization makes the current pre-seed attack invalid. So the cracker is forced to use the post-seed attack. But the randomization on the clear seeds makes the current post-seed attack invalid. So the cracker is forced to a new form of attack. Remember that I was foiled by the post-seed countermeasure early on when I was following the seeds around and saw the random data. I didn't mention it but I was foiled by the pre-seed countermeasure early on because I tried the pre-seed attack also. You can not deal with one form of attack without dealing with the other. You need both countermeasures in place. For example if the encryption was only personalized the cracker could still do the post-seed attack. If the decryption output was only randomized the cracker could still do the pre-seed attack.

Part of what decreased the effectiveness of these countermeasures is that the target did not change its seeds when switching to a new version of FLEXlm. This might have been done for practical reasons. The ironic thing is that this not only compromised the target's security, it compromised the security of all targets using the FLEXlm feature lc_new_job.

Appendix - exploiting object code/libraries

As Nolan Blender pointed out in an essay, FLEXlm is composed of a ton of object files located in a object library. This is cool because not only are API calls visible, but internal calls between object libraries are visible too. Also, you can find out all the object files that use a particular function. It is nice to have the symbol info from the object files to annotate the disassembly.

Even C standard functions are annotated!. Have you ever traced through printf by mistake not knowing thats what it was? If I understand pilgrim right, IDA can read object files and find and annotate the code in a built application (use the FLAIR tools on Ilfak Guilfanov's site).

Final Notes

I would like to thank Nolan Blender, VoxQuietis, and pilgrim for their correspondence. Also, 1000 thanks to them and others such as Siul+Hacky, Acme, and CrackZ who wrote the informative essays on FLEXlm. If you analyse my method of cracking this target, you will see that I could have found quick solutions early on by just following through on things. However, at the time it was not that obvious what I should be doing. Sometimes you need to experiment before following through with a certain approach. If you want the textbook version of this - read the sections in reverse order!.

<EOF>