litao3rd 发表于 2016-7-23 19:03

一个CrackMe的分析

PS:
本文已经发表在看雪论坛,现由于申请吾爱破解论坛账号需要,再次发表在本论坛。谢谢。

原文链接:http://bbs.pediy.com/showthread.php?t=210822

原文内容如下
-------------------------------------

最近学习了CCDebuger前辈的【OllyDBG 入门系列】文章(PS:虽然这系列文章已经发表十年了,但对我这个菜鸟依然很有帮助,谢谢作者)。

原文链接在这里: http://bbs.pediy.com/showthread.php?threadid=21532

在原文中,作者详细介绍了使用消息断点和RUN跟踪的方法找到这个CrackMe的关键部分,但是没有对关键部分进行分析,小菜我就斗胆分享一下我的分析结果,由于这个CrackMe的算法有点复杂,注册码无法通过简单的逆向计算得到,需要进行一定的爆破猜解,就没有写注册机代码了(其实是技术太菜人又太懒),但是找到了一个可用的注册码。

无论是使用作者介绍的消息断点还是函数参考,我们都可以定位到下面这个函数中。这个函数即为本CrackMe的主体函数,全部逻辑都在这个函数中实现,或者调用其他函数实现。

0040109C/$C705 82214000>MOV DWORD PTR DS:,FEDCBA98
004010A6|.6A 11         PUSH 11                                  ; /Count = 11 (17.)
004010A8|.68 71214000   PUSH cycle.00402171                      ; |Buffer = cycle.00402171
004010AD|.68 E9030000   PUSH 3E9                                 ; |ControlID = 3E9 (1001.)
004010B2|.FF75 08       PUSH DWORD PTR SS:                ; |hWnd
004010B5|.E8 0F020000   CALL <JMP.&USER32.GetDlgItemTextA>       ; \get serial number
004010BA|.0BC0          OR EAX,EAX                               ;判断是否得到序列号的有效输入
004010BC|.74 61         JE SHORT cycle.0040111F
004010BE|.6A 11         PUSH 11                                  ; /Count = 11 (17.)
004010C0|.68 60214000   PUSH cycle.00402160                      ; |Buffer = cycle.00402160
004010C5|.68 E8030000   PUSH 3E8                                 ; |ControlID = 3E8 (1000.)
004010CA|.FF75 08       PUSH DWORD PTR SS:                ; |hWnd
004010CD|.E8 F7010000   CALL <JMP.&USER32.GetDlgItemTextA>       ; \GetDlgItemTextA
004010D2|.0BC0          OR EAX,EAX                               ;判断是否得到用户名有效输入
004010D4|.74 49         JE SHORT cycle.0040111F
004010D6|.B9 10000000   MOV ECX,10                               ;如果用户输入的用户名短于16个字符
004010DB|.2BC8          SUB ECX,EAX                              ;则将其复制扩展为16个字符长度
004010DD|.BE 60214000   MOV ESI,cycle.00402160
004010E2|.8BFE          MOV EDI,ESI
004010E4|.03F8          ADD EDI,EAX
004010E6|.FC            CLD
004010E7|.F3:A4         REP MOVS BYTE PTR ES:,BYTE PTR DS:[>
004010E9|.33C9          XOR ECX,ECX
004010EB|.BE 71214000   MOV ESI,cycle.00402171
004010F0|>41            /INC ECX                                 ;判断输入的序列号字符串每个字符是否有效
004010F1|.AC            |LODS BYTE PTR DS:                  ;有效字符的范围在ASCII码的 0x30 到 0x7E 之间
004010F2|.0AC0          |OR AL,AL
004010F4|.74 0A         |JE SHORT cycle.00401100
004010F6|.3C 7E         |CMP AL,7E
004010F8|.7F 06         |JG SHORT cycle.00401100
004010FA|.3C 30         |CMP AL,30
004010FC|.72 02         |JB SHORT cycle.00401100
004010FE|.^ EB F0         \JMP SHORT cycle.004010F0
00401100|>83F9 11       CMP ECX,11                               ;判断输入的序列号长度是否为17
00401103|.75 1A         JNZ SHORT cycle.0040111F         ;这里包括16个合法字符和1个0x00 的字符串结束符
00401105|.E8 E7000000   CALL cycle.004011F1            ;第一个转换函数,具体在下面进行分析,这里使用了全局的数据段进行传参,计算结果保存在EAX和EBX中
0040110A|.B9 01FF0000   MOV ECX,0FF01
0040110F|.51            PUSH ECX                                 ;第二个转换函数,这里也到了一个全局变量var_402182和EAX EBX ECX 进行传参
00401110|.E8 7B000000   CALL cycle.00401190                      ;ECX 在上面进行了初始化,EAX和EBX则由sub_4011f1 这个函数进行赋值
00401115|.83F9 01       CMP ECX,1                              ;这里是第一次验证,如果ECX返回值不为1则验证失败,程序报错,否则进行第二次验证
00401118|.74 06         JE SHORT cycle.00401120
0040111A|>E8 47000000   CALL cycle.00401166                      ;输出验证失败消息函数
0040111F|>C3            RETN
00401120|>A1 68214000   MOV EAX,DWORD PTR DS:            ;这里进行了第二次验证,验证的参数分别是
00401125|.8B1D 6C214000 MOV EBX,DWORD PTR DS:            ;输入的用户名后8位字符,转换成两个DWORD进行异或操作
0040112B|.33C3          XOR EAX,EBX
0040112D|.3305 82214000 XOR EAX,DWORD PTR DS:            ;将上面计算的结果与sub_401190 函数对全局变量var_402182的计算结果进行异或
00401133|.0D 40404040   OR EAX,40404040                        ;置位和复位上面计算的结果的相应bit位
00401138|.25 77777777   AND EAX,77777777
0040113D|.3305 79214000 XOR EAX,DWORD PTR DS:            ;继续将结果与输入的序列号的后8个字符进行异或操作
00401143|.3305 7D214000 XOR EAX,DWORD PTR DS:
00401149|.^ 75 CF         JNZ SHORT cycle.0040111A               ;第二次验证,如果上面得到的结果为0,则验证通过,否则验证失败
0040114B|.E8 2B000000   CALL cycle.0040117B                      ;输出验证通过消息函数
00401150\.C3            RETN

上面这个函数主要做了四个工作:

1)得到用户输入的用户名和序列号字符串,判断用户名长度是否达到16字符长度,如果没有,则复制拷贝,下面的C代码实现了类似的功能。判断用户输入的序列号是否在合法字符范围内,
合法字符范围为ASCII码的0x30-0x7E之间,并且序列号长度也为16个有效字符。

