selph
selph
Published on 2022-10-14 / 235 Visits
0
0

新160个CrackMe练习:064-CR-Game0.7

咕咕咕了好久,今天开始正式开始继续更新!

算法难度:⭐⭐⭐⭐

爆破难度:⭐

信息收集

运行情况:

总共有7个难度,可以点击Level 001 进行切换

image

查壳与脱壳:

无壳

image

调试分析

定位到Register函数

这是个简单的win32程序,一般这种的找窗口函数很简单,从头往下看很快就有,这里开始应该是个WinMain函数:

image

跟进去在注册窗口类填充字段的时候找到窗口函数:

image

通过xspy查看到程序的Register按钮句柄是0xbbf:

image

然后在窗口函数消息循环处理里找WM_COMMAND消息,且参数为0xbbf的分支:

这里开始是在拼接ini文件的路径:

image

往下发现跟这个ini文件的内容好像没啥关系,这里最终调用哪个Register函数取决于当前Level,随cm附带了一个记事本,说了个快捷键:CTRL+SHIFT+ALT+E​,通过这个快捷键可以进行Level的设置

image

现在找到了不同Level的注册函数了,开始进行分析

Register1

Register1是硬编码比对,没啥好说的:

image

结果:

image

Register2

一堆比较:首先是第一轮比较,第8个字符不为C,然后后面的是硬编码

这里后面表示ret的表示函数就退出了,没有的则是向下跳了一个判断

image

第二轮比较:第6个字符+0x3B需要等于0x6C,然后重新对比了几个字符

image

整理一下就是硬编码:2leveL1GC

结果:

image

Register3

首先是获取Name和Serial,这个0D的消息号是WM_GETTEXT,然后进行长度判断:用户名长度在5-14之间,且和序列号等长

image

接下来是复制了一个字符串,然后把用户名中的小写转大写:

image

最后进行比对,比对规则是Name作为索引,索引上面那个字符串的值,索引出来的值等同于序列号对应位置的值

image

注册机:

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

image

Register4

跟Register3一样的开头,获取Name和Serial,然后判断长度,不同的是,这里序列号的长度是Name的两倍

image

与3类似,不过是复制了两个字符串到变量里

image

再往下就是一个遍历Name所有字符的循环

首先是判断大小写,如果是小写,从str1索引字符和序列号比对,如果是大写就从str2索引字符比对

image

再往下是下一个循环:如果是小写就从str2索引比对,大写就从str1索引做比对,就是刚刚索引字符的反着来的过程

因为走了两边,正好判断了2*Name长度的字符,也就是Serial的长度

image

注册机:

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

image

Register5

与3一样:Name与Serial长度需要相同且在5~20之间

image

然后类似Level4:复制了两个字符串

image

然后是个循环,功能是对Name小写转大写:

image

然后是一个大循环套一个小循环:遍历Name的字符,获取其在str1里的位置,然后从str2里取该位置的字符与Serial中的字符比对:

image

注册机:

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

image

Register6

这次获取了三个值:分别有三个长度限制

image

紧接着的是前三个字节的硬编码判断:分别是JiP

image

再往下则是后10个字节的填充

首先是第4-5个字节,取值取决于Company首字符和Name首字符

然后是第5-12个字节,5-8字节随意自取,然后根据5-8字节和Name的前4字节,计算出9-12字节的值

image

然后跟了一波转大写的循环:

image

最后是前面类似的套路,复制一个字符串,用Name计算索引,取值和Serial后面的位进行比对

Serial长度是Name长度+12字节,最后会填充Name长度的内容

image

注册机:因为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

image

Register7

首先获取Serial,Serial长度需要是16字节,然后调用一个函数去生成一个字符串

image

接下来进去分析下:开始这段代码的功能执行了4遍,分别对应serial的前4字节,都是取一个字节调用一个函数,然后累加返回值左移6位,最后一次的时候仅累加,不进行位移

image

这个GetIndex函数的功能很简单,就是从字符串里找到当前字符的索引:

image

然后紧接着是生成字符串的操作,这里用刚刚计算的值,进行一些and和右移操作,计算出3个字符,填充到了Buffer里

image

然后依次循环直到序列号遍历完成

一开始我还没注意到,4个字符生成3个字符,从这个极其特殊的字符串:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/​找索引

这不就是Base64解码算法嘛,刚好这个字符串是标准base64的pattern

这里就是获取了Serial,然后base64解码,得到原字符串

接下来我又逆了两个自写函数发现就是库函数的功能,已在截图里rename:

后面就是把name和company拼接起来,和解码后的字符串进行比对

image

因为需要序列号长度为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==

image

总结

这个cm自写了好多字符串相关的库函数,用来练习逆向还是不错的,最后一个Level使用了一个简单的编码算法,也很好理解


Comment