2016游戏安全技术竞赛题PC第一题逆向分析详解
本帖最后由 DeathMemory 于 2016-5-31 10:35 编辑最近看到腾讯的 2016游戏安全技术竞赛,咱不是大学生只能以凑热闹的形式参与一下。
遂下载第一题进行分析
由于要求写注册机,所以这里不考虑暴破
先看一下注册机成功结果:
正式开始分析:
从图中可以看出注册成功,失败都以 static text 控件来显示
用OD载入,搜索注册关键字符都没有找到
在 GetWindowTextW GetDlgItemTextWGetDlgItem 下断,在GetDlgItem 处断下,向上返回到程序领域就是注册算法主体了。
单步向下调试,先走一遍,最大程度的浏览一遍有用信息,这里可以发现,注册信息是在代码段拼接的
00CA1F90 .8D47 FA lea eax, dword ptr
00CA1F93 .83F8 0E cmp eax, 0E
00CA1F96 .0F87 4B020000 ja 00CA21E7
此外是用户名长度判断,用户长度在 6-20 之间
接下来是一系列加密算法,用IDA载入并整理
下面是用户名的第一次加密
OD:
00CA1FE0 > /8BC1 mov eax, ecx
00CA1FE2 . |99 cdq
00CA1FE3 . |F7FF idiv edi
00CA1FE5 . |8DB40C 88000000 lea esi, dword ptr
00CA1FEC . |41 inc ecx
00CA1FED . |0FBE8414 9C000000 movsx eax, byte ptr
00CA1FF5 . |8D142E lea edx, dword ptr
00CA1FF8 . |0FAFC2 imul eax, edx
00CA1FFB . |0FAFC7 imul eax, edi
00CA1FFE . |0106 add dword ptr , eax
00CA2000 . |83F9 10 cmp ecx, 10
00CA2003 .^\7C DB jl short 00CA1FE0
IDA:
do // 加密用户名 ↓
{
tmp_i = i % namelen;
ptrTmp = &name_encrypt;
*(_DWORD *)ptrTmp += namelen * (_DWORD)&ptrTmp * username;
}
while ( i < 16 );
接下来的两个Call是跟密码加密相关的,回头再看,先把用户名的加密跑完
到第二次用户名加密的地方
OD:
.text:00402102 mov ecx, dword ptr
.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,
.text:0040211E sub edx, edi
.text:00402120 mov , ecx ; 计算结果
.text:00402124 cmp esi, edx
.text:00402126 jb short loc_402131
.text:00402128 call __invalid_parameter_noinfo
.text:0040212D mov edi,
.text:00402131
.text:00402131 loc_402131: ; CODE XREF: sub_401E60+2C6j
.text:00402131 mov eax,
.text:00402134 mov , eax
.text:00402138 add esi, 4
.text:0040213B cmp esi, 14h
.text:0040213E jl short loc_402102
IDA:
do
{
v12 = *(_DWORD *)&name_encrypt / 10;
len1 = val_1 - (_DWORD)val_0_1;
name_encrypt_2 = v12; // 用户名第二次加密
if ( index >= len1 )
{
_invalid_parameter_noinfo(v12);
val_0_1 = val_0;
}
pass_encrypt = *(_DWORD *)((char *)val_0_1 + index);// 密码解密编码后的密码
index += 4;
}
while ( (signed int)index < 20 );
再往下就是加密后的用户名和密码进行验证:
if ( pass_encrypt + name_encrypt_2 != pass_encrypt
|| pass_encrypt + name_encrypt_2 != 2 * pass_encrypt
|| pass_encrypt + name_encrypt_2 != pass_encrypt
|| pass_encrypt + name_encrypt_2 != 2 * pass_encrypt
|| name_encrypt_2 + pass_encrypt != 3 * name_encrypt_2 )// 验证
以上验证如果有一条为真,则跳转到失败。
分析到这里,我们还没有分析密码的加密,这个是关键点,我们先设为 X
则就有了以下这条假设的公式: 用户名 + x = 验证
所以在已知用户名和验证关系后,我们可以推导出加密后 X 的值
void EncryptUsername(char* username) {
BYTE name_encrypt = { NULL };
int namelen = strlen((char*)username);
int i = 0;
do// 第一次加密
{
int tmp_i = i % namelen;
BYTE* tmp_ptr = &name_encrypt;
*(DWORD *)tmp_ptr += namelen * (DWORD)&tmp_ptr * username;
} while (i < 16);
i = 0;
BYTE name_encrypt_2 = { NULL };
do// 第二次加密
{
int tmp = *(int*)&name_encrypt / 10;
*(int*)&name_encrypt_2 = tmp;
i += 4;
} while (i < 20);
/************************************************************************/
/* pass_encrypt + name_encrypt_2 != pass_encrypt
|| pass_encrypt + name_encrypt_2 != 2 * pass_encrypt
name_encrypt_2 + name_encrypt_2 = pass_encrypt
|| pass_encrypt + name_encrypt_2 != pass_encrypt
|| pass_encrypt + name_encrypt_2 != 2 * pass_encrypt
name_encrypt_2 + name_encrypt_2 = pass_encrypt
|| name_encrypt_2 + pass_encrypt != 3 * name_encrypt_2 )// 验证
pass_encrypt = 3 * name_encrypt_2 - name_encrypt_2
*/
/************************************************************************/
DWORD * ptrdwNameEncrypt = (DWORD*)name_encrypt_2;
DWORD ptrdwPassEncrypt = { NULL };// 注意这里是 DWORD
ptrdwPassEncrypt = ptrdwNameEncrypt + ptrdwNameEncrypt;
ptrdwPassEncrypt = ptrdwNameEncrypt + ptrdwNameEncrypt;
ptrdwPassEncrypt = 3 * ptrdwNameEncrypt - ptrdwNameEncrypt;
ptrdwPassEncrypt = ptrdwPassEncrypt + ptrdwNameEncrypt;
ptrdwPassEncrypt = 2 * ptrdwPassEncrypt - ptrdwNameEncrypt;
memcpy_s(username, 20, (char*)ptrdwPassEncrypt, 20);
}
接下来重点分析密码加密的部分
发现有两处加密
第一处:
.text:00401A72 mov al,
.text:00401A76 lea ebx,
.text:00401A7A mov byte ptr , al
.text:00401A7E call selectFromBaseCode
.text:00401A83 mov , al
.text:00401A87 inc edi
.text:00401A88 cmp edi, 4
.text:00401A8B jl short loc_401A72
这里将密码以四字节为单位,逐字节传入 selectFromBaseCode 函数中返回的密码基串所在位置的索引
简单讲一下 selectFromBaseCode(自定义名称) 函数机制:
参数:BYTE (例如:=> C)
从密码基串中计算出 C 的位置索引
密码基串为:
char* BASE_CODE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@%";
返回:位置索引(例如:=> 2 错误返回 -1)
第二处(关键算法):
OD:
.text:00401A91 mov al, cl ; val
.text:00401A93 add al, al ; val *= 2
.text:00401A95 mov dl, ch ; val
.text:00401A97 shr dl, 4 ; val >>= 4
.text:00401A9A and dl, 3 ; val &= 3
.text:00401A9D add al, al ; val *= 2
.text:00401A9F add dl, al ; val += val
.text:00401AA1 mov al, ; val
.text:00401AA5 mov , dl ; key0 = val
.text:00401AA9 mov dl, al ; val
.text:00401AAB shr dl, 2 ; val >>= 2
.text:00401AAE mov cl, ch ; val
.text:00401AB0 shl al, 6 ; y = val << 6
.text:00401AB3 add al, ; y += val
.text:00401AB7 and dl, 0Fh ; val &= 0xF
.text:00401ABA shl cl, 4 ; val <<= 4
.text:00401ABD xor dl, cl ; val ^= val
.text:00401ABF mov , dl ; key1 = val
.text:00401AC3 mov , al ; key2 = y
HAND: (这里IDA给出的整理不容易观察我们自己手动整理)
key = (val >> 4) & 3 + (val << 2)
key = (val >> 2) & 0xF ^ (val << 4)
key = val << 6 + val
到这里核心算法就完成了,你会发现下面还有一部分加密,经过分析后会知道下面的加密跟这里的相同,只是对密码后3位进行了一下特殊处理
我们再假设一个公式:x(加密后的密码已知) + 逆算 = 注册码
接下来就是重点了,如何将算法逆回去就可以得出注册码了
从上面看是将 val => key, 逆算就是要将 key => val
正算时是将4字节转成3字节,而逆算是将3字节转成4字节,这相当于有4个未知数,而只有3条关系式,乍看上去不好逆算,这里也是我花费时间比较长的地方,解决方法主要是得开窍哈哈,下面贴上主要分析过程:
key0 = (val >> 4) & 3 + (val << 2)
/**
因为 (val >> 4) & 3=> 0000 0011b
+ (val << 2) => 1111 1100b
= key0
所以
val = key0 >> 2
val = (key0 & 3) << 4
**/
key1 = (val >> 2) & 0xF ^ (val << 4)
/**
同理 (val >> 2) & 0xF=> 0000 1111b
^ (val << 4) => 1111 0000b
= key1
所以
val = key1 >> 4
val = (key1 & 0xF) << 2
**/
key2 = val << 6 + val
/**
因为 val << 6 => 1100 0000b
+ val => val < 0x40
= key3
所以
val = key3 >> 6
val = key3 & 0x3F
**/
/********************************************************
因为 val = (key0 & 3) << 4
和 val = key1 >> 4
均为真
所以是或关系
则 val = (key0 & 3) << 4 | key1 >> 4
同理 val = (key1 & 0xF) << 2 | key3 >> 6
**/
之后再以公式的形式展现一下现在的进度:验证用户名串(已知)+ 逆算(已知)= 注册码
算法函数:
void calcSN(char* str) {
BYTE* curptr = (BYTE*)str;
BYTE idxArr = { NULL };
char sn = { NULL };
for (int i = 0, j = 0; i < 20; i += 3, j += 4, curptr += 3){
idxArr = curptr >> 2;
idxArr = (curptr & 3) << 4 | curptr >> 4;
idxArr = (curptr & 0xF) << 2 | curptr >> 6;
idxArr = curptr & 0x3F;
for (int idx = 0; idx < ((i == 18) ? 3 : 4); ++idx){// 最后3位稍做字符串上的处理
sn = BASE_CODE];
}
}
strcpy_s(str, MAX_PATH, sn);
}
完整调用:
int _tmain(int argc, _TCHAR* argv[]) {
char username = { 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 新人贴 加精鼓励。 wsxqaz13245 发表于 2016-6-1 09:10
@DeathMemory楼主请教个问题,你都是用什么逆向工具逆向的?比如IDA,OD,还有什么别的IDE吗?比如说第三张 ...
呃,一共就两张截图,黑底的是插入的代码段格式,本文中主要也就用到了IDA和OD,实际应用中还可以配合使用一些查壳工具PEID等其他工具,看需要
DeathMemory 发表于 2016-6-1 11:05
呃,一共就两张截图,黑底的是插入的代码段格式,本文中主要也就用到了IDA和OD,实际应用中还可以配合使 ...
好的,谢谢楼主 不错,下载地址丢了呦~ Hmily 发表于 2016-5-31 10:31
不错,下载地址丢了呦~
本来想以添加链接的方式增加下载地址,后来发现添加的链接无效,只好把地址都贴出来 good , .pc时候再处理 make 哇,膜拜大神啊!!! 练习后是各种失败,郁闷的 王美君 发表于 2016-5-31 11:25
练习后是各种失败,郁闷的
加油~~~等搞出来了就会觉得很畅快哈哈 谢谢分享,学习一下 woc好厉害。。