|
Describes how crypt filters are implemented and cracked using standard tools |
||
by +Tsehp | ||
| fra_00xx 98xxxx handle 1100 NA PC | ||
This essay describes the theory, operation, and cracking of the FLEXlm user crypt filter code. I am going to provide the information on how this protection works, how it is is generally implemented, and how this added protection can be cracked.
For this essay I am going to use the FLEXlm 6.1 SDK, although the principle is
the same for later versions (to 7.0d at least) of the FLEXlm toolkit.
What are crypt filters?
Crypt filters are an added protection mechanism that can be implemented in
FLEXlm protected applications. Programs that use this protection have to use
the FLEXible API, and some additional programming knowledge is required.
Standard FLEXlm keys rely only on the values of the encryption seeds which are
defined in lm_code.h. Once these seed values are discovered, FLEXlm keys can be
made for products for which the seeds are known. Crypt filters add an
additional encryption to the key so that simply knowing the encryption seeds is
no longer sufficient to generate valid keys.
IMPLEMENTATION.
The implementation of user crypt filters requires making the relevant filter
programs, then modifying the keygen, daemon, and runtime executables so that
they utilize this additional code. There are two additional modules; one for
the generator program, and one that is linked against the shippable executable.
The module that is linked against shippable (that is, ones that will go to the
end customer) is special in that it requires the correct result value as input
so that it is not possible to search for memory echoes of the correct key value.
I will explain this in depth later.
The first step is to create the Filter Generator files. For this example, I
created a directory "filter_gen" in the i86_n3 dir to hold the new source
files. The seeds I used were 1471 2134 4211. I am providing this information so
you can follow along, but normally you would not have access to this
information.
D:\blenderd\i86_n3\filter_gen>../lmrand1 -filter_gen 1471 2134 4211
** Filter-Generator: Additional security -- not needed by most companies **
This generates 2 source files, which you must *never edit*:
lmappfil.c: must be linked into vendor daemon, and all applications
calling lc_checkout(). These applications must call
lc_set_attr(lm_job, LM_A_USER_CRYPT_FILTER,
(LM_A_VAL_TYPE)user_crypt_filter);
after lc_new_job(). Also, lsvendor.c must have
extern void user_crypt_filter();
void (*ls_a_user_crypt_filter)() = user_crypt_filter;
lmkeyfil.c: this must be linked into all license generators:
makekey, lmcrypt, programs that call lc_cryptstr().
In these programs, after lc_init(), call
lc_set_attr(lm_job, LM_A_USER_CRYPT_FILTER_GEN,
(LM_A_VAL_TYPE)user_crypt_filter_gen);
The seeds you picked are (in hex): 0x5bf 0x856 0x1073.
Make sure that you save these seeds somewhere safe.
You can recreate these files by rerunning this program with the same seeds.
(Use -q in the future to skip this message.)
At this point, we have two C files conveniently generated by lmrand1.exe -
lmappfil.c and lmkeyfil.c. lmappfil.c contains the code which will be linked
against the shipping applications, and lmkeyfil contains the code that will be
linked against the keygens, and not shipped to customers. lmappfil.c contains
special anti cracking code that prevents memory echo sniffing, where lmkeyfil.c
contains the filter algorithm only.
The next step is to copy these files to the machind directory.
After this, the makefile in the i86_n3 directory (or whatever platform you're
building for) must be modified to include rules for the new files. I added
these lines for the object files in makefile:
lmappfil.obj : ..\machind\lmappfil.c ..\machind\lmclient.h
$(CC) $(CFLAGS) $(INCS) /c ..\machind\lmappfil.c
lmkeyfil.obj : ..\machind\lmkeyfil.c ..\machind\lmclient.h
$(CC) $(CFLAGS) $(INCS) /c ..\machind\lmkeyfil.c
For this exercise, I've only modified lmcrypt.exe (the key generator)
blenderd.exe (the daemon) and lmflex.exe (the client). User filters are only
applicable to the FLEXible interface.
Now that the build rules for the filter objects have been added, we'll approach
the license generator first.
First, we'll change the build rules so that lmcrypt.exe is linked with
lmkeyfil.obj
In makefile:
lmcrypt.exe : lmcrypt.obj lmkeyfil.obj
$(LD) $(LFLAGS) /out:$*.exe lmcrypt.obj lmkeyfil.obj \
$(LIBS) $(CLIBS)
Linking against the new object is not sufficient to get the keygen to work
though. We have to add a call to lc_set_attr so that FLEXlm knows to use this
filter.
First, add a prototype to lmcrypt.c:
extern void user_crypt_filter_gen();
Then after the call to lc_init()
lc_set_attr(lm_job, LM_A_USER_CRYPT_FILTER_GEN,
(LM_A_VAL_TYPE) user_crypt_filter_gen);
This tells flexlm to use our crypt filter when generating keys.
The client must be similarly modified to link against lmappfil.obj.
In makefile:
lmflex.exe : lmflex.obj lm_new.obj lmappfil.obj
$(LD) $(LFLAGS) /out:$*.exe lmflex.obj lm_new.obj lmappfil.obj \
$(LIBS) $(CLIBS)
In lmflex.c we have to do similar things to what we did in the keygen so the
client uses the filter as well:
This prototype must be added
extern void user_crypt_filter();
and after lc_new_job()
lc_set_attr(lm_job, LM_A_USER_CRYPT_FILTER, user_crypt_filter);
Now we can test the new filter with an uncounted license - first put this in
license.dat:
FEATURE f1 blenderd 1.0 permanent uncounted 0 HOSTID=ANY ck=0
then run
lmcrypt license.dat
If LM_LICENSE_FILE is set to the license.dat file, we should be able to check
out feature f1.
Modifying the daemon.
The makefile must be modified to include lmappfil.obj in the daemon first.
blenderd.exe : lmrand1.exe lsvendor.obj lmappfil.obj
$(LD) $(LFLAGS) /out:$*.exe lsvendor.obj lm_new.obj lmappfil.obj \
$(DAEMON_LIBS) $(CLIBS)
In lsvendor.c:
extern void user_crypt_filter();
and later
void (*ls_a_user_crypt_filter)() = user_crypt_filter;
This makes the vendor daemon use the crypt filter.
Understanding what the lmappfil and lmkeyfil really do.
The first program we'll examine is lmappfil.
Although the code is written to be non-obvious about what is happening, a
careful examination of the code reveals what is really happening. The input
value (inchar) is first xored with a value, then the bits permuted. If this
value is the same as ec (which I suspect stands for "expected code") that value
is returned. If a bit is encountered which is different from the expected code,
then the value is immediately xored with some junk value then returned. Since
the "expected code" is one from the key, it's not easy to figure out from
tracing what the expected code should be.
A careful examination of the program reveals that there's really 20 different
xor/permutation operations. Variables identified by "num" (for example, num0,
num1, num2) are used to test the input value "idx" or the index into the
xor/permute table. Values identified by "bit" are really masks for bit
operations - although these are stored as 16 bit values, this is only done to
confuse the issue since only the lower 8 bits are used, since the character
being tested "c" is an 8 bit value.
So effectively
bit0 = 0x01
bit1 = 0x02
bit2 = 0x04
bit3 = 0x08
bit4 = 0x10
bit5 = 0x20
bit6 = 0x40
bit7 = 0x80
Let's look at the first part of the permutation check:
if (idx == num4)
{
if (in_c & bit1) c |= bit0;
if ((c & bit0) != (expchar & bit0)) { *inchar ^= 0x74; return;}
}
c was initially 0, so here we can see that the program is mapping the value at
bit 1 in in_c to bit0 in expchar, which is really ec&0xff. What this means is
that ec is being checked one bit at a time for correctness, and if there's a bad
bit, junk is returned. Since the expected char is really successive bytes in
the license key, the correct sequence will not show up in memory unless a
correct key was supplied initially. It's still possible to attack this type of
filtering though.
Attacking the daemon.
At this point, we'll assume that we don't know the specific values used for the
filter/encryption seeds, and have to derive them ourselves.
Now that we've ascertained what is basically happening in the algorithm, we can
now more effectively reverse the program. In this case, we'll attack the vendor
daemon blenderd.exe, since usually the daemon is supplied, and there's usually
less additional code to decompile than in the applications.
First, the daemon was disassembled using IDA 4.14, then the FLEXlm 6.1f
signature was applied. symfix.idc was then applied to rename the internal
functions. A map was then created, and mksym (which really calls the softice
utils msym and nmsym) was used to generate the .nms file.
The first task is to locate the encryption seeds, a process which has been
documented before in Dan's essay, and an earlier essay of mine. l_sg is first
located, and the call to the lm_new routine located - this is a call to pointer.
00406486 push [ebp+arg_8]
00406489 push [ebp+arg_4]
0040648C push ecx
0040648D call eax
0040648F add esp, 0Ch
The call is at 40648d - at that point we'll overwrite the job ptr on the stack
with a zero, step over the call, and examine the data in the third argument to
recover the real encryption seeds.
A license with a valid structure is first created so the program will call the
decrypting routine for the encryption seeds.
SERVER this_host ANY
VENDOR blenderd
FEATURE f2 blenderd 1.0 permanent 20 123456789ABC ck=0
The symbol table is then loaded in using the SoftICE symbol loader, then the EXE
loaded in. A breakpoint is then set at 40648d.
The program is then run with arguments that will cause it to try to check out
that license.
blenderd -t 6.1 3 -c license.dat
At the breakpoint, the value at esp is set to 0, and then the address at esp+8
is examined, and the call stepped over. You should now have the encryption
seeds.
The next step is to find where the crypt filter really resides in the program.
Since it's unique for each vendor, it's not going to have a signature in the
file. How can we identify it then?
Well, for one thing, there must be a call to lc_set_attr telling FLEXlm where
this routine resides. Let's set a breakpoint in lc_set_attr, and watch the
calls to this routine. By looking at our IDA disassembly we can locate
_lc_set_attr at 0x404374 and set a breakpoint in softice.
If we do this, we see that the crypt filter is located at 4019ec since we see
lc_set_attr called with a second argument of 0x4a (LM_A_USER_CRYPT_FILTER) and
that value.
Now we can examine that routine and disassemble in earnest.
Using IDA, we go to that location, and can see immediately that it's not yet
considered to be a function. This is because there were no "call" references to
this address and IDA has no reason to consider it a seperate function. By
pressing "p" with the pointer at this address, we can convert it to a function.
The next step is to change the stack variable names - we know what the arguments
to this function are so let's name them appropriately - arg_4 becomes inchar,
arg_8 becomes idx, arg_c becomes ec. The job structure (arg_0) isn't used
anywhere, so it wasn't converted into a stack variable - we won't bother because
we don't care about variables that aren't used.
We can then step through the listing, and rename variables, since we know their
values and equivalent names in the original code. Ones that were num0-19 are
easy. The x_0 to x_19 variables were renamed to include the hex values, so less
back and forth work would be needed when deriving the xor table.
This is a typical entry - in this case, the value for idx13 is xored with 0x37
00401A15 loc_401A15: ; CODE XREF: sub_4019EC+1Ej
00401A15 cmp esi, num13
00401A1B jnz short loc_401A26
00401A1D xor dl, myval_37
00401A23 mov [ebp+var_2], dl
After doing a bit of work, we can see how the disassembly will relate to the
original source.
00401B5C loc_401B5C: ; CODE XREF: sub_4019EC+165j
00401B5C cmp esi, ecx ; ecx is num4
00401B5E mov ecx, bit0
00401B64 jnz short loc_401B86 ; Skip over this if not num4
00401B66 test byte ptr bit1, dl ; Check bit 1
00401B6C jz short loc_401B71
00401B6E mov [ebp+var_1], cl ; char c
00401B71
00401B71 loc_401B71: ; CODE XREF: sub_4019EC+180j
00401B71 mov dl, [ebp+ec]
00401B74 mov bl, [ebp+var_1] ; char c
00401B77 and edx, ecx
00401B79 and ebx, ecx
00401B7B cmp bl, dl
00401B7D jz short loc_401B86 ; Check to see if that bit is the same on both ec and the derived char
00401B7F
00401B7F loc_401B7F: ; CODE XREF: sub_4019EC+8A1j
00401B7F xor al, 74h ; Fill with crud if no good
00401B81 jmp loc_403C6F ; Then bail out
00401B86 ; ---------------------------------------------------------------------------
It's clear by looking at this that we can figure out what input bits are mapped
to what output bits for what index. Here, the check is done against num4, so it
must be for index 4. The test is done against bit 1, and the bit anding
operation is done against bit0 (it was stored in ecx earlier). This means that
the permutation will move the bit in location 1 to location 0. By examining the
entire subroutine, the table can be fully recovered. Once we have the tables, we
can write an equivalent filter for the key generator. If you have followed this
exercise and have the lmkeyfil.c program, you'll see that the behavior of the
routine below is identical to that of lmkeyfil.c
In the following program, the actual algorithm is implemented for
blenderd.
#include <lmclient.h>
typedef struct permute_s
{
int shiftvals[8];
} permute_t;
int xorvals[]=
{
0x14, 0x39, 0x38, 0x11,
0x32, 0x32, 0x33, 0x1d,
0x3e, 0x20, 0x18, 0x39,
0x19, 0x37, 0x09, 0x1b,
0x16, 0x15, 0x2e, 0x1a
};
permute_t tab1[] = {
{4,5,7,1,2,6,0,3}, /* idx 00 */
{0,6,5,1,4,3,2,7}, /* idx 01 */
{5,3,7,0,6,1,2,4}, /* idx 02 */
{3,6,1,5,0,4,2,7}, /* idx 03 */
{4,0,6,1,7,5,3,2}, /* idx 04 */
{1,7,2,4,0,3,5,6}, /* idx 05 */
{3,7,0,6,4,2,5,1}, /* idx 06 */
{3,6,5,1,4,2,7,0}, /* idx 07 */
{6,4,5,7,0,3,1,2}, /* idx 08 */
{7,5,2,3,4,0,1,6}, /* idx 09 */
{6,1,3,2,4,0,5,7}, /* idx 10 */
{1,2,4,7,3,0,5,6}, /* idx 11 */
{0,2,7,6,1,5,3,4}, /* idx 12 */
{5,0,4,7,6,2,3,1}, /* idx 13 */
{7,6,4,5,2,0,3,1}, /* idx 14 */
{5,2,4,7,1,3,0,6}, /* idx 15 */
{0,4,3,5,7,1,2,6}, /* idx 16 */
{3,7,0,1,2,6,4,5}, /* idx 17 */
{2,3,7,1,4,6,0,5}, /* idx 18 */
{7,5,1,3,6,2,0,4} /* idx 19 */
};
/*-------------------------------------------------------------*
*
* user_crypt_filter_gen
*-------------------------------------------------------------*/
int user_crypt_filter_gen(job, inchar, idx)
LM_HANDLE *job;
char *inchar;
int idx;
{
char tmpchr;
char outchr;
/* Initial XOR transform */
tmpchr = *inchar ^ xorvals[idx];
/* Final permutation */
permute(&tmpchr, tab1[idx].shiftvals, &outchr);
*inchar = outchr;
return(0);
}
/*-------------------------------------------------------------*
*
* permute: permute the bits in inchar to outchar using the bit
* order in shiftvals
*-------------------------------------------------------------*/
int permute(char *inchar, int *shiftvals, char *outchar)
{
int outval = 0;
int i;
int shbit; /* Test bit */
shbit = 1;
for (i = 0; i < 8; i++)
{
if (*inchar & shbit)
{
outval |= (1<<(shiftvals[i]));
}
shbit = shbit<<1;
}
*outchar = (char) (outval&0xff);
return(0);
}
This is the contents of mksym.bat:
"c:\program files\numega\softicent\util16\msym" %1.map
"c:\program files\numega\softicent\nmsym" %1.sym
This is the contents of symfix.idc:
#include <idc.idc>
static main()
{
auto ea, old_name, old_cmt, sav_cmt;
auto errcode;
sav_cmt = "NOSUCHFUNCT";
ea = SegStart( ScreenEA() );
while (ea != BADADDR) {
old_cmt = GetFunctionCmt( ea,1);
if (strlen(old_cmt) > 1)
{
if (old_cmt != sav_cmt)
{
Message ( atoa(ea)+" "+old_cmt );
Message ("\n");
errcode = MakeName(ea, old_cmt);
if (errcode == 0)
{
errcode = MakeName(ea, "alt"+old_cmt);
}
if (errcode == 0)
{
errcode = MakeName(ea, "alt1"+old_cmt);
}
sav_cmt = old_cmt;
}
}
ea = NextAddr(ea);
}
Message("Done.\n");
}
Although this protection is vastly improved by adding crypt filters, it can still be defeated by careful analysis and emulation of the behavior of the routines in question. Since the lmkeyfil routine is not supplied to the end user, cracking must be accomplished by understanding what is going on in the program.