static void fullfill_name(char *name, size_t size)
{
    int i;
    size_t len = strlen(name) - 1;         // 键盘输入,会多一个回车符,需要删除
    for(i = 0; i < size-len; ++i){
      name = name;
    }
    name = '\0';
}

2)调用第一个转换函数 cycle.004011F1,这个函数使用的参数为输入的用户名和输入的序列号,返回结果保存在 EAX 和 EBX 寄存器中,作为下一个转换函数的参数。
004011F1/$A1 60214000   MOV EAX,DWORD PTR DS:            ;将输入的16字节长度的用户名和序列号分别转换为整型数组name, serial
004011F6|.8B1D 64214000 MOV EBX,DWORD PTR DS:            ;一个整数长度为4字节,16字节可视作具有4个整数的整型数组
004011FC|.3305 71214000 XOR EAX,DWORD PTR DS:            ;分别取整型数组的0号位和1号位进行异或处理,保存在EAX和EBX中
00401202|.331D 75214000 XOR EBX,DWORD PTR DS:            ;EAX=name ^ serial, EBX = name ^ serial
00401208|.25 0F1F3F7F   AND EAX,7F3F1F0F                         ;取相应的有效位
0040120D|.81E3 00010307 AND EBX,7030100                        ;下面是一个for循环,ECX作为循环变量,初始值为0,进行8次循环
00401213|.33C9          XOR ECX,ECX                              ;循环先得到EAX和EBX的副本,保存在ESI和EDI寄存器中
00401215|>8BF0          /MOV ESI,EAX
00401217|.8BFB          |MOV EDI,EBX
00401219|.D3E6          |SHL ESI,CL                              ;对副本先进行移位ECX位,ECX是循环控制变量
0040121B|.D3E7          |SHL EDI,CL
0040121D|.81E6 80808080 |AND ESI,80808080                        ;取副本的每个字节的最高位(副本长度为4个字节长)
00401223|.81E7 80808080 |AND EDI,80808080
00401229|.8BD6          |MOV EDX,ESI                           ;下面是将得到的副本的每个字节的最高位组合放在一个字节的4位中
0040122B|.C0EE 07       |SHR DH,7                              ;这一块处理的是EAX的副本ESI
0040122E|.66:C1E2 07    |SHL DX,7
00401232|.C1EA 08       |SHR EDX,8
00401235|.C0EE 07       |SHR DH,7
00401238|.66:C1E2 07    |SHL DX,7
0040123C|.C1EA 08       |SHR EDX,8
0040123F|.C0EE 07       |SHR DH,7
00401242|.66:D1EA       |SHR DX,1                              ;这里移位1位是为了将所有最高位保存在DX的最低字节的高4位
00401245|.8BF2          |MOV ESI,EDX
00401247|.8BD7          |MOV EDX,EDI                           ;同样将EBX的副本EDI的每个字节的最高位组合成一个字节的4位
00401249|.C0EE 07       |SHR DH,7
0040124C|.66:C1E2 07    |SHL DX,7
00401250|.C1EA 08       |SHR EDX,8
00401253|.C0EE 07       |SHR DH,7
00401256|.66:C1E2 07    |SHL DX,7
0040125A|.C1EA 08       |SHR EDX,8
0040125D|.C0EE 07       |SHR DH,7
00401260|.66:C1EA 05    |SHR DX,5                              ;这里移位5位是为了将所有最高位保存在DX的最低字节的低4位
00401264|.8BFA          |MOV EDI,EDX
00401266|.33FE          |XOR EDI,ESI                           ;上面处理之后的ESI、EDI分别保存了原副本每个字节的最高位,两个寄存器一共保存了8位
00401268|.8BD7          |MOV EDX,EDI                           ;上面分别处理的副本又重新组合成一个8位的字节码,保存在EDX寄存器中
0040126A|.81E2 FF000000 |AND EDX,0FF
00401270|.51            |PUSH ECX
00401271|.52            |PUSH EDX
00401272|.BA 08000000   |MOV EDX,8
00401277|.91            |XCHG EAX,ECX                            ;判断循环变量是否大于3,如果大于3,则将EBX进行循环移位,否则将EAX进行循环移位
00401278|.83F8 03       |CMP EAX,3
0040127B|.7F 0F         |JG SHORT cycle.0040128C
0040127D|.F6E2          |MUL DL                                  ;如果ECX小于等于3,则循环移位EAX,移位次数为 ECX * 8 + 8
0040127F|.5A            |POP EDX
00401280|.83C0 08       |ADD EAX,8
00401283|.91            |XCHG EAX,ECX
00401284|.D3C0          |ROL EAX,CL
00401286|.33C2          |XOR EAX,EDX                           ;移位之后与上面得到的8位字节码做异或处理,结果写回原寄存器中
00401288|.D3C8          |ROR EAX,CL                              ;再将EAX反向循环移位ECX * 8 + 8
0040128A|.EB 0D         |JMP SHORT cycle.00401299
0040128C|>83E8 03       |SUB EAX,3
0040128F|.F6E2          |MUL DL                                  ;如果ECX大于3,则循环移位EBX,移位次数为 (ECX-4) * 8 + 8
00401291|.5A            |POP EDX
00401292|.91            |XCHG EAX,ECX
00401293|.D3C3          |ROL EBX,CL                              ;这里的处理步骤与上面类似,只是操作数为EBX
00401295|.33DA          |XOR EBX,EDX
00401297|.D3CB          |ROR EBX,CL
00401299|>59            |POP ECX
0040129A|.41            |INC ECX
0040129B|.83F9 08       |CMP ECX,8
0040129E|.^ 0F85 71FFFFFF \JNZ cycle.00401215
004012A4\.C3            RETN

