Sparc 平台上的 FlexLM 7.0 用户滤波函数分析

作为一个比较老旧的保护工具,FlexLM的初级保护已经被分析的差不多了,默认情况下内存中能截到明文的签名,这点很多前辈的文章中都有提到。但本人近日在分析一款SunOS Sparc平台下的FlexLM 7.0时遇上了 user_crypt_filter 机制,这种机制会对签名加以变形运算,导致内存中不会出现明文,给破解的捕获增加了一些难度。后来参考Nolan Blender前辈的文章,总算破掉了这个用户滤波函数的保护,在此写出来总结一下供有相同需要的朋友参考与讨论。
本文没有提到到具体的软件名称,也不涉及到Feature、Vendor与版本号的跟踪捕获,甚至也和抓取VendorKey和加密种子等没有关系。另外Sparc平台的汇编可能大伙都不熟悉,这儿尽量多加以解释。
工具:DDD+GDB、IDA

一、按传统方法定位明文比较处。

参考laoqian的《制作Flexlm license总结》的文章,以及laowanghai的《LabWindows CVI 8.0》中列出来的详尽x86汇编代码,通过在IDA中搜索常数66D8B337,可以定位到几个地方。以下是laowanghai的《LabWindows CVI 8.0》文章中列出的部分x86的代码供参考:

105C9CDB      894D F4                    mov dword ptr ss:[ebp-C],ecx
105C9CDE    ^ E9 EEFAFFFF                jmp CVI_1.105C97D1
105C9CE3      817D 18 37B3D866           cmp dword ptr ss:[ebp+18],66D8B337
105C9CEA      0F85 96000000              jnz CVI_1.105C9D86
105C9CF0      33D2                       xor edx,edx
……
105C9D86      C785 78FEFFFF 08000000     mov dword ptr ss:[ebp-188],8
105C9D90      817D 18 37B3D866           cmp dword ptr ss:[ebp+18],66D8B337
105C9D97      75 0F                      jnz short CVI_1.105C9DA8
……
105C9EB0      8B95 30FEFFFF              mov edx,dword ptr ss:[ebp-1D0]
105C9EB6      81E2 FF000000              and edx,0FF
105C9EBC      8B85 7CFEFFFF              mov eax,dword ptr ss:[ebp-184]
105C9EC2      33C9                       xor ecx,ecx
105C9EC4      8A88 98358A10              mov cl,byte ptr ds:[eax+108A3598]
105C9ECA      3BD1                       cmp edx,ecx                     ; 与正确SIGN逐字节的比较

这里,比较之处前面一个and edx,0FF比较的显目,可以作为一个小标志。
类似的,在我们对付的SPARC平台下的软件,其比较代码也类似:

 loc_3515CC:                             ! CODE XREF: sub_3508E4+6B0j
.text:003515CC                                         ! sub_3508E4+CE0j
.text:003515CC                 ld      [%fp+arg_54], %o5
.text:003515D0                 set     0x66D8B337, %o7 ! 绝对是这儿的常数
.text:003515D8                 cmp     %o5, %o7
.text:003515DC                 be      loc_3515EC
.text:003515E0                 nop
……
 loc_351684:                             ! CODE XREF: sub_3508E4+D00j
.text:00351684                 mov     8, %o7          ! 也是这儿的常数
.text:00351688                 st      %o7, [%fp+var_8]
.text:0035168C                 ld      [%fp+arg_54], %l0
.text:00351690                 set     0x66D8B337, %l1
.text:00351698                 cmp     %l0, %l1
.text:0035169C                 be      loc_3516AC
.text:003516A0                 nop
……
.text:003517F8 loc_3517F8:                             ! CODE XREF: sub_3508E4+EE0j
.text:003517F8                 ld      [%fp+var_4_idx], %o3
.text:003517FC                 set     byte_4DD515, %o4
.text:00351804                 ldub    [%o3+%o4], %o5  ! ldub取一位字节,相当于同时做了上面的and 0xff了
.text:00351808                 ldub    [%fp+var_180_oursign+3], %o7
.text:0035180C                 cmp     %o7, %o5        ! 是这儿的比较
.text:00351810                 bne     loc_351820
.text:00351814                 nop
.text:00351814

