首页
社区
课程
招聘
[原创]2016腾讯游戏安全技术竞赛题PC第一题
2022-4-23 09:22 9796

[原创]2016腾讯游戏安全技术竞赛题PC第一题

2022-4-23 09:22
9796

2016腾讯游戏安全技术竞赛题PC第一题

忘了从哪儿找到的这个题目,偶然得知,就尝试着弄一下。
2016腾讯游戏安全技术竞赛PC第一题

1.结果:

图片描述
结论:

 

​ 纵观全局,才知道原来序列号就是把用户名两次加密后,再Base64编码为字符串。

 

只是不同于一般常用的64个字符串罢了。

 

这里输出"helloworld",计算出来的序列号是"VASdBcsKGhNDMRwPXv85%61MNQR"

 

实际上,最后一个字符改成Q/R/S/T都是可以的。因为长度不是4的整数倍,使用了'*'填充

 

事后来看,尽量写得有条不紊,井井有条。
实际调试的时候,总是注意看寄存器的变化,真是非常的耗费精力。
注册机啥的,还真没接触过,毕竟,没有加密,也没加壳的程序,直接爆破可轻松多了。
不动手操作,只停留理论,觉得自己掌握了方法,就能一番风顺,无异于纸上谈兵,是不会有进步的。

2.过程
1.入手点:注册机,肯定对文本有操作,所以要找到和文本相关的地方
 

​ 我们随便输入,点击注册,发现没有提示框之类的,只是单纯的提示注册失败。

 

使用OD一搜索,也没有相应的字符。

 

唯一有用的,或许就是这个了。

 

图片描述

 

64个字符,出现了好几次。猜想或许会使用base64编码的方式。

 

既然没有直接有用的信息,就只能从API下手了。

 

使用PE工具一看,好几个和text相关的API

1
2
3
4
5
6
7
8
9
10
11
0x29304                 0x2AB           SetWindowTextA
0x29270                 0xC6            DrawTextExA
0x29264                 0xC5            DrawTextA
0x29252                 0x2C6           TabbedTextOutA
0x2923A                 0x18D           GetWindowTextLengthA
0x29228                 0x18C           GetWindowTextA
0x29632                 0x29F           TextOutA
0x296A4                 0x258           ScaleViewportExtEx
0x29690                 0x28F           SetViewportExtEx
0x295CE                 0x28D           SetTextColor
0x2963E                 0x122           ExtTextOutA

感觉能用的就是GetWindowTextA,下断点一测试,毫无反应。

 

至于其他没导入的API,比如GetDlgItemTextA之类的,在一一尝试后,都没断下。说明获取文本不是用的这类API。

 

考虑到这是一个很明显的MFC程序,于是百度一搜索:MFC程序获取文本控件
MFC控件编程之按钮编辑框

 

这一篇可谓非常详尽了。这个注册机就用的GetDlgItem和消息循环的方式。

 

果然,在输入文本以后,点击注册,就断在了user32.GetDlgItem里面。

 

直接返回即可。

 

图片描述

 

返回到获取句柄的地方,啥也没干就返回了。

 

返回后继续看。

 

图片描述

 

果然,就是这样获取文本的。(获取控件句柄,再使用消息循环)

 

接下来,同样的方式获取序列号的文本。

 

然后esp+0x9C是用户名文本,esp+0x1A0是序列号文本。

 

获取了文本,就开始准备注册结果的字符串。

 

分别是 注册00000失败成功 。等下就把结果复制到前面4个0那里,组成最终的注册结果

 

图片描述

 

紧接着就是获取用户名的长度,限定为6-20位

 

紧随其后,就是对用户名的加密。开辟了0x14个字节的空间。

 

从esp+0x88开始,也就是用户名前面的0x14个字节

 

图片描述

 

这一段还是好理解的,原样的翻译了以下,虽然其中一些数据不知道来源,但是我们直接写死也是不影响结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void encrystUsernameFirst(string username)
{
    unsigned char data[0x14];
    memset(data, 0, 0x14);
    int eax = 0;
    int ebp = 0x1339E7E;
    int esp = (int)&data - 0x88;
    int edx = (int)&data;
    int ecx = 0;
    int edi = username.length();
    int* esi = 0;
    ebp = ebp - edx;
    while (ecx<0x10)
    {
        eax = ecx;
        edx = eax%edi;
        esi = (int*)&data[ecx];
        ecx++;
        eax = username[edx];
        edx = (int)esi + ebp;
        eax = eax*edx;
        eax = eax*edi;
        (*esi) = (*esi) + eax;
    }
    for (int i = 0; i<0x14; i++)
    {
        printf("%2X ", data[i]);
        if ((i + 1) % 16 == 0)
        {
            cout << endl;
        }
    }
}

