好友
阅读权限30
听众
最后登录1970-1-1
|
本帖最后由 三年三班三井寿 于 2019-4-8 15:40 编辑
小白第二贴
本帖的主角是虐了我快一周的crackme05
一开始只是无聊想看看五星是啥难度,后来被虐得来论坛找思路,但在各个论坛上都没发现053的破解全过程。
一直到昨天晚上卡在注册机最后一步的实现,最后还是靠室友帮忙找到了相关的源码(用了数论的NTL库)。
正真进入主题之前,一般CM都是要有一些前戏的。
而很可惜,这个程序很直接,并没有任何反调试,只是用了upx压缩了一下,而它五星的难度完全在于注册的算法。
1.简单脱了壳之后,通过peid可以直接扫出开发环境——VC6
2.通过VC6按钮特征码sub eax,0a可以直接定位到加密的算法。
第一步对name的加密,其实一开始在这也花了很多时间,论坛上有人说是md5,然后又花了点时间看了一下md5具体的实现流程,之后才完全弄明白它的第一步加密(虽然我到现在也不确定是不是md5,基本和md5加密一致,但分组那一块似乎有点区别)。
1.第一关 :对name的升级
通过前面的特征码搜索,我们找到了战场
也是从这里开始逆向
最上面2个call,分别获取了name和code,接下来它将name字符串反转拼接到了原来的后面
而它这么做的动机是什么?
难道这就是第一关的加密方式?
还是混淆?但这程序就没有任何混淆啊。
福尔摩斯说过,排除所有不可能剩下那个多不可思议,都是事实真相。
对md5有过了解的朋友可能会猜到,作者在这边先进行了加盐
通俗点说就是:先把明文进行一些变换增加长度之类的操作,然后再去进行加密的计算。
我不是小福尔摩斯,我是看了之后的反汇编才知道的。
如果你看了上面这一段反汇编就知道了,那你就是小福尔摩斯了。
上面这一段的功能其实和第一段是一样的,只是我没法截图截完整。
作者并不是只加了一次盐,而是杀人诛心般地加了五次。
不过好在,基本都在同一块地方。
大概总结一下这第一步的操作吧:
1.先把输入的用户名name反过来加到原来name后面
2.通过注册表获取ProductID再加到后面(所以每台机器生成的注册码应该不同),不过这里有一点坑,就是我的主机上通过它这种方式是获取不到ProductID的,不过在32位虚拟机上可以获取到,而如果没有获取到ID的话,在接着的拼接操作上就会再一次拼接之前那个反name。
3.通过注册表获取用户名RegisteredOwner(这个是都能获取到的),拼接到最后。
4.补充长度(末尾加1和N个0),这一步在md5算法中也有类似操作(为了之后的分组),但这个程序补充的长度好像有些不一样.
5.通过长度计算出一个特征值放到最后哪些全0的地方(具体位置也是算出来的)
以上就是加密前的一些处理,你要说这本身就是一种加密也没任何毛病。
下面放上第一关的通关秘籍
主要逻辑大致如下
[C++] 纯文本查看 复制代码 //获得系统名称
void GetSysEdition(char*&lpProductName,char*&UserName)
{
HKEY hKey;
DWORD dwSize = 50; // 键值的值长度最大值,过小会导致RegQueryValueEx()调用失败
DWORD dwSize2 = 100;
DWORD dwDataType = REG_SZ;
LPCSTR subKey = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
long errorCode;
//判断32/64位系统
SYSTEM_INFO sys;
GetNativeSystemInfo(&sys);
if (sys.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64 ||
sys.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_IA64)
{
errorCode = RegOpenKeyExA(HKEY_LOCAL_MACHINE,
subKey, NULL, KEY_READ | KEY_WOW64_64KEY, &hKey);
UserName = new CHAR[dwSize2];
RegQueryValueExA(hKey, "RegisteredOwner", NULL, &dwDataType, (LPBYTE)UserName, &dwSize2);
}
else
{
errorCode = RegOpenKeyExA(HKEY_LOCAL_MACHINE, subKey, NULL, KEY_READ, &hKey);
lpProductName = new CHAR[dwSize];
UserName = new CHAR[dwSize2];
RegQueryValueExA(hKey, "ProductID", NULL, &dwDataType, (LPBYTE)lpProductName, &dwSize);
RegQueryValueExA(hKey, "RegisteredOwner", NULL, &dwDataType, (LPBYTE)UserName, &dwSize2);
}
RegCloseKey(hKey);
}
[C++] 纯文本查看 复制代码 //初始化加盐
DWORD CMFCApplication1Dlg::Init()
{
//1.反转用户名,name=name+反name
UpdateData(TRUE);
USES_CONVERSION;
char *temp = W2A(m_name);
_strrev(temp);
CString temp2 = A2W(temp);
//2.获取版本号
char* WindowsId = NULL;
char* UserName = NULL;
GetSysEdition(WindowsId, UserName);
if (WindowsId)
{
CString temp_WindowsId = A2W(WindowsId);
CString temp_UserName = A2W(UserName);
m_name = m_name + temp2 + temp_WindowsId + temp_UserName;
}
else
{
CString temp_UserName = A2W(UserName);
m_name = m_name + temp2+ temp2 + temp_UserName;
}
char*chName = W2A(m_name);
DWORD orilen = strlen(chName);
name = new char[orilen + 80]{0};
memcpy(name, chName, orilen);
//末尾加盐0x80
name[orilen] = 0x80;
//指定位置加盐(len*8)
//1.计算加盐位置
DWORD eax = 0x40 - ((orilen + 1) & 0x3f);
if (eax <= 7)
eax = 0x80 - ((orilen + 1) & 0x3f);
DWORD position= eax + orilen + 1;
//2.盐:len<<0x3
DWORD salt = strlen(name)<<0x3;
//3.加盐
*(DWORD*)(name + position - 0x8) = salt;
if(WindowsId)
delete[]WindowsId;
if(UserName)
delete[]UserName;
return position;
}
2.第二关 :name离散序列的计算
没错,这一关的代码就是这么少 只有一个函数
进去之后看看,你会发现:
这个函数里没有任何其他函数的调用,
不过目测有几百行的代码。
充钱上IDA,双剑合璧
大体逻辑如下,只截取了一半,一共有四个大循环,每个循环都比较类似,最终会得到一个新得离散序列ABCD。
其实这部分就是MD5的计算方式,计算一模一样,幻数也一模一样。(只是这个程序是把之前升级后的用户名,以0x40进行分组,然后计算的)
如果熟悉md5的加密方式的话,这一关刷的会很速度
还原后的代码如下:
[C++] 纯文本查看 复制代码 /*循环左移*/
unsigned rol(unsigned val, int size)
{
unsigned res = val << size;
res |= val >> (32 - size);
return res;
}
[C++] 纯文本查看 复制代码 //幻数初始化
void CMFCApplication1Dlg::InitMagic()
{
magic[0] = 0x67452301;
magic[1] = 0xEFCDAB89;
magic[2] = 0x98BADCFE;
magic[3] = 0x10325476;
DWORD temp1[]=
{0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821};
DWORD temp2[]= {
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a };
DWORD temp3[]= {
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665 };
DWORD temp4[]= {
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 };
memcpy(ti1, temp1, sizeof(ti1));
memcpy(ti2, temp2, sizeof(ti2));
memcpy(ti3, temp3, sizeof(ti4));
memcpy(ti4, temp4, sizeof(ti4));
}
//用户名md5计算
void CMFCApplication1Dlg::Name_md5(char*name)
{
//幻数
DWORD A = magic[0];
DWORD B = magic[1];
DWORD C = magic[2];
DWORD D = magic[3];
//第一轮计算
for (DWORD i = 0; i < 0x10; i+=0x4)
{
DWORD a = rol(*(DWORD*)(name+i*4) +
(C & B | D & ~B) + ti1[i] + A,7);
A = B + a;
DWORD b=rol(*(DWORD*)(name + (i + 1) * 4) +
(A & B | C & ~A) + ti1[i + 1] + D, 12);
D = A + b;
DWORD c = rol(*(DWORD*)(name + (i + 2) * 4) +
(D & A | B & ~D)+ti1[i+2]+C, 17);
C = D + c;
DWORD d = rol(*(DWORD*)(name + (i + 3) * 4)+
(D & C | A & ~C) + ti1[i + 3] + B, 22);
B = C + d;
}
//第二轮计算
for (DWORD i = 0; i < 0x10; i += 0x4)
{
DWORD a = rol(*(DWORD*)(name + ((i+1)&0xf) * 4)+
(D & B | C & ~D) + ti2[i] + A,5);
A = B + a;
DWORD b = rol(*(DWORD*)(name + ((i + 6) & 0xf) * 4)+
(C & A | B & ~C) + ti2[i+1] + D, 9);
D = A + b;
DWORD c = rol(*(DWORD*)(name + ((i + 11) & 0xf)*4)+
(D & B |A & ~B) + ti2[i + 2] + C,14);
C = D + c;
DWORD d = rol(*(DWORD*)(name + (i & 0xf) * 4)+
(C & A | D & ~A) + ti2[i + 3] + B,20);
B = C + d;
}
//第三轮计算
for (DWORD i = 0, position=5; i < 0x10; i += 0x4, position+=12)
{
DWORD a = rol(*(DWORD*)(name + (position & 0xf) * 4) +
(D ^ C ^ B)+ ti3[i] + A,4);
A = B + a;
DWORD b= rol(*(DWORD*)(name + ((position+3) & 0xf) * 4) +
(C ^ A ^ B) + ti3[i+1]+D, 11);
D = A + b;
DWORD c = rol(*(DWORD*)(name + ((position + 6) & 0xf) * 4) +
(B ^ D ^A) + ti3[i + 2]+C, 16);
C = D + c;
DWORD d = rol(*(DWORD*)(name + ((position + 9) & 0xf) * 4) +
(A ^ D ^ C)+ ti3[i + 3]+B, 23);
B = C + d;
}
//第四轮运算
for (DWORD i = 0, position = 0; i < 0x10; i += 0x4, position += 12)
{
DWORD a = rol(*(DWORD*)(name + (position & 0xf) * 4) +
(C ^ (B | ~D)) + ti4[i] + A, 6);
A = B + a;
DWORD b = rol(*(DWORD*)(name + ((position + 7) & 0xf) * 4) +
(B ^ (A | ~C)) + ti4[i + 1] + D, 10);
D = A + b;
DWORD c = rol(*(DWORD*)(name + ((position + 14) & 0xf) * 4) +
(A ^ (D | ~B)) + ti4[i + 2] + C, 15);
C = D + c;
DWORD d = rol(*(DWORD*)(name + ((position + 5) & 0xf) * 4) +
(D ^ (C | ~A)) + ti4[i + 3] + B, 21);
B = C + d;
}
//修改幻数
magic[0] += A; magic[1] += B; magic[2] += C; magic[3] += D;
}
想要了解md5的具体实现流程可以看以下博客
https://www.cnblogs.com/foxclever/p/7668369.html
3.第三关 :code的校验
看到下面那个字符串中醒目的good了吗?说明我们快通关了(并不).
从下往上看,最下方的call就是一个MessageBox弹窗,也是我们对于程序本身分析的终点。
往上两个JNZ是在循环,实际上就是判断了三组值是否相等,如果都相等,那就通过了。
也可以这样说,它判断了一个两个长度为12字节的数是否相等。
其中一个12字节的数正是前一步求出的离散值(求出的16字节只对比了12字节)
而另一个12字节的数(3个DWORD 数)是上方sub_00401AD0计算出的。
再往上的话实际上就是把输入的code 转化为3个DWORD 型的数(必须输入0-f这些16进制数),中间以空格隔开(不按次要求则在0040234C处弹窗离开)。
剩下的分析就只有sub_00401AD0这个函数了,该函数将我们输入的code计算出了一个离散值,并和之前算出的name的离散值进行比对。
进入到sub_00401AD0 后,代码量并没有之前name计算的长,貌似会简单点。
该函数一个调用了两次,传入了三个参数,第一个参数就是输入的code(3个16进制的4字节数code1,code2,code3)的地址,第二个第三个参数都是一个全局性的大数组。(第一次传入的是code1地址,第二次传入的是code2地址,code123地址是连续的,实际上在函数内部,第一次用到了code1,code2,第二次用到了code2,code3)
里面有两个循环,并且多次调用了一个函数,该函数的实现非常简单,但具体功能并不是很明显。
动态调试几次后发现,它返回的是:参数的二进制数形式所含1的个数
而两个循环里所做的事情就是不断地将参数1的值,和参数2数组相与,并将将此传入上面的函数,计算出含1的个数。
同样,将(参数1+4)的值与参数3数组相与,并将将此传入上面的函数,计算出含1的个数。
将上两个数相异或并与上1(换句话说就是,(*arg2 & *key1的1个数的奇偶性)^(*arg3 & *key2的1个数的奇偶性))。
并将每一次运算后的值(0或1)作为独立的一位保存下来,循环一共32次,这样就会得到一个32位的数。
同样这里有两个循环,就会得到两个32位的数,最终把参数1所在位置的后64位(2个DWORD数)设置成这两个数。
到此,整个code离散值的计算也就结束了,需要注意的是:这次离散值的计算函数是调用了两次的,第一次传入的code1所在位置,在结束后,code1,code2的值都被改变了,而第二次传入了code2所在的位置,不过这时code2已经被修改了,在这次调用完成后,code2再次被修改,同时code3也被修改。
[C++] 纯文本查看 复制代码 DWORD fun(DWORD arg)
{
DWORD count = 0;
while (arg)
{
count++;
arg = arg & (arg - 1);
}
return count;
}
//返回值:*key=res,*(key+1)=res2
//res从高位到低位:
//arg2[i] & (*key)二进制数中1的个数的奇偶性 异或
//arg3[i] & (*key+1)二进制数中1的个数的奇偶性
BOOL decode(DWORD* key, DWORD* arg2, DWORD* arg3)
{
DWORD k1 = *key; DWORD k2 = *(key + 1);
DWORD res = 0; DWORD res2 = 0;
for (DWORD i = 0; i < 0x20; i++)
{
DWORD temp = arg3[i] & k2;
DWORD temp2 = arg2[i] & k1;
res *= 2;
res = ((fun(temp) ^ fun(temp2)) & 1) ^ res;
}
for (DWORD i = 0; i < 0x20; i++)
{
DWORD temp = arg3[i + 0x20] & k2;
DWORD temp2 = arg2[i + 0x20] & k1;
res2 *= 2;
res2 = ((fun(temp) ^ fun(temp2)) & 1) ^ res2;
}
*key = res;
*(key + 1) = res2;
return TRUE;
}
算法整个都还原了,接下来是?
4.第四关:注册机的实现
假设我们已确定输入了一个name,那么我们可以通过之前的计算算出一个离散值,而这个离散值也应该是最后code计算出的离散值。我们现在的问题就是:如何在已知最后离散值的情况下(res1,res2,res3,res4),算出传入的参数code1,code2,code3
这里也是卡了最久的地方,一开始用穷举爆破的方式跑了十几分钟,然后放弃了,运气好点的话大概跑个几天可能会出来。
没办法,我们不得不退回到上一关看看有没有什么隐藏BOSS。
注意到这个表达式“(换句话说就是,(*arg2 & *key1的1个数的奇偶性)^(*arg3 & *key2的1个数的奇偶性) )”
*arg2 & *key1的1个数的奇偶性 :
首先,我们先将这两个数看成两个32位的二进制数,然后求两个数相与后含1的个数,其实可以把它看成两个向量的点乘。
再然后,含1个数的奇偶性,其实可以把这看作是在二元域上的运算。
第三点,在二元域中,后面的异或运算可以当作+。
第四点,key1会循环和整个arg2数组进行运算,我们可以把arg2当作一个32*32的矩阵。下面循环中的arg3也是同样。
现在,我们就已经找出数学规律了:
第一次调用code加密计算的时候,就相当于
1. arr_41c2c0*code1+addr_41c3c0*code2=res1;
2. arr_41c340*code1+addr_41c440*code2=temp;
第二次调用的时候code1已经变成res1,code2变成了temp
3. arr_41c4c0*temp+addr_41c5c0*code3=res2;
4. arr_41c540*temp+addr_41c640*code3=res3;
联立3,4可以先求出temp以及code3,然后再通过1,2就可以求出code1和code2了,当然这一切的计算都是在二元域上完成的。
昨天晚上就一直在解这个方程组,因为要对32阶矩阵求逆,跑得也很不比爆破快多少。后来室友找到了源码https://flandre-scarlet.moe/blog/625/ (vec_2_int返回值以及参数需要修改),直接用一个NTL库搞定算法基本差不多,但把我上面的流程简化了不少,比如将上面12,34给合并了,当成64位的去算,而code也是当作64位的计算。
不过源码中有一些bug,修改后如下,每一步都加入了注释
[C++] 纯文本查看 复制代码 //res为最后离散值
unsigned int res[3] = {
0x8eca5143, 0x647d9071, 0x06715bd2 };
//code为输入序列号
unsigned int code[3] = { 0,0,0 };
GF2 d;
//分别用户存储code以及res的gf(2)形式
vec_GF2 gf2_code, gf2_res;
mat_GF2 mat_arr1, mat_arr2;
//设置矩阵大小
mat_arr1.SetDims(64, 64);
mat_arr2.SetDims(64, 64);
gf2_code.SetLength(64);
gf2_res.SetLength(64);
//将数组转化为2进制矩阵
fill_mat(mat_arr1, (unsigned long*)addr_0x41c4c0.data(), (unsigned long*)addr_0x41c5c0.data());
//将res转化到gf(2)
for (int i = 0; i < 32; ++i)
{
gf2_res[i] = !!((res[1] << i) & 0x80000000);
gf2_res[i + 32] = !!((res[2] << i) & 0x80000000);
}
// 解第二次调用时的方程 gf2_code* mat_arr1=gf2_res,求出gf2_code
solve(d, gf2_code, mat_arr1, gf2_res);
//先求出code3,在gf2_code后32位中(前32位是temp)
code[2] = vec_2_int(gf2_code,32, 64);
for (int i = 0; i < 32; ++i)
{
gf2_res[i] = !!((res[0] << i) & 0x80000000);
gf2_res[i + 32] = gf2_code[i];
}
fill_mat(mat_arr2, (unsigned long*)addr_0x41c2c0.data(), (unsigned long*)addr_0x41c3c0.data());
//解第一次调用时的方程
solve(d, gf2_code, mat_arr2, gf2_res);
//后32位为code2
code[1] = vec_2_int(gf2_code, 32, 64);
//前32位为code1
code[0] = vec_2_int(gf2_code, 0, 32);
打完收工!
|
免费评分
-
查看全部评分
|