由x86与Sparc的汇编代码对比可以看出,.text:0035180C处确实是我们传入的伪造签名值(我传的是1234567890AB)与正确签名值的比较之处,byte_4DD515这个地址所指的一片内存区域,应该是我们需要的正确签名。于是在0035180C处下断,第一次来到此处时查看0x4DD515地址的内容,得到六个字节EEB53723B248。想当然地拿它放到license文件里头一试,很不幸,通不过。

于是开始了摸索,在0035180C之前下断,逐步来,发现0035180C之前有一处很关键的代码:

.text:003517CC loc_3517CC:                             ! CODE XREF: sub_3508E4+ED8j
.text:003517CC                 ld      [%fp+arg_44_job], %o0
.text:003517D0                 ld      [%o0+0x234], %g1 ! Job偏移234处是啥?原来是user_crypt_filter函数地址。
.text:003517D4                 ld      [%fp+var_4_idx], %o1
.text:003517D8                 set     byte_4DD515, %o2
.text:003517E0                 add     %o1, %o2, %o1   ! char*,指向正确的未filter的内容
.text:003517E4                 ldub    [%fp+var_180_oursign+3], %o3 ! 我们的sign的字符
.text:003517E8                 ld      [%fp+arg_44_job], %o0 ! Job!
.text:003517EC                 ld      [%fp+var_4_idx], %o2 ! Index?
.text:003517F0                 call    %g1             ! 3323a0,user_crypt_filter,结果放在o1指的内容
.text:003517F4                 nop

那个call %g1是对解出的明文密码进行的一次变换。刚才抓到的六个字节EEB53723B248,其中EE是已经经过了第一次变换的结果,变换出来的结果和我们传入的伪造签名的第一个字节12不一致,因此出错。如果在003517F0之前下断查看byte_4DD515的内容,则可得到未经变换的第一个字节值。抓出的初始的正确签名值是C1B53723B248,但如果拿这个签名值去license文件里头试的话仍然通不过,因为这串签名值还会经过一次变换,这个变换便是臭名昭著的user_crypt_filter:用户滤波函数。

二、分析用户滤波函数

查FlexLM的资料得知,user_crypt_filter是对签名字符进行的进一步可逆变换机制,包括生成时的user_crypt_filter解码函数和user_crypt_filter_gen编码函数。我们的任务,是根据user_crypt_filter函数的实现,反推出user_crypt_filter_gen函数来。

从代码中以及从前辈们的破解资料中可得知,user_crypt_filter 附近的调用机制基本上是:

for (i = 0; i < j; i++) /* compare user-input and real checksums */
{
  ......
  if (user_crypt_filter)
    (*user_crypt_filter)(job, &x, i, y[i]); // 使用了滤波函数进行进一步的对比!
    
  if (x != y[i]) 
    return 0;
}

而网上查到的很多中文文章中的FlexLM都是没用user_crypt_filter的,也就是user_crypt_filter为false而跳过(*user_crypt_filter)指针所指函数的变换,因此下面的x != y[i]便是正确签名字符与我们输入的伪造签名字符的明文比较。而我们碰到的情况是会调用user_crypt_filter的函数的。

user_crypt_filter 的函数原型为:
user_crypt_filter(job, char* KeySign, int index, char OurSign);

其中job在函数体内基本无用。KeySign为传入的未变换的签名的地址(下面称为原始签名),index为此字符在密文中的位置,如签名为1234567890AB,则12的index为0,34的为1,以此类推。

user_crypt_filter是逐个处理字符的,它根据未变换的原始签名字符、此字符的位置、以及我们传入的伪造签名字符进行一系列的运算返回一个字符,这个字符必须等于我们传入的伪造签名字符。这么说可能有点绕,换种说法就是:未变换的原始签名字符、此字符的位置、以及变换后的签名字符三者必须符合一定的运算法则,user_crypt_filter函数拿我们传入的伪造签名字符作为验证来和原始签名字符参与运算,如果运算通过,则表示伪造的签名字符是正确的变换后的签名字符,那么就将此正确的签名字符返回供上层代码(x != y[i])比较,其实此时已经无需比较了。