第一个转换函数使用了输入的用户名和序列号的字符串的前8位,先得到每个字节的最高位,组合成一个掩码,将EAX或者EBX的某一字节与掩码进行异或处理,然后重新计算新的掩码,再进
行下一个字节的异或处理。循环八次,将EAX和EBX的每一个字节都与相应的最高有效位进行异或保存。

// 这个函数将参数的最高有效位保存在一个整数中返回。
static int gather_byte_msb(unsigned int value)
{
int ret = 0;
int i;
for(i = 0; i < sizeof(value); ++i){
      ret |= ((value >> (i * 8)) & 0x80) >> (7-i);
}
return ret;
}

// 实现了上面的基本逻辑的C代码
static void sub_4011f1(const char *name, const char *serial, unsigned int *eax, unsigned int *ebx)
{
int i;
*eax = ((int *)name);
*ebx = ((int *)name);

*eax ^= ((int *)serial);
*ebx ^= ((int *)serial);

*eax &= 0x7f3f1f0f;
*ebx &= 0x7030100;

for(i = 0; i < 8; ++i){
    int esi = *eax;
    int edi = *ebx;
    int edx, tmp;

    esi <<= i;
    edi <<= i;

    esi = gather_byte_msb(esi);
    edi = gather_byte_msb(edi);

      // 将上面得到的两个半字节数组合成一个字节数
    edi = (esi << 4) ^ edi;

    // edx 最为掩码对相应字节进行异或处理
      edx = edi & 0xff;

      tmp = 8;
      if(i <= 3){
            tmp = 8 * i + 8;
            *eax = rol(*eax, tmp);
            *eax ^= edx;
            *eax = ror(*eax, tmp);
      }else{
            tmp = 8 * (i-3);
            *ebx = rol(*ebx, tmp);
            *ebx ^= edx;
            *ebx = ror(*ebx, tmp);
      }
}
}

