|
Flexlm v6.1
lc_new_job()
|
flexlm
|
26 September 1999
| by
dan
|
|
|
Courtesy of Fravia's page of
reverse engineering
|
|
fra_00xx 98xxxx handle 1100 NA PC
|
A great essay. A good reverser throws some light in a very 'obfuscated' prortection
scheme, and explains his reversing work in a fascinating style. A must for all those
that want to learn how you should tackle a complicated reversing job.
| |
|
There is a crack, a crack in everything
That's how the light gets in
| |
Rating
|
( )Beginner (X)Intermediate ( )Advanced ( )Expert
| |
"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
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 relavent 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 I used:
softice
wdasm
lib (Micro$oft - sorry!)
dumpbin (Micro$oft - sorry!)
egrep (pc users - search cigwin)
perl (cigwin again)
Its hard to say what the target is - is it the "target" or is it flexlm?
Let's just say:
A program that integrates flexlm v6.1 and calls lc_new_job()
Refer to past essays - those targets may have been updated.
Flexlm has been broken before. They added enhanced security.
**************************************************************
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
"dude 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 definitly 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 ohter hand will probably not have the right encryption
seeds (which might be why you're reading this junk). 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 50 push eax pram
:00440009 56 push esi pram
:0044000A E8BD1E0000 call 00441ECC call l_sg(job,code,??)
:0044000F 83C40C add esp, 0000000C restore
:00440012 817DCC21436587 cmp dword ptr [ebp-34], 87654321 compare seed to default
:00440019 7409 je 00440024
:0044001B 817DD078563412 cmp dword ptr [ebp-30], 12345678 compare other seed
:00440022 7527 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 wdasm output of the program to my "annotated
object dissassembly" (see appendix). The first part of l_sg was:
l_sg(???,name,vendorcode):
:00441ECC 55 push ebp - after this push, 8 extra bytes on stack before params
:00441ECD 8B4C2408 mov ecx, dword ptr [esp+08] - this is first param to l_sg
:00441ED1 8BEC mov ebp, esp
:00441ED3 83EC04 sub esp, 00000004 - we're going to use 4 bytes in this routine
:00441ED6 8B4150 mov eax, dword ptr [ecx+50] - ok first param is a ptr.
...
:00441EDC F6804101000080 test byte ptr [eax+00000141], 80 - ok another derefrence, confusing!!!
:00441EE3 741E je 00441F03 - test failed here on my trace
:00441EE5 833D74024F0000 cmp dword ptr [004F0274], 00000000 - this is a call vector
:00441EEC 7415 je 00441F03 - vector zero so don't call it
:00441EEE FF7510 push [ebp+10] - set up params
:00441EF1 FF750C push [ebp+0C]
:00441EF4 51 push ecx
:00441EF5 FF1574024F00 call dword ptr [004F0274] - vector call !!!
:00441EFB 83C40C add esp, 0000000C
:00441EFE E9A5000000 jmp 00441FA8
:00441F03 8B7D10 mov edi, dword ptr [ebp+10] - jump here edi = vendorcode
:00441F06 8B750C mov esi, dword ptr [ebp+0C] - this is vendor name
:00441F09 8D470C lea eax, dword ptr [edi+0C] - = vendorcode+0c=vendorkeys
:00441F0C 50 push eax
:00441F0D 56 push esi
:00441F0E E83FAD0000 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 895708 mov dword ptr [edi+08], edx - store something in vc.data[1] (seed 2)
:00441FA5 894704 mov dword ptr [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 relavent: 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 relavence 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 wdasm 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 heiarachy I deducted 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 E8A7B3FFFF call 0043FA40 <-call lc_init
:00444699 83C410 add esp, 00000010
:0044469C 8B0E mov ecx, dword ptr [esi] <- ecx = *job
:0044469E 8B5150 mov edx, dword ptr [ecx+50] <- ok this is familiar (see l_sg)
:004446A1 5E pop esi
:004446A2 808A4101000080 or byte ptr [edx+00000141], 80 <-set flag!!!
:004446A9 8BE5 mov esp, ebp
:004446AB 5D pop ebp
:004446AC C3 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. anyway...
Well there it was, the same screwey 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 rememberd reading about lc_new_job in the documents. It was strange
because they talked about something I felt was inapropriate 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 relavant 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 compromized the target's security, it compromized 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 libraray. 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. I didn't know this
at the time. However I did understand that I wanted that symbol info.
I tried to use wdasm to disassemble the object file but that didn't work.
It is highly probable I just didn't know how to use it right.
I guess the way some people handle this is to build their own flexlm client
app with debug switches on. Then they can reverse engineer this app. With
all the symbol info. This of course is a sane way to do things.
I wasn't interested in this approach. I wanted to stick with my target.
The basic method went like this:
Target a function
Search the library for references to the function using dumpbin.exe
If I wanted the actual function - the reference would not be "undef"
If I wanted calls to the function - the reference would be a "undef"
Extract the relavant object file using lib.exe
Extract the reloc/header info into one file using dumpbin.exe
Extract the object disassembly into another file using dumpbin.exe
Combine info from the reloc and disassembly files
into one "annotated object disassembly" file using a perl script file
Once I found some object code that I wanted to see in the target, I would
manually search for the "signature" of it in the target.
There maybe tools that can do what I wanted to do. I would like to know.
But it didn't matter in the end. This approach worked very well.
Eventually, I had three batch files, one was wheresym.bat: (find symbols in obj or lib)
dumpbin /archivemembers /symbols %1 | egrep "member|%2" > result.txt
Second was extract.bat: (extract .obj from .lib)
lib /extract:%2 %1
The third was gronk.bat: (extract an obj and create annotated disassembly)
call extract lmgr.lib SINTEL_REL\%1.obj
dumpbin /relocations /headers %1.obj > %1.sym
dumpbin /disasm %1.obj > %1.dis
perl sym.pl %1.sym %1.dis > %1.txt
Note that all objects in lmgr.lib had SINTEL_REL\ in front of their name.
The perl script sym.pl is included later. The tools dumpbin and lib have
equivalents such as objdump and ar.
Gronk.bat creates 3 files *.sym *.dis and *.txt. I am only interested in
*.txt - the final output of the process.
Here's an example,
say I was interested in l_sg. I would type:
>wheresym lmgr.lib l_sg
The result.txt file would contain:
...
Archive member name at 1AB84: /176 SINTEL_REL\l_cksum.obj
013 00000000 UNDEF notype () External | _l_sg
...
Archive member name at 4621A: /1729 SINTEL_REL\lm_ckout.obj
092 00000000 SECT1D notype () External | _l_sg
...
Archive member name at 5E9F2: /2249 SINTEL_REL\lm_init.obj
015 00000000 UNDEF notype () External | _l_sg
This tells me that l_sg is in lm_ckout.obj and is referenced in lm_init.obj
and l_cksum.obj. Note that l_sg might be called by a function in lm_ckout.obj
or not. I would have to gronk lm_ckout.obj in order to find out.
So it depends on what I wanted to do.
Say I type:
gronk lm_ckout.obj
Searching the resultant lm_ckout.txt for l_sg, I find some calls and then the
actual function (I only show code for illustration of this method):
New proc searching...
Found proc 164
_l_sg:
00000000: 55 push ebp
...
00000015: A1 00 00 00 00 mov eax,[00000000]
Found 00000016 _l_n36_buff
...
00000039: 57 push edi
0000003A: E8 00 00 00 00 call 0000003F
Found 0000003B _l_key
0000003F: 8B F0 mov esi,eax
The "Found" lines and "New proc" line are from the perl script. The code is from
the .dis file. The found lines state an address of a reference. For example,
this code is saying the l_n36_buff reference is at address 0x16. You should realize
this object code will be located at a different address in linked code and all reloc
info will be resolved. The "call 0x3f" is not really calling address 0x3f. The
four zero's are just placeholders for the reloc info. So this call is a call to l_key.
My point is I can take this file and compare against the target to find function names.
Another powerful feature is finding all the calls by just searching on the names.
Now if I wanted to go down the heiarchy, I would find l_key and study its references.
If I wanted to go up, I would look for other references to l_sg.
For example, also in lm_ckout.txt is good_lic_key:
New proc searching...
Found proc 79
_good_lic_key:
...
00000016: E8 00 00 00 00 call 0000001B
Found 00000017 _memcpy
...
0000002D: E8 00 00 00 00 call 00000032
Found 0000002E _l_sg
...
00000041: E8 00 00 00 00 call 00000046
Found 00000042 _l_extract_date
...
0000004B: E8 00 00 00 00 call 00000050
Found 0000004C _l_ckout_crypt
...
Next, I could query good_lic_key to see who calls it and so on.
So I could work in the space of the object files or the space of my target.
It was a minor inconvenience to switch between them.
The perl script first processes the symbol info in the .sym file. It
creates an array of what function,address,and reloc name are in an entry.
It assumes the entries' addresses are in increasing order.
Then it processes the disassembly file and uses the array it built up
previously. I was in a wierd mood when I wrote it so the file processing is
in a state machine. I don't think the split stuff is necessary but whatever.
I have that habit from using a2p (awk to perl) which does a similar thing.
Note that this script is highly dependant on the output syntax of my dumpbin.exe
which was from msvc++ 5.0.
Here is the perl script sym.pl
#***************************************************************
$arg1 = shift;
$arg2 = shift;
@proca = ();
@addra = ();
@syma = ();
open (IN,$arg1);
$state = 0;
while () {
if($state==2) {
$proc_name = "";
}
if(($state==2)&&(/Communal/)) {
chop;
@Fld=split(' ',$_,999);
$proc_name=$Fld[$#Fld];
# print $_;
$state=3;
}
if($state==5) {
chop;
@Fld=split(' ',$_,999);
if($#Fld!=-1) {
push(@proca,$proc_name);
push(@addra,$Fld[0]);
push(@syma,$Fld[$#Fld]);
# print "$proc_name $Fld[0] $Fld[$#Fld]\n";
}
# print $_;
}
if($state==1) {
if (/text/){$state=2;}
else {$state=0;}
}
$state=1 if(/^SECTION HEADER/);
if(($state==3)&&(/RELOCATIONS/)){$state=4;}
if(($state==4)&&(/-------/)){$state=5;}
if(($state==5)&&(/^$/)){$state=0;}
# print "$state $_";
}
close IN;
#for ($i=0;$i<=$#proca;$i++) {
# print "$proca[$i] $addra[$i] $syma[$i]\n";
#}
open (IN,$arg2);
$state = 0;
while () {
chop;
$symfound=0;
if($state==3) {
@Fld=split(' ',$_,999);
if($Fld[0] =~ /[0123456789ABCDEF]*:/) {
$addr=$Fld[0];
$addr=~s/://;
$j = hex $addra[$searchi];
$j;
$k=hex $addr;
if($searchi<=$#addra) {
if(($j-$k<=0)&&
($proca[$searchi] eq $proc)) {
print "Found $addra[$searchi] $syma[$searchi]\n";
$symfound=1;
$searchi++;
} else {
# print "Looking for $addr == $addra[$searchi] $syma[$searchi]\n";
}
}
# print "$addr\n";
}
}
if(/^$/){$state=0;}
if(/^_/){$state=1;}
if($state==1) {
$proc = $_;
$proc =~ s/://;
print "New proc searching...\n";
$searchi=0;
while(($searchi<=$#proca)&&($proc ne $proca[$searchi])) {
$searchi++;
}
if($searchi>$#proca) {
print "Not found proc\n";
$state=2;
} else {
print "Found proc $searchi\n";
$state=3;
}
# print "$proc\n";
}
{ print "$_\n";}
}
close IN;
#***************************************************************
I would like to thank Nolan Blender, VoxQuietis, and pilgrim for their
correspondance. Also, 1000 thanks to them and others such as Siul+Hacky,
Acme, and CrackZ who wrote the informative essays on flexlm.
If you analyze my method of cracking this target, you will see that
I could have found quick soloutions 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>
I wont even bother explaining you
that you should BUY this target program if you intend to use it for a
longer period than the allowed one. Should you want to STEAL this
software instead, you don't need to crack its protection scheme at all:
you'll find it on most Warez sites, complete and already regged,
farewell, don't come back.
You are deep inside fravia's page of reverse engineering,
choose your way out:
homepage
links
search_forms
+ORC
how to protect
academy database
reality cracking
how to search
javascript wars
tools
anonymity academy
cocktails
antismut CGI-scripts
mail_fravia+
Is reverse engineering legal?