破解ecc保护的flexlm (翻译by nujia)
说句老实话,我很早就知道这个缺陷,:),但是因为RNG(random number generator,随机数发生器)做得足够很安全了,所以现在发表这个缺陷,将不会损害flexlm使用者的利益(除非他们没有更新RNG),Crackz。
简介
本文主要描述了怎样获得ecc flexlm保护的seed3和seed4。虽然本文是是讲述方法的,但是如果你破解一个真正的软件的话,下面这些软件将是必须的。
* NuMega SoftICE
* Datarescue IDA (4.15 或更新的版本)
* FLEXlm v7.2h SDK
* Microsoft Visual C++
* TASM 假如你想分析某些代码的话
怎样开始工作呢?
标准的老的flexlm版本用两个32位的加密种子(seed1和seed2)来加密软件。这两个种子提供全部的加密保护。种子会在目标程序中出现,所以程序可以制作注册机,编译得到lmcrypt(flexlm key 生成器),这样cracker就能生成有效的license了。ecc flexlm需要两个额外的加密种子(seed3和seed4)。这两个种子用来生成公钥/私钥对,并且只有公钥存在于发布的软件中。Certicom Security Builder ECC 库用来生成公钥/私钥对,看起来比较安全。我没有发现用来生成公钥/私钥对的seed3和seed4与公钥之间有什么关系(就是由公钥是不能推导出seed3和seed4的)。为了暴力破解seed3和seed4,很明显需要2^64次尝试。这是不太现实的。
Globetrotter在使用seed3和seed4时犯了一个错误。虽然seed3和seed4不能像seed1和seed2那样完全的复原,但是也为缩短暴力破解的时间提供了足够的信息,这样能在可以接受的时间内暴力破解出seed3和seed4。
缺陷出现在_do_handshake函数里。这个函数用seed3和seed4来验证vendor daemon的真假。下面是使用seed3和seed4的部分代码。
mov edx, dword ptr ds:_seed3 ;将seed3放入edx
xor edx, dword ptr ds:_seed4 ;将seed3和seed4取异或,结果在edx
shr edx, 10h ;edx右移16位,
and edx, 0FFFFh ;将edx的低16位填充ffff
push edx ;将edx压栈
mov eax, dword ptr ds:_seed3 ;将seed3放入eax
add eax, dword ptr ds:_seed4 ;将seed3和seed4相加,结果在eax
and eax, 0FFFFh ;将eax的低16位填充ffff
push eax ;将eax压栈
mov ecx, dword ptr ds:_seed3 ;将seed3放入ecx
xor ecx, dword ptr ds:_seed4 ;将seed3和seed4取异或,结果在ecx
and ecx, 0FFFFh ;将ecx的低16位填充ffff
push ecx ;将ecx压栈
call _srand16 ;调用随机函数_srand16,三个输入,因为压栈了三个数
add esp, 0Ch ;函数外调整堆栈,
call _rand16 ;四次调用另外一个随机函数_rand16
call _rand16 ;返回结果在eax里
call _rand16 ;
call _rand16 ;
mov [ebp+var_60], eax ; "random" numbers are used as constants in lm_new :)
;将返回值送入[ebp+var_60],这个随机数在lm_new保持常数,
;于是缺陷就产生了,:)
call _rand16 ;调用随机函数_rand16,无输入,因为没有压栈
mov [ebp+var_5C], eax ;将返回结果送入[ebp+var_5C]
call _rand16 ;调用随机函数_rand16
mov [ebp+var_58], eax ;将返回结果送入[ebp+var_58]
call _rand16 ;调用随机函数_rand16
mov [ebp+var_54], eax ;将返回结果送入[ebp+var_54]
假如我们可以复原传递给函数srand16的seed值,那我们就知道了很多关于seed3和seed4的信息了。仅凭一个由rand16产生的值,我们很难复原伪随机数发生器的初始状态(译者注:也就是由返回的eax的值确定随机函数rand1的输入值是不可能的,否则就不叫随机数发生器了,:))。为了复原传递给随机函数的输入值,我们需要仔细检查rand16函数和srand16函数。
/*------------------------------------------------------------------------*
*
* my_srand16: emulate the srand16 routine. ;自己写的my_srand16函数,模仿srand16函数
* seeds: seed values.
*------------------------------------------------------------------------*/
int my_srand16(int seed1, int seed2, int seed3)
{
sdata_0 = seed1 % 32363; //32363就是7E6B
sdata_1 = seed2 % 31727; //31727就是7BEE
sdata_2 = seed3 % 31657; //31657就是7BA8,这三个数应该都是质数。
if (sdata_0 == 0) sdata_0 += 32362;
if (sdata_1 == 0) sdata_1 += 31726;
if (sdata_2 == 0) sdata_2 += 31656;
return(0);
}
/*------------------------------------------------------------------------*
*
* my_rand16: emulate the rand16 routine. ;自己写的my_rand16函数,模仿rand16函数
*------------------------------------------------------------------------*/
int my_rand16()
{
int var_8;
int var_4;
int edxval;
int eaxval;
var_4 = sdata_0 / 206;
edxval = (sdata_0 / 206) * 206;
eaxval = sdata_0 - edxval;
sdata_0 = eaxval * 157 - (var_4 * 21);
if (sdata_0 <= 0)
{
sdata_0 += 32363;
}
var_4 = sdata_1 / 217;
edxval = var_4 * 217;
eaxval = sdata_1 - edxval;
sdata_1 = eaxval * 146 - (var_4 * 45);
if (sdata_1 <= 0)
{
sdata_1 += 31727;
}
var_4 = sdata_2 / 222;
edxval = var_4 * 222;
eaxval = sdata_2 - edxval;
sdata_2 = eaxval * 142 - (var_4 * 133);
if (sdata_2 <= 0)
{
sdata_2 += 31657;
}
var_8 = sdata_0 - sdata_1;
if (var_8 > 706)
{
var_8 = var_8 - 32362;
}
var_8 = var_8 + sdata_2;
if (var_8 <= 0)
{
var_8 += 32362;
}
return(var_8 >> 1);
}
尽管看起来比较乱,但是它确实是起到了伪随机数发生器的功能。这个rand16函数的算法和Bruce Schneier编的《实用密码学》(Applied Cryptography) 里提到的线性同余发生器的例子相同。(译者注:线性同余发生器是最常见的随机数发生器方法之一,比如1/7=0.142857142857....,当分母7换成一个足够大的质数p的时候,它的商的不同位看起来就像随机产生的)在这本书里作者提到了这个算法不具有加密安全性,并且给出怎样破解这个随机数发生器。这个随机数发生器由三个发生器组成,每个发生器产生不同长度的随机数。当每个发生器初始化后,每产生一个随机数,发生器就变换到下一个状态。给定一个输出和两个发生器的初始状态,第三个发生器的状态就可以确定了。
第一个随机数可以从目标程序中获得,而且RNG开始两个LCG是循环码,第三个LCG从目标程序获得。假如随机数序列匹配超过四个值,我们就确定了RNG的状态了。三个LCG的全部的循环都在一个巨大的数组了,所以我们可以获得RNG的状态,而且无需任何计算,只要一个指向LCG的指针即可。一旦确定RNG状态的那四个值出现了,LCG就一步步回退,直到找到传递给PRNG的那个初始值。因为数据在给LCG时已经经过取模运算,这样肯定会有不同的值经过取模运算后得到相同的值传递给LCG。所有有效的排列都循环传递给srand函数。经过这些工作后,我们知道关于seed的如下信息。
高16位seed3 ^ seed4 == value1
低16位seed3 + seed4 & 0xffff == value2
低16位seed3 ^ seed4 == value3
(译者注:这个结论是上面提到的_do_handshake函数的部分代码的功能)
知道这些后,根据value1、value2、value3的值,暴力破解可以减少到大约2^30次,这样暴力破解才是可行的。在flexlm目标程序里有三个ecc公钥。选择那个最小的公钥,因为从seed3和seed4算公钥/私钥对时,最快算到这个值。seed值通过vendor名被最低限度的“模糊”了。
int reveal_data(sb_keydata_t *inkey, char *inname)
{
int i;
char *p;
p = inname;
for (i = 0; i < inkey->nbytes; i++)
{
if (*p == '\0') p = inname;
if (i & 0x0000001)
{
if (i%3)
{
inkey->keydata[i] = inkey->keydata[i] + *p;
}
else
{
inkey->keydata[i] = inkey->keydata[i] ^ *p;
}
}
else
{
inkey->keydata[i] = inkey->keydata[i] - *p;
}
p++;
}
return(0);
}
一旦真正的公钥恢复了,我们可以用程序循环计算出可能的seed3和seed4,就说道这儿。我还要简单提一下seed1和seed2,他们仍然用相同的方法隐藏在相同的地方,所以前面提到通过job结构和vendorcode结构找seed1和seed2的方法依然有效。
Nolan Blender.
附do_handshake函数
static void do_handshake(char *charptrname, char *idxname, char *switchname)
{
char charname[MAXVARNAME];
int i[4];
char zeroname[MAXVARNAME];
randvarname(charname, "charname");
randvarname(zeroname, "zero");
srand16((seed3 ^seed4) & 0xffff, (seed3 + seed4) & 0xffff,
((seed3 ^ seed4) >> 16) & 0xffff, lmrands);
rand16(lmrands, 1);
rand16(lmrands, 1);
rand16(lmrands, 1);
i[0] = rand16(lmrands, 1);
i[1] = rand16(lmrands, 1);
i[2] = rand16(lmrands, 1);
i[3] = rand16(lmrands, 1);
fprintf(ofp,
" if (%s == %s) %s\n\
{\n\
unsigned char %s = *%s;\n\
unsigned int %s = %s/%d; %s\n\
\n\
if ((%s == %s) && ((%s ^ %d) & 0xff)) %s ^= %d;\n\
if ((%s == (%s + 1)) && ((%s ^ %d) & 0xff)) %s ^= %d;\n\
if ((%s == (%s + 3)) && ((%s ^ %d) & 0xff)) %s ^= %d;\n\
if ((%s == (%s + 2)) && ((%s ^ %d) & 0xff)) %s ^= %d;\n\
*%s = %s;\n\
return 0;\n\
}\n", switchname, bytenames[1],
DEVCOMMENT("/* == 1 for handshake */"),
charname, charptrname,
zeroname, charname, rand16(lmrands, 1)%400 + 257,
DEVCOMMENT("/* The result of the divide is guaranteed to be zero */"),
idxname, zeroname, charname, i[0], charname, i[0],
idxname, zeroname, charname, i[1], charname, i[1],
idxname, zeroname, charname, i[2], charname, i[2],
idxname, zeroname, charname, i[3], charname, i[3],
charptrname, charname);
}
static
int
decode(job, d, buf, key, junk, rcvmsg)
LM_HANDLE *job;
long d;
char *buf;
long key;
char *junk;
int rcvmsg;
{
int verrev;
#ifndef RELEASE_VERSION
which_failed++;
#endif
verrev = (job->daemon->ver * 100) + job->daemon->rev;
memset(junk, 0, MAX_HAND_DATA_LEN+1);
l_encode_long_hex(junk, l_modify((long)d));
/*
* Handle the case where it's a pre-v6 server, and
* it's the extra handshake tests
*/
if ((rcvmsg == -1 && (verrev < 579) )) return 1;
if (rcvmsg != LM_HANDSHAKE && rcvmsg != -1)
{
LM_SET_ERRNO(job, LM_BADHANDSHAKE, 375, 0);
return 0;
}
l_str_crypt(junk, MAX_HAND_DATA_LEN, key, 1);
if (!memcmp( buf, junk, MAX_HAND_DATA_LEN)) return 1;
if (rcvmsg == -1) LM_SET_ERRNO(job, LM_BADHANDSHAKE, 376, 0)
else LM_SET_ERRNO(job, LM_BADHANDSHAKE, 377, 0)
return 0;
}
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课