3)调用 cycle.00401190函数,这个函数是一个递归函数,递归的参数保存在ECX中,初始值为0xFF01,这两个字节,高字节0xFF作为掩码,进行与运算;低字节0x01,作为移位次数的控制
变量,这里的0x01,又被分为两个半字节,高半字节控制EBX,低半字节控制EAX,但是不会同时有效,所以一次调用中,这个字节只会有某1个bit位被置1,即如果该值为0x01,则将EAX循环
移动1个8位,如果该值为0x08,则将EAX循环移位4个8位。如果该值为0x40,则将EBX循环移位3个8位。移位之后,再将移位的副本与ECX中的掩码参数进行与,得到一个字节,如果该字节不
为0,则找到该字节的最高有效位,比如该字节为0xA8,则最高有效位为0x80,将该有效位清除,即该字节变为0x28,同时ECX的掩码中的相应有效位清除,移位中的相应位置位,作为下一
次递归调用的参数,如前所述,如果最高有效位为0x80,则ECX进行计算(0xFF01 & 0xFF00 ) ^ 0x8080==0x7F80,作为下一次递归调用的ECX参数,同时将全局变量dword_402182自增一次,
作为统计。

004011F1/$A1 60214000   MOV EAX,DWORD PTR DS:            ;将输入的16字节长度的用户名和序列号分别转换为整型数组name, serial
004011F6|.8B1D 64214000 MOV EBX,DWORD PTR DS:            ;一个整数长度为4字节,16字节可视作具有4个整数的整型数组
004011FC|.3305 71214000 XOR EAX,DWORD PTR DS:            ;分别取整型数组的0号位和1号位进行异或处理,保存在EAX和EBX中
00401202|.331D 75214000 XOR EBX,DWORD PTR DS:            ;EAX=name ^ serial, EBX = name ^ serial
00401208|.25 0F1F3F7F   AND EAX,7F3F1F0F                         ;取相应的有效位
0040120D|.81E3 00010307 AND EBX,7030100                        ;下面是一个for循环,ECX作为循环变量,初始值为0,进行8次循环
00401213|.33C9          XOR ECX,ECX                              ;循环先得到EAX和EBX的副本,保存在ESI和EDI寄存器中
00401215|>8BF0          /MOV ESI,EAX
00401217|.8BFB          |MOV EDI,EBX
00401219|.D3E6          |SHL ESI,CL                              ;对副本先进行移位ECX位,ECX是循环控制变量
0040121B|.D3E7          |SHL EDI,CL
0040121D|.81E6 80808080 |AND ESI,80808080                        ;取副本的每个字节的最高位(副本长度为4个字节长)
00401223|.81E7 80808080 |AND EDI,80808080
00401229|.8BD6          |MOV EDX,ESI                           ;下面是将得到的副本的每个字节的最高位组合放在一个字节的4位中
0040122B|.C0EE 07       |SHR DH,7                              ;这一块处理的是EAX的副本ESI
0040122E|.66:C1E2 07    |SHL DX,7
00401232|.C1EA 08       |SHR EDX,8
00401235|.C0EE 07       |SHR DH,7
00401238|.66:C1E2 07    |SHL DX,7
0040123C|.C1EA 08       |SHR EDX,8
0040123F|.C0EE 07       |SHR DH,7
00401242|.66:D1EA       |SHR DX,1                              ;这里移位1位是为了将所有最高位保存在DX的最低字节的高4位
00401245|.8BF2          |MOV ESI,EDX
00401247|.8BD7          |MOV EDX,EDI                           ;同样将EBX的副本EDI的每个字节的最高位组合成一个字节的4位
00401249|.C0EE 07       |SHR DH,7
0040124C|.66:C1E2 07    |SHL DX,7
00401250|.C1EA 08       |SHR EDX,8
00401253|.C0EE 07       |SHR DH,7
00401256|.66:C1E2 07    |SHL DX,7
0040125A|.C1EA 08       |SHR EDX,8
0040125D|.C0EE 07       |SHR DH,7
00401260|.66:C1EA 05    |SHR DX,5                              ;这里移位5位是为了将所有最高位保存在DX的最低字节的低4位
00401264|.8BFA          |MOV EDI,EDX
00401266|.33FE          |XOR EDI,ESI                           ;上面处理之后的ESI、EDI分别保存了原副本每个字节的最高位,两个寄存器一共保存了8位
00401268|.8BD7          |MOV EDX,EDI                           ;上面分别处理的副本又重新组合成一个8位的字节码,保存在EDX寄存器中
0040126A|.81E2 FF000000 |AND EDX,0FF
00401270|.51            |PUSH ECX
00401271|.52            |PUSH EDX
00401272|.BA 08000000   |MOV EDX,8
00401277|.91            |XCHG EAX,ECX                            ;判断循环变量是否大于3,如果大于3,则将EBX进行循环移位,否则将EAX进行循环移位
00401278|.83F8 03       |CMP EAX,3
0040127B|.7F 0F         |JG SHORT cycle.0040128C
0040127D|.F6E2          |MUL DL                                  ;如果ECX小于等于3,则循环移位EAX,移位次数为 ECX * 8 + 8
0040127F|.5A            |POP EDX
00401280|.83C0 08       |ADD EAX,8
00401283|.91            |XCHG EAX,ECX
00401284|.D3C0          |ROL EAX,CL
00401286|.33C2          |XOR EAX,EDX                           ;移位之后与上面得到的8位字节码做异或处理,结果写回原寄存器中
00401288|.D3C8          |ROR EAX,CL                              ;再将EAX反向循环移位ECX * 8 + 8
0040128A|.EB 0D         |JMP SHORT cycle.00401299
0040128C|>83E8 03       |SUB EAX,3
0040128F|.F6E2          |MUL DL                                  ;如果ECX大于3,则循环移位EBX,移位次数为 (ECX-4) * 8 + 8
00401291|.5A            |POP EDX
00401292|.91            |XCHG EAX,ECX
00401293|.D3C3          |ROL EBX,CL                              ;这里的处理步骤与上面类似,只是操作数为EBX
00401295|.33DA          |XOR EBX,EDX
00401297|.D3CB          |ROR EBX,CL
00401299|>59            |POP ECX
0040129A|.41            |INC ECX
0040129B|.83F9 08       |CMP ECX,8
0040129E|.^ 0F85 71FFFFFF \JNZ cycle.00401215
004012A4\.C3            RETN

