本帖最后由 Rooking 于 2020-1-2 19:08 编辑
飘云阁全网首发,未经许可,禁止转载
一直使用的都是 破解版的IDM,但版本太低了,有时出现无法截获下载请求的问题。再加上网上只有各种补丁工具,没有找到有人分析IDM 注册机制的文章,今天就把最新的IDM6.36做掉,实现无需对程序进行任何patch,就能实现我们想要的注册效果。
首先通过字符串切入到关键点,没用的图片就不贴了。点击注册按钮,胡乱输入注册信息会提示注册码错误('You have entered incorrect Serial Number.'),对该字符串查找交叉引用得到唯一一处结果如下: 此处将某指针指向了该字符串,继续查找该指针的交叉引用,会得到多组结果,其中一处猜测为验证函数。 此处完全可以静态分析出注册码的格式为“xxxxx-xxxxx-xxxxx-xxxxx”,当然网上也有现成的注册码供我们参考。刚好以‘-’为分隔符,分成了四部分,接下来程序会对这四部分进行验证。 注释写的很清楚,就不过多解释了,四部分全是相同的操作,这里跳过重复的部分直接分析下一环节的验证过程。 验证完毕后就是注册表的操作了,将注册码和身份信息录入注册码,程序会退出。此时本地注册机制已分析完毕,仅给出暴力穷举函数的实现代码,依次暴力穷举四部分,再随机组合就能得到程序全部可用的注册码(给出一组“222YA-2NT7U-2C419-2WKXT”)。 [Python] 纯文本查看 复制代码 import itertools
def getSN(num):
table = '2YOPB3AQCVUXMNRS97WE0IZD4KLFGHJ8165T'
for tmp in itertools.product(range(len(table)),repeat=5):
sum=0
for i in tmp:
sum += sum * 36 + i
if sum % num == 0:
print "".join(map(lambda x:table[x],tmp)) 但此时我们的工作还没结束,如果我们断开网络,IDM自然也能正常运行。但一旦联网就会弹出虚假注册码的提示,很明显是网络校验,网上绝大多数人的做法都是找到验证点patch掉。即使绕过启动时的一处验证,在运行时程序也会开子线程进行验证,patch思路这里不做探讨。 今天我们尝试另辟蹊径,我们发现只要程序连过一次网,无论以后网络是否连接都会直接弹出虚假注册码提示,有理由怀疑这个虚假提示信息被存储到了本地。 后续分析发现,这些弹窗提示字符串都是加密存储的,会在内存中动态解密。因此对MessageBox下断并向上回溯。 定位到此处,分析发现只有该函数返回为0时,程序不会弹出错误提示,并能正常运行出主界面(后续还会有校验,即仅patch此处也行不通),接下来分析该函数。 该函数逻辑有些混乱,感兴趣可以自己分析一下。这里仅关注关键点,直接给出正确的执行路径。首先程序从注册表中读取MData表项,并将其作为sub_452030的参数进行解密操作,最后将结果与我们输入的注册码进行比较,相等则成功。接下来分析sub_452030函数。
很清晰的加解密函数结构,为了防止FindCrypt插件,程序作者对加密算法的S盒进行了加密,内存中动态解密。通过识别S盒可以得到IDM使用的算法为RC2,而密钥则是传入的字符串“58BE20ast4si5ls2D13”,没有看到IV自然是ECB模式,至此难点都已分析完毕。我们只需要修改注册表中MData表项的值为RC2算法加密后的HEX值即可,给出与我上面提供的注册码(“222YA-2NT7U-2C419-2WKXT”)对应的MData “6806f3ac00e2967a1aaafc9cfd47b60806cd6e7f4828b021”。
最后啰嗦一句,在32位系统上注册表的路径是不一样的,如果你无法确定路径是什么,或者以后IDM更新了,请使用注册表监视工具Process Monitor。本文主要分析IDM的注册与网络验证机制,最后给出的无需patch的注册操作仅供参考,测试到目前为止还没有出现问题,如果翻车了,就当学习一下思路吧。
=================================================================================================
最终不出所料,果真翻车了,IDM后续还有几处隐藏检测点。只输入上述的内容,会不定时弹出虚假注册码的弹窗,但弹窗不会影响正常使用,即使被呼出注册窗口直接关闭即可。
但对于强迫症来说依然受不了,本文则刨根问底,一探究竟。注意到当对某链接重新下载时,会立即弹出错误提示。最优的做法是监控注册表,程序可能在某个位置又把注册码拿出来校验了一次。实验发现,在开始下载和下载完成两个时机程序都会主动检测,如下图所示:
这里有重要,如上图所示,在读Serial表项前后程序会读取某表的默认项,如上图高亮处所示,而这里存放的注册码的密文,该密文是什么请继续往下看。
上图为一处验证点,sub_452030依然是上文讲到的RC2解密函数,密钥为“lmW02kjM4002FAnzwwsA1”,最后strcmp则是比较解密结果和注册码是否一致,不再赘述。唯一与上文不同的是,这次多了一个sub_593700函数,因此我们对注册码加密后,还必须分析该函数,再进行一层编码。
[C++] 纯文本查看 复制代码 int __cdecl sub_593700(_BYTE *a1, int a2, int *a3)
{
_BYTE *i; // edi
char *v4; // eax
int v5; // edx
int v6; // ebx
int v7; // eax
int v8; // esi
int result; // eax
_BYTE *v10; // esi
_BYTE *v11; // ecx
unsigned int v12; // ebp
int v13; // edx
_BYTE *v14; // esi
int v15; // eax
int v16; // [esp+14h] [ebp+4h]
for ( i = a1; *i == ' ' || *i == 9; ++i )
;
v4 = i + 1;
if ( dword_665B5C[*i] <= 0x3F )
{
do
v5 = *v4++;
while ( dword_665B5C[v5] <= 0x3F );
}
v6 = v4 - i - 1;
v7 = (v6 + 3) / 4;
v8 = 3 * v7;
v16 = 3 * v7;
result = sub_5806A0(a2, 3 * v7 + 7);
if ( result )
{
if ( a3 )
*a3 = v8;
v10 = sub_580690(a2);
v11 = i;
if ( v6 > 0 )
{
v12 = (v6 + 3) >> 2;
v6 -= 4 * v12;
do
{
v13 = v11[1];
v11 += 4;
*v10 = 4 * LOBYTE(dword_665B5C[*(v11 - 4)]) | (dword_665B5C[v13] >> 4);
v14 = v10 + 1;
*v14 = 16 * LOBYTE(dword_665B5C[*(v11 - 3)]) | (dword_665B5C[*(v11 - 2)] >> 2);
v10 = v14 + 2;
--v12;
*(v10 - 1) = LOBYTE(dword_665B5C[*(v11 - 1)]) | (LOBYTE(dword_665B5C[*(v11 - 2)]) << 6);
}
while ( v12 );
}
if ( v6 & 3 )
{
if ( dword_665B5C[*(v11 - 2)] <= 63 )
v15 = v16 - 1;
else
v15 = v16 - 2;
v16 = v15;
}
*(sub_580690(a2) + v16) = 0;
result = 1;
}
return result;
}
代码如上,很明显是base64解码算法,此时不能确实是否为标准base64,我们只需要用rc2加密注册码再以标准base64编码,替换到对应默认表项测试程序即可验证(最终符合我们猜测,提供与我上文提供的注册码对应的密文“Ib1wA+FbvWMoIqeSo2c2n003utBSSh0w”)。
此时,开始下载的检测点就被干掉了,而下载完成的监测点是一模一样的使用相同的方式进行替换。程序依然再测试中,不能保证后续没有其他验证。
总结一下目前为止发现的验证机制:
1. 本机注册码校验:非常挫的四则运算最后取余
2. MData表项校验:来自服务器下发,为RC2加密的HEX值
3. 默认表项校验:目前发现两处,来自服务器下发,为RC2加密的Base64值
本文更多是作为分析软件验证机制,不提供傻瓜式一键Hack成品。而注册表项不同电脑可能不一样,需要你再看懂文章的前提下使用ProcMon找到自己电脑的位置(看MData结尾的自然就一定对了,找默认表项则是看读Serial表项的前后)。
更理想的方案是分析注册时软件与服务器的通信协议,直接伪造服务器下发的MData和默认表项密文,这样就不需要考虑找注册表的问题了,而且不会遗漏。
=======================================================================================================================================
和rooking表哥交流了一下,把注册表CLSID的生成算法搞定了,给做通用注册机的表哥填坑。
[C++] 纯文本查看 复制代码 if ( sub_4C20F0(&v119) ) // a22fd67
{
sub_624442(&v28, &v120);
LOBYTE(v123) = 1;
if ( sscanf(v28, aLx, &sum) == 1 )
{
if ( strlen(::a3) > 4 )
{
strncpy(v117, ::a3, 5u);
v118 = 0;
v1 = 1;
v2 = 0;
do
{
v3 = v117[v2++];
v1 = 7 * v1 + 2 * v3;
}
while ( v2 < 5 );
sum += v1;
}
v4 = v28;
v5 = 1.33;
if ( strlen(v28) < 0x20 )
{
while ( 1 )
{
v121 = sum;
v6 = (sum * v5);
v122 = v5 + 0.27;
sub_624442(v117, &v28);
LOBYTE(v123) = 2;
sub_621421(&v28, aSLx, *v117, v6);
LOBYTE(v123) = 1;
sub_6246CD(v117);
v4 = v28;
if ( strlen(v28) >= 0x20 )
break;
v5 = v122;
}
}
}
else
{
v4 = v28;
}
if ( strlen(v4) > 0x1F )
{
memset(&a3, 0x2Du, 0x18u);
v7 = 0;
v8 = v4 + 31;
do
*(&a3 + v7++) = *v8--;
while ( v7 < 8 );
v9 = 9;
if ( v7 < 12 )
{
v10 = 12 - v7;
v11 = &v4[-v7 + 31];
v7 = 12;
do
{
*(&a3 + v9++) = *v11--;
--v10;
}
while ( v10 );
}
v12 = v9 + 1;
if ( v7 < 16 )
{
v13 = 16 - v7;
v14 = &v4[-v7 + 31];
v7 = 16;
do
{
*(&a3 + v12++) = *v14--;
--v13;
}
while ( v13 );
}
v15 = v12 + 1;
if ( v7 < 20 )
{
v16 = 20 - v7;
v17 = &v4[-v7 + 31];
v7 = 20;
do
{
*(&a3 + v15++) = *v17--;
--v16;
}
while ( v16 );
}
v18 = v15 + 1;
if ( v7 < 32 )
{
v19 = &v4[-v7 + 31];
v20 = &a3 + v18;
v21 = 32 - v7;
do
{
*v20++ = *v19--;
--v21;
}
while ( v21 );
}
算法很简单,基本上就是copy就完事,会获取到磁盘驱动器卷标序列号和注册码的前5位进行计算,这里仅贴出以上代码对应的Python算法。
[Python] 纯文本查看 复制代码 import math
VolumeSerialNumber="CA22FD67"
sn="222YA"
sum = 1
for i in sn:
sum = 7 * sum + 2 * ord(i)
sum += int(VolumeSerialNumber,16)
v5 = 1.33
clsid = VolumeSerialNumber.lower()
while(len(clsid) < 0x20):
tmp = sum * v5
v5 += 0.27
clsid += "%lx"%(math.trunc(tmp)&0xFFFFFFFF)
clsid = list(clsid[:32])
clsid.insert(12,'-')
clsid.insert(17,'-')
clsid.insert(22,'-')
clsid.insert(27,'-')
print "".join(clsid[::-1])
===============================================================================================================================
后续发现两处CLSID生成算法差异有点大,就将两处算法整合到一个脚本中,省去了基础不好的朋友寻找注册表位置的麻烦。只需要将VolumeSerialNumber变量替换为你本机的磁盘序列号,修改对应的注册码sn变量即可。
直接再cmd窗口输入vol命令即可得到磁盘序列号,请手动去掉中间的‘-’:
[Bash shell] 纯文本查看 复制代码 C:\Users\fxb>vol
驱动器 C 中的卷是 Windows
卷的序列号是 CA22-FD67
[Python] 纯文本查看 复制代码 import math
VolumeSerialNumber="CA22FD67"
sn="222YA"
def getCLSID1(VolumeSerialNumber,sn):
sum = 1
for i in sn:
sum = 7 * sum + 2 * ord(i)
sum += int(VolumeSerialNumber, 16)
v5 = 1.33
clsid = VolumeSerialNumber.lower()
while (len(clsid) < 0x20):
tmp = sum * v5
v5 += 0.27
clsid += "%lx" % (math.trunc(tmp) & 0xFFFFFFFF)
clsid = list(clsid[:32])
clsid.insert(12, '-')
clsid.insert(17, '-')
clsid.insert(22, '-')
clsid.insert(27, '-')
print "".join(clsid[::-1])
def getCLSID2(VolumeSerialNumber,sn):
sum = 1
for i in sn:
sum = 5 * sum + 2 * ord(i)
sum += int(VolumeSerialNumber,16)
sum /= 3
v5 = 1.55
clsid = ''
while(len(clsid) < 0x20):
sum = math.trunc(sum * v5)&0xFFFFFFFF
v5 += 0.25
clsid += "%lx"%(sum)
clsid = list(clsid[:32])
clsid.insert(8,'-')
clsid.insert(13,'-')
clsid.insert(18,'-')
clsid.insert(23,'-')
print "".join(clsid)
getCLSID1(VolumeSerialNumber,sn)
getCLSID2(VolumeSerialNumber,sn)
程序功能依然在测试中,暂时不能保证后续不会有弹窗提示,但不会影响正常使用。
|