随便输入文本试了试,算出来和注册机的结果是一样的,那就问题不大。

 

继续F8步过,发现获取密码长度的地方以及一些call,先不急着看call,直接走下去。

 

走到这里,来了一个大跳转。
图片描述
图片描述

 

通过观察寄存器的值,可以看见直接拼凑成了字符串注册失败。

 

没有任何其他的判断,直接 xor eax,eax,将eax置为0,就是注册失败。

 

因为成功在失败的后面,所以eax为1的时候,就显示注册成功。

 

走到取字符的地方,看见从其他地方跳转到这里,正巧 mov eax,1,也就是说那条路才是成功的道路,而我们之前跳转的地方,是没有任何回头的死路。

 

图片描述

 

不用调试,直接往上查看反汇编。

 

图片描述

 

可见好几个到同一目的地的跳转,然后跳转以后,都没有包含 mov eax,1的指令,可见,都是失败的。也就是这些jne,一个都不能跳。

 

翻译一下,也就是好几个并列条件

1
2
3
4
5
6
7
8
if(a&&b&&c&&d&&e)
{
    success;
}
else
{
    fail;
}

失败重来。这次注意看edx是如何来的

 

图片描述

 

edx = 0x2f65824,edi = 0x2f65820,感觉应该是两个地址,看下内存

 

图片描述

 

猜测这一段也是类似于取文本长度一样,从字节数组结尾的地方减去字节数组开始的地方,也就是字节数组的长度。

 

刚才输入的序列号是123456。我们直接Ctrl+F2重新启动,多输入几个字符,但是前面还是123456。

 

同样观察一下地址上的数据,可见前面部分保持一致。输入变长,这一段也变长,但是还不到0x14.我们继续加长。

 

当长度来到27位的时候,终于能继续正常往下走了。也就是限定密码长度为27.

 

正确跳转以后,就来到了这里。

 

图片描述

 

这一段的手法和第一次加密用户名类似啊,开辟0x24个字节的空间。

 

看循环里面,有用到之前加密过的用户名,也就是再次加密。

 

观察里面的数据。

1
mov eax,dword ptr ds:[esi+edi]

这里面的数据,和我们之前判断序列号长度看见的数据很相似。于是再次重新运行

1
cmp edx,14

在判断长度的地方断下,并进去查看esi地址上的数据(只需要看0x14个字节即可),可见,和循环里面的数据是一样的。

 

当然,其实也不用看,因为中间没有修改edi的值,而esi也是从0开始,随着循环增大而变化。

 

这一段,还是可以翻译成C++代码的形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void imul(int& eax, int &ecx, int &edx)
{
    long long tmp = eax;
    tmp = tmp*ecx;
    eax = tmp & 0xffffffff;
    edx = tmp >> 32;
}
void encrystUsernameSecond(unsigned char data[0x14])
{
    unsigned char total[0x92];
    memcpy(total + 0x88, data, 0x14);
    memset(total + 0x30, 0, 0x24);
    memset(total + 0x2c, 0, 0x14);
    int esi = 0;
    int ecx = 0;
    int eax = 0;
    int edx = 0x14;
    int i24 = 0x2995c04;
    unsigned char ediData[0x14] = { 0xD7,0x6D,0xF8,0xE7,0xAE,0xFC,0xF7,0x4D,0x76,0xDF,0x8E,0x7A,0xEF,0xCF,0x74,0xD7,0x6D,0xF8,0xE7,0xAE };//这个只和输入的序列号有关系。
    int* edi = (int*)&ediData;
    int unknowndata = 0x3156c04;
    while (esi<0x14)
    {
        ecx = *(int*)&data[esi];
        eax = 0x66666667;
        imul(eax, ecx, edx);
        edx >>= 2; //sar eax,2
        ecx = edx;
        unsigned int tmp = ecx;
        tmp >>= 31;
        //shr ecx,0x1f
        //__asm {
        //    shr tmp,0x1F
        //}
        ecx = tmp;
        ecx += edx;
        edx = 0x14;     //合并起来,固定为0x14
        printf("ecx = %X\n", ecx);
        memcpy(total + 0x2c + esi, (unsigned char*)&ecx, 4);
        if (esi<edx)
        {
            eax = *(int*)&ediData[esi];
            memcpy(total + 0x40 + esi, (unsigned char*)&eax, 4);
            esi += 4;
        }
        cout << endl;
    }
}

