CM 053破解全过程,并通过二元域矩阵实现注册机
本帖最后由 三年三班三井寿 于 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的地方(具体位置也是算出来的)
以上就是加密前的一些处理,你要说这本身就是一种加密也没任何毛病。
下面放上第一关的通关秘籍
主要逻辑大致如下
//获得系统名称
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;
RegQueryValueExA(hKey, "RegisteredOwner", NULL, &dwDataType, (LPBYTE)UserName, &dwSize2);
}
else
{
errorCode = RegOpenKeyExA(HKEY_LOCAL_MACHINE, subKey, NULL, KEY_READ, &hKey);
lpProductName = new CHAR;
UserName = new CHAR;
RegQueryValueExA(hKey, "ProductID", NULL, &dwDataType, (LPBYTE)lpProductName, &dwSize);
RegQueryValueExA(hKey, "RegisteredOwner", NULL, &dwDataType, (LPBYTE)UserName, &dwSize2);
}
RegCloseKey(hKey);
}
//初始化加盐
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{0};
memcpy(name, chName, orilen);
//末尾加盐0x80
name = 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的加密方式的话,这一关刷的会很速度
还原后的代码如下:
/*循环左移*/
unsigned rol(unsigned val, int size)
{
unsigned res = val << size;
res |= val >> (32 - size);
return res;
}
//幻数初始化
void CMFCApplication1Dlg::InitMagic()
{
magic = 0x67452301;
magic = 0xEFCDAB89;
magic = 0x98BADCFE;
magic = 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;
DWORD B = magic;
DWORD C = magic;
DWORD D = magic;
//第一轮计算
for (DWORD i = 0; i < 0x10; i+=0x4)
{
DWORD a = rol(*(DWORD*)(name+i*4) +
(C & B | D & ~B) + ti1 + A,7);
A = B + a;
DWORD b=rol(*(DWORD*)(name + (i + 1) * 4) +
(A & B | C & ~A) + ti1 + D, 12);
D = A + b;
DWORD c = rol(*(DWORD*)(name + (i + 2) * 4) +
(D & A | B & ~D)+ti1+C, 17);
C = D + c;
DWORD d = rol(*(DWORD*)(name + (i + 3) * 4)+
(D & C | A & ~C) + ti1 + 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 + A,5);
A = B + a;
DWORD b = rol(*(DWORD*)(name + ((i + 6) & 0xf) * 4)+
(C & A | B & ~C) + ti2 + D, 9);
D = A + b;
DWORD c = rol(*(DWORD*)(name + ((i + 11) & 0xf)*4)+
(D & B |A & ~B) + ti2 + C,14);
C = D + c;
DWORD d = rol(*(DWORD*)(name + (i & 0xf) * 4)+
(C & A | D & ~A) + ti2 + 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 + A,4);
A = B + a;
DWORD b= rol(*(DWORD*)(name + ((position+3) & 0xf) * 4) +
(C ^ A ^ B) + ti3+D, 11);
D = A + b;
DWORD c = rol(*(DWORD*)(name + ((position + 6) & 0xf) * 4) +
(B ^ D ^A) + ti3+C, 16);
C = D + c;
DWORD d = rol(*(DWORD*)(name + ((position + 9) & 0xf) * 4) +
(A ^ D ^ C)+ ti3+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 + A, 6);
A = B + a;
DWORD b = rol(*(DWORD*)(name + ((position + 7) & 0xf) * 4) +
(B ^ (A | ~C)) + ti4 + D, 10);
D = A + b;
DWORD c = rol(*(DWORD*)(name + ((position + 14) & 0xf) * 4) +
(A ^ (D | ~B)) + ti4 + C, 15);
C = D + c;
DWORD d = rol(*(DWORD*)(name + ((position + 5) & 0xf) * 4) +
(D ^ (C | ~A)) + ti4 + B, 21);
B = C + d;
}
//修改幻数
magic += A; magic += B; magic += C; magic += 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也被修改。
DWORD fun(DWORD arg)
{
DWORD count = 0;
while (arg)
{
count++;
arg = arg & (arg - 1);
}
return count;
}
//返回值:*key=res,*(key+1)=res2
//res从高位到低位:
//arg2 & (*key)二进制数中1的个数的奇偶性异或
//arg3 & (*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 & k2;
DWORD temp2 = arg2 & k1;
res *= 2;
res = ((fun(temp) ^ fun(temp2)) & 1) ^ res;
}
for (DWORD i = 0; i < 0x20; i++)
{
DWORD temp = arg3 & k2;
DWORD temp2 = arg2 & 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库搞定{:1_925:}算法基本差不多,但把我上面的流程简化了不少,比如将上面12,34给合并了,当成64位的去算,而code也是当作64位的计算。
不过源码中有一些bug,修改后如下,每一步都加入了注释
//res为最后离散值
unsigned int res = {
0x8eca5143, 0x647d9071, 0x06715bd2 };
//code为输入序列号
unsigned int code = { 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 = !!((res << i) & 0x80000000);
gf2_res = !!((res << 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 = vec_2_int(gf2_code,32, 64);
for (int i = 0; i < 32; ++i)
{
gf2_res = !!((res << i) & 0x80000000);
gf2_res = gf2_code;
}
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 = vec_2_int(gf2_code, 32, 64);
//前32位为code1
code = vec_2_int(gf2_code, 0, 32);
打完收工!
学习了,算法我分析出来了,卡在了最后一步逆不出来,没能理解到矩陈上去。:rggrg
/**
* 统计一个整数中有多少位bits是1
*/
int bitsCount(unsigned int aInt) {
int count = 0;
for (; aInt!=0; aInt &= aInt - 1 ) {
count++;
}
return count; //// return (0~32)
}
/**
假码 12345678 abcdef00 78787878
0019F1EC78 56 34 12 00 EF CD AB 78 78 78 78
0019F1EC6E 2B 42 BC 05 58 AA 94 78 78 78 78
0019F1EC6E 2B 42 BC 57 FE 42 38 CD 8C 37 DA
*/
void Encrypt(unsigned int *sn, unsigned int * arr2, unsigned int * arr3) {
unsigned int ret1=0, ret2=0;
unsigned int a, b;
int idx=0;
//// first loop
for(int i=0; i<32; i++, idx++) {
a = sn & arr2;//// idx=0~31
b = sn & arr3;
//ret1 = (ret1*2) ^ ((bitsCount(a) ^ bitsCount(b)) & 0x01);
//ret1 = (ret1<<1) ^ ((bitsCount(a) ^ bitsCount(b)) & 0x01);
//ret1 = (ret1<<1) + ((bitsCount(a) ^ bitsCount(b)) & 0x01);
ret1 = (ret1<<1) + ((bitsCount(a) + bitsCount(b)) & 0x01);
//// ret1 由 ((bitsCount(a) ^ bitsCount(b)) & 0x01) 生成的bit组成,32个bits
}
//// second loop
for(int i=0; i<32; i++, idx++) {
a = sn & arr2;//// idx=32~63
b = sn & arr3;
//ret2 = (ret2*2) ^ ((bitsCount(a) ^ bitsCount(b)) & 0x01);
//ret2 = (ret2<<1) ^ ((bitsCount(a) ^ bitsCount(b)) & 0x01);
//ret2 = (ret2<<1) + ((bitsCount(a) ^ bitsCount(b)) & 0x01);
ret2 = (ret2<<1) + ((bitsCount(a) + bitsCount(b)) & 0x01);
//// ret2 由 ((bitsCount(a) ^ bitsCount(b)) & 0x01) 生成的bit组成,32个bits
}
//// return result
sn = ret1;
sn = ret2;
}
在贴一个今天才写好的crackme 092的注册机,花了一天多的时间,感觉这个更适合新手练习
像高手学习了 又涨了知识 学习一下大佬的 思维 前排支持大佬 审核了快三天{:1_925:} 果然没人看呀{:1_925:} 三年三班三井寿 发表于 2019-4-29 15:47
果然没人看呀
已看!已看!已看!:lol:lol:lol
(凑字数)
页:
[1]
2