本帖最后由 DeathMemory 于 2016-5-31 10:35 编辑
最近看到腾讯的 2016游戏安全技术竞赛,咱不是大学生只能以凑热闹的形式参与一下。
遂下载第一题进行分析
由于要求写注册机,所以这里不考虑暴破
先看一下注册机成功结果:
正式开始分析:
从图中可以看出注册成功,失败都以 static text 控件来显示
用OD载入,搜索注册关键字符都没有找到
在 GetWindowTextW GetDlgItemTextWGetDlgItem 下断,在GetDlgItem 处断下,向上返回到程序领域就是注册算法主体了。
单步向下调试,先走一遍,最大程度的浏览一遍有用信息,这里可以发现,注册信息是在代码段拼接的
[Asm] 纯文本查看 复制代码 00CA1F90 . 8D47 FA lea eax, dword ptr [edi-6]
00CA1F93 . 83F8 0E cmp eax, 0E
00CA1F96 . 0F87 4B020000 ja 00CA21E7
此外是用户名长度判断,用户长度在 6-20 之间
接下来是一系列加密算法,用IDA载入并整理
下面是用户名的第一次加密
OD:
[Asm] 纯文本查看 复制代码 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:
[C++] 纯文本查看 复制代码 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:
[Asm] 纯文本查看 复制代码 .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:
[C++] 纯文本查看 复制代码 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 );
再往下就是加密后的用户名和密码进行验证:
[C++] 纯文本查看 复制代码 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 的值
[C++] 纯文本查看 复制代码 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[i] / 10;
*(int*)&name_encrypt_2[i] = 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);
}
接下来重点分析密码加密的部分
发现有两处加密
第一处:
[Asm] 纯文本查看 复制代码 .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 的位置索引
密码基串为:
[C++] 纯文本查看 复制代码 char* BASE_CODE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@%";
返回:位置索引(例如:=> 2 错误返回 -1)
第二处(关键算法):
OD:
[Asm] 纯文本查看 复制代码 .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给出的整理不容易观察我们自己手动整理)
[C++] 纯文本查看 复制代码 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条关系式,乍看上去不好逆算,这里也是我花费时间比较长的地方,解决方法主要是得开窍哈哈,下面贴上主要分析过程:
[C++] 纯文本查看 复制代码 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
**/
之后再以公式的形式展现一下现在的进度:验证用户名串(已知)+ 逆算(已知)= 注册码
算法函数:
[C++] 纯文本查看 复制代码 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);
}
完整调用:
[C++] 纯文本查看 复制代码 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调试结果,或直接修改内存来验证一下。
附件有点大,直接发个链接吧,下载地址:http://download.csdn.net/download/deathmemory/9474750 |