<<加密与解密三>>比赛试题分析报告
论坛上有不少人一看到用户名,就会大概判定出其是否使用暴破,其实很简单,只要有简单地判定下面的三个条件是否都符合,只有一个不符合,就是暴破出结果的.
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
修正之后再用IDA分析, 附有IDA除花指令的IDC “去花指令.idc”,
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
4 MessageBox首参数的判定
在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;
}
}
d. sub_4017F0()函数中的通过第三个数组对注册码进行验证
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;
}
由于Word对代码的排序不太好,代码也可查看”分析.txt”
五 注册机
不多说了,直接看代码吧. 偷懒了,有些代码直接从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