虽然中间也有一些不知道是什么的数据,但是观察运行后,发现写死就能正常运行,也就不管了。完全的翻译了一下,虽然后来发现,复制的数据完全没用到。
这个imux还挺麻烦的,直接写成一个函数了。

 

加密完成,来到了决胜的地方。

 

要同时满足这些条件,才能注册成功。

 

还是习惯性的翻译成高级语言吧。

 

把第二段加密后的total作为esp的开端,当作参数传进去。

 

first:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void test3(unsigned char data[])
{
    int ecx = 0;
    int eax = 0;
    int edx = 0;
    int esi = 0;
    ecx = *(int*)&data[0x2c];
    eax = *(int*)&data[0x50];
    edx = eax + ecx;
    ecx = *(int*)&data[0x48];
    if (edx == ecx)
    {
        edx = *(int*)&data[0x30];
        edx += ecx;
        eax += eax;
        if (edx == eax)
        {
            ecx = *(int*)&data[0x4c];
            eax = *(int*)&data[0x34];
            edx = *(int*)&data[0x40];
            esi = ecx + eax;
            if (esi == edx)
            {
                esi = *(int*)&data[0x38];
                esi += edx;
                ecx += ecx;
                if (esi == ecx)
                {
                    edx = *(int*)&data[0x3c];
                    ecx = *(int*)&data[0x44];
                    ecx += edx;
                    edx = eax + eax * 2;
                    if (ecx == edx)
                    {
                        cout << "success\n";
                        return;
                    }
                }
            }
        }
    }
    cout << "fail1\n";
}

second:精简一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void test4(unsigned char data[])
{
    int arr1[5];
    int arr2[5];
    memcpy((unsigned char*)&arr1, data + 0x2c, 0x14);
    memcpy((unsigned char*)&arr2, data + 0x40, 0x14);
    int ecx = 0;
    int eax = 0;
    int edx = 0;
    int esi = 0;
    ecx = arr1[0];
    eax = arr2[4];
    edx = ecx + eax;
    ecx = arr2[2];
    if (edx == ecx)//(a[0] + b[4] == b[2])
    {
        edx = arr1[1];
        edx += ecx;
        eax += eax;
        if (edx == eax)//(a[1] + b[2] == 2*b[4])
        {
            ecx = arr2[3];
            eax = arr1[2];
            edx = arr2[0];
            esi = ecx + eax;
            if (esi == edx)//(a[2] + b[3] == b[0])
            {
                esi = arr1[3];
                esi += edx;
                ecx += ecx;
                if (esi == ecx)//(a[3] + b[0] == 2*b[3])
                {
                    edx = arr1[4];
                    ecx = arr2[1];
                    ecx += edx;
                    edx = eax + eax * 2;
                    if (ecx == edx) // (a[4] + b[1] == 3*a[2])
                    {
                        cout << "success\n";
                        return;
                    }
                }
            }
        }
    }
    cout<<"fail\n";
}

last:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void test5(unsigned char data[])
{
    int a[5];
    int b[5];
    memcpy((unsigned char*)&a, data + 0x2c, 0x14);
    memcpy((unsigned char*)&b, data + 0x40, 0x14);
    //if (((a[0] + b[4])== b[2]) && ((a[1] + b[2]) == (2 * b[4])) && ((a[2] + b[3]) == b[0]) && ((a[3] + b[0]) == (2 * b[3])) && ((a[4] + b[1]) == (3 * a[2])))
    if((2*a[2]+a[3]==b[0])&&(3*a[2]-a[4]==b[1])&&(2*a[0]+a[1]==b[2])&&(a[2]+a[3]==b[3])&&(a[0]+a[1]==b[4]))
    {
        cout << "success\n";
    }
    else
    {
        cout << "fail\n";
    }
}

精简再精简,最终可以通过二次加密的用户名求出正确的字节数组。

 

都到这里了,显然,未知的那一部分就是和密码有关的东西。

 