可以看出,user_crypt_filter函数中不会出现我们需要的正确签名字符,只有当我们传入的伪造签名字符等于正确签名字符时才会验证通过,才会返回正确签名字符。下面我们要搞清楚的是user_crypt_filter函数究竟对未变换的原始签名字符、此字符的位置、以及我们传入的伪造签名字符三者进行了怎样的变换,从而努力反推出来如何根据未变换的原始签名字符以及此字符的位置计算正确的签名字符。

user_crypt_filter函数非常长,从.text:003323A0 到.text:003374B8,反汇编代码有七千多行,很是吓人。不过经过大致浏览,可以看出它是根据传入的Idx的范围0到19有比较重复的20大段代码,20大段里头每大段会对字符进行异或并对其8位进行处理,共20加160处变换,搞明白了它做啥就好办了。

首先是将传入的字符异或,比如我们传入的Idx是0的话:

.text:003324E0                 ld      [$XADqrkBrM9j3y7__num0], %l0
.text:003324E4                 cmp     %l1, %l0
.text:003324E8                 bne     loc_332504
.text:003324EC                 nop
.text:003324EC
.text:003324F0                 ldub    [%fp+var_2_GoodChar_xor], %l2
.text:003324F4                 sethi   %hi(unk_496800), %l0
.text:003324F8                 ldub    [$XADqrkBrM9j3y7__x_0], %l1 ! 
// 取出num0对应的某常量值(x_0)和GoodChar一异或,存入局部变量 GoodChar_Xor。
.text:003324FC                 xor     %l2, %l1, %l0
.text:00332500                 stb     %l0, [%fp+var_2_GoodChar_xor]

根据传入的Idx的不同,将未变换的原始签名字符和某一值进行异或,得到临时变量GoodChar_xor。

然后启用另外一个Char变量,这里命名为TmpChar,用来挨个检查GoodChar_xor的位,比如这儿是检查其中一位:

 loc_332740:                             ! CODE XREF: user_crypt_filter+384j
.text:00332740                 ld      [%fp+arg_4C_Idx], %l1
.text:00332744                 sethi   %hi(unk_496800), %l0
.text:00332748                 ld      [$XADqrkBrM9j3y7__num8], %l0
.text:0033274C                 cmp     %l1, %l0
.text:00332750                 bne     loc_3327BC // 如果此字符的index是8,那么检查
.text:00332754                 nop
.text:00332754
.text:00332758                 ldub    [%fp+var_2_GoodChar_xor], %l2
.text:0033275C                 sethi   %hi(unk_496800), %l0
.text:00332760                 ld      [$XADqrkBrM9j3y7__bit3], %l1
.text:00332764                 andcc   %l2, %l1, %l0
.text:00332768                 be      loc_332784  // 如果异或后的字符的bit3位是1 则执行下面的,
.text:0033276C                 nop
.text:0033276C
.text:00332770                 ldub    [%fp+var_1_TmpChar], %l2
.text:00332774                 sethi   %hi(unk_496800), %l0
.text:00332778                 ld      [$XADqrkBrM9j3y7__bit4], %l1  
// 如果异或后的字符的bit3位是1,则将临时变量TmpChar的bit4置为1,
.text:0033277C                 or      %l2, %l1, %l0
.text:00332780                 stb     %l0, [%fp+var_1_TmpChar]
.text:00332780
.text:00332784
.text:00332784 loc_332784:                             ! CODE XREF: user_crypt_filter+3C8j
.text:00332784                 ldub    [%fp+var_1_TmpChar], %l1
.text:00332788                 sethi   %hi(unk_496800), %l0
.text:0033278C                 ld      [$XADqrkBrM9j3y7__bit4], %l2
.text:00332790                 and     %l1, %l2, %l1
.text:00332794                 ldub    [%fp+var_3_OurChar], %l0
.text:00332798                 and     %l0, %l2, %l0
.text:0033279C                 cmp     %l1, %l0
.text:003327A0                 be      loc_3327BC // 然后检查我们伪造的签名字符的bit4,和TmpChar的bit4是否相同。
.text:003327A4                 nop
.text:003327A4
.text:003327A8                 ld      [%fp+arg_48_charX], %l1
.text:003327AC                 ldsb    [%l1], %l0
.text:003327B0                 btog    -0x3E, %l0 // 在俩bit不同的情况下,返回垃圾值3E
.text:003327B4                 ba      locret_3374B8
.text:003327B8                 stb     %l0, [%l1]
.text:003327B8