4)做了两次验证,第一次验证cycle.00401190函数结束后,ECX是否为0x01,如果 ECX != 1,则输出验证失败消息,本次验证结束。如果ECX为1,则进行第二部分的验证,本部分的验证
使用了输入的用户名和序列号的后八个字符,利用其进行异或运算,最终判断输入的用户名和序列号是否匹配。

00401110|.E8 7B000000   CALL cycle.00401190                      ;ECX 在上面进行了初始化,EAX和EBX则由sub_4011f1 这个函数进行赋值
00401115|.83F9 01       CMP ECX,1                              ;这里是第一次验证,如果ECX返回值不为1则验证失败,程序报错,否则进行第二次验证
00401118|.74 06         JE SHORT cycle.00401120
0040111A|>E8 47000000   CALL cycle.00401166                      ;输出验证失败消息函数
0040111F|>C3            RETN
00401120|>A1 68214000   MOV EAX,DWORD PTR DS:            ;这里进行了第二次验证,验证的参数分别是
00401125|.8B1D 6C214000 MOV EBX,DWORD PTR DS:            ;输入的用户名后8位字符,转换成两个DWORD进行异或操作
0040112B|.33C3          XOR EAX,EBX
0040112D|.3305 82214000 XOR EAX,DWORD PTR DS:            ;将上面计算的结果与sub_401190 函数对全局变量var_402182的计算结果进行异或
00401133|.0D 40404040   OR EAX,40404040                        ;置位和复位上面计算的结果的相应bit位
00401138|.25 77777777   AND EAX,77777777
0040113D|.3305 79214000 XOR EAX,DWORD PTR DS:            ;继续将结果与输入的序列号的后8个字符进行异或操作
00401143|.3305 7D214000 XOR EAX,DWORD PTR DS:
00401149|.^ 75 CF         JNZ SHORT cycle.0040111A               ;第二次验证,如果上面得到的结果为0,则验证通过,否则验证失败
0040114B|.E8 2B000000   CALL cycle.0040117B                      ;输出验证通过消息函数
00401150\.C3            RETN


