还是跟前一个版本的一样,都是要藏按钮。
分析工具:
- IDR
- DelphiDecompiler(DeDe)
- IDA
- OllyDbg
本篇博客采用的测试数据如下:
Name:1234567890 Codice:098765432
老样子,DeDe打开,有个AgainClick,难不成隐藏了按钮?
确实显示这个程序还有一个按钮。
然后IDR辅助分析
RegisterzClick
OD中定位,结合IDR给的注释,单步分析,可以看到执行完4429A8之后的返回值决定了是否发生跳转
分析并单步测试可以看出不跳转就会出现一个新的按钮(果然没这么简单),且register按钮会消失,要是发生跳转了就相当于失败,不会有任何变化。
所以我们着重分析4429A8函数,看看能不能把正确的codice搞出来
先在IDA中静态反编译出伪代码查看一下,下面以及根据一些函数功能做了函数重命名操作。可以看出是两层循环处理
int __fastcall sub_4429A8(int a1, int a2, int a3)
{
int v4; // ebx
int v5; // eax
int v6; // esi
int v7; // eax
int v8; // ebx
unsigned int v10[2]; // [esp-Ch] [ebp-24h] BYREF
int *v11; // [esp-4h] [ebp-1Ch]
int v12; // [esp+Ch] [ebp-Ch]
int v13; // [esp+10h] [ebp-8h] BYREF
int v14; // [esp+14h] [ebp-4h]
int savedregs; // [esp+18h] [ebp+0h] BYREF
v13 = a3;
v14 = a2;
LStrAddRef(a3);
v11 = &savedregs;
if ( lstrlen(v13) <= 4 )
{
v8 = 0;
}
else
{
v4 = 0;
v5 = lstrlen(v13);
if ( v5 > 0 )
{
v12 = v5;
v6 = 1;
do
{
v7 = lstrlen(v13);
if ( v7 >= 1 )
{
do
v4 += a1 * *(v13 + v7-- - 1) * *(v13 + v6 - 1);
while ( v7 );
}
++v6;
--v12;
}
while ( v12 );
}
v8 = abs32(v4) % 666666;
v14 = v14 % 80 + v14 / 89 + 1;
if ( v8 == v14 )
LOBYTE(v8) = 1;
else
v8 = 0;
}
LStrClr(&v13);
return v8;
}
动态调试一下看看具体是如何实现的,
突然发现上面v4 += a1 * *(v13 + v7-- - 1) * *(v13 + v6 - 1);
中的a1是作为参数传递进入这个函数的(在运行时是edi寄存器存储这个参数的值),OD动态调试发现这个参数是0,那么这个双层嵌套循环其实到最后结果始终为0,根本不必分析内部逻辑,看来这个循环下面的代码才是主要的。
切回到IDA看看,既然v4是0,给他取绝对值之后模666666得到的v8也是0,那么最终就是要让v14==0成立这个程序才算结束,这里的v14就是数值型的codice。
分析v4 += a1 * *(v13 + v7-- - 1) * *(v13 + v6 - 1);
对应的汇编代码如下
目前看来我们需要一个codice
满足codice%80+codice/89+1==0
,于是认为这个值存在的我就写了个脚本爆破,当然肯定不是直接-1000000000
到0
直接一个个带进去算,这样效率太低了。经过分析发现codice的值必须是在
−
81
×
89
∼
0
-81\times 89\sim 0
−81×89∼0即
−
7029
∼
0
-7029\sim 0
−7029∼0之间,这是因为
c
o
d
i
c
e
%
80
codice\%80
codice%80的值肯定在80以内。
所以限制了范围之后写了下面脚本。
for i in range(-7209,0,1):
print(i)
if i%80+i/89+1==0:
break
但最后并没有成功得到那个值,这就很匪夷所思。。。
唯一的可能就是传入的那个参数根本不是0,不然不可能没有正确的codice,而之前分析的那个循环也可能是有意义的,而不是作者用来干扰的。
于是开始寻根溯源先发现这个参数来自于一个地址为445830的内存单元内的值(如下),然后才有了进入到4429A8之后右eax传给了edi进而参与算法处理。
既然445830出了问题,那么直接下内存访问断点,看是哪里对这个内存单元进行了处理,然后几次调试下来没有找到。。。
参考了别人的wp之后破案了,原来对这个内存单元的访问藏在了我们认为不应该进入的那个分支里,如下图。
回顾一下这个分支,它是提醒你输入不合规范,即包含了字母,需要重新输入。
也就是说第一次运行的时候你需要输入一个含字母的codice(尽管你可能知道这是会弹出窗口报错的),让程序提醒你你输入的字符里面必须不能有字母(“You MUST insert a valid Long Integer Value in the Code Editor… Thank you”),程序弹出这个窗口之后做的操作就是给这个445830内存单元中写入值。
细思极恐的是,只有这样,这个程序运行期间,每一次你输入只包含数字的codice,程序才会按照正常的算法(不会乘以0)运算、检验,而在你做这个之前所有输入的codice都不可能成功
所以刚刚本来调试过程中输入的codice都是数字的做法导致程序根本不会进入那个分支,那个内存单元里一直是默认的0,既然没有进入,那么下内存断点程序也不会断在那儿。
这也算是一种反调试了吧,正常人都不会把那个显然错误、需要避免跳入的分支考虑在内,而更多的是去向程序正确的输入靠拢,所以如果破解者只是顾着分析加密算法就会永远蒙在🥁里。
接下来分析那个分支就好,给那个跳转语句下断点
输入username:1234567890
和codice:ABCDEFG
之后,程序断下之后单步执行,发现关键函数442A8C,运行完这个函数之后就把返回值写入445830内存单元。
这里我没有选择单步步入这个函数,而是直接在IDA中将这个函数反编译得到伪代码,稍作修改如下。
unsigned int __fastcall Libmain::TWindowDesigner::SelectAll(Libmain::TWindowDesigner *this)
{
int v; // esi
int j; // ebx
int i; // ecx
Libmain::TWindowDesigner *codice; // [esp+Ch] [ebp-4h] BYREF
//unsigned int v5[2]; // [esp-Ch] [ebp-1Ch] BYREF
//int *v6; // [esp-4h] [ebp-14h]
//int savedregs; // [esp+10h] [ebp+0h] BYREF
codice = this;
LStrAddRef(this);
//v6 = &savedregs;
//v5[1] = &loc_442B21;
//v5[0] = NtCurrentTeb()->NtTib.ExceptionList;
//__writefsdword(0, v5);
if ( lstrlen(codice) <= 5 )
{
v = 0;
}
else
{
v = 891;
j = lstrlen(codice) - 1;
if ( j > 0 )
{
i = 1;
do
{
v += codice[i - 1] * (codice[i] % 17 + 1);
++i;
--j;
}
while ( j );
}
}
//__writefsdword(0, v5[0]);
//v6 = &loc_442B28;
LStrClr(&codice);
return abs32(v % 29000);
}
用python复现一下
def get_mem_445830(codice):
v=891
j=len(codice)-1
if j>0:
i=1
while True:
v+=ord(codice[i-1])*(ord(codice[i])%17+1)
i+=1
j-=1
if j==0:
break
return v%29000
if __name__=='__main__':
codice=input("Codice:")
print(get_mem_445830(codice))
由于我用的codice
是‘ABCDEFG’
,用脚本运行一下看看结果如下。
也就是说以后内存单元中的值就是3743
,换算成16进制就是0xe9f
接下来直接OD中按F9,这下修改codice输入框,改成原来的数据0987654321,程序断在了函数RegisterzClick。然后点击F8,看一下效果,这次终于不再是0了,也变成了值0xe9f
。
重新分析4429A8,这次直接用IDA得到伪C代码,整理之后如下
int __fastcall sub_4429A8(int mem, int a2, int a3)
{
int temp; // ebx
int v5; // eax
int i; // esi
int k; // eax
int v; // ebx
int j; // [esp+Ch] [ebp-Ch]
int Name; // [esp+10h] [ebp-8h] BYREF
int codice; // [esp+14h] [ebp-4h]
Name = a3;
codice = a2;
if ( lstrlen(Name) <= 4 )
v = 0;
else
{
temp = 0;
length = lstrlen(Name);
if ( length > 0 )
{
j = length;
i = 1;
do
{
k = lstrlen(Name);
if ( k >= 1 )
{
do
temp += mem * Name[k-1] * Name[i - 1],k--;
while ( k );
}
++i;
--j;
}
while ( j );
}
v = abs32(temp) % 666666;
codice = codice % 80 + codice / 89 + 1;
if ( v == codice )
LOBYTE(v) = 1;
else
v = 0;
}
LStrClr(&Name);
return v;
}
根据这个可以写注册机如下
def get_mem_445830(codice):
v=891
j=len(codice)-1
if j>0:
i=1
while True:
v+=ord(codice[i-1])*(ord(codice[i])%17+1)
i+=1
j-=1
if j==0:
break
return v%29000
def register(Name,mem):
if len(Name) <= 4:
v = 0
else:
temp = 0
length = len(Name)
if length > 0:
j = length
i = 1
while True:
k = len(Name)
if k >= 1:
while True:
temp += mem*ord(Name[k-1])*ord(Name[i-1])
k-=1
if k==0:
break
#print('0x%x'%temp)
i+=1
j-=1
if j==0:
break
#print('final:0x%x'%temp)
v = temp % 666666
#print(v)
truecodice=(80-((v-1)*89%80))+(v-1)*89
return truecodice
if __name__=='__main__':
name=input("Name:")
codice=input("Codice:")
mem=get_mem_445830(codice)
print(register(name,mem))
这个时候出现了第二个按钮,其实用的是一样的套路,同样的操作再做一遍,效果如下:
这里不再用测试数据,而是换成自己的网名🤞
其实最主要是输入的那个英文字母的codice,最后的最后再总结一下流程
- 先输入含英文字母的codice
- 点击register按钮
- 输入正确的codice
- 点击register按钮
- register按钮消失,出现Again按钮
- 先输入含英文字母的codice(与第1步一样)
- 点击again按钮
- 输入正确的codice(与第3步一样)
- 点击again按钮
- again按钮消失
- 标题改成了我们的名字
参考:https://bbs.pediy.com/thread-249429.htm