一个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源程序请到原文链接下载。 附件是使用C语言实现类似的验证,基本就是汇编翻译到C代码。CrackMe源程序请到原文链接下载。
说好的附件呢 高手,分析的很详细啊。{:1_919:} 学习了,谢谢分享 不错。学习 表示看不懂啊,继续学习
页:
[1]