<<加密与解密三>>比赛试题分析报告
论坛上有不少人一看到用户名,就会大概判定出其是否使用暴破,其实很简单,只要有简单地判定下面的三个条件是否都符合,只有一个不符合,就是暴破出结果的.
1 用户名的长度为6个字符
2 用户名的每一个字符都在[‘b’-‘z’]区间中
3 用户名最后一个字符是否为’p’
一 使用工具
二 保护概要
三 保护部分的分析
四 算法整体的分析
五 注册机
一 使用工具
IDA5.2+F5键(Hex-Rays.Decompiler.v1.0.for.DataRescue.IDA.Pro.Advanced.v5.2-YAG)
二 保护概要
1 花指令
2 异常处理
3 通过改变函数的返回地址来改变执行流程
4 MessageBox首参数的判定
5 用户名和序列号通过变换验证
三 保护部分的分析
1 花指令
直接看花指令代码吧,没有太多好说的,只有一种
代码:
.text:00401A30 598 EB 05 jmp short loc_401A37 .text:00401A32 ; --------------------------------------------------------------------------- .text:00401A32 .text:00401A32 loc_401A32: ; CODE XREF: sub_4019F0:loc_401A37 p .text:00401A32 000 F9 stc ;表示借位 .text:00401A33 000 73 01 jnb short near ptr byte_401A36 ;不会跳转 .text:00401A35 000 C3 retn .text:00401A35 ; --------------------------------------------------------------------------- .text:00401A36 000 FF byte_401A36 db 0FFh ; CODE XREF: sub_4019F0+43 j .text:00401A37 ; --------------------------------------------------------------------------- .text:00401A37 .text:00401A37 loc_401A37: ; CODE XREF: sub_4019F0+40 j .text:00401A37 598 E8 F6 FF FF FF call loc_401A32 通过查看机器码,即可得到 eb 05 f9 73 01 c3 ff e8 f6 ff ff ff ==> 90 90 90 90 90 90 90 90 90 90 90 90
2 异常处理
在0x401BB0()函数中, 通过向 [29Ch] 内存中写入6, 始终会触发一个C++异常, 然后去执行sub_4019F0(),以保证程序是通过父进程名称为explorer.exe,防止动态加载调试
3 通过改变函数的返回地址来改变执行流程
在0x401BB0()函数中
代码:
.text:00401BD2 070 mov eax, [ebp+arg_0] .text:00401BD5 070 sub eax, 10h .text:00401BD8 070 mov [ebp+var_14], eax .text:00401BDB 070 mov ecx, [ebp+var_14] .text:00401BDE 070 mov dword ptr [ecx], offset sub_4017F0 其函数中有两处验证,一处直接出错退出,另一处就是修改返回地址为出错函数处理的地址 .text:00401C54 070 mov ecx, [ebp+var_14] .text:00401C57 070 mov dword ptr [ecx], offset sub_4017B0 .text:00401C29 070 call sub_4017B0
在sub_4017F0函数中,经过一系列处理之后始通过会执行到这一块,这也是困惑我最久的地方
[/CODE]
.text:004018BB loc_4018BB: ; CODE XREF: sub_4017F0+33 j
.text:004018BB 090 push 0 ; uType
.text:004018BD 094 push offset aA ; "错了!"
.text:004018C2 098 push offset aMJ ; "继续努力!"
.text:004018C7 09C movsx ecx, g_code+0Ah
.text:004018CE 09C sub ecx, 55h
.text:004018D1 09C neg ecx
.text:004018D3 09C sbb ecx, ecx
.text:004018D5 09C inc ecx
.text:004018D6 09C push ecx ; hWnd
.text:004018D7 0A0 call ds:MessageBoxA
.text:004018DD 090 mov [ebp+var_28], eax
.text:004018E4 090 jz short loc_4018FB
.text:004018E6 090 call ds:GetCurrentProcess
.text:004018EC 090 mov [ebp+hProcess], eax
.text:004018EF 090 push 0 ; uExitCode
.text:004018F1 094 mov edx, [ebp+hProcess]
.text:004018F4 094 push edx ; hProcess
.text:004018F5 098 call ds:TerminateProcess
.text:004018FB
.text:004018FB loc_4018FB: ; CODE XREF: sub_4017F0+F4 j
F5反编译之后对应的语句
v2 = MessageBoxA((HWND)(g_code[10] == 85), "继续努力!", "错了!", 0);
v7 = v2;
if ( v2 )
{
v3 = GetCurrentProcess();
hProcess = v3;
TerminateProcess(v3, 0);
}
[/CODE]
当注册机数组的第10个元素为85时其句柄值为1,此时MessageBoxA无法正确提示出错,并返回0,从而通过验证机制.根据用户名的字符与注册码的关系1(见下面),其用户名的第5个字符(即最后一个字符)为85+27=112即字符’p’
5 用户名和序列号通过变换验证
我就简单描述一下,详细的算法的整体分析和注册机代码
a. subOnOK ()函数(0x00401CC0)中,对用户名进行检查, 用户名长度必须为6个字符且用户名字符一定在['b'-'y']区间内
b. sub_401BB0()函数的检查
( *(_BYTE *)(v11 + name) != *(_BYTE *)(v12 + code) + 27 ) //用户名的字符与注册码的关系1
( *(_BYTE *)(v11 + name) > *(_BYTE *)(v12 + code + 1) + 32 ) //用户名的字符与注册码的关系2
c. sub_4017F0()函数中的用户名转换为第三个数组
代码:
v5 = "ABCDEFGHIJKLMNOPQRSTUVWXY"; v6 = "ABCDEFGHIJKLMNOPQRSTUVWXY"; // 转换字符串 v7 = 0; while ( v7 != 24 ) { if ( g_name == *v5 + 32 || byte_40416B[strlen(&g_name)] == *v5 + 32 ) ++v5; v8[v7++] = *v5; // 用户名转换后的数组,后面与注册码进行验证 v5 += 2; if ( !*v5 ) { if ( v7 < 24 ) v5 = v6 + 1; } }
代码:
v7 = 0; v10 = 5; v11 = 0; while ( v7 != 12 ) // 注册码进行验证 { v12 = 4 * v10 - 4; do { v0 = g_code[v7]; v4 = v8[v12++]; // 用户名转换后的数组 if ( v0 == v4 ) break; ++v11; } while ( v11 <= 4 ); // 用户名转换后的有五次碰撞机会. 我生成注册码时也就通过这个来完成的,对这个转换方法不太了解 if ( v11 == 5 ) { v13 = "ABCDEFGHIJKLMN"; // 迷惑人的 v14 = "OPQRSTUVWXYZ"; v11 = v0; if ( v0 > 0 ) sub_4017B0(); sub_4017B0(); // 出错提示后退出 } v7 += 2; --v10; if ( !v10 ) v10 = 6; v11 = 0; }
四 算法整体的分析
IDA中选择相应的函数后直接按F5键即可得到类C的代码,可直接看出其函数执行的流程,在写注册机时也可以直接Ctrl+C, Ctrl+V了.
OnOk() //确定之后的处理函数
sub_401BB0() //用户名与注册码对应关系的检查
sub_4017F0() // 核心验证
sub_401770() // 过关提示
sub_4017B0() // 出错提示
sub_4019F0() // sub_401BB0()中的异常处理时 防动态调试的保护
反编译的函数
代码:
//.text:00401CC0 int __fastcall subOnOK(int a1) { int result; // eax@2 int v2; // ST08_4@1 int v3; // eax@1 int v4; // eax@1 int v5; // eax@1 int v6; // eax@1 int v7; // eax@1 int v8; // [sp+5Ch] [bp-4h]@1 char *code; // [sp+58h] [bp-8h]@1 char *name; // [sp+54h] [bp-Ch]@1 signed int v11; // [sp+50h] [bp-10h]@5 int v12; // [sp+0h] [bp-60h]@12 int *v13; // [sp+4Ch] [bp-14h]@12 v8 = a1; v2 = a1 + 96; v3 = CWnd__GetDlgItem(1001); CWnd__GetWindowTextA(v3, v2); v4 = sub_401E20(); code = (char *)CString__GetBuffer(v8 + 96, v4); v5 = CWnd__GetDlgItem(1002); CWnd__GetWindowTextA(v5, v8 + 100); v6 = sub_401E20(); v7 = CString__GetBuffer(v8 + 100, v6); name = (char *)v7; if ( strlen((const char *)v7) ) { if ( strlen(code) ) { v11 = 0; while ( name[v11] && name[v11] > 'a' && name[v11] < 'z' ) //用户名字符一定在['b'-'y']区间内 ++v11; if ( v11 == 6 ) //用户名长度必须为6个字符 { strcpy(&g_name, name); strcpy(g_code, code); v13 = &v12; result = sub_401BB0(&v12, name, code); } else { result = CWnd__MessageBoxA(v8, "非法用户!", 0, 0); } } else { result = CWnd__MessageBoxA(v8, "请输入注册码!", 0, 0); } } else { result = CWnd__MessageBoxA(v8, "请输入用户名!", 0, 0); } return result; } int __usercall sub_401BB0<eax>(int a1<eax>, int a2, int name, int code) // 注意通过第一个参数可以修改函数的返回地址 { int result; // eax@1 signed int v5; // [sp+68h] [bp-4h]@1 int (*v6)(); // [sp+64h] [bp-8h]@1 int v7; // [sp+60h] [bp-Ch]@1 int v8; // [sp+0h] [bp-6Ch]@1 int *v9; // [sp+5Ch] [bp-10h]@1 int v10; // [sp+58h] [bp-14h]@1 signed int v11; // [sp+54h] [bp-18h]@1 int v12; // [sp+50h] [bp-1Ch]@1 v5 = -1; v6 = SEH_401BB0; v7 = a1; v9 = &v8; result = a2 - 16; v10 = a2 - 16; *(_DWORD *)(a2 - 16) = sub_4017F0; // 函数返回地址 正确通过 v11 = 0; v12 = 0; while ( v11 < 6 ) { if ( *(_BYTE *)(v11 + name) != *(_BYTE *)(v12 + code) + 27 ) // 用户名的字符与注册码的关系1 sub_4017B0(); // 直接错误提示退出 if ( *(_BYTE *)(v11 + name) > *(_BYTE *)(v12 + code + 1) + 32 ) // 用户名的字符与注册码的关系2 *(_DWORD *)v10 = sub_4017B0; // 函数返回地址 错误提示退出 v12 += 2; result = v11++ + 1; } v29c = 6; // 觖发异常 执行sub_4019F0()函数 return result; } int __cdecl sub_4017F0() // 核心验证 { signed int v0; // eax@15 int v2; // eax@10 HANDLE v3; // eax@11 signed int v4; // edx@15 char *v5; // [sp+88h] [bp-4h]@1 char *v6; // [sp+84h] [bp-8h]@1 signed int v7; // [sp+64h] [bp-28h]@1 _BYTE v8[28]; // [sp+68h] [bp-24h]@6 HANDLE hProcess; // [sp+60h] [bp-2Ch]@11 signed int v10; // [sp+5Ch] [bp-30h]@12 signed int v11; // [sp+58h] [bp-34h]@12 int v12; // [sp+54h] [bp-38h]@14 char *v13; // [sp+50h] [bp-3Ch]@20 char *v14; // [sp+4Ch] [bp-40h]@20 v5 = "ABCDEFGHIJKLMNOPQRSTUVWXY"; v6 = "ABCDEFGHIJKLMNOPQRSTUVWXY"; // 转换字符串 v7 = 0; while ( v7 != 24 ) { if ( g_name == *v5 + 32 || byte_40416B[strlen(&g_name)] == *v5 + 32 ) ++v5; v8[v7++] = *v5; // 用户名转换后的数组,后面与注册码进行验证 v5 += 2; if ( !*v5 ) { if ( v7 < 24 ) v5 = v6 + 1; } } v2 = MessageBoxA((HWND)(g_code[10] == 85), "继续努力!", "错了!", 0); // 容易困惑的地方 注意注册码的第11个元素 v7 = v2; if ( v2 ) { v3 = GetCurrentProcess(); hProcess = v3; TerminateProcess(v3, 0); } v7 = 0; v10 = 5; v11 = 0; while ( v7 != 12 ) // 注册码进行验证 { v12 = 4 * v10 - 4; do { v0 = g_code[v7]; v4 = v8[v12++]; // 用户名转换后的数组 if ( v0 == v4 ) break; ++v11; } while ( v11 <= 4 ); // 用户名转换后的有五次碰撞机会. 我生成注册码时也就通过这个来完成的,对这个转换方法不太了解 if ( v11 == 5 ) { v13 = "ABCDEFGHIJKLMN"; // 迷惑人的 v14 = "OPQRSTUVWXYZ"; v11 = v0; if ( v0 > 0 ) sub_4017B0(); sub_4017B0(); // 出错提示后退出 } v7 += 2; --v10; if ( !v10 ) v10 = 6; v11 = 0; } return sub_401770(); } BOOL __cdecl sub_401770() { HANDLE v1; // eax@1 MessageBoxA(0, "过关!", "恭喜!", 0); v1 = GetCurrentProcess(); return TerminateProcess(v1, 0); } BOOL __cdecl sub_4017B0() { HANDLE v1; // eax@1 MessageBoxA(0, "继续努力!", "错了!", 0); v1 = GetCurrentProcess(); return TerminateProcess(v1, 0); } BOOL __cdecl sub_4019F0() // sub_401BB0()中的异常处理时 防动态调试的保护 { BOOL result; // eax@15 HANDLE v1; // eax@1 BOOL v2; // eax@1 HANDLE v3; // eax@2 BOOL v4; // eax@11 HANDLE v5; // eax@13 HANDLE v6; // [sp+468h] [bp-12Ch]@1 DWORD v7; // [sp+45Ch] [bp-138h]@1 DWORD v8; // [sp+460h] [bp-134h]@1 char Dst; // [sp+58h] [bp-53Ch]@1 HANDLE hSnapshot; // [sp+464h] [bp-130h]@1 PROCESSENTRY32 pe; // [sp+46Ch] [bp-128h]@1 BOOL v12; // [sp+458h] [bp-13Ch]@1 HANDLE hProcess; // [sp+54h] [bp-540h]@2 int v14; // [sp+50h] [bp-544h]@3 HANDLE v15; // [sp+4Ch] [bp-548h]@13 v6 = 0; v7 = 0; v8 = 0; memset(&Dst, 0, 0x400u); v1 = CreateToolhelp32Snapshot(0xFu, 0); hSnapshot = v1; pe.dwSize = 296; v2 = Process32First(v1, &pe); v12 = v2; if ( !v2 ) { v3 = GetCurrentProcess(); hProcess = v3; TerminateProcess(v3, 0); } v14 = 0; while ( v12 && !v7 && !v8 ) { if ( pe.th32ProcessID == GetCurrentProcessId() ) { v8 = pe.th32ParentProcessID; // 当前的父进程ID v6 = OpenProcess(0x1F0FFFu, 1, pe.th32ParentProcessID); } else { if ( !strcmp(pe.szExeFile, "explorer.exe") ) v7 = pe.th32ProcessID; // Explorer的 ID } v4 = Process32Next(hSnapshot, &pe); v12 = v4; if ( !v4 ) { if ( !v14 ) { v5 = GetCurrentProcess(); v15 = v5; TerminateProcess(v5, 0); } } ++v14; } result = v8; if ( v8 != v7 ) // 当前的父进程不为Explorer,直接挂了 result = TerminateProcess(v6, 0); return result; }
五 注册机
不多说了,直接看代码吧. 偷懒了,有些代码直接从IDA中复制过来,连变量名都不改.
附有源文件和编译后的执行程序
代码:
/* Dev-cpp 4.9.9.2 下编译通过 */ #include <windows.h> #include <stdio.h> #include <string.h> #include <time.h> /* 生成随机用户名 */ void GenUserName(char*username) { srand(time(NULL)); for(int i=0; i<5; i++) { username[i]='b'+rand()%('z'-'b'); } username[5]='p'; } void GenCheck(char *username, char *v8, char *v6) { char*v5=v6; int v7 = 0; while ( v7 != 24 ) { if ( username[0] == *v5 + 32 || username[strlen(username)-1] == *v5 + 32 ) ++v5; v8[v7++] = *v5; v5 += 2; if ( !*v5 ) { if ( v7 < 24 ) v5 = v6 + 1; } } } void GenKey(char *username, char *key) { for(int i=0; i<6; i++) { key[i*2]=username[i]-27; key[i*2+1]=username[i]-(rand()%32); } } int CheckKey(char *username, char *key, char *v8) { int v7 = 0; int v10 = 5; int v11 = 0; while ( v7 != 12 ) { int v12 = 4 * v10 - 4; do { BYTE v0 = key[v7]; BYTE v4 = v8[v12++]; if ( v0 == v4 ) break; ++v11; } while ( v11 <= 4 ); if ( v11 == 5 ) { username[v7>>1]=v8[v12-1-rand()%5]+27; if(!(username[v7>>1]>'a' && username[v7>>1]<'z')) for(int i=0; i<5; i++) { username[v7>>1]=v8[v12-1-i]+27; if((username[v7>>1]>'a' && username[v7>>1]<'z')) break; } return v7; } v7 += 2; --v10; if ( !v10 ) v10 = 6; v11 = 0; } return -1; } int main(void) { char username[256]=""; char key[512]=""; bool b=true; GenUserName(username); char v8[28]={0}; char*v6 = "ABCDEFGHIJKLMNOPQRSTUVWXY\0"; do { GenCheck(username, v8, v6); GenKey(username, key); } while(CheckKey(username, key, v8)>=0); printf("UserName: %s\n", username); printf("Key: %s\n", key); system("pause"); }
最后运行来幅截图

由于本人水平有限,不足之处还请指教.
谢谢!
pcasa
2008-7-12