近日看到腾讯的游戏安全竞赛题,利用业余时间搞了一下从来没有写过注册机,对算法也不了解,看到这个题目就尝试弄了一下。顺便学习一下!
分析的是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 题目
最后第一次发帖排版有些乱 ,表达不到位的地方欢迎指正!
[课程]FART 脱壳王!加量不加价!FART作者讲授!