以上是对Index为8时的未签名字符bit3位的判断检查过程:如果GoodChar_xor的bit3是1,则TmpChar的bit4也置一,因为TmpChar原始为0,因此bit4原始也为0,所以这一步就是把GoodChar_xor的bit3搬移到了TmpChar的bit4上。
然后TmpChar的bit4和我们传入的伪造签名字符OurChar的bit4进行比较,相同的话这位检查通过,继续检查下一位,否则返回一个垃圾值-0x3E用以迷惑俺们。总的来说,这步就是检查异或后的GoodChar_xor的bit3是否等于伪造签名字符的bit4,等的话才通过。

参考Nolan Blender的文章,这里检查一个bit的过程用C代码描述如下(这里是index为0时检查GoodChar_xor的bit2与TmpChar的bit5是否相等的情况):

if (idx == num0)
{
    if (GoodChar_xor & bit2)  TmpChar |= bit5; 
    // 如果inc_c的2置位,那么把另外一个C的5置位,相当于2位换到5位去
    if ((TmpChar & bit5) != (OurChar & bit5)) { *KeySign = 0xxx; return; // 不等则返回垃圾}
    // 其实就是GoodChar_xor的bit2要和输入签名字符的bit5相等。
}

以下还有很多类似的检验,对于每一个index,都要检查GoodChar_xor的8个bit和OurChar的8个bit是否相同,但这8个bit并不是顺序上一一对应,而是乱序(permute)了。比如GoodChar_xor的bit0应该等于OurChar的bit3、GoodChar_xor的bit2必须等于OurChar的bit7,等等。

由此可知,只要将GoodChar_xor按user_crypt_filter中特定的index处所指明的bit置换规则,把各个bit换一下,则可得到解密后的明文签名字符。这也就是说,如果我们在代码中找到了针对一个index的字符的八条位置换规则,如GoodChar_xor的bit0应该等于OurChar的bit3、GoodChar_xor的bit2应该等于OurChar的bit7等共八条,那么我们只要新起一个变量TmpChar,将GoodChar_xor的bit0放到变量TmpChar的bit3,GoodChar_xor的bit2换到TmpChar的bit7,换过8个bit后,这个TmpChar的每一位必然等于解密后的明文签名字符OurChar。这就是user_crypt_filter检查运算的逆运算!

三、编写逆运算程序

逆运算分析出来了,就可以写还原程序了。
这儿参考了Nolan Blender的思想,将0到19个index,每个index所对应的8位bit置换规则写成一个大数组,共一百六十项,从七千多行汇编代码中整理出来相当费力,但所幸只是体力活儿。而且如果是短的签名的话,只要搜集0到5的index所对应的四十八条bit置换规则即可。

以下代码用Delphi实现:

type
  TShiftVals = array[0..7] of Integer;
  TPermute = packed record
    shiftvals: TShiftVals;
  end;

PerTab: array[0..19] of TPermute = // 搜集得到的位置换规则数组
(           // BIT  0 1 2 3 4 5 6 7
       (shiftvals: (0,4,5,3,1,2,6,7)), // idx 00 //
       (shiftvals: (4,0,5,2,1,3,6,7)), // idx 01 //
       (shiftvals: (7,1,3,4,0,5,2,6)), // idx 02 //
       (shiftvals: (4,7,3,6,1,5,2,0)), // idx 03 //
       (shiftvals: (0,3,7,4,2,5,6,1)), // idx 04 //
       (shiftvals: (4,3,5,6,7,0,1,2)), // idx 05 //
       (shiftvals: (2,7,4,0,6,5,3,1)), // idx 06 //
       (shiftvals: (4,5,1,7,0,3,6,2)), // idx 07 //
       (shiftvals: (1,0,5,4,3,6,7,2)), // idx 08 //
       (shiftvals: (2,7,5,3,0,6,1,4)), // idx 09 //
       (shiftvals: (3,5,7,0,6,4,2,1)), // idx 10 //
       (shiftvals: (7,6,5,0,4,3,2,1)), // idx 11 //
       (shiftvals: (3,4,5,6,0,2,1,7)), // idx 12 //
       (shiftvals: (0,4,6,3,5,2,1,7)), // idx 13 //
       (shiftvals: (1,5,0,2,6,3,4,7)), // idx 14 //
       (shiftvals: (2,6,5,7,4,3,1,0)), // idx 15 //
       (shiftvals: (2,3,0,5,1,7,6,4)), // idx 16 //
       (shiftvals: (1,7,4,2,3,0,6,5)), // idx 17 //
       (shiftvals: (1,5,2,3,4,7,6,0)), // idx 18 //
       (shiftvals: (3,0,6,7,4,5,2,1))  // idx 19 //
);

如 (shiftvals: (0,4,5,3,1,2,6,7)), // idx 00 //,表示对于index为0的字符,其bit0位置换到bit0位(两位可以相同),bit1置换到bit4,bit2置换到bit5,等等以此类推。

另外还有一次异或,此异或的数字也是常数,根据index不同而不同,是上文代码中的x_0等形式的变量。总结得出一个异或数组:

var
  xorvals: array[0..19] of Integer =
  (
    $1E, $16, $3E, $24,
    $04, $1E, $1E, $13,
    $15, $0C, $2D, $33,
    $3D, $21, $26, $2E,
    $12, $34, $01, $2B
  );

然后写一个解密函数与bit置换函数(参考了:Nolan Blender的思想)

// 传入计算来的原始的签名字符与位置,返回变换后的正确签名字符
function user_crypt_filter_gen(inchar: Char; idx: Integer): Char;
var
  tmpchr: Char;
begin
  tmpchr := Chr(Ord(inchar) xor xorvals[idx]); // 先异或还原
  Result := Permute(tmpchr, PerTab[idx].shiftvals); // 再置换位置
end;

// 将传入字符按大数组表内的规则进行置换位置
function permute(inchar: Char; shiftvals: TShiftVals): Char;
var
  outval: Integer;
  i: Integer;
  shbit: Integer; // Test bit */
begin
  outval := 0;
  shbit := 1;
  for i := 0 to 7 do
  begin
    if ((Ord(inchar) and shbit) <> 0) then
    begin
      outval := outval or (1 shl (shiftvals[i]));
    end;
    shbit := shbit shl 1;
  end;
  Result := Chr(outval and $FF);
end;

然后,利用上面两个写好的函数对抓出的未变换签名值C1B53723B248进行运算:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Edit1.Text := IntToHex(Ord(user_crypt_filter_gen(#$C1, 0)), 2)+
    IntToHex(Ord(user_crypt_filter_gen(#$B5, 1)), 2) +
    IntToHex(Ord(user_crypt_filter_gen(#$37, 2)), 2) +
    IntToHex(Ord(user_crypt_filter_gen(#$23, 3)), 2) +
    IntToHex(Ord(user_crypt_filter_gen(#$B2, 4)), 2) +
    IntToHex(Ord(user_crypt_filter_gen(#$48, 5)), 2);
end;

运行程序后,Edit1.Text中得到生成的正确签名值FB999098AEAA。拿这串签名填入license文件中,测试通过。

再次跟踪user_crypt_filter可知,比如对index为0的情况,传入原始字符C1与index 0,C1与1E异或后的值经过八次位置换得到FB,和我们传入的FB相等,因此通过,返回FB(只要有一位不等,就会返回垃圾值,外部的比较必然通不过)。user_crypt_filter外部再用返回的FB与我们传入的FB比较,自然相等,也就通过了。

四、总结

user_crypt_filter的核心是字符的位置换规则,需要通篇阅读代码以总结出一百六十个位置换规则来,这一点比较的耗体力并且不能出错,否则通不过的情况下再次调试就需要跟入七千多行的user_crypt_filter以找到出错点,这体力耗费的就不是总结位置换规则所能比的了。我在总结过程中还好没在位置换规则中出错,但搞错了一位xorvals中的异或值,事后查起来也耗了一点力气,等到所幸最后还是找出来的时候已经快累趴下了。

参考资料:
http://www.woodmann.com/crackz/Tutorials/Nbufilt.htm
laoqian的《制作Flexlm license总结》的文章
laowanghai的《LabWindows CVI 8.0》
其他看雪论坛的精华文章
http://www-curri.u-strasbg.fr/documentation/calcul/doc/ProPack/3SP1/docs/doc/lmsgi-9.2.3/flexref/chap21.htm