算法总结:
1)使用输入的用户名和序列号,分别转换为2个整型数组name, serial
2)进行第一次转换,使用了name, name, serial, serial,转换结果保存在EAX和EBX寄存器中
3)进行第二次转换,使用上面转换得到的EAX、EBX寄存器,ECX==0xFF01, 全局变量dword_402182进行统计结果初始值为 0xfedcba98,转换过程中使用dword_402182进行统计结果
4)验证上述转换后ECX 是否等于1,如果否则验证失败,结束,否则进行再次验证
5)第二次验证,使用了 name, name, serial, serial, dword_402182,依次进行相应的运算
6)如果第5步结果为0,则验证成功,输出成功消息,否则验证失败

得到一组注册信息:
用户名:litao3rd
序列号:123456782332ASaA

附件是使用C语言实现类似的验证,基本就是汇编翻译到C代码。CrackMe源程序请到原文链接下载。

wgz001 发表于 2016-7-25 08:39

附件是使用C语言实现类似的验证,基本就是汇编翻译到C代码。CrackMe源程序请到原文链接下载。

说好的附件呢

Skyeddy 发表于 2016-7-25 09:37

高手,分析的很详细啊。{:1_919:}

yangcg 发表于 2017-8-16 12:55

学习了,谢谢分享

coolqi 发表于 2017-8-19 19:07

不错。学习

Hadis 发表于 2017-8-19 22:02

表示看不懂啊,继续学习
页: [1]
查看完整版本: 一个CrackMe的分析