CrackMe练习¶
CrackMe01¶

首先看一看文件信息,为32位程序,使用Delphi编写
随后我们看一下程序页面

程序主要分为两个主要模块,一个是序列码注册模块,一个是输入用户名和注册码验证模块,显然我们并不知道用户名和注册码,我们将使用静态分析和动态分析破解这个程序
静态分析¶
那么我们首先要定位到相关的代码,经测试,当输入错误时,会提示"Sorry, The serial is incorect!",那么我们就从字符串入手,一步步定位到使用该字符串的代码

双击跳转到字符串变量

查看变量的引用(快捷键X),双击引用的地方

可以看到这部分就是检验的逻辑,伪C代码如下:
int __usercall TNS_BitBtn1Click@<eax>(int a1@<eax>, int a2@<ebx>, int a3@<esi>)
{
int v4; // esi
int v5; // esi
int v6; // ecx
char v7; // zf
unsigned int v9[2]; // [esp-14h] [ebp-2Ch] BYREF
int *v10; // [esp-Ch] [ebp-24h]
int v11; // [esp-8h] [ebp-20h]
int v12; // [esp-4h] [ebp-1Ch]
int v13; // [esp+0h] [ebp-18h] BYREF
int v14; // [esp+4h] [ebp-14h] BYREF
unsigned __int8 *v15; // [esp+8h] [ebp-10h] BYREF
int v16; // [esp+Ch] [ebp-Ch] BYREF
int v17; // [esp+10h] [ebp-8h] BYREF
int v18; // [esp+14h] [ebp-4h] BYREF
int savedregs; // [esp+18h] [ebp+0h] BYREF
v16 = 0;
v15 = 0;
v14 = 0;
v13 = 0;
v12 = a2;
v11 = a3;
v10 = &savedregs;
v9[1] = (unsigned int)&loc_42FB67;
v9[0] = (unsigned int)NtCurrentTeb()->NtTib.ExceptionList;
__writefsdword(0, (unsigned int)v9);
dword_431750 = 41;
sub_41AA58(*(_DWORD *)(a1 + 476), &v15);
dword_43176C = sub_403AB0(v15);
sub_41AA58(*(_DWORD *)(a1 + 476), &v15);
v4 = 7 * *v15;
sub_41AA58(*(_DWORD *)(a1 + 476), &v14);
dword_431754 = 16 * *(unsigned __int8 *)(v14 + 1) + v4;
sub_41AA58(*(_DWORD *)(a1 + 476), &v15);
v5 = 11 * v15[3];
sub_41AA58(*(_DWORD *)(a1 + 476), &v14);
dword_431758 = 14 * *(unsigned __int8 *)(v14 + 2) + v5;
if ( sub_406930(dword_43176C) < 4 )
goto LABEL_4;
sub_41AA58(*(_DWORD *)(a1 + 476), &v15);
dword_431750 *= *v15;
dword_431750 *= 2;
sub_403708(&v18, &str_CW[1]);
sub_403708(&v17, &str_CRACKED[1]);
sub_406718(dword_431750, &v13);
sub_4039AC(&v16, 5, v6, &str___5[1], v13, &str___5[1], v17);
sub_41AA58(*(_DWORD *)(a1 + 480), &v15);
sub_4039FC(v16, v15);
if ( v7 )
sub_42A170(*off_430A48, "Good job dude =)", "Congratz !!", 0);
else
LABEL_4:
sub_42A170(*off_430A48, "Sorry , The serial is incorect !", "Try Again!", 0);
__writefsdword(0, v9[0]);
v10 = (int *)&loc_42FB6E;
sub_403670(&v13);
sub_403694(&v14, 2);
return sub_403694(&v16, 3);
}
我们可以发现其中最重要的代码为if分支,那么为避免干扰,我们只关注该if分支涉及到的函数和变量,梳理如下:
1. 首先if分支包括函数sub_42A170,变量v7(寄存器ZF的值),所以v7的值却决于sub_4039FC函数,并且LABEL_4也会导致验证失败分支执行
2. LABEL_4涉及到if ( sub_406930(dword_43176C) < 4 ),其中涉及到sub_406930函数和dword_43176C变量,sub_4039FC函数涉及两个参数v16和v15
3. 进一步,dword_43176C变量的产生涉及sub_403AB0函数和变量v15
4. 变量v15由sub_41AA58产生,变量v16与函数sub_4039AC有关
我们结合以上步骤一步步分析,首先分析if ( sub_406930(dword_43176C) < 4 )语句,sub_406930函数伪C代码如下:
返回字符串的长度,sub_403AB0函数伪C代码如下:
所以该语句其实就是判断某一个字符串的长度是否小于4,如果成立,则输出注册码错误,我们可以猜测这个变量就是用户名或者注册码,那么由于v15由sub_41AA58函数产生,我们猜测其为读取用户输入的函数,我们将其重命名(快捷键N)为GetUserInput
随后我们分析函数sub_4039AC,其伪C代码如下:
CrackMe02¶