到这里,在二次加密的末尾,添加上这一段,就可以求出正确的加密后的序列号数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (true)
    {
        //if ((2 * a[2] + a[3] == b[0]) && (3 * a[2] - a[4] == b[1]) && (2 * a[0] + a[1] == b[2]) && (a[2] + a[3] == b[3]) && (a[0] + a[1] == b[4]))
        int a[5];
        int b[5];
        memcpy((unsigned char*)&a, total + 0x2c, 0x14);
        memcpy((unsigned char*)&b, total + 0x40, 0x14);
        b[0] = 2 * a[2] + a[3];
        b[1] = 3 * a[2] - a[4];
        b[2] = 2 * a[0] + a[1];
        b[3] = a[2] + a[3];
        b[4] = a[0] + a[1];
        memcpy(total + 0x2c, (unsigned char*)&a, 0x14);
        memcpy(total + 0x40, (unsigned char*)&b, 0x14);
    }
    cout << endl;
    cout << "after 0x40" << endl;
    for (int i = 0; i<0x14; i++)
    {
        printf("%02X,", total[i + 0x40]);
    }

实践是检测真理的唯一标准。回到调试器里面,在用户名加密完成以后,用这一段替换esp+0x40后面的0x14字节,运行结束,注册成功。

 

到这里,离成功就很近了,接下来就需要分析序列号是如何加密的就好了。

 

首先,要找到在哪里用到了序列号。

 

可以直接在序列号那里使用硬件访问断点。
图片描述

 

不过没必要一上来就断下,因为在取序列号长度的地方会反复断下。

 

等运行到图示的位置,再使用硬件断点即可。

 

图片描述

 

直接F9运行,来到了不知道是哪里。

 

图片描述

 

通过观察,分析,知道这一段用来拷贝序列号。那么,拷贝后的内存地址就是我们需要关注的地方。

 

不断的F8或者直接运行到函数结束,不断的出CALL,终于回到了取长度后第一个CALL的下一句。所以,这个call就是用来拷贝序列号的。

 

留给我们的call也不多了,就这两个。

 

图片描述

 

需要注意的是,这个程序函数调用采取C的调用方式,堆栈由调用方进行平衡。

 

对堆栈的相关内容,可以参见MASM32汇编中关于栈的总结 - 念秋 - 博客园 (cnblogs.com)

 

虽然这里面没有关于清理堆栈的内容。

 

对这两个函数分别查看参数和运行结果。

 

第一个不太明显,但是第2个,从结果看来,貌似就是清理了一下内存。也就是清除了之前复制的序列号。清除,那就是善后工作了。

测试一下,我们直接把这个call改成nop。等第二次加密用户名的时候,可以看见加密后的序列号还是和之前一样。所以可以知道这一个call和加密序列号无关,直接忽略就好了。

最后,也就剩这一个call可以加密序列号了。

 

这里面call很多,然后一堆循环。

 

进入这个call,也没有什么好的思路,一开始,追踪寄存器的值,反而搞得头昏脑涨。

 

不知从何处下手,那就先F8一步一步看看。

 

图片描述

 

走到这里,发现ebx指向复制后的序列号。然后对eax和ecx的操作都是对称的。

 

从ebx那个字节数组里面取出一个字节保存到(unsigned char*)(esp+0x18)[eax]处。

 

有比较,又有跳转,可以猜测是不是进入了循环。

 

这个复制完值,往下走,紧接着两个很远的跳转,暂时不清楚是什么。

 

图片描述

 

走到这里,发现参数来自序列号。

1
2
3
4
5
6
7
8
9
10
int littleProc(unsigned char ch)
{
    unsigned char data[0x100] = {0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x28,0x00,0x28,0x00,0x28,0x00,0x28,0x00,0x28,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0x48,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x84,0x00,0x84,0x00,0x84,0x00,0x84,0x00,0x84,0x00,0x84,0x00,0x84,0x00,0x84,0x00,0x84,0x00,0x84,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x81,0x00,0x81,0x00,0x81,0x00,0x81,0x00,0x81,0x00,0x81,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x82,0x00,0x82,0x00,0x82,0x00,0x82,0x00,0x82,0x00,0x82,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x20,0x00};
    int eax = ch;
    short*p = (short*)&data;
    unsigned short temp = (unsigned short)*((unsigned char*)p + eax*2);
    eax = temp;
    eax&=0x107;
    return eax;
}

这个小的函数调用,不知道有什么用。翻译了一下,最后还是没用上。

 

一直F8走吧,走吧。又走到了上面那个对称的地方。

 

