最近学习了CCDebuger前辈的【OllyDBG 入门系列】文章(PS:虽然这系列文章已经发表十年了,但对我这个菜鸟依然很有帮助,谢谢作者)。
原文链接在这里: http://bbs.pediy.com/showthread.php?threadid=21532
在原文中,作者详细介绍了使用消息断点和RUN跟踪的方法找到这个CrackMe的关键部分,但是没有对关键部分进行分析,小菜我就斗胆分享一下我的分析结果,由于这个CrackMe的算法有点复杂,注册码无法通过简单的逆向计算得到,需要进行一定的爆破猜解,就没有写注册机代码了(其实是技术太菜人又太懒),但是找到了一个可用的注册码。
无论是使用作者介绍的消息断点还是函数参考,我们都可以定位到下面这个函数中。这个函数即为本CrackMe的主体函数,全部逻辑都在这个函数中实现,或者调用其他函数实现。
0040109C /$ C705 82214000>MOV DWORD PTR DS:[402182],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:[EBP+8] ; |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:[EBP+8] ; |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:[EDI],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:[ESI] ; 有效字符的范围在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:[402168] ; 这里进行了第二次验证,验证的参数分别是
00401125 |. 8B1D 6C214000 MOV EBX,DWORD PTR DS:[40216C] ; 输入的用户名后8位字符,转换成两个DWORD进行异或操作
0040112B |. 33C3 XOR EAX,EBX
0040112D |. 3305 82214000 XOR EAX,DWORD PTR DS:[402182] ; 将上面计算的结果与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:[402179] ; 继续将结果与输入的序列号的后8个字符进行异或操作
00401143 |. 3305 7D214000 XOR EAX,DWORD PTR DS:[40217D]
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[i+len] = name[i];
}
name[size] = '\0';
}
2)调用第一个转换函数 cycle.004011F1,这个函数使用的参数为输入的用户名和输入的序列号,返回结果保存在 EAX 和 EBX 寄存器中,作为下一个转换函数的参数。
004011F1 /$ A1 60214000 MOV EAX,DWORD PTR DS:[402160] ; 将输入的16字节长度的用户名和序列号分别转换为整型数组name, serial
004011F6 |. 8B1D 64214000 MOV EBX,DWORD PTR DS:[402164] ; 一个整数长度为4字节,16字节可视作具有4个整数的整型数组
004011FC |. 3305 71214000 XOR EAX,DWORD PTR DS:[402171] ; 分别取整型数组的0号位和1号位进行异或处理,保存在EAX和EBX中
00401202 |. 331D 75214000 XOR EBX,DWORD PTR DS:[402175] ; EAX=name[0] ^ serial[0], EBX = name[1] ^ serial[1]
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)[0];
*ebx = ((int *)name)[1];
*eax ^= ((int *)serial)[0];
*ebx ^= ((int *)serial)[1];
*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:[402160] ; 将输入的16字节长度的用户名和序列号分别转换为整型数组name, serial
004011F6 |. 8B1D 64214000 MOV EBX,DWORD PTR DS:[402164] ; 一个整数长度为4字节,16字节可视作具有4个整数的整型数组
004011FC |. 3305 71214000 XOR EAX,DWORD PTR DS:[402171] ; 分别取整型数组的0号位和1号位进行异或处理,保存在EAX和EBX中
00401202 |. 331D 75214000 XOR EBX,DWORD PTR DS:[402175] ; EAX=name[0] ^ serial[0], EBX = name[1] ^ serial[1]
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:[402168] ; 这里进行了第二次验证,验证的参数分别是
00401125 |. 8B1D 6C214000 MOV EBX,DWORD PTR DS:[40216C] ; 输入的用户名后8位字符,转换成两个DWORD进行异或操作
0040112B |. 33C3 XOR EAX,EBX
0040112D |. 3305 82214000 XOR EAX,DWORD PTR DS:[402182] ; 将上面计算的结果与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:[402179] ; 继续将结果与输入的序列号的后8个字符进行异或操作
00401143 |. 3305 7D214000 XOR EAX,DWORD PTR DS:[40217D]
00401149 |.^ 75 CF JNZ SHORT cycle.0040111A ; 第二次验证,如果上面得到的结果为0,则验证通过,否则验证失败
0040114B |. E8 2B000000 CALL cycle.0040117B ; 输出验证通过消息函数
00401150 \. C3 RETN 算法总结:
1)使用输入的用户名和序列号,分别转换为2个整型数组name[0..3], serial[0..3]
2)进行第一次转换,使用了name[0], name[1], serial[0], serial[1],转换结果保存在EAX和EBX寄存器中
3)进行第二次转换,使用上面转换得到的EAX、EBX寄存器,ECX==0xFF01, 全局变量dword_402182进行统计结果初始值为 0xfedcba98,转换过程中使用dword_402182进行统计结果
4)验证上述转换后ECX 是否等于1,如果否则验证失败,结束,否则进行再次验证
5)第二次验证,使用了 name[2], name[3], serial[2], serial[3], dword_402182,依次进行相应的运算
6)如果第5步结果为0,则验证成功,输出成功消息,否则验证失败
得到一组注册信息:
用户名:litao3rd
序列号:123456782332ASaA
附件是使用C语言实现类似的验证,基本就是汇编翻译到C代码。CrackMe源程序请到原文链接下载。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)