首先查看文件信息

win32,使用VB编写,该程序当输入错误时会提示"You Get Wrong"
我们先进行动态分析

首先我们定位到主模块,通过字符串去定位代码,双击该提示

定位到字符串后,我们查找引用了该字符串的代码

我们在这里打个断点(快捷键F2)看一下

此时已经在Push错误提示了,这说明程序的判断过程还要在前面,我们应该在该语句之前打断点

提示vbstrcmp函数,这通常是在比较字符串,我们在此处打上断点

在此处断下后发现了字符串AKA-487776,这可能是序列号,我们继续调试

我们的输入的序列号123456已经入栈

继续调试,会发现程序执行repe cmpsw,该语句表示逐字循环比较ESI和EDI寄存器存放的值CX寄存器存放值次,此时[ESI]=L"123456",[EDI]=L"AKA-487776",[CX]=6,所以这其实就是在比较我们输入的序列号的正确的序列号,所以正确的序列号就是AKA-487776,我们返回程序验证

输入正确,但此时如果我们换一个用户名再次调试,会发现key也会发生变化,显然key的生成与用户名有关,那么我们接下来就要找到其生成的逻辑

我们原断点的上面一点再打一个断点,可以看到此时已经生成了一个值487752,并且前缀AKA-也出现了,我们可以推算其逻辑大致如下:
那么显然,我们就要找到这个值的生成逻辑,我们继续往上找

我们找到了两个字符串函数,在此处打上断点

进一步调试后发现在此处函数调用完成之后,487752就产生了,显然这个函数就是相关的生成函数,在此处打上断点

75FAB3BA | 6A 0A | push 0Ah ; 将 10 压栈(作为除数)
75FAB3BC | 8BC3 | mov eax, ebx ; eax = 当前要处理的数字(初始为用户名首字母 ASCII)
75FAB3BE | 33D2 | xor edx, edx ; edx = 0(为 div 做准备,edx:eax 是 64 位被除数)
75FAB3C0 | 59 | pop ecx ; ecx = 10(从栈中弹出)
75FAB3C1 | F7F1 | div ecx ; edx:eax ÷ ecx → 商在 eax,余数在 edx
; 即:eax = eax / 10, edx = eax % 10
75FAB3C3 | 8BD8 | mov ebx, eax ; ebx = 商(用于下一轮循环)
75FAB3C5 | 8D42 30 | lea eax, [edx + 30h] ; eax = edx + 0x30(0x30 = '0' 的 ASCII)
; 即:将余数(0~9)转为 ASCII 字符('0'~'9')
75FAB3C8 | 66:8907 | mov word ptr [edi], ax ; 将 ax(2 字节)写入 [edi]
; 注意:这里写的是 word,但实际只用低字节(ax = 0x00XX)
75FAB3CB | 83C7 02 | add edi, 2 ; edi += 2(因为写的是 word)
75FAB3CE | 85DB | test ebx, ebx ; 检查商是否为 0
75FAB3D0 | 75 E8 | jne 75FAB3BA ; 如果不为 0,继续循环
发现关键代码如上,487752就是通过以上循环产生的,其中push A实际上是push用户名的第一个字母,总结一下生成逻辑:
1. 取用户名的第一位字母的大写,转换为ascii码
2. 将ebx赋给eax
3. 计算eax(ebx)除ecx(首字母的ascii码),商就是生成的值,并赋给ebx用于下次循环
4. 如果商不为0,继续循环
那么我们可以还原出以下代码:
def main():
username = input("Enter your username: ")
first_upper_char_ascii = ord(username[0].upper())
key = []
ebx = ?
while(1):
code = ebx / first_upper_char_ascii
edx = ebx % first_upper_char_ascii
ebx = code
if ebx == 0:
break
print(f"Code: {code}, Remainder: {edx}")
key.append(code)
print("Generated key:", key)
if __name__ == '__main__':
main()
那么显然我们还需要知道在循环开始前的ebx的值,我们继续调试