多走几次,发现esp+0x18上面变成了从序列号拷贝的4个字节。

 

图片描述

 

此时,来到了 cmp eax,4 而eax=4。就不会再进行刚才的循环了。

 

走下去,发现又是一个循环。

 

图片描述

 

也就是每次一个字符,进入call进行变化。也就是读取一个字节,修改,然后写回去。

 

这个进去以后,一堆常量还是什么的。

 

图片描述

 

在这个call之前,完全不知道是在干什么。

 

图片描述

 

通过堆栈,可以发现,这是一个3个参数的函数。其中两个参数是base64字符串和要转换的字符,另一个不知道是什么。

 

进去看看,不长,而且末尾非常类似。那就可能是根据运算结果从数组里面选择一个值回去。

 

尝试翻译,但是感觉有点费劲,而且翻译出来也没用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
void change(char base64[64],char ch,char flag)
{
    int eax = flag;
    int edx = (int)&base64;
    unsigned int ebx = 0;
    ebx = ch;
    int edi = 0;
    unsigned int ecx;
    if(edx&&3)
    {
        eax-=4;
        edi = ebx;
        ebx<<=8;
        ebx+=edi;
        edi = ebx;
        ebx<<=16;
        ebx+=edi;
        edx = 0;
        {
            ecx = *(int*)&base64[edx];
            ecx^=ebx;
            edi = 0x7EFEFEFF;
            edi+=ecx;
            ecx^=0xffffffff;
            ecx^=edi;
            edx+=4;
            while(ecx&0x81010100)
            {
                loop:
                eax-=4;
                ecx = *(int*)&base64[edx];
                ecx^=ebx;
                edi = 0x7EFEFEFF;
                edi+=ecx;
                ecx^=0xffffffff;
                ecx^=edi;
                edx+=4;
            }
            ecx = *(int*)&base64[edx-4];
            if((char)ecx^(char)ebx)
            {
                eax = *(int*)&base64[edx-4];
            }
            else if((char)(ecx>>8)^(char)ebx)
            {
                eax = *(int*)&base64[edx-3];
            }
            else if((char)(ecx>>16)^(char)ebx)
            {
                eax = *(int*)&base64[edx-2];
            }
            else if((char)(ecx>>24)^(char)ebx)
            {
            }
            else
            {
                goto loop;
            }
        }
    }
}

虽然这个汇编代码是从高级语言编译出来的,但是再从汇编转为C++,就感觉翻译不出来啊。

 

不知道写得对不对,翻译出来都不想验证一下了。直接返回,发现转换的字符重新写回去了。

 

循环运行吧,循环结束,走到了这里。

 

图片描述

 

发现刚才的4个字节已经全部改变了。

 

图片描述

 

继续往下走。

 

图片描述

 

发现这一段,主要改变了3个字节的数据。

 

图片描述

 

非常的眼熟啊,之前已经看了好几遍了。

1
0xD7,0x6D,0xF8,0xE7,0xAE,0xFC,0xF7,0x4D,0x76,0xDF,0x8E,0x7A,0xEF,0xCF,0x74,0xD7,0x6D,0xF8,0xE7,0xAE

正是第二次用户名加密中拷贝的加密后的序列号。

 

既然出现再这里,那就代表没有别的地方来加密序列号了。至于这个数据写到哪里,我们不用管了。

 

我们只需要关心是不是每次运行到这里,出现的数据都是最后的加密结果。

 

直接在xor edi,edi的地方下个断点,直接F9运行。可以看见加密后的序列号不断的出现,每次都是转换为3个字节。
前有64个字符串的字符替换,后有4字节到3字节的压缩。我们可以猜测是否为base64解码的过程。

 

总结一下序列号的加密,就是4个字节,先转换一下,再经过上面的各种位操作,就得到了最终结果。

 

这一段的移位操作虽然有点长,但是输入输出都是明显的,也就很好翻译,翻译的同时注意看二进制串,希望就是期待的编码规则,这样可以省点力气。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include<iostream>
#include<string.h>
#include<map>
#include<time.h>
using namespace std;
void encode(unsigned char data[4])
{
    unsigned int ecx = *(int*)&data;
    unsigned char al = data[0];
    al+=al;
    unsigned char dl = data[1];
    dl>>=4;
    dl&=3;
    al+=al;
    dl+=al;
    al = data[2];
    unsigned char result[4];
    memset(result,0,4);
    result[0] = dl;
    dl = al;
    dl>>=2;
    unsigned char cl = data[1];
    al<<=6;
    al+=data[3];
    dl&=0xf;
    cl<<=4;
    dl^=cl;
    result[1] = dl;
    result[2] = al;
    for(int i=0;i<3;i++)
    {
        printf("%2X\n",result[i]);
        char tmp[16];
        itoa(result[i],tmp,2);
        printf("%s\n",tmp);
    }
}
int main()
{
    unsigned char data[4] = {0x35,0x36,0x37,0x38};
    encode(data);
    cout<<"data\n";
    for(int i=0;i<4;i++)
    {
        char tmp[16];
        itoa(data[i],tmp,2);
        printf("%s\n",tmp);
        printf("%X\n",data[i]);
    }
}

运行结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
D7
11010111
 
6D
01101101
 
F8
11111000
 
data
 
110101
35
 
110110
36
 
110111
37
 
111000
38

成功的把0x35 0x36 0x37 0x38转换为了 0xD7 0x6D 0xF8

 

通过观察二进制,可以发现。输入的4字节和输出的3字节恰好满足base64编码的中间规则。38bit转换为46bit,每个bit高2位取0.
再不断的带入注册机的内存结果到程序,发现转换没问题,说明程序翻译得没错。
且都满足base64编解码的中间过程。

 

也就是上述的运算结果可以互相转换。

 

我们可以把之前自己求出来的加密后的字节数组带进去,反推出经过第一次字符转换后的字节数组。

 

也就是

 

字符串“1234” 字符转换为 "5678" ,再编码成了 0xD7 0x6D 0xF8

 

现在我们已经有了D7 6D F8 ,可以推出 "5678",接下来只需要观察字节转换的函数,看下如何变回"1234"即可。

 

不过也可以不用看了,这里已经确定是base64编码的一部分。那么之前那个函数大概率就是在base64字符串里面找下标的。
直接写个完整的base64编码,把我们求出来的加密后序列号带进去看看。

 

base64转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include<iostream>
#include<string.h>
#include<map>
#include<time.h>
using namespace std;
char base64str[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@%=";
map<unsigned char,unsigned char> m;
void encode(unsigned char str[],int len)
{
    string s = "";
    unsigned char tmp[3];
    unsigned char t[4];
    for(int i=0;i<=len;i+=3)
    {
        memcpy(tmp,str+i,3);
        char a[10];
        memset(t,0,4);
        itoa(tmp[0],a,2);
        itoa(tmp[1],a,2);
        itoa(tmp[2],a,2);
        t[0] = tmp[0]>>2;
        itoa(t[0],a,2);
 
        unsigned char f1 = tmp[0]<<6;
        f1>>=2;
        t[1] = (f1)^(tmp[1]>>4);
        itoa(t[1],a,2);
 
        f1 = tmp[1]<<4;
        f1>>=2;
        t[2] = (f1)^(tmp[2]>>6);
        itoa(t[2],a,2);
 
        f1 = tmp[2]<<2;
        f1 >>=2;
        t[3] = f1;
        itoa(t[3],a,2);
 
        t[0] = base64str[t[0]];
        t[1] = base64str[t[1]];
        t[2] = base64str[t[2]];
        t[3] = base64str[t[3]];
        for(int i=0;i<4;i++)
        {
            s+=t[i];
        }
    }
//    cout<<s.length()<<endl;
    for(int i=0;i<27;i++)
    {
        printf("%c",s[i]);
    }
    cout<<endl;
}
 
int main()
{
    for(int i=0;i<64;i++)
    {
        m[base64str[i]]=i;
    }
    unsigned char data[21] = {0xD7,0x6D,0xF8,0xE7,0xAE,0xFC,0xF7,0x4D,0x76,0xDF,0x8E,0x7A,0xEF,0xCF,0x74,0xD7,0x6D,0xF8,0xE7,0xAE,0x0};
    encode(data,21);
    return 0;
}

123456789012345678901234564

 

程序从"123456789012345678901234567"转换的字节数组,经过我们的逆转,基本已经接近了我们的输入,那么几乎可以确定可以逆转回去了。也可以确定对序列号的操作就是base64解码的操作,因为我们是从字节数组编码到字符串,而注册机是从明文字符串解码为字节数组。

 

经过这次转换,程序对序列号的加密已经拿捏了。
可以推断:
通过正确的加密的序列号字节数组,就能转到原本的序列号上。

 

我们将用户名为"helloworld"的密码数组求出来。

1
0x54,0x04,0x9D,0x05,0xCB,0x0A,0x1A,0x13,0x43,0x31,0x1C,0x0F,0x5E,0xFF,0x39,0xFF,0xAD,0x4C,0x35,0x04

再代入转换一下:

1
VASdBcsKGhNDMRwPXv85%61MNQQ

图片描述

 

终于成功了。此时,再回头看这个加密函数。一开始不懂的地方也渐渐豁然开朗。

 

图片描述

 

平时我们用的base64字符串,涉及到需要补位的时候,都是用的'=',而这个程序,应该是用的"*”吧。

 

所以每次都会判断输入的字符是不是0x2A,来判断长度之类的。完善下上面的程序,我们就设定第21个字符为 0x2A 即可

 

再看一下,发现ecx的值随着循环的进行,也慢慢变大,也就是ecx代表大循环。eax代表小循环。

1
2
3
4
5
6
7
for(ecx = 0;ecx<0x1B;)
{
    for(eax = 0;eax<4;eax++)
    {
        change()
    }
}

大概类似于这样吧。
虽然求出来了结果,但是仍有一些地方不太理解。不过本就是逆向关键算法,也就没必要什么都弄懂。
本来就调试得头昏眼花了,就不想再多看了。
大家有兴趣可以带着整体思路来看一下程序的完整流程。

 

总的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#include<iostream>
#include<string.h>
#include<map>
#include<time.h>
using namespace std;
char base64str[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@%*";
map<unsigned char,unsigned char> m;
/*
2c 0x14 保存对用户名二次加密的数据
40 0x14 保存对序列号加密的数据
88 0x14 存储第一次用户名加密的数据
9C 用户名
1A0 密码
*/
void encode(unsigned char str[],int len)
{
    string s = "";
    unsigned char tmp[3];
    unsigned char t[4];
    for(int i=0;i<=len;i+=3)
    {
        memcpy(tmp,str+i,3);
        char a[10];
        memset(t,0,4);
        itoa(tmp[0],a,2);
        itoa(tmp[1],a,2);
        itoa(tmp[2],a,2);
        t[0] = tmp[0]>>2;
        itoa(t[0],a,2);
 
        unsigned char f1 = tmp[0]<<6;
        f1>>=2;
        t[1] = (f1)^(tmp[1]>>4);
        itoa(t[1],a,2);
 
        f1 = tmp[1]<<4;
        f1>>=2;
        t[2] = (f1)^(tmp[2]>>6);
        itoa(t[2],a,2);
 
        f1 = tmp[2]<<2;
        f1 >>=2;
        t[3] = f1;
        itoa(t[3],a,2);
 
        t[0] = base64str[t[0]];
        t[1] = base64str[t[1]];
        t[2] = base64str[t[2]];
        t[3] = base64str[t[3]];
        for(int i=0;i<4;i++)
        {
            s+=t[i];
        }
    }
    cout<<"序列号"<<endl;
    for(int i=0;i<27;i++)
    {
        printf("%c",s[i]);
    }
    cout<<endl;
}
void imul(int& eax, int &ecx, int &edx)
{
    long long tmp = eax;
    tmp = tmp*ecx;
    eax = tmp & 0xffffffff;
    edx = tmp >> 32;
}
void test5(unsigned char data[])
{
    int a[5];
    int b[5];
    memcpy((unsigned char*)&a, data + 0x2c, 0x14);
    memcpy((unsigned char*)&b, data + 0x40, 0x14);
    //if (((a[0] + b[4])== b[2]) && ((a[1] + b[2]) == (2 * b[4])) && ((a[2] + b[3]) == b[0]) && ((a[3] + b[0]) == (2 * b[3])) && ((a[4] + b[1]) == (3 * a[2])))
    if((2*a[2]+a[3]==b[0])&&(3*a[2]-a[4]==b[1])&&(2*a[0]+a[1]==b[2])&&(a[2]+a[3]==b[3])&&(a[0]+a[1]==b[4]))
    {
        cout << "success\n";
    }
    else
    {
        cout << "fail\n";
    }
}
 
void encyptUsernameSecond(unsigned char data[0x14])
{
    unsigned char total[0x92];
    memcpy(total + 0x88, data, 0x14);
    memset(total + 0x30, 0, 0x24);
    memset(total + 0x2c, 0, 0x14);
    int esi = 0;
    int ecx = 0;
    int eax = 0;
    int edx = 0x14;
    int i24 = 0x2995c04;
    unsigned char ediData[0x14] = { 0xD7,0x6D,0xF8,0xE7,0xAE,0xFC,0xF7,0x4D,0x76,0xDF,0x8E,0x7A,0xEF,0xCF,0x74,0xD7,0x6D,0xF8,0xE7,0xAE };//这个只要只和输入的序列号有关系。
    memset(ediData,0,0x14);
    int* edi = (int*)&ediData;
    int unknowndata = 0x3156c04;
    while (esi<0x14)
    {
        ecx = *(int*)&data[esi];
        eax = 0x66666667;
        imul(eax, ecx, edx);
        edx >>= 2; //sar eax,2
        ecx = edx;
        unsigned int tmp = ecx;
        tmp >>= 31;        //基础要打好,总是一知半解的,用的时候还要重新查。移位很多,涉及到符号之类的。
        //shr ecx,0x1f
        //__asm {
        //    shr tmp,0x1F
        //}
        ecx = tmp;
        ecx += edx;
        edx = 0x14;     //合并起来,固定为0x14
        memcpy(total + 0x2c + esi, (unsigned char*)&ecx, 4);
        if (esi<edx)
        {
            eax = *(int*)&ediData[esi];
            memcpy(total + 0x40 + esi, (unsigned char*)&eax, 4);
            esi += 4;
        }
    }
    if (true)
    {
        //if ((2 * a[2] + a[3] == b[0]) && (3 * a[2] - a[4] == b[1]) && (2 * a[0] + a[1] == b[2]) && (a[2] + a[3] == b[3]) && (a[0] + a[1] == b[4]))
        int a[5];
        int b[5];
        memcpy((unsigned char*)&a, total + 0x2c, 0x14);
        memcpy((unsigned char*)&b, total + 0x40, 0x14);
        b[0] = 2 * a[2] + a[3];
        b[1] = 3 * a[2] - a[4];
        b[2] = 2 * a[0] + a[1];
        b[3] = a[2] + a[3];
        b[4] = a[0] + a[1];
        memcpy(total + 0x2c, (unsigned char*)&a, 0x14);
        memcpy(total + 0x40, (unsigned char*)&b, 0x14);
    }
    cout << "after 0x40" << endl;
    for (int i = 0; i<0x14; i++)
    {
        printf("0x%02X,", total[i + 0x40]);
    }
    cout<<endl;
    total[0x54] = 0x2A;
    encode(total+0x40,21);
//    test5(total);
}
void encyptUsernameFirst(string username)
{
    unsigned char data[0x14];
    memset(data, 0, 0x14);
    int eax = 0;
    int ebp = 0x1339E7E;
    int esp = (int)&data - 0x88;
    int edx = (int)&data;
    int ecx = 0;
    int edi = username.length();
    int* esi = 0;
    ebp = ebp - edx;
    while (ecx<0x10)
    {
        eax = ecx;
        edx = eax%edi;
        esi = (int*)&data[ecx];
        ecx++;
        eax = username[edx];
        edx = (int)esi + ebp;
        eax = eax*edx;
        eax = eax*edi;
        (*esi) = (*esi) + eax;
    }
    encyptUsernameSecond(data);
}
int main()
{
    for(int i=0;i<64;i++)
    {
        m[base64str[i]]=i;
    }
    cout << "请输入username" << endl;
    string str = "helloworld";
    cout<<"username = "<<str<<endl;
    encyptUsernameFirst(str);
    system("pause");
 
}

[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2022-4-24 21:45 被kanxue编辑 ,原因: 上传附件
上传的附件:
收藏
点赞5
打赏
分享
最新回复 (3)
雪    币: 4016
活跃值: (5833)
能力值: ( LV7,RANK:102 )
在线值:
发帖
回帖
粉丝
fjqisba 2022-4-23 19:31
2
0
应该是想要考察基础算法,BASE64,AES之类的,不了解这些算法,就很吃亏
雪    币: 1519
活跃值: (1982)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
Chords 2022-4-25 20:27
3
0
今年初赛决赛都是虚拟机
雪    币: 3232
活跃值: (1947)
能力值: ( LV4,RANK:55 )
在线值:
发帖
回帖
粉丝
yyjeqhc 1 2022-4-25 20:58
4
0
没看,就是偶然发现做一做,基础还差远了。
游客
登录 | 注册 方可回帖
返回