破解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;
}