欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]2016腾讯的游戏安全竞赛题PC第一题解答

[原创]2016腾讯的游戏安全竞赛题PC第一题解答

近日看到腾讯的游戏安全竞赛题,利用业余时间搞了一下
从来没有写过注册机,对算法也不了解,看到这个题目就尝试弄了一下。顺便学习一下!
分析的是PC题目,要求写出注册机,不能爆破,打补丁!
题目http://gslab.qq.com/competition/firstTurn.shtml
工具:OD,IDA,VC

注册失败是一个标签,不是MessageBox。老规矩先OD加载下断点
老规矩先OD加载对所有对话框下断点
咦?咋没断下来?
好那换一种思路,搜索【注册失败】字符串也没有
突然想到不管怎么样它的设置标签文本吧,那必须要先获取控件句柄才能操作。
GetDlgItem 函数下断点
00207A5C  /$  8BFF          mov edi,edi
00207A5E  |.  55            push ebp
00207A5F  |.  8BEC          mov ebp,esp
00207A61  |.  8379 4C 00    cmp dword ptr ds:[ecx+4C],0
00207A65  |.  75 16         jnz short Tencent2.00207A7D
00207A67  |.  FF75 08       push [arg.1]                             ; /ControlID
00207A6A  |.  FF71 20       push dword ptr ds:[ecx+20]               ; |hWnd
00207A6D  |.  FF15 98332200 call dword ptr ds:[<&USER32.GetDlgItem>] ; 断下返回\GetDlgItem
到这里接着跟
00201EFC   .  FFD7          call edi          ; \SendMessageA 发送 WM_GETTEXT消息获取
控件内容,也就是用户名和序列号
003CF2B8  00333231  123.
003CF3BC   ASCII "123123"
都获取到了,接着往下跟到这里 
00201F23   .  C64424 74 D7  mov byte ptr ss:[esp+74],0D7
00201F28   .  C64424 75 A2  mov byte ptr ss:[esp+75],0A2
00201F2D   .  C64424 76 B2  mov byte ptr ss:[esp+76],0B2
00201F32   .  C64424 77 E1  mov byte ptr ss:[esp+77],0E1
00201F37   .  885C24 78     mov byte ptr ss:[esp+78],bl
00201F3B   .  885C24 79     mov byte ptr ss:[esp+79],bl
00201F3F   .  885C24 7A     mov byte ptr ss:[esp+7A],bl
00201F43   .  885C24 7B     mov byte ptr ss:[esp+7B],bl
00201F47   .  885C24 7C     mov byte ptr ss:[esp+7C],bl
00201F4B   .  C64424 7D CA  mov byte ptr ss:[esp+7D],0CA
00201F50   .  C64424 7E A7  mov byte ptr ss:[esp+7E],0A7
00201F55   .  C64424 7F B0  mov byte ptr ss:[esp+7F],0B0
00201F5A   .  C68424 800000>mov byte ptr ss:[esp+80],0DC
00201F62   .  C68424 810000>mov byte ptr ss:[esp+81],0B3
00201F6A   .  C68424 820000>mov byte ptr ss:[esp+82],0C9
00201F72   .  C68424 830000>mov byte ptr ss:[esp+83],0B9
00201F7A   .  C68424 840000>mov byte ptr ss:[esp+84],0A6
看下堆栈这是什么?
003CF290  注册.....失败成功
原来在这里,怪不得搜索不到关键字符串 
char s[]="注册",char sa[]="失败成功"这样就搜不到字符串了。
继续
00201F90   .  8D47 FA       lea eax,dword ptr ds:[edi-6]
00201F93   .  83F8 0E       cmp eax,0E
00201F96   .  0F87 4B020000 ja Tencent2.002021E7             ;  关键失败跳转1
比较用户名长度小于6或大于20直接跳走到这里
002021E7   > \33C0          xor eax,eax   清空EAX
002021E9   >  8B5484 7D     mov edx,dword ptr ss:[esp+eax*4+7D] 这里是关键点
EAX=0指向失败字符 EAX=1指向成功
002021ED   .  8B4C24 54     mov ecx,dword ptr ss:[esp+54]
002021F1   .  8D4424 74     lea eax,dword ptr ss:[esp+74]
002021F5   .  50            push eax                                 ; /lParam
002021F6   .  53            push ebx                                 ; |wParam
002021F7   .  6A 0C         push 0C                                  ; |Message = WM_SETTEXT
002021F9   .  68 ED030000   push 3ED                                 ; |/Arg1 = 000003ED
002021FE   .  899424 880000>mov dword ptr ss:[esp+88],edx            ; ||
00202205   .  E8 52580000   call Tencent2.00207A5C                   ; |\Tencent2.00F07A5C
0020220A   .  8B48 20       mov ecx,dword ptr ds:[eax+20]            ; |
0020220D   .  51            push ecx                                 ; |hWnd
0020220E   .  FF15 24332200 call dword ptr ds:[<&USER32.SendMessageA>; \发送消息
到这里爆破点找到了 只要在002021E7   > \33C0          xor eax,eax   清空EAX这里修改EAX=1就显示注册成功了
当然还有别的方法爆破比如改跳转直接跳到MOV EAX,1 那个位置等等 由于要求写注册机这个就不多说了。
==================================
找到了关键CALL 上IDA
00201FE0   > /8BC1          mov eax,ecx
00201FE2   . |99            cdq
00201FE3   . |F7FF          idiv edi
00201FE5   . |8DB40C 880000>lea esi,dword ptr ss:[esp+ecx+88]
00201FEC   . |41            inc ecx
00201FED   . |0FBE8414 9C00>movsx eax,byte ptr ss:[esp+edx+9C]
00201FF5   . |8D142E        lea edx,dword ptr ds:[esi+ebp]
00201FF8   . |0FAFC2        imul eax,edx
00201FFB   . |0FAFC7        imul eax,edi
00201FFE   . |0106          add dword ptr ds:[esi],eax
00202000   . |83F9 10       cmp ecx,10
00202003   .^\7C DB         jl short Tencent2.00201FE0
直接到重点用户名第一段加密
IDA:
  do
  {
    v8 = v7 % v6;
    v9 = &v48[v7++];
    *(_DWORD *)v9 += v6 * (_DWORD)&v9[20160126 - (_DWORD)v48] * lParam[v8];
  }
  while ( v7 < 16 );
往下跟发现序列号长度也是有要求的,必须为27个字符否则直接跳转到失败
继续往下就是第二段的用户名加密了
用户名第二段加密
00202102   > /8B8C34 880000>mov ecx,dword ptr ss:[esp+esi+88]
00202109   . |B8 67666666   mov eax,66666667
0020210E   . |F7E9          imul ecx
00202110     |C1FA 02       sar edx,2
00202113   . |8BCA          mov ecx,edx
00202115   . |C1E9 1F       shr ecx,1F
00202118   . |03CA          add ecx,edx
0020211A   . |8B5424 24     mov edx,dword ptr ss:[esp+24]
0020211E   . |2BD7          sub edx,edi
00202120   . |894C34 2C     mov dword ptr ss:[esp+esi+2C],ecx
00202124   . |3BF2          cmp esi,edx
00202126   . |72 09         jb short Tencent2.00202131
00202128   . |E8 2DEF0000   call Tencent2.0021105A
0020212D   . |8B7C24 20     mov edi,dword ptr ss:[esp+20]
00202131   > |8B043E        mov eax,dword ptr ds:[esi+edi]
00202134   . |894434 40     mov dword ptr ss:[esp+esi+40],eax
00202138   . |83C6 04       add esi,4
0020213B   . |83FE 14       cmp esi,14
0020213E   .^\7C C2         jl short Tencent2.00202102
IDA:
 do
  {
    v16 = *(_DWORD *)&v48[v15] / 10;
    v17 = v23 - (_DWORD)v14;
    *(int *)((char *)&v25 + v15) = v16;
    if ( v15 >= (unsigned int)v17 )
    {
      _invalid_parameter_noinfo(v16);
      v14 = v22;
    }
    *(int *)((char *)&v30 + v15) = *(_DWORD *)((char *)v14 + v15);
    v15 += 4;
  }
  while ( v15 < 20 );//用户名二次加密
这下面一段就是关键算法和比较了对2次加密的用户名继续第三次加密比较
  if ( v34 + v25 != v32 || v32 + v26 != 2 * v34 || v33 + v27 != v30 || v30 + v28 != 2 * v33 || v29 + v31 != 3 * v27 )
经过分析这3段加密VC代码如下,代码有点搓,对付看吧 不要喷
//用户名生成系列号A
void CalcName(char *namestr)
{
  int strsize;
  int i=0;
  unsigned int a;
  BYTE stra[20] = { 0 };
  BYTE strb[20] = {0};
  BYTE *b;
  strsize = strlen(namestr);
  do
  {
    a = i % strsize;
    b = &strb[i++];
    *(DWORD *)b += strsize * (DWORD)&b[20160126 - (DWORD)strb] * namestr[a];
  } while (i < 16);
  i = 0;
  do
  {
    a = (unsigned int)*(DWORD *)&strb[i];
    __asm
    {      
      mov ecx, a
      mov eax, 66666667h
      imul ecx
      sar edx, 2
      mov ecx, edx
      shr ecx, 1Fh
      add ecx, edx
      mov a, ecx
    }  
    //a = (unsigned int)*(DWORD *)&strb[i] / (unsigned int)10;
    *(int *)((char *)&stra + i) = a;
    i += 4;
  } while (i < 20);
  *(DWORD*)&strb[16] = *(DWORD*)&stra[0] + *(DWORD*)&stra[4];
  *(DWORD*)&strb[12] = *(DWORD*)&stra[8] + *(DWORD*)&stra[12];
  *(DWORD*)&strb[8] = *(DWORD*)&strb[16] + *(DWORD*)&stra[0];
  *(DWORD*)&strb[0] = *(DWORD*)&stra[8] + *(DWORD*)&strb[12];
  *(DWORD*)&strb[4] = *(DWORD*)&stra[8] * 3 - *(DWORD*)&stra[16];
  memcpy(namestr, strb, 20);
}
其中有一段是用汇编写的 因为 第二次加密是每4字节 /10操作  
用C++    //a = *(DWORD *)&strb[i] / 10;这句会出现负数结果出错 请指教C++如何写才能实现编译后生成那段汇编  请回帖解答

加密用户名我们跟完事了,接下来是序列号的算法,因为不是明码比较!
从头跟起到这里:
通过序列号逐个字符查找在下面字符串中出现的位置保存下来作为加密数据。
01F61330 ASCII "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@%"
00201A7E  |.  E8 9D090000   ||call Tencent2.00202420 \序列号查找字符位置 call
这里就是加密上面生成的数据了。
00201A8D  |.>|mov ecx,dword ptr ss:[esp+18]           ;  加密1
00201A91  |.>|mov al,cl
00201A93  |.>|add al,al
00201A95  |.>|mov dl,ch
00201A97  |.>|shr dl,4
00201A9A  |.>|and dl,3
00201A9D  |.>|add al,al
00201A9F  |.>|add dl,al
00201AA1  |.>|mov al,byte ptr ss:[esp+1A]
00201AA5  |.>|mov byte ptr ss:[esp+14],dl
00201AA9  |.>|mov dl,al
00201AAB  |.>|shr dl,2
00201AAE  |.>|mov cl,ch
00201AB0  |.>|shl al,6
00201AB3  |.>|add al,byte ptr ss:[esp+1B]
00201AB7  |.>|and dl,0F
00201ABA  |.>|shl cl,4
00201ABD  |.>|xor dl,cl
00201ABF  |.>|mov byte ptr ss:[esp+15],dl
00201AC3  |.>|mov byte ptr ss:[esp+16],al
00201AC7    >xor edi,edi
IDA:
        v26 = 4 * v29 + ((BYTE1(v29) >> 4) & 3);
        v27 = 16 * BYTE1(v29) ^ (BYTE2(v29) >> 2) & 0xF;
        v28 = BYTE3(v29) + (BYTE2(v29) << 6);
由于是27个字符这里是对末尾最后3个字节处理
00201B6F  |.>mov ecx,dword ptr ss:[esp+18]            ;  加密2
00201B73  |.>mov al,cl
00201B75  |.>add al,al
00201B77  |.>add al,al
00201B79  |.>mov dl,ch
00201B7B  |.>shr dl,4
00201B7E  |.>and dl,3
00201B81  |.>add dl,al
00201B83  |.>mov al,byte ptr ss:[esp+1A]
00201B87  |.>mov byte ptr ss:[esp+14],dl
00201B8B  |.>mov dl,al
00201B8D  |.>shl al,6
00201B90  |.>add al,byte ptr ss:[esp+1B]
00201B94  |.>shr dl,2
00201B97  |.>mov byte ptr ss:[esp+16],al
00201B9B  |.>mov eax,dword ptr ss:[esp+20]
00201B9F  |.>mov cl,ch
00201BA1  |.>and dl,0F
00201BA4  |.>shl cl,4
00201BA7  |.>dec eax
00201BA8  |.>xor dl,cl
00201BAA  |.>xor ebx,ebx
00201BAC  |.>mov byte ptr ss:[esp+15],dl
00201BB0  |.>mov dword ptr ss:[esp+24],eax
00201BB4  |.>test eax,eax
IDA:
      v26 = 4 * v29 + ((BYTE1(v29) >> 4) & 3);
      v28 = BYTE3(v29) + (BYTE2(v29) << 6);
      result = v31 - 1;
      v21 = 0;
      v27 = 16 * BYTE1(v29) ^ (BYTE2(v29) >> 2) & 0xF;
2段算法基本一样 只是对最后一个字节FF填充处理
这2段加密算法是把逐个4字节数据加密成3字节数据,最后由于是27个字节最后3个字节末尾加上FF 加密
加密后的数据取前20个字节 和前面用户名生成的进行比较。
先逆向出解密算法 然后从ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@%
逐个查找对应位置的字符 这样就生成了系列号
逆向出的生成 算法代码
----------------------------------
//加密成序列号
void CalcSN(char *stra)
{
  char v25;
  char v26;
  char v27;
  char v28;
  BYTE v1[25] = { 0 };
  memcpy(v1, stra, 21);
  v1[21] = { 0x3F };
  int v2;
  char abc[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@%";
  char codes[260];
  char a[260];
  memset(a, 0, 260);
  memset(codes, 0, 260);
  for (int i = 0; i < 21;i+=3)
  {
    if (i==18)//特殊处理最后3位
    {
      v25 = v1[i] / 4;
      //v28 = v1[i + 2] % 0X40;
      v28 = 0xFF;
      v2 = (BYTE(v1[i + 1]) & 0xF) << 8;
      v27 = (v2 + 0x40) >> 6;
      v2 = (v1[i] % 4) << 4;
      v26 = v2 ^ (BYTE(v1[i + 1]) >> 4);
      a[0] = abc[v25];
      strcat_s(codes, a);
      a[0] = abc[v26];
      strcat_s(codes, a);
      a[0] = abc[v27];
      strcat_s(codes, a);
      a[0] = 0;
      strcat_s(codes, a);
    } 
    else
    {
      v25 = v1[i] / 4;
      v28 = v1[i + 2] % 0X40;
      v2 = (BYTE(v1[i + 1]) & 0xF) << 8;
      v27 = (v2 + (v1[i + 2] - v28)) >> 6;
      v2 = (v1[i] % 4) << 4;
      v26 = v2 ^ (BYTE(v1[i + 1]) >> 4);
      a[0] = abc[v25];
      strcat_s(codes, a);
      a[0] = abc[v26];
      strcat_s(codes, a);
      a[0] = abc[v27];
      strcat_s(codes, a);
      a[0] = abc[v28];
      strcat_s(codes, a);
    }
  }
  memset(stra, 0, 200);
  memcpy(stra, codes, strlen(codes));
}
-------------------------
void CMFCApplication1Dlg::OnBnClickedButton1()
{
  // TODO:  在此添加控件通知处理程序代码
  CString myname;
  char namem[260];
  medit1.GetWindowTextA(myname);
  strcpy_s(namem, myname);
  if (strlen(namem) >= 6 && strlen(namem) <= 20)
  {
    CalcName(namem);//用户名生成系列号A
    CalcSN(namem);//加密成序列号
    medit2.SetWindowTextA(namem);
  }else AfxMessageBox("用户名太长或太短!");
}
好到这里 整个注册机 就完成了!

总结:
1,SendMessageA 发送 WM_GETTEXT消息获取控件文本一般的断点断不下来,给新手增加
了些难度
2,从用户名到生成序列号 要用4段不同的算法,有一定的难度,不过加密代码清晰还是不难分析

3,附件1,VC2013编译代码。附件2,注册机,附件3   题目
最后第一次发帖排版有些乱 ,表达不到位的地方欢迎指正!

上传的附件 3.png
MFCApplication1.rar
KeyGen20160312A.rar
firstTurn_PC.zip

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功