点击About - Help之后可以发现这个游戏的规则就是输入正确的Username和Codice,让按钮控件OK(无法click)和Cancella消失(可以click),然后显示出被其遮挡的logo
分析工具:
- IDR
- DelphiDecompiler(DeDe)
- IDA
- OllyDbg
本篇博客采用的测试数据如下:
Name:1234567890 Codice:098765432
首先DelphiDecompiler
查看一下这个程序的事件、控件等基本信息
下面是各个事件函数及对应的地址映射,可以根据这个在OD
中定位分析
DeDe中每一个事件(函数)点击进入反汇编窗口之后都可以看到DeDe给我们列出来的一些有用的注释,方便我们反汇编分析。
例如CodiceChange函数,就标出了与之相关的控件和所调用的自定义函数。
但这个还是不够详细,这方面IDR其实更详细一些,可以两个结合着看
下面是一些控件信息。
object Principale: TPrincipale
Left = 279
Top = 228
BorderStyle = bsDialog
Caption = 'CrackMe by - aLoNg3x - v. 1.00'
ClientHeight = 132
ClientWidth = 292
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
Position = poScreenCenter
PixelsPerInch = 96
TextHeight = 13
object _Nome: TLabel
Left = 8
Top = 8
Width = 31
Height = 13
Caption = 'Nome:'
end
object _Codice: TLabel
Left = 8
Top = 32
Width = 36
Height = 13
Caption = 'Codice:'
end
object Pannello: TPanel
Left = 0
Top = 64
Width = 292
Height = 68
Align = alBottom
BevelOuter = bvLowered
TabOrder = 0
object Image1: TImage
Left = 1
Top = 1
Width = 290
Height = 66
Align = alClient
Picture.Data = {
...}
Stretch = True
end
object Ok: TButton
Left = 24
Top = 16
Width = 113
Height = 41
Caption = 'Ok'
Enabled = False
TabOrder = 0
OnClick = OkClick
end
object Cancella: TButton
Left = 152
Top = 16
Width = 113
Height = 41
Caption = 'Cancella'
TabOrder = 1
OnClick = CancellaClick
end
end
object Nome: TEdit
Left = 56
Top = 8
Width = 89
Height = 21
MaxLength = 10
TabOrder = 1
OnChange = NomeChange
end
object Codice: TEdit
Left = 56
Top = 32
Width = 89
Height = 21
TabOrder = 2
Text = '0'
OnChange = CodiceChange
end
object About: TButton
Left = 168
Top = 8
Width = 105
Height = 49
Caption = 'About - Help'
TabOrder = 3
OnClick = AboutClick
end
end
CodiceChange
既然OK没办法点,那么首先分析CodiceChange
函数。
从DeDe
可以看出这个函数起始地址在0x442C78,那么在OD中Ctrl+G定位到这个函数,并下断点,经过测试发现这个函数在我们输入Codice时会断下来,即被调用。
为了看清楚这个函数对Codice和Name的处理,先Alt+B把这个断点去掉,输入测试用的Name和Codice之后再恢复那个函数的断点,鼠标移至被调试程序的窗口(这样做是为了给程序传入一个“鼠标移动”的消息,让程序不是处于等待消息的状态),这样程序就断在了这个函数处,接着就可以动态调试。
一直单步运行下去发现给函数442A3C传入了Name,同时可以注意到在DeDe中这个函数还是一个自定义函数
再看这个442A3C函数执行之后还有一个test语句和jnz语句,我们试着爆破把这个je跳转语句变成jnz,可以发现Ok按钮已经可以按了,说明这个函数的返回值至关重要。
下面看看这个函数的算法,为了更便于理清算法思路,可以结合IDA静态分析,IDA中先函数窗口Ctrl+F定位到CodiceChange函数,然后定位到这个sub_442A3C函数,伪代码如下:
int __fastcall sub_442A3C(int a1, int a2)
{
int v2; // ebx
int v3; // edx
int v4; // eax
int v5; // ebx
unsigned int v7[2]; // [esp-Ch] [ebp-1Ch] BYREF
int *v8; // [esp-4h] [ebp-14h]
int v9; // [esp+8h] [ebp-8h] BYREF
int v10; // [esp+Ch] [ebp-4h]
int savedregs; // [esp+10h] [ebp+0h] BYREF
v9 = a2;
v10 = a1;
System::__linkproc__ LStrAddRef(a1);
System::__linkproc__ LStrAddRef(v9);
v8 = &savedregs;
v7[1] = &loc_442AE5;
v7[0] = NtCurrentTeb()->NtTib.ExceptionList;
__writefsdword(0, v7);
if ( unknown_libname_32(v10) <= 5 )
{
v5 = 0;
}
else
{
v2 = unknown_libname_32(v10);
v3 = unknown_libname_32(v10) - 1;
if ( v3 > 0 )
{
v4 = 1;
do
{
v2 += v4 * *(v10 + v4) * *(v10 + v4 - 1);
++v4;
--v3;
}
while ( v3 );
}
v5 = v2 - sub_407670(v9);
if ( v5 == 666 )
LOBYTE(v5) = 1;
else
v5 = 0;
}
__writefsdword(0, v7[0]);
v8 = &loc_442AEC;
System::__linkproc__ LStrArrayClr(&v9, 2);
return v5;
}
直接看返回值v5在伪代码中的逻辑
根据这个666可以对应OD中的具体汇编代码部分,可以发现这个跳转明显是不能实现的
往上翻,看算法处理逻辑,差不多是一个循环中Name两两相乘,再乘ax中的值。每一回合ax都自增,循环结束之后最后还要减去Codeice,最终才跟666比较。
算法主体的反汇编即注释如下:
00442A8E |. B8 01000000 mov eax, 1
00442A93 |> 8B4D FC /mov ecx, dword ptr [ebp-4] ; [加密算法的循环]
00442A96 |. 0FB64C01 FF |movzx ecx, byte ptr [ecx+eax-1]
00442A9B |. 8B75 FC |mov esi, dword ptr [ebp-4]
00442A9E |. 0FB63406 |movzx esi, byte ptr [esi+eax]
00442AA2 |. 0FAFCE |imul ecx, esi
00442AA5 |. 0FAFC8 |imul ecx, eax ; 每相邻2个相乘再乘ax,初始值ax=1(但后面会发现每次做完ax++)
00442AA8 |. 03D9 |add ebx, ecx ; 结果跟上次循环的结果一直累加存储在bx,bx初始值是用户名的长度
00442AAA |. 40 |inc eax ; 每次循环之后ax++
00442AAB |. 4A |dec edx ; 记录循环次数的
00442AAC |.^ 75 E5 \jnz short 00442A93 ; 循环过后记录一下,测试数据最终处理结果是bx=0x1FD56
00442AAE |> 8B45 F8 mov eax, dword ptr [ebp-8] ; 取出序列号传参传入下面的函数
00442AB1 |. E8 BA4BFCFF call 00407670 ; 相当于atoi函数【记录一下返回值0x05E30A78是98765432的16进制形式】
00442AB6 |. 2BD8 sub ebx, eax ; 运算结果-返回值,与0x29A即666的16进制作比较
00442AB8 |. 81FB 9A020000 cmp ebx, 29A ; 上面的结果作比较,等于0x29A就OK可用
00442ABE |. 75 04 jnz short 00442AC4
还原一下这部分代码如下:
char Nome[] = "1234567890" // eax
char Codice[] = "098765432"
int length = strlen(Nome); // esi
int nRet = length; // ebx
for( int i=1;i<length;i++)
{
nRet += Nome[i-1]*Nome[i]*i;
}
nRet -= atoi(Codice);
if( nRet == 0x29A )
{
// 返回0x01,OK按钮可以使用
}else{
// 返回0,OK按钮禁止使用
}
根据这个用python写了个局部的注册机
name=input("Nome:")
length=len(name)
temp=length
for i in range(1,length):
temp+=ord(name[i-1])*ord(name[i])*i
codice=temp-666
codice=print("Codice:"+str(codice))
那么为了方便下一步分析,就先计算出Name为1234567890的正确对应的Codice为0x1FD56-0x29A
,即130390-666=129724
,如下图。
发现按下OK之后Codice输入框就立刻清零了,接下来开始分析OkClick函数。
OkClick
给OkClick事件函数下断点,F9跑飞之后输入测试数据,这里输入1234567890和129724,OK可以点击之后点击,程序断在OKClick事件函数处。
分析发现这个函数先检测了一下Cancel控件是否可见,下面的跳转直接改成je(避免它检测出来cancel控件设置的可见导致不运行加密函数)
接着就是和CodiceChange函数一样的设计,关键函数明显就是442BA0,返回值决定了Ok控件是否可见。
直接单步步入这个函数
在执行这个函数主要的加密算法之前,这个函数还进行了一些无关紧要的操作,虽然可以忽视,但为了以防万一,还是大概查了查这些API函数是干什么用的。
接下来从长度获取之后看这个函数的具体逻辑,先IDA静态分析看看伪C代码(已经根据IDR对一些未解析出来名字的函数的名称稍作修改,并进行必要删减)
int __usercall sub_442BA0@<eax>(int a1@<eax>, int a2@<edx>, int a3@<ebx>, int a4@<esi>)
{
int v5; // ebx
int v6; // esi
int v7; // eax
char v8; // zf
unsigned int v10[2]; // [esp-14h] [ebp-20h] BYREF
int *v11; // [esp-Ch] [ebp-18h]
int v12; // [esp-8h] [ebp-14h]
int v13; // [esp-4h] [ebp-10h]
int v14; // [esp+0h] [ebp-Ch] BYREF
int v15; // [esp+4h] [ebp-8h]
int v16; // [esp+8h] [ebp-4h]
int savedregs; // [esp+Ch] [ebp+0h] BYREF
v15 = 0;
v14 = 0;
v13 = a3;
v12 = a4;
v16 = a1;
LStrAddRef(a1);
v11 = &savedregs;
v10[1] = &loc_442C67;
v5 = 0;
IntToStr(a2);
LStrLAsg(&v14, v15);
if ( lstrlen(v15) <= 5 ) //长度必须大于5
{
v5 = 0;
}
else
{
v6 = lstrlen(v15); //获取长度
if ( v6 >= 1 )
{
do
{
v7 = UniqueString(&v14);
*(v7 + v6 - 1) = v6 * (*(v15 + v6 - 1) * *(v15 + v6 - 1)) % 25 + 65;
--v6;
}
while ( v6 );
}
LStrCmp(v14, v16);
if ( v8 )
{
LStrCmp(v16, v14);
if ( v8 )
LOBYTE(v5) = 1;
else
v5 = 0;
}
}
v11 = &loc_442C6E;
LStrArrayClr(&v14, 3);
return v5;
}
分析好之后还原一下算法。主要含义就是从最后一位开始计算,一个个替换成大写字母,最后再跟Name比较。。
char Nome[] = "1234567890" // eax
char code[] = "129724"
int nLen = strlen(code); // esi
int nRet = 0; // eax
for( int i=0;i<nLen;i++)
{
int nTmp = code[nLen-1-i]*code[nLen-1-i]*(nLen-i);
nRet = nTmp%25 + 0x41; // 转换为大写字母
code[nLen-1-i] = nRet;
}
int nrt = strCmp(code,Nome); //x2,两次比较用户名和生成后的字符串是否相同
if( nrt == 0 )
{
// 返回0x01,OK按钮隐藏
}else{
// 返回0,OK按钮显示
}
现在看来,从理论上来说,从这个程序的设计来看没办法写注册机,现在只能强制爆破。。。
CancellaClick
看内部算法把注册机搞出来的概率比较小了,而且这个函数的算法还是跟前面2个的类似,所以直接暴破了