2016游戏安全技术竞赛题PC第一题逆向分析详解

5 篇文章 0 订阅
2 篇文章 0 订阅

最近看到腾讯的 2016游戏安全技术竞赛,咱不是大学生只能以凑热闹的形式参与一下。

遂下载第一题进行分析

由于要求写注册机,所以这里不考虑暴破

先看一下注册机成功结果:


正式开始分析:

从图中可以看出注册成功,失败都以 static text 控件来显示

用OD载入,搜索注册关键字符都没有找到

在 GetWindowTextW GetDlgItemTextWGetDlgItem 下断,在GetDlgItem 处断下,向上返回到程序领域就是注册算法主体了。

单步向下调试,先走一遍,最大程度的浏览一遍有用信息,这里可以发现,注册信息是在代码段拼接的



00CA1F90   .  8D47 FA                     lea     eax, dword ptr [edi-6]
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 [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:

  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:

.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:

  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 );

再往下就是加密后的用户名和密码进行验证:

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 的值

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);
}

接下来重点分析密码加密的部分

发现有两处加密

第一处:

.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 的位置索引

密码基串为:

char* BASE_CODE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@%";
返回:位置索引(例如:=> 2 错误返回 -1)


第二处(关键算法):

OD:

.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给出的整理不容易观察我们自己手动整理)

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条关系式,乍看上去不好逆算,这里也是我花费时间比较长的地方,解决方法主要是得开窍哈哈,下面贴上主要分析过程:

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
**/

之后再以公式的形式展现一下现在的进度:验证用户名串(已知)+ 逆算(已知)= 注册码

算法函数:

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);
}

完整调用:

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调试结果,或直接修改内存来验证一下。

题目和Keygen:下载


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值