DeathMemory 发表于 2016-5-31 10:22

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

Sound 发表于 2016-5-31 15:36

新人贴 加精鼓励。

DeathMemory 发表于 2016-6-1 11:05

wsxqaz13245 发表于 2016-6-1 09:10
@DeathMemory楼主请教个问题,你都是用什么逆向工具逆向的?比如IDA,OD,还有什么别的IDE吗?比如说第三张 ...

呃,一共就两张截图,黑底的是插入的代码段格式,本文中主要也就用到了IDA和OD,实际应用中还可以配合使用一些查壳工具PEID等其他工具,看需要

wsxqaz13245 发表于 2016-6-2 09:05

DeathMemory 发表于 2016-6-1 11:05
呃,一共就两张截图,黑底的是插入的代码段格式,本文中主要也就用到了IDA和OD,实际应用中还可以配合使 ...

好的,谢谢楼主

Hmily 发表于 2016-5-31 10:31

不错,下载地址丢了呦~

DeathMemory 发表于 2016-5-31 10:37

Hmily 发表于 2016-5-31 10:31
不错,下载地址丢了呦~

本来想以添加链接的方式增加下载地址,后来发现添加的链接无效,只好把地址都贴出来

Sound 发表于 2016-5-31 10:53

good , .pc时候再处理 make

a5680497 发表于 2016-5-31 11:02

哇,膜拜大神啊!!!

王美君 发表于 2016-5-31 11:25

练习后是各种失败,郁闷的

DeathMemory 发表于 2016-5-31 11:30

王美君 发表于 2016-5-31 11:25
练习后是各种失败,郁闷的

加油~~~等搞出来了就会觉得很畅快哈哈

天地无空 发表于 2016-5-31 12:58

谢谢分享,学习一下

红客鄙哥 发表于 2016-5-31 15:43

woc好厉害。。
页: [1] 2 3 4 5 6
查看完整版本: 2016游戏安全技术竞赛题PC第一题逆向分析详解