算法难度:⭐⭐⭐⭐
爆破难度:⭐⭐
信息收集
运行情况:
直接打开说需要一个dat文件
创建了之后,说你确定这是正确的keyfile?感觉意思是启动时验证,根据文件内容验证
查壳与脱壳:
无壳:C++程序
调试分析
C++程序就抄起IDA干!
首先是创建对话框,这里主要进这个窗口函数去看
这里是窗口过程函数,根据不同的消息码有一大堆分支,这里只有一个分支调用了自定义的函数:
上面一堆无关紧要的部分,这个函数很可疑
这就是个check函数了:
首先是一堆变量初始化工作,就跳过不管了,程序首先是通过push和pop给edi赋值了初值:3
然后紧接着就是打开文件key.dat,打开失败(文件不存在)就显示提示信息,如果有这个文件就往下跳过
往下就是读取文件内容的操作,这里会判断读取的字节数,如果为0则意味着没内容,就跳到能通往Fail的程序路线
接下来读取到内容了之后,有两个循环:
- 循环1:首先是使用读取的字节数对每一个字节进行一次异或操作
- 接下来对于前三个字节,额外再异或一个魔数,分别异或0x54,0x4d,0x47
- 循环2:三个字节为一组,向后异或,1和4,2和5,3和6的值进行异或,然后4和7,5和8,6和9的值进行异或,依次进行下去,处理完输入的长度,因为是三个一组,所以最后可能长度比输入的长度长,需要手动在字节数组结尾赋值0来截断
接下来:获取一个地址,405030
,这是个字符数组(注意这个值!!后面有用到)
然后使用我们的输入的前三个字节,循环去异或这个字节数组,依然是3个一组,对于字符数组,每三个字节,都分别与输入的前三字节异或一遍,直到字符数组的结尾标识FF出现
跳出循环之后,是第一层校验:前三个字节的乘积为0x2A8BF4
如果不是,则就显示提示信息说我们的输入是错误的
接下来紧接着又是一个循环,遍历输入的值,复制到一个缓冲区里,结束标志是出现0x20,给缓冲区的字符串一个00结尾(实际上这一段是在定位UserName)
接下来是另一个循环,循环遍历的对象依然是我们的输入,这个循环的起点是上一个循环的终点,在0x20之后开始,把每一个字节都复制到另一个局部变量里[ebp+ver_58](实际这就是解密出来的用户名)
接下来是一个神奇的VirtualProtect调用,可能有人觉得到前面验证三数乘积的时候就结束了,实际上并没有,之前对一个字符数组进行了大量的异或,然后这里使用VirtualProtect函数给字符数组修改了内存属性,然后直接call了这个字符数组的地址
合着这字符数组是shellcode啊,然后那一堆异或是解密操作!
关于shellcode的功能,就不详细分析了,具体功能是生成字符串Registered to: 然后拼接用户名
到这里就分析完了验证流程,接下来该写注册机了,接下来分析注册机怎么写
注册机分析
这里验证如下:
- 经过一系列异或之后的前三字节的乘积为固定值
- 前三个字节可以用于解密shellcode,解密的结果可执行
- 输入的字符里存在分隔符,分隔符经过一系列异或之后的值为0x20,分隔符之后的内容则是UserName,经过一系列异或解密出UserName,如果没有这个分隔符则会奔溃退出程序
首先处理第一个验证,三个值的乘积为固定值,这三个值是输入的前三字节经过已知数值的异或而得到,所以这个计算是可逆的;处理思路是找到满足要求的三字节,然后反推找到原始的前三字节,数据量不大,简单粗暴处理:
void getvalue() {
for (size_t i = 0; i < 0xff; i++)
for (size_t j = 0; j < 0xff; j++)
for (size_t k = 0; k < 0xff; k++)
if (i * j * k == 0x2A8BF4)
std::cout << i << " " << j << " " << k << std::endl;;
得到一组结果:
unsigned char val[] = {
85, 139, 236,
85, 236, 139,
118, 139, 170,
118, 170, 139,
139, 85, 236,
139, 85, 236,
139, 118, 170,
139, 170, 118,
139, 236, 85,
170, 118, 139,
170, 139, 118,
236, 85, 139,
236, 139, 85 };
接下来使用这组结果去进行异或反推得到新的一组输入值:
每个字节会异或字符数组对应的字节,然后异或固定的值,然后异或读取字符长度
int main()
{
//字符数组的前三字节:0x1E, 0xBF, 0xA2
for (int i = 0; i < 39; i+=3)
{
int len = 3;
val[i] ^= 0x1E^0x54^len;
val[i+1] ^= 0xbf^0x4d^len;
val[i+2] ^= 0xa2^0x47^len;
printf("%02x %02x %02x \n",val[i],val[i+1],val[i+2]);
}
}
得到一组结果:
1c 7a 0a
1c 1d 6d
3f 7a 4c
3f 5b 6d
c2 a4 0a
c2 a4 0a
c2 87 4c
c2 5b 90
c2 1d b3
e3 87 6d
e3 7a 90
a5 a4 6d
a5 7a b3
这一组结果作为输入的前三字节,经过一系列异或之后的乘积满足固定值要求
因为结果有限,可能只有一组,也可能有多组答案能解密shellcode,所以这里先不管
接下来是计算分隔符,没有分隔符程序会崩溃:因为把分隔符放在第四位,所以根据之前的异或操作规律,需要和第一个值进行异或一下:
int main()
{
for (int i = 0; i < 39; i+=3)
{
int len = 3+1;
val[i] ^= 0x1E^0x54^len;
val[i+1] ^= 0xbf^0x4d^len;
val[i+2] ^= 0xa2^0x47^len;
char tmp = 0x20 ^ val[i] ^ 0x54;
printf("%02x %02x %02x %02x \n",val[i],val[i+1],val[i+2],tmp);
}
}
得到新的一组结果:
1b 7d 0d 6f
1b 1a 6a 6f
38 7d 4b 4c
38 5c 6a 4c
c5 a3 0d ffffffb1
c5 a3 0d ffffffb1
c5 80 4b ffffffb1
c5 5c 97 ffffffb1
c5 1a b4 ffffffb1
e4 80 6a ffffff90
e4 7d 97 ffffff90
a2 a3 6a ffffffd6
a2 7d b4 ffffffd6
到这里如果计算没错的话,程序应该可以正常运行了,使用第一组输入试试:成功运行,显示提示字符已注册
但是这里用户名还没有显示出来,用户名经过的操作只有两次,一次是最初的对每个字符都异或一遍读取长度,然后就是用前三字节去循环异或用户名了:
int main()
{
char name[100] = {0};
std::cin >> name;
int tmp_len = strlen(name);
int nLen = tmp_len % 3 == 0 ? tmp_len : tmp_len + 3-(tmp_len % 3);
//0x1E, 0xBF, 0xA2
for (int i = 0; i < 39; i+=3)
{
int len = 3+1+nLen;
val[i] ^= 0x1E^0x54^len;
val[i+1] ^= 0xbf^0x4d^len;
val[i+2] ^= 0xa2^0x47^len;
for (int j = 0; j < strlen(name); j += 3) {
name[j] ^= val[i+1]^0x4d;
name[j+1] ^= val[i+2]^0x47;
name[j+2] ^= val[i]^0x54;
}
char tmp = 0x20 ^ val[i] ^ 0x54;
printf("%02x %02x %02x %02x ",val[i],val[i+1],val[i+2],tmp);
for (int j = 0; j < strlen(name); j++)
{
printf("%02x ",name[j]);
}
printf("\n");
}
}
计算结果:用户名:selph
,结果:15 73 03 61 4d 21 2d 4e 2c 41
,用010 editor写入key文件,打开程序:用户名也出来了,到此完成这个CM 的算法分析
总结
这个有意思,通过key文件进行校验,key里有分隔符决定程序是否会崩溃,分隔符后面的是用户名,通过异或操作进行解密
这里还通过前三字节作为密码去解密一段shellcode,然后执行shellcode来生成验证通过的字符串进行显示