1、申 请 I D:DeathMemory
2、个人邮箱:DeathMemory@163.com
3、原创技术文章:2016游戏安全技术竞赛题PC第一题逆向分析详解
最近看到腾讯的 2016游戏安全技术竞赛,咱不是大学生只能以凑热闹的形式参与一下。
遂下载第一题进行分析 由于要求写注册机,所以这里不考虑暴破 先看一下注册机成功结果:
正式开始分析: 从图中可以看出注册成功,失败都以 static text 控件来显示
用OD载入,搜索注册关键字符都没有找到
在 GetWindowTextW GetDlgItemTextWGetDlgItem 下断,在GetDlgItem 处断下,向上返回到程序领域就是注册算法主体了。 单步向下调试,先走一遍,最大程度的浏览一遍有用信息,这里可以发现,注册信息是在代码段拼接的
[cpp] view plain copy
- 00CA1F90 . 8D47 FA lea eax, dword ptr [edi-6]
- 00CA1F93 . 83F8 0E cmp eax, 0E
- 00CA1F96 . 0F87 4B020000 ja 00CA21E7
此外是用户名长度判断,用户长度在 6-20 之间
接下来是一系列加密算法,用IDA载入并整理 下面是用户名的第一次加密 OD:
[cpp] view plain copy
- 00CA1FE0 > /8BC1 mov eax, ecx
- 00CA1FE2 . |99 cdq
- 00CA1FE3 . |F7FF idiv edi
- 00CA1FE5 . |8DB40C 88000000 lea esi, dword ptr [esp+ecx+88]
- 00CA1FEC . |41 inc ecx
- 00CA1FED . |0FBE8414 9C000000 movsx eax, byte ptr [esp+edx+9C]
- 00CA1FF5 . |8D142E lea edx, dword ptr [esi+ebp]
- 00CA1FF8 . |0FAFC2 imul eax, edx
- 00CA1FFB . |0FAFC7 imul eax, edi
- 00CA1FFE . |0106 add dword ptr [esi], eax
- 00CA2000 . |83F9 10 cmp ecx, 10
- 00CA2003 .^\7C DB jl short 00CA1FE0
IDA:
[cpp] view plain copy
- do // 加密用户名 ↓
- {
- tmp_i = i % namelen;
- ptrTmp = &name_encrypt[i++];
- *(_DWORD *)ptrTmp += namelen * (_DWORD)&ptrTmp[20160126 - (_DWORD)name_encrypt] * username[tmp_i];
- }
- while ( i < 16 );
接下来的两个Call是跟密码加密相关的,回头再看,先把用户名的加密跑完
到第二次用户名加密的地方 OD:
[cpp] view plain copy
- .text:00402102 mov ecx, dword ptr [esp+esi+2B4h+name_encrypt]
- .text:00402109 mov eax, 66666667h
- .text:0040210E imul ecx
- .text:00402110 sar edx, 2
- .text:00402113 mov ecx, edx
- .text:00402115 shr ecx, 1Fh
- .text:00402118 add ecx, edx
- .text:0040211A mov edx, [esp+2B4h+val_1]
- .text:0040211E sub edx, edi
- .text:00402120 mov [esp+esi+2B4h+name_encrypt_2], ecx ; 计算结果
- .text:00402124 cmp esi, edx
- .text:00402126 jb short loc_402131
- .text:00402128 call __invalid_parameter_noinfo
- .text:0040212D mov edi, [esp+2B4h+val_0]
- .text:00402131
- .text:00402131 loc_402131: ; CODE XREF: sub_401E60+2C6j
- .text:00402131 mov eax, [esi+edi]
- .text:00402134 mov [esp+esi+2B4h+pass_encrypt], eax
- .text:00402138 add esi, 4
- .text:0040213B cmp esi, 14h
- .text:0040213E jl short loc_402102
IDA:
[cpp] view plain copy
- do
- {
- v12 = *(_DWORD *)&name_encrypt[index] / 10;
- len1 = val_1 - (_DWORD)val_0_1;
- name_encrypt_2[index / 4] = v12; // 用户名第二次加密
- if ( index >= len1 )
- {
- _invalid_parameter_noinfo(v12);
- val_0_1 = val_0;
- }
- pass_encrypt[index / 4] = *(_DWORD *)((char *)val_0_1 + index);// 密码解密编码后的密码
- index += 4;
- }
- while ( (signed int)index < 20 );
再往下就是加密后的用户名和密码进行验证:
[cpp] view plain copy
- if ( pass_encrypt[4] + name_encrypt_2[0] != pass_encrypt[2]
- || pass_encrypt[2] + name_encrypt_2[1] != 2 * pass_encrypt[4]
- || pass_encrypt[3] + name_encrypt_2[2] != pass_encrypt[0]
- || pass_encrypt[0] + name_encrypt_2[3] != 2 * pass_encrypt[3]
- || name_encrypt_2[4] + pass_encrypt[1] != 3 * name_encrypt_2[2] )// 验证
以上验证如果有一条为真,则跳转到失败。
分析到这里,我们还没有分析密码的加密,这个是关键点,我们先设为 X 则就有了以下这条假设的公式: 用户名 + x = 验证 所以在已知用户名和验证关系后,我们可以推导出加密后 X 的值
[cpp] view plain copy
- void EncryptUsername(char* username) {
- BYTE name_encrypt[20] = { NULL };
- int namelen = strlen((char*)username);
- int i = 0;
- do// 第一次加密
- {
- int tmp_i = i % namelen;
- BYTE* tmp_ptr = &name_encrypt[i++];
- *(DWORD *)tmp_ptr += namelen * (DWORD)&tmp_ptr[20160126 - (DWORD)name_encrypt] * username[tmp_i];
- } while (i < 16);
- i = 0;
- BYTE name_encrypt_2[20] = { NULL };
- do// 第二次加密
- {
- int tmp = *(int*)&name_encrypt / 10;
- *(int*)&name_encrypt_2 = tmp;
- i += 4;
- } while (i < 20);
-
- /************************************************************************/
- /* pass_encrypt[4] + name_encrypt_2[0] != pass_encrypt[2]
- || pass_encrypt[2] + name_encrypt_2[1] != 2 * pass_encrypt[4]
- name_encrypt_2[0] + name_encrypt_2[1] = pass_encrypt[4]
- || pass_encrypt[3] + name_encrypt_2[2] != pass_encrypt[0]
- || pass_encrypt[0] + name_encrypt_2[3] != 2 * pass_encrypt[3]
- name_encrypt_2[2] + name_encrypt_2[3] = pass_encrypt[3]
- || name_encrypt_2[4] + pass_encrypt[1] != 3 * name_encrypt_2[2] )// 验证
- pass_encrypt[1] = 3 * name_encrypt_2[2] - name_encrypt_2[4]
- */
- /************************************************************************/
-
- DWORD * ptrdwNameEncrypt = (DWORD*)name_encrypt_2;
- DWORD ptrdwPassEncrypt[5] = { NULL };// 注意这里是 DWORD
-
- ptrdwPassEncrypt[4] = ptrdwNameEncrypt[0] + ptrdwNameEncrypt[1];
- ptrdwPassEncrypt[3] = ptrdwNameEncrypt[2] + ptrdwNameEncrypt[3];
- ptrdwPassEncrypt[1] = 3 * ptrdwNameEncrypt[2] - ptrdwNameEncrypt[4];
- ptrdwPassEncrypt[2] = ptrdwPassEncrypt[4] + ptrdwNameEncrypt[0];
- ptrdwPassEncrypt[0] = 2 * ptrdwPassEncrypt[3] - ptrdwNameEncrypt[3];
- memcpy_s(username, 20, (char*)ptrdwPassEncrypt, 20);
- }
接下来重点分析密码加密的部分
发现有两处加密 第一处:
[cpp] view plain copy
- .text:00401A72 mov al, [esp+edi+30h+index_code]
- .text:00401A76 lea ebx, [esp+30h+passByte]
- .text:00401A7A mov byte ptr [esp+30h+passByte], al
- .text:00401A7E call selectFromBaseCode
- .text:00401A83 mov [esp+edi+30h+index_code], al
- .text:00401A87 inc edi
- .text:00401A88 cmp edi, 4
- .text:00401A8B jl short loc_401A72
这里将密码以四字节为单位,逐字节传入 selectFromBaseCode 函数中返回的密码基串所在位置的索引 简单讲一下 selectFromBaseCode(自定义名称) 函数机制: 参数:BYTE (例如:=> C) 从密码基串中计算出 C 的位置索引 密码基串为: [cpp] view plain copy
- char* BASE_CODE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@%";
返回:位置索引(例如:=> 2 错误返回 -1)
第二处(关键算法):OD:
[cpp] view plain copy
- .text:00401A91 mov al, cl ; val[0]
- .text:00401A93 add al, al ; val[0] *= 2
- .text:00401A95 mov dl, ch ; val[1]
- .text:00401A97 shr dl, 4 ; val[1] >>= 4
- .text:00401A9A and dl, 3 ; val[1] &= 3
- .text:00401A9D add al, al ; val[0] *= 2
- .text:00401A9F add dl, al ; val[1] += val[0]
- .text:00401AA1 mov al, [esp+30h+index_code+2] ; val[2]
- .text:00401AA5 mov [esp+30h+encryptKey], dl ; key0 = val[1]
- .text:00401AA9 mov dl, al ; val[2]
- .text:00401AAB shr dl, 2 ; val[2] >>= 2
- .text:00401AAE mov cl, ch ; val[1]
- .text:00401AB0 shl al, 6 ; y = val[2] << 6
- .text:00401AB3 add al, [esp+30h+index_code+3] ; y += val[3]
- .text:00401AB7 and dl, 0Fh ; val[2] &= 0xF
- .text:00401ABA shl cl, 4 ; val[1] <<= 4
- .text:00401ABD xor dl, cl ; val[2] ^= val[1]
- .text:00401ABF mov [esp+30h+encryptKey+1], dl ; key1 = val[2]
- .text:00401AC3 mov [esp+30h+encryptKey+2], al ; key2 = y
HAND: (这里IDA给出的整理不容易观察我们自己手动整理)
[cpp] view plain copy
- key[0] = (val[1] >> 4) & 3 + (val[0] << 2)
- key[1] = (val[2] >> 2) & 0xF ^ (val[1] << 4)
- key[2] = val[2] << 6 + val[3]
到这里核心算法就完成了,你会发现下面还有一部分加密,经过分析后会知道下面的加密跟这里的相同,只是对密码后3位进行了一下特殊处理 我们再假设一个公式:x(加密后的密码已知) + 逆算 = 注册码 接下来就是重点了,如何将算法逆回去就可以得出注册码了
从上面看是将 val[4] => key[3], 逆算就是要将 key[3] => val[4] 正算时是将4字节转成3字节,而逆算是将3字节转成4字节,这相当于有4个未知数,而只有3条关系式,乍看上去不好逆算,这里也是我花费时间比较长的地方,解决方法主要是得开窍哈哈,下面贴上主要分析过程:
[cpp] view plain copy
- key0 = (val[1] >> 4) & 3 + (val[0] << 2)
- /**
- 因为 (val[1] >> 4) & 3 => 0000 0011b
- + (val[0] << 2) => 1111 1100b
- = key0
- 所以
- val[0] = key0 >> 2
- val[1] = (key0 & 3) << 4
- **/
- key1 = (val[2] >> 2) & 0xF ^ (val[1] << 4)
- /**
- 同理 (val[2] >> 2) & 0xF => 0000 1111b
- ^ (val[1] << 4) => 1111 0000b
- = key1
- 所以
- val[1] = key1 >> 4
- val[2] = (key1 & 0xF) << 2
- **/
- key2 = val[2] << 6 + val[3]
- /**
- 因为 val[2] << 6 => 1100 0000b
- + val[3] => val[3] < 0x40
- = key3
- 所以
- val[2] = key3 >> 6
- val[3] = key3 & 0x3F
- **/
- /********************************************************
- 因为 val[1] = (key0 & 3) << 4
- 和 val[1] = key1 >> 4
- 均为真
- 所以是或关系
- 则 val[1] = (key0 & 3) << 4 | key1 >> 4
- 同理 val[2] = (key1 & 0xF) << 2 | key3 >> 6
- **/
之后再以公式的形式展现一下现在的进度:验证用户名串(已知)+ 逆算(已知)= 注册码 算法函数:
[cpp] view plain copy
- void calcSN(char* str) {
- BYTE* curptr = (BYTE*)str;
- BYTE idxArr[5] = { NULL };
- char sn[260] = { NULL };
- for (int i = 0, j = 0; i < 20; i += 3, j += 4, curptr += 3){
- idxArr[0] = curptr[0] >> 2;
- idxArr[1] = (curptr[0] & 3) << 4 | curptr[1] >> 4;
- idxArr[2] = (curptr[1] & 0xF) << 2 | curptr[2] >> 6;
- idxArr[3] = curptr[2] & 0x3F;
-
- for (int idx = 0; idx < ((i == 18) ? 3 : 4); ++idx){// 最后3位稍做字符串上的处理
- sn[j + idx] = BASE_CODE[idxArr[idx]];
- }
- }
- strcpy_s(str, MAX_PATH, sn);
- }
完整调用:
[cpp] view plain copy
- int _tmain(int argc, _TCHAR* argv[]) {
- char username[MAX_PATH] = { NULL };
- printf("input username: ");
- scanf_s("%s", username, MAX_PATH);
- int namelen = strlen(username);
- if (namelen < 6 || namelen > 20){
- printf("username length must be between 6 and 20\n");
- }
- else {
- EncryptUsername(username);
- //EncryptPassword(NULL);
- calcSN(username);
- printf("sn: %s\n", username);
- }
- system("pause");
- return 0;
- }
到这里就结束了,在逆向过程中验证逆算的正确性可以结合OD调试结果,或直接修改内存来验证一下。
|