跳转至

CrackMe练习

CrackMe01

image-20260108162449874

首先看一看文件信息,为32位程序,使用Delphi编写

delphi(应用程序开发工具)_百度百科

随后我们看一下程序页面

image-20260108162624453

程序主要分为两个主要模块,一个是序列码注册模块,一个是输入用户名和注册码验证模块,显然我们并不知道用户名和注册码,我们将使用静态分析和动态分析破解这个程序

静态分析

那么我们首先要定位到相关的代码,经测试,当输入错误时,会提示"Sorry, The serial is incorect!",那么我们就从字符串入手,一步步定位到使用该字符串的代码

image-20260108164315729

双击跳转到字符串变量

image-20260108164355559

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

image-20260108164457691

可以看到这部分就是检验的逻辑,伪C代码如下:

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分支涉及到的函数和变量,梳理如下:

Text Only
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代码如下:

C
unsigned int __fastcall sub_406930(const char *a1)
{
  return strlen(a1);
}

返回字符串的长度,sub_403AB0函数伪C代码如下:

C
char *__fastcall sub_403AB0(char *result)
{
  if ( !result )
    return &byte_403AB5;
  return result;
}

所以该语句其实就是判断某一个字符串的长度是否小于4,如果成立,则输出注册码错误,我们可以猜测这个变量就是用户名或者注册码,那么由于v15由sub_41AA58函数产生,我们猜测其为读取用户输入的函数,我们将其重命名(快捷键N)为GetUserInput

随后我们分析函数sub_4039AC,其伪C代码如下:

Text Only

CrackMe02

image-20260108223101302

首先查看文件信息

image-20260108223128468

win32,使用VB编写,该程序当输入错误时会提示"You Get Wrong"

我们先进行动态分析

image-20260108224115326

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

image-20260108224229977

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

image-20260108224312848

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

image-20260108225429707

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

image-20260108225753008

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

image-20260108230011130

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

image-20260108230210996

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

image-20260108233457614

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

image-20260108233841306

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

image-20260117124552618

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

Text Only
1. 根据用户名(可能还有输入的key)生成一个值
2. 将这个值与AKA-拼接,形成正确的key

那么显然,我们就要找到这个值的生成逻辑,我们继续往上找

image-20260117125131057

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

image-20260117130024937

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

image-20260117130756007

GAS
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用户名的第一个字母,总结一下生成逻辑:

Text Only
1. 取用户名的第一位字母的大写,转换为ascii码
2. 将ebx赋给eax
3. 计算eax(ebx)除ecx(首字母的ascii码),商就是生成的值,并赋给ebx用于下次循环
4. 如果商不为0,继续循环

那么我们可以还原出以下代码:

Python
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的值,我们继续调试