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

WriteUp 2年前 (2022) admin
798 0 0

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

本文为看雪论坛精华文章
看雪论坛作者ID:yyjeqhc


2016腾讯游戏安全技术竞赛PC第一题
https://gslab.qq.com/competition/firstTurn.shtml

1.结果:
2016腾讯游戏安全技术竞赛题PC第一题
结论:
纵观全局,才知道原来序列号就是把用户名两次加密后,再Base64编码为字符串。只是不同于一般常用的64个字符串罢了。
 
这里输出”helloworld”,计算出来的序列号是”VASdBcsKGhNDMRwPXv85%61MNQR”。
 
实际上,最后一个字符改成Q/R/S/T都是可以的。因为长度不是4的整数倍,使用了’*’填充。
 
事后来看,尽量写得有条不紊,井井有条。实际调试的时候,总是注意看寄存器的变化,真是非常的耗费精力。

注册机啥的,还真没接触过,毕竟没有加密,也没加壳的程序,直接爆破可轻松多了。不动手操作,只停留理论,觉得自己掌握了方法,就能一番风顺,无异于纸上谈兵,是不会有进步的。

2.过程

(1)入手点:注册机,肯定对文本有操作,所以要找到和文本相关的地方。
 
我们随便输入,点击注册,发现没有提示框之类的,只是单纯的提示注册失败。
 
使用OD一搜索,也没有相应的字符。唯一有用的,或许就是这个了。
2016腾讯游戏安全技术竞赛题PC第一题
64个字符,出现了好几次。猜想或许会使用base64编码的方式。
 
既然没有直接有用的信息,就只能从API下手了。
 
使用PE工具一看,好几个和text相关的API。
0x29304                 0x2AB           SetWindowTextA0x29270                 0xC6            DrawTextExA0x29264                 0xC5            DrawTextA0x29252                 0x2C6           TabbedTextOutA0x2923A                 0x18D           GetWindowTextLengthA0x29228                 0x18C           GetWindowTextA0x29632                 0x29F           TextOutA0x296A4                 0x258           ScaleViewportExtEx0x29690                 0x28F           SetViewportExtEx0x295CE                 0x28D           SetTextColor0x2963E                 0x122           ExtTextOutA

感觉能用的就是GetWindowTextA,下断点一测试,毫无反应。
 
至于其他没导入的API,比如GetDlgItemTextA之类的,在一一尝试后,都没断下。说明获取文本不是用的这类API。
 
考虑到这是一个很明显的MFC程序,于是百度一搜索:MFC程序获取文本控件。

MFC控件编程之按钮编辑框(
https://www.cnblogs.com/iBinary/p/9652668.html
 
这一篇可谓非常详尽了。这个注册机就用的GetDlgItem和消息循环的方式。
 
果然,在输入文本以后,点击注册,就断在了user32.GetDlgItem里面。
 
直接返回即可。
2016腾讯游戏安全技术竞赛题PC第一题
返回到获取句柄的地方,啥也没干就返回了,返回后继续看。
2016腾讯游戏安全技术竞赛题PC第一题
果然,就是这样获取文本的。(获取控件句柄,再使用消息循环)。
 
接下来,同样的方式获取序列号的文本。然后esp+0x9C是用户名文本,esp+0x1A0是序列号文本。
 
获取了文本,就开始准备注册结果的字符串。
 
分别是注册00000失败成功。等下就把结果复制到前面4个0那里,组成最终的注册结果。
2016腾讯游戏安全技术竞赛题PC第一题
紧接着就是获取用户名的长度,限定为6-20位。
 
紧随其后,就是对用户名的加密。开辟了0x14个字节的空间。
 
从esp+0x88开始,也就是用户名前面的0x14个字节。
2016腾讯游戏安全技术竞赛题PC第一题
 
这一段还是好理解的,原样的翻译了以下,虽然其中一些数据不知道来源,但是我们直接写死也是不影响结果。
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,直接走下去。
 
走到这里,来了一个大跳转。
2016腾讯游戏安全技术竞赛题PC第一题
2016腾讯游戏安全技术竞赛题PC第一题
通过观察寄存器的值,可以看见直接拼凑成了字符串注册失败。
 
没有任何其他的判断,直接 xor eax,eax,将eax置为0,就是注册失败。
 
因为成功在失败的后面,所以eax为1的时候,就显示注册成功。
 
走到取字符的地方,看见从其他地方跳转到这里,正巧 mov eax,1,也就是说那条路才是成功的道路,而我们之前跳转的地方,是没有任何回头的死路。
2016腾讯游戏安全技术竞赛题PC第一题
不用调试,直接往上查看反汇编。
2016腾讯游戏安全技术竞赛题PC第一题
可见好几个到同一目的地的跳转,然后跳转以后,都没有包含 mov eax,1的指令,可见,都是失败的。也就是这些jne,一个都不能跳。
 
翻译一下,也就是好几个并列条件。
if(a&&b&&c&&d&&e){    success;}else{    fail;}

失败重来。这次注意看edx是如何来的。
2016腾讯游戏安全技术竞赛题PC第一题
edx = 0x2f65824,edi = 0x2f65820,感觉应该是两个地址,看下内存。
2016腾讯游戏安全技术竞赛题PC第一题
猜测这一段也是类似于取文本长度一样,从字节数组结尾的地方减去字节数组开始的地方,也就是字节数组的长度。
 
刚才输入的序列号是123456。我们直接Ctrl+F2重新启动,多输入几个字符,但是前面还是123456。
 
同样观察一下地址上的数据,可见前面部分保持一致。输入变长,这一段也变长,但是还不到0x14.我们继续加长。
 
当长度来到27位的时候,终于能继续正常往下走了。也就是限定密码长度为27。
 
正确跳转以后,就来到了这里。
2016腾讯游戏安全技术竞赛题PC第一题
这一段的手法和第一次加密用户名类似啊,开辟0x24个字节的空间。
 
看循环里面,有用到之前加密过的用户名,也就是再次加密。
 
观察里面的数据。
mov eax,dword ptr ds:[esi+edi]

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

在判断长度的地方断下,并进去查看esi地址上的数据(只需要看0x14个字节即可),可见,和循环里面的数据是一样的。
 
当然,其实也不用看,因为中间没有修改edi的值,而esi也是从0开始,随着循环增大而变化。
 
这一段,还是可以翻译成C++代码的形式。
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 = %Xn", 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:
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 << "successn";                        return;                    }                }            }        }    }    cout << "fail1n";}

second:精简一下。
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 << "successn";                        return;                    }                }            }        }    }    cout<<"failn";}

last:
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 << "successn";    }    else    {        cout << "failn";    }}

精简再精简,最终可以通过二次加密的用户名求出正确的字节数组。都到这里了,显然未知的那一部分就是和密码有关的东西。
 
到这里,在二次加密的末尾,添加上这一段,就可以求出正确的加密后的序列号数组。
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字节,运行结束,注册成功。
 
到这里,离成功就很近了,接下来就需要分析序列号是如何加密的就好了。
 
首先,要找到在哪里用到了序列号。可以直接在序列号那里使用硬件访问断点。
2016腾讯游戏安全技术竞赛题PC第一题

不过没必要一上来就断下,因为在取序列号长度的地方会反复断下。
 
等运行到图示的位置,再使用硬件断点即可。
2016腾讯游戏安全技术竞赛题PC第一题
直接F9运行,来到了不知道是哪里。
2016腾讯游戏安全技术竞赛题PC第一题
通过观察,分析,知道这一段用来拷贝序列号。那么,拷贝后的内存地址就是我们需要关注的地方。
 
不断的F8或者直接运行到函数结束,不断的出CALL,终于回到了取长度后第一个CALL的下一句。所以,这个call就是用来拷贝序列号的。
 
留给我们的call也不多了,就这两个。
2016腾讯游戏安全技术竞赛题PC第一题
需要注意的是,这个程序函数调用采取C的调用方式,堆栈由调用方进行平衡。
 
对堆栈的相关内容,可以参见MASM32汇编中关于栈的总结 – 念秋 – 博客园 (cnblogs.com)(https://www.cnblogs.com/dayq/p/15994441.html
 
虽然这里面没有关于清理堆栈的内容,对这两个函数分别查看参数和运行结果。
 
第一个不太明显,但是第2个,从结果看来,貌似就是清理了一下内存。也就是清除了之前复制的序列号。清除,那就是善后工作了。

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


最后,也就剩这一个call可以加密序列号了。这里面call很多,然后一堆循环。
 
进入这个call,也没有什么好的思路,一开始,追踪寄存器的值,反而搞得头昏脑涨。
 
不知从何处下手,那就先F8一步一步看看。
2016腾讯游戏安全技术竞赛题PC第一题
走到这里,发现ebx指向复制后的序列号。然后对eax和ecx的操作都是对称的。
 
从ebx那个字节数组里面取出一个字节保存到(unsigned char*)(esp+0x18)[eax]处。
 
有比较,又有跳转,可以猜测是不是进入了循环。
 
这个复制完值,往下走,紧接着两个很远的跳转,暂时不清楚是什么。
2016腾讯游戏安全技术竞赛题PC第一题
 
走到这里,发现参数来自序列号。
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个字节。
2016腾讯游戏安全技术竞赛题PC第一题
 
此时,来到了 cmp eax,4 而eax=4。就不会再进行刚才的循环了。
 
走下去,发现又是一个循环。
2016腾讯游戏安全技术竞赛题PC第一题
 
也就是每次一个字符,进入call进行变化。也就是读取一个字节,修改,然后写回去。
 
这个进去以后,一堆常量还是什么的。
2016腾讯游戏安全技术竞赛题PC第一题
在这个call之前,完全不知道是在干什么。
2016腾讯游戏安全技术竞赛题PC第一题
通过堆栈,可以发现,这是一个3个参数的函数。其中两个参数是base64字符串和要转换的字符,另一个不知道是什么。
 
进去看看,不长,而且末尾非常类似。那就可能是根据运算结果从数组里面选择一个值回去。
 
尝试翻译,但是感觉有点费劲,而且翻译出来也没用。
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++,就感觉翻译不出来啊。
 
不知道写得对不对,翻译出来都不想验证一下了。直接返回,发现转换的字符重新写回去了。
 
循环运行吧,循环结束,走到了这里。
2016腾讯游戏安全技术竞赛题PC第一题
发现刚才的4个字节已经全部改变了。
2016腾讯游戏安全技术竞赛题PC第一题
继续往下走。
2016腾讯游戏安全技术竞赛题PC第一题
发现这一段,主要改变了3个字节的数据。
2016腾讯游戏安全技术竞赛题PC第一题
 
非常的眼熟啊,之前已经看了好几遍了。
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个字节,先转换一下,再经过上面的各种位操作,就得到了最终结果。
 
这一段的移位操作虽然有点长,但是输入输出都是明显的,也就很好翻译,翻译的同时注意看二进制串,希望就是期待的编码规则,这样可以省点力气。
#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("%2Xn",result[i]);        char tmp[16];        itoa(result[i],tmp,2);        printf("%sn",tmp);    }}int main(){    unsigned char data[4] = {0x35,0x36,0x37,0x38};    encode(data);    cout<<"datan";    for(int i=0;i<4;i++)    {        char tmp[16];        itoa(data[i],tmp,2);        printf("%sn",tmp);        printf("%Xn",data[i]);    }}

运行结果。
D711010111 6D01101101 F811111000 data 11010135 11011036 11011137 11100038

成功的把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转换:
#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”的密码数组求出来。
0x54,0x04,0x9D,0x05,0xCB,0x0A,0x1A,0x13,0x43,0x31,0x1C,0x0F,0x5E,0xFF,0x39,0xFF,0xAD,0x4C,0x35,0x04

再代入转换一下:
VASdBcsKGhNDMRwPXv85%61MNQQ

2016腾讯游戏安全技术竞赛题PC第一题
 
终于成功了。此时,再回头看这个加密函数。一开始不懂的地方也渐渐豁然开朗。
2016腾讯游戏安全技术竞赛题PC第一题
 
平时我们用的base64字符串,涉及到需要补位的时候,都是用的’=’,而这个程序,应该是用的”*”吧。
 
所以每次都会判断输入的字符是不是0x2A,来判断长度之类的。完善下上面的程序,我们就设定第21个字符为 0x2A 即可。
 
再看一下,发现ecx的值随着循环的进行,也慢慢变大,也就是ecx代表大循环。eax代表小循环。
for(ecx = 0;ecx<0x1B;){    for(eax = 0;eax<4;eax++)    {        change()    }}

大概类似于这样吧,虽然求出来了结果,但是仍有一些地方不太理解。不过本就是逆向关键算法,也就没必要什么都弄懂。

本来就调试得头昏眼花了,就不想再多看了。
大家有兴趣可以带着整体思路来看一下程序的完整流程。
 
总的代码:
#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 << "successn";    }    else    {        cout << "failn";    }} 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"); }



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


看雪ID:yyjeqhc

https://bbs.pediy.com/user-home-951761.htm

*本文由看雪论坛 yyjeqhc 原创,转载请注明来自看雪社区

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


# 往期推荐

1.Android APP漏洞之战——调试与反调试详解

2.Fuzzm: 针对WebAssembly内存错误的模糊测试

3.0rays战队2021圣诞校内招新赛题解

4.2022腾讯游戏安全初赛一题解析

5.一文读懂PE文件签名并手工验证签名有效性

6.CNVD-2018-01084 漏洞复现报告(service.cgi 远程命令执行漏洞)



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



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

球分享

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

球点赞

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

球在看



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

点击“阅读原文”,了解更多!

原文始发于微信公众号(看雪学苑):2016腾讯游戏安全技术竞赛题PC第一题

版权声明:admin 发表于 2022年6月5日 下午6:00。
转载请注明:2016腾讯游戏安全技术竞赛题PC第一题 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...