咕咕咕了好久,今天开始正式开始继续更新!
算法难度:⭐⭐⭐⭐
爆破难度:⭐
信息收集
运行情况:
总共有7个难度,可以点击Level 001 进行切换
查壳与脱壳:
无壳
调试分析
定位到Register函数
这是个简单的win32程序,一般这种的找窗口函数很简单,从头往下看很快就有,这里开始应该是个WinMain函数:
跟进去在注册窗口类填充字段的时候找到窗口函数:
通过xspy查看到程序的Register按钮句柄是0xbbf:
然后在窗口函数消息循环处理里找WM_COMMAND消息,且参数为0xbbf的分支:
这里开始是在拼接ini文件的路径:
往下发现跟这个ini文件的内容好像没啥关系,这里最终调用哪个Register函数取决于当前Level,随cm附带了一个记事本,说了个快捷键:CTRL+SHIFT+ALT+E
,通过这个快捷键可以进行Level的设置
现在找到了不同Level的注册函数了,开始进行分析
Register1
Register1是硬编码比对,没啥好说的:
结果:
Register2
一堆比较:首先是第一轮比较,第8个字符不为C,然后后面的是硬编码
这里后面表示ret的表示函数就退出了,没有的则是向下跳了一个判断
第二轮比较:第6个字符+0x3B需要等于0x6C,然后重新对比了几个字符
整理一下就是硬编码:2leveL1GC
结果:
Register3
首先是获取Name和Serial,这个0D的消息号是WM_GETTEXT,然后进行长度判断:用户名长度在5-14之间,且和序列号等长
接下来是复制了一个字符串,然后把用户名中的小写转大写:
最后进行比对,比对规则是Name作为索引,索引上面那个字符串的值,索引出来的值等同于序列号对应位置的值
注册机:
void Level3()
{
string str = "QWERTYUIOPASDFGHJKLZXCVBNM";
string? name = Console.ReadLine();
if (name.Length < 5 || name.Length > 20) return;
for (int i = 0; i < name.Length; i++)
{
name = name.ToUpper();
int index = (int)name[i] - 0x41;
Console.Write(str[index]);
}
}
效果:selph:LTSHI
Register4
跟Register3一样的开头,获取Name和Serial,然后判断长度,不同的是,这里序列号的长度是Name的两倍
与3类似,不过是复制了两个字符串到变量里
再往下就是一个遍历Name所有字符的循环
首先是判断大小写,如果是小写,从str1索引字符和序列号比对,如果是大写就从str2索引字符比对
再往下是下一个循环:如果是小写就从str2索引比对,大写就从str1索引做比对,就是刚刚索引字符的反着来的过程
因为走了两边,正好判断了2*Name长度的字符,也就是Serial的长度
注册机:
void Level4(string? name)
{
string str1 = "polkiujmnhytgbvfredcxswqaz";
string str2 = "QWERTYUIOPASDFGHJKLZXCVBNM";
string serial1 = "";
string serial2 = "";
foreach(var c in name)
{
if(c>='a' && c <= 'z')
{
int i = (int)c-'a';
serial1 += str1[i];
serial2 += str2[i];
}
if(c>='A' && c <= 'Z')
{
int i = (int)c - 'A';
serial1 += str2[i];
serial2 += str1[i];
}
}
Console.WriteLine(serial1 + serial2);
}
效果:selph:ditfmLTSHI
Register5
与3一样:Name与Serial长度需要相同且在5~20之间
然后类似Level4:复制了两个字符串
然后是个循环,功能是对Name小写转大写:
然后是一个大循环套一个小循环:遍历Name的字符,获取其在str1里的位置,然后从str2里取该位置的字符与Serial中的字符比对:
注册机:
void Level5(string? name)
{
string str1 = "QWERTYUIOPASDFGHJKLZXCVBNM";
string str2 = "pOlKiUjmnhytgbVfredCXSwqaZ";
string Name = name.ToUpper();
string serial = "";
foreach (var c in Name)
{
int i = str1.IndexOf(c);
serial+=str2[i];
}
Console.WriteLine(serial);
}
测试:selph:tldhf
Register6
这次获取了三个值:分别有三个长度限制
紧接着的是前三个字节的硬编码判断:分别是JiP
再往下则是后10个字节的填充
首先是第4-5个字节,取值取决于Company首字符和Name首字符
然后是第5-12个字节,5-8字节随意自取,然后根据5-8字节和Name的前4字节,计算出9-12字节的值
然后跟了一波转大写的循环:
最后是前面类似的套路,复制一个字符串,用Name计算索引,取值和Serial后面的位进行比对
Serial长度是Name长度+12字节,最后会填充Name长度的内容
注册机:因为CSharp没法像C++一样把可以char数组直接当int类型处理,所以这里根据小端序来逐字节操作
void Level6(string ? name,string? company)
{
string str = "JiP4ZAQWSXCDERFVBGTYHNMJUIKLOP";
string Name = name;
char[] serial = new char[20];
serial[0] = (char)('M' - 3);
serial[1] = 'i';
serial[2] = (char)('M' + 3);
serial[3] = (char)((company[0] & 6) + Name[0]);
serial[4] = (char)((Name[0] & 9) + company[0]);
serial[5] = 'a';
serial[6] = 'a';
serial[7] = 'a';
serial[8] = 'a';
serial[12] = (char)(((serial[8] ^ Name[3])& 0x09) + Name[3]);
serial[11] = (char)(((serial[7] ^ Name[2])& 0x07) + Name[2]);
serial[10] = (char)(((serial[6] ^ Name[1])& 0x05) + Name[1]);
serial[9] = (char)(((serial[5] ^ Name[0])& 0x03)+ Name[0]);
// serial[9] = (char)(((int)serial[5] ^ (int)Name[0]) & 0x09070503 + Name[0]);
Name = Name.ToUpper();
int i = 0xD;
foreach(var c in Name)
{
serial[i++] = (char)(str[c - '=']);
}
Console.WriteLine(serial);
}
测试:Selph,c,JiPudaaaauiqqMSVYD
Register7
首先获取Serial,Serial长度需要是16字节,然后调用一个函数去生成一个字符串
接下来进去分析下:开始这段代码的功能执行了4遍,分别对应serial的前4字节,都是取一个字节调用一个函数,然后累加返回值左移6位,最后一次的时候仅累加,不进行位移
这个GetIndex函数的功能很简单,就是从字符串里找到当前字符的索引:
然后紧接着是生成字符串的操作,这里用刚刚计算的值,进行一些and和右移操作,计算出3个字符,填充到了Buffer里
然后依次循环直到序列号遍历完成
一开始我还没注意到,4个字符生成3个字符,从这个极其特殊的字符串:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
找索引
这不就是Base64解码算法嘛,刚好这个字符串是标准base64的pattern
这里就是获取了Serial,然后base64解码,得到原字符串
接下来我又逆了两个自写函数发现就是库函数的功能,已在截图里rename:
后面就是把name和company拼接起来,和解码后的字符串进行比对
因为需要序列号长度为16字节,所以生成base64编码的时候,Name+Company长度需要是10-12字节
注册机:
void Level7(string? name, string? company)
{
string? str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
string buffer = name + company;
byte[] byteArray = System.Text.Encoding.Default.GetBytes(buffer);
string serial = Convert.ToBase64String(byteArray);
Console.WriteLine(serial);
// 解密过程:
// 取4个字节 取索引,累加 左移
// 结果and 00ff0000,右移16位,保存
// 结果and 0000ff00,右移8位,保存
// 结果and 000000ff,保存
}
测试:selph,selph,c2VscGhzZWxwaA==
总结
这个cm自写了好多字符串相关的库函数,用来练习逆向还是不错的,最后一个Level使用了一个简单的编码算法,也很好理解