好友
阅读权限30
听众
最后登录1970-1-1
|
本帖最后由 pk8900 于 2018-6-25 23:17 编辑
前言
很长时间没有发帖子了,这段时间一直在饿补,随着逆向学习深入,发现自己要学的东西太多了,许多东西过去都是只听说过,跟本没学习过,于是看网上的各种教程,并寻找用来练手的东西,发现看雪CTF里有不少CRACKME,于是拿来分析研究。
这个CRACKME的作者叫[凉飕飕],CTF要求是追码,整个追码过程断续的进行2天时间,然后写的这个帖子,尽量还原原始分析步骤,如果有遗漏和不对的地方,欢迎大家请跟贴讨论。
此贴并非技术贴,大牛可以一带而过,和我一样的新手也许可以从中学习到一些方法和经验。
下载地址
CRACKME原帖及下载地址:https://ctf.pediy.com/game-fight-2.htm
crack简介
PEid显示,Microsoft Visual C++ 8 *,图标是个MFC图标,估计是VC++的程序错不了。界面如下:
输入任意注册码,点击OK,有提示信息框:something you lost!,并且每尝试一次,左边还有一个文本框递增次数。
使用工具
IDA7.0,X64DBG,辅助工具:PEid,慧星小助手(查看控件ID,本例中:密码框为1002,次数框为1000)
摸清流程
既然程序有提示字符串,那就从字符串下手。X64DBG载入,运行后程序自动退出,有反调试。
打开IDA进行静态分析,查找字符串,找到结果如下:
.text:00402423 sub_402120 push offset Text ; "something you lost!"
转到sub_402120调用处,此处IDA代码如下:
[C++] 纯文本查看 复制代码 BeginPaint(hWnd, &Paint); EndPaint(hWnd, &Paint);
return 0;
}
return DefWindowProcW(hWnd, Msg, wParam, lParam);
}
if ( (unsigned __int16)wParam > 0x40Au )
{
switch ( (unsigned __int16)wParam )
{
case 0x40Bu: // 注册成功条件:wParam==0x40B
*(_OWORD *)v20 = xmmword_41DB98;
v23 = 0;
v21 = xmmword_41DBA8;
v22 = xmmword_41DBB8;
memset(&v24, 0, 0x96u);
MessageBoxW(hWnd, v20, L"Successed", 0);// 成功位置
return 0;
case 0x40Cu:
sub_402970(lParam, &v19);
v9 = operator new[](2 * (v19 + 1));
sub_402870(v9, lParam);
sub_4029B0(v9);
sub_402A00(v9);
memset(v20, 0, 0xC8u);
sub_402870(v20, v9);
j_j_j___free_base(v9);
v10 = 0;
if...
if ( sub_402810(lParam, v20) )
SendMessageW(hWnd, 0x111u, 0x40Au, 0);
return 0;
case 0x40Fu:
MessageBoxW(0, L"something you lost!", L"Failed", 0);// 失败处
return 0;
}
return DefWindowProcW(hWnd, 0x111u, wParam, lParam);
}
if ( (unsigned __int16)wParam == 0x40A )
{
*(_OWORD *)Text = xmmword_41DB60;
v26 = xmmword_41DB70;
v27 = xmmword_41DB80;
v28 = qword_41DB90;
memset(&v29, 0, 0x2Cu);
MessageBoxW(hWnd, Text, L"Failed", 0); // 失败处
在失败对话框上方,有明显"Successed"成功字样,分析得出成功的条件就是:wParam==0x40B,再往上可以看到:
[C++] 纯文本查看 复制代码 switch ( Msg )
{
case 0x113u:
v13 = operator new(0x14u);
v40 = 0;
*(_OWORD *)v39 = 0i64;
v14 = GetDlgItem(hWnd, 1000);
GetDlgItemTextW(v14, 1000, v39, 10);
v19 = -858993460;
sub_4027C0(v39, (const char *)L"%d", &v19);
*v13 = v19;
v13[1] = hWnd;
v13[3] = GetDlgItem(hWnd, 1002);
v13[2] = GetDlgItem(hWnd, 1000);
v13[4] = v13;
“.............................”
while ( *((_WORD *)&v32 + v15) );
if ( OpenEventW(0x1F0003u, 0, L"Cracker") )
CreateThread(0, 0, StartAddress, v13, 0, 0);
return 0;
“.............................”
return DefWindowProcW(hWnd, Msg, wParam, lParam);
case 0x113u,百度查询,0x113为时间事件,也就是程序里有一个时钟事件,用做检测,事件中包含"Cracker",“Failed”等可疑字串,未发现成功字串,只有一个可疑的CreateThread(0, 0, StartAddress, v13, 0, 0);函数,创建新线程。
(此期间尝试跟踪此时间事件,结果一点进展都没有,关于程序反调试,我采用的附加的方法轻松绕过,反调试部分放在最后分析。)
绕了半天的弯路后,又回到这里,决定从成功注册的条件:wParam==0x40B入手,IDA查的常量:0x40B(IDA操作为查找立即数),果然找到了关键位置,sub_401870,右键对函数重命名为:check_right_401870(命名方便在函数列表中查找,保留地址是方便X64DBG中进行调试定位),在这个子程序中调用了 return PostMessageW((HWND)struct_count_list->win_handle, 0x111u, 0x40Bu, 0);,这样wParam就被赋值0x40B,就可以出现成功的提示了。(此期间尝试在X64DBG中对基址+1870地址下断,根本断不下来,说明验证过程中没有调用这里)
继续IDA中静态分析,查找check_right_401870函数调用关系,(IDA操作:鼠标定位于check_right_401870函数中,右键“邻近浏览器”),在邻近浏览器图表视图中通过双击父结点,及隐藏非重要节点,得出如下流程图:
成功只有一个路线,IDA的这个功能直的很有用,如果想爆破实现的话,那就可以按图索骥了。
3.逐段分析
这个过程是漫长的,也可以说是经验积累的过程,大牛通常能一眼看穿的代码,而新手就要推敲半天,我想这就是大牛和新手之间最大的差别吧~~~。
3.1msgcheck_2120
此函数代码很长,下面仅贴上关键部分(IDA F5 伪代码):
[C++] 纯文本查看 复制代码 if ( (unsigned __int16)wParam == 0x3EB ) // 点击了OK按钮
{
sub_402790(String, 10, (const char *)L"%d", ++dword_420318);
v4 = GetDlgItem(hWnd, 1000);
SetWindowTextW(v4, String);
v5 = (struct_count_array *)operator new(0x14u);
v40 = 0;
v6 = v5;
*(_OWORD *)v39 = '\0';
v7 = GetDlgItem(hWnd, 1000);
GetWindowTextW(v7, v39, 10);
vsscanf_27C0((int)v39, (const char *)L"%d", &v19);
*(_DWORD *)&v6->Now_count = v19;
v6->win_handle = (int)hWnd;
v6->passwd_handle = (int)GetDlgItem(hWnd, 1002);
v6->count_handle = (int)GetDlgItem(hWnd, 1000);
v6->field_10 = (int)v6;
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)check_01_4020E0, v6, 0, 0);// 调用检查函数
return 0;
}
msgcheck_402120是MSG处理函数,这里处理所有返回的事件,上面代码为OK按钮事件,用CreateThread函数创建一个新线程,处理按钮事件,参数V6,即上面的V5,通过X64DBG中查看,估计是一个结构体,于是在IDA中自定义了一个结构体:如下
[Asm] 纯文本查看 复制代码 struct_count_array struc ; (sizeof=0x14, mappedto_101)
00000000 Now_count dw ?
00000002 db ? ; undefined
00000003 db ? ; undefined
00000004 win_handle dd ?
00000008 count_handle dd ?
0000000C passwd_handle dd ?
00000010 field_10 dd ?
00000014 struct_count_array ends
在IDA中将变量V5指定为结构体指针,这样代码会更加清晰,结构体中:Now_count=当前尝试次数 win_handle=主窗体句柄 count_handle=显示次数文本框句柄 passwd_handle=密码框句柄 field_10=当前结构体指针,通过这结构体可以轻松访问密码框和次数框的文本内容。
结构体大小是20,从v5 = (struct_count_array *)operator new(0x14u);这句中就可以确定。
3.2 check_01_20E0
代码比较少,调用了check_02_401CB0,参数共2个,第一个是我们自定义的那个结构体数据,第二个参数是0
[C++] 纯文本查看 复制代码 int __stdcall check_01_4020E0(struct_count_array *lpMem)
{
check_02_401CB0((int)lpMem, 0);
sub_402CE4(lpMem);
return 0;
}
3.3 check_02_401CB0
伪代码如下:
[C++] 纯文本查看 复制代码 void __fastcall check_02_401CB0(int a1, int a2)
{
struct_count_array *canshu_1; // edi
int v3; // ecx
unsigned int len; // esi
WCHAR *v5; // eax
_WORD *next_passwd; // ebx
clock_t v7; // [esp+4h] [ebp-D0h]
WCHAR String; // [esp+8h] [ebp-CCh]
canshu_1 = (struct_count_array *)a1;
if ( a1 )
{
if ( a2 )
{
v7 = clock();
memset(&String, 0, 200u);
GetWindowTextW((HWND)canshu_1->passwd_handle, &String, 200);
len = 0;
v5 = &String;
if ( String )
{
do
{
++v5;
++len;
}
while ( *v5 );
}
next_passwd = operator new[](2 * (len + 1));
if ( clock() - v7 > 2 )
exit(0);
sub_402870((int)next_passwd, &String); // 修改了PASSWORD
if ( len >= 7 )
{
if ( len <= 7 )
{
check_03_401A60((int)canshu_1, next_passwd);// len>=7 并且<=7 ,所以密码长度只能为7位
return;
}
SendMessageW((HWND)canshu_1->win_handle, 0x40Du, 0, 0);
}
else
{
SendMessageW((HWND)canshu_1->win_handle, 0x40Eu, 0, 0);
}
j_j_j___free_base(next_passwd);
return;
}
if ( checkhave_b_401C00((HWND *)a1) // 检查是否含有‘b’
&& (memset(&String, 0, 0xC8u),
GetWindowTextW((HWND)canshu_1->passwd_handle, &String, 100),
checkhave_402A50(v3, (__int16 *)&String, 'p')) )// 检查是否含有 p
{
check_02_401CB0((int)canshu_1, 1);
}
else
{
SendMessageW((HWND)canshu_1->win_handle, 0x111u, 0x40Fu, 0);
}
}
}
这个函数很有意思,参数a2=0时,检测密码中是否含有字母‘p’和‘b’,如果有则调用自身,传a2参数为1,当a2=1时,检查密码长度是否为7位,通过这两个条件即可进入下一验证环节。
3.4check_03_1A60
这个函数是一个加密函数,check_03_401A60(struct_count_array *a1, _WORD *a2),a2取我们输入的密码,在这个过程中被加密,加密过程也比较简单,只是做了一下异或加密。前半部分分析了半天貌似没有实际做用,估计是作者加的干扰代码。
函数最后调用:return check_right_401870(next_canshu_a1, next_passwd);,将加密后的密码传入下一验证步骤。
3.5check_right_1870
伪代码如下:
[C++] 纯文本查看 复制代码 BOOL __fastcall check_right_401870(struct_count_array *a1, _WORD *a2)
{
struct_count_array *struct_count_list; // ebx
_WORD *password; // edi
char *char_N; // ecx
signed int v5; // eax
signed int v6; // eax
__int16 *v7; // ecx
unsigned int count; // edx
__int16 *i; // eax
unsigned int v10; // ecx
unsigned int v11; // eax
unsigned int len_pw; // ecx
_WORD *v13; // eax
unsigned int v14; // eax
unsigned int len_pw1; // edx
_WORD *j; // eax
unsigned int v17; // ecx
unsigned int v18; // eax
int n; // esi
__int16 enc_s; // cx
__int64 *v21; // ebx
__int16 *_pswd; // eax
__int16 v23; // dx
__int16 *v24; // ecx
int v25; // eax
int v26; // ecx
__int64 *v27; // eax
_WORD *v28; // esi
unsigned int v29; // ecx
struct_count_array *tmp_arg_1; // [esp+Ch] [ebp-54h]
__int16 enc_list[28]; // [esp+10h] [ebp-50h]
char v33; // [esp+48h] [ebp-18h]
__int64 v34; // [esp+50h] [ebp-10h]
__int16 v35; // [esp+58h] [ebp-8h]
struct_count_list = a1;
password = a2;
tmp_arg_1 = a1;
memset(enc_list, 0, 54u);
char_N = &v33;
v5 = '0';
do
{
*(_WORD *)char_N = v5;
char_N += 2;
++v5;
}
while ( v5 <= '9' ); // 填充数字 0-9
v6 = 'a';
v7 = enc_list;
do
{
*v7 = v6;
++v7;
++v6;
}
while ( v6 <= 'z' ); // 填充小写字母 a-z
count = 0;
for ( i = enc_list; *i; ++count ) // 计算个数
++i;
v10 = 0;
if ( count )
{
do
{
v11 = (unsigned __int16)enc_list[v10];
if ( v11 >= 'a' && v11 <= 'z' )
enc_list[v10] = v11 - 32;
++v10;
}
while ( v10 < count ); // 小字字母转为大写字母
}
len_pw = 0;
v13 = password;
if ( password )
{
if ( *password )
{
do
{
++v13;
++len_pw;
}
while ( *v13 ); // count_pw = Password长度
}
v14 = 0;
if ( len_pw )
{
do
{
if ( v14 >= 2 )
{
if ( v14 >= 4 )
password[v14] ^= 'B';
else
password[v14] ^= 'P';
}
else
{
password[v14] ^= 0xFu;
}
++v14;
}
while ( v14 < len_pw );
} // 还原已经加密过的PASSWORD
len_pw1 = 0;
for ( j = password; *j; ++len_pw1 )
++j;
v17 = 0;
if ( len_pw1 )
{
do
{
v18 = (unsigned __int16)password[v17];
if ( v18 >= 'a' && v18 <= 'z' )
password[v17] = v18 - 32;
++v17;
}
while ( v17 < len_pw1 ); // password小写转大写
}
}
n = 0;
v34 = '\0';
v35 = 0;
if ( *password )
{
enc_s = enc_list[0];
v21 = &v34;
_pswd = password;
do
{
if ( enc_s )
{
v23 = *_pswd;
v24 = enc_list;
v25 = 0;
while ( v23 != *v24 )
{
v24 = &enc_list[++v25];
if ( !enc_list[v25] )
goto LABEL_37;
}
*(_WORD *)v21 = enc_list[v25];
v21 = (__int64 *)((char *)v21 + 2);
LABEL_37:
enc_s = enc_list[0];
}
_pswd = &password[++n];
}
while ( password[n] );
struct_count_list = tmp_arg_1;
} // 取出第一个字母的位置 V34
v26 = 0;
v27 = &v34;
if ( (_WORD)v34 )
{
do
{
v27 = (__int64 *)((char *)v27 + 2);
++v26;
}
while ( *(_WORD *)v27 );
if ( v26 == 2 )
{
LODWORD(v34) = '5\01';
HIDWORD(v34) = &unk_420050; // “15PB”
v28 = password + 2;
v35 = 0;
v29 = 0;
while ( *((_WORD *)&v34 + v29) == *v28 )
{
++v29;
++v28;
if ( v29 >= 4 )
{
if ( !sub_401740(struct_count_list, password) )
return PostMessageW((HWND)struct_count_list->win_handle, 0x111u, 0x40Au, 0);// --40B--成功--
return PostMessageW((HWND)struct_count_list->win_handle, 0x111u, 0x40Bu, 0);
}
}
}
}
return PostMessageW((HWND)struct_count_list->win_handle, 0x111u, 0x40Au, 0);
}
这一函数很长,实现的功能也很复杂,函数首先构建一个密码表“123456789AB......Z”,实际用到的只有前9个数字字符,然后还原上一步聚中加密的密码,并转为大写,然后对比了我们输入密码的3至6位是否为“15PB”,这样我们就可以确定我们的密码第3-6位正确的应该是:“15pb”
函数另外调用了sub_401740(struct_count_list, password)来进行验证,IDA伪代码如下:
[C++] 纯文本查看 复制代码 signed int __fastcall sub_401740(_DWORD *a1, _WORD *a2)
{
_WORD *the_password; // ebx
struct_count_array *v3; // edx
__int128 *v4; // ecx
signed int char_1; // eax
int len; // esi
_WORD *v7; // eax
char *new_string; // edi
unsigned __int16 *last_passwd; // eax
int v10; // esi
__int128 *v11; // ecx
__int16 v12; // di
__int16 v13; // ax
__int128 *v14; // ecx
signed int v15; // edx
unsigned __int16 *v17; // [esp+4h] [ebp-50h]
struct_count_array *v18; // [esp+8h] [ebp-4Ch]
char *v19; // [esp+Ch] [ebp-48h]
__int128 charlist_1to9; // [esp+30h] [ebp-24h]
__int64 v21; // [esp+40h] [ebp-14h]
int v22; // [esp+48h] [ebp-Ch]
__int16 v23; // [esp+4Ch] [ebp-8h]
the_password = a2;
v22 = 0;
v3 = (struct_count_array *)a1;
v23 = 0;
v18 = (struct_count_array *)a1;
charlist_1to9 = '\0';
v4 = &charlist_1to9;
char_1 = '1';
v21 = '\0';
do
{
*(_WORD *)v4 = char_1;
v4 = (__int128 *)((char *)v4 + 2);
++char_1;
} // charlist_1to9="123456789"
while ( char_1 <= '9' );
len = 0;
v7 = the_password;
if ( the_password && *the_password )
{
do
{
++v7;
++len;
}
while ( *v7 ); // 计算密码长度
}
new_string = cat_4028D0((char *)&charlist_1to9, &the_password[*(_DWORD *)&v3->Now_count]);
last_passwd = &the_password[len - 1];
v19 = new_string;
v10 = 0;
v17 = last_passwd;
if ( (_WORD)charlist_1to9 )
{
v11 = &charlist_1to9;
v12 = *last_passwd & 1;
while ( 1 )
{
v13 = v12 + (*(_WORD *)v11 >> 2);
if ( v13 == '2' )
break;
if ( v13 != 'd' )
{
v11 = (__int128 *)((char *)&charlist_1to9 + 2 * ++v10);
if ( *(_WORD *)v11 )
continue;
}
new_string = v19;
goto LABEL_12;
}
}
else
{
LABEL_12:
v14 = &charlist_1to9;
v15 = '1';
while ( *(_WORD *)v14 == *(_WORD *)((char *)v14 + (char *)the_password - (char *)&charlist_1to9) )// 第一位 是 1 the_password 是原始密码
{
v15 += 6;
v14 = (__int128 *)((char *)v14 + 2);
if ( v15 > '9' )
{
if ( (unsigned __int16)*the_password + *((unsigned __int16 *)new_string + 9) == 'c'// 第二位即:*((unsigned __int16 *)new_string + 9,所以第
二位是2
&& *v17 == *(_DWORD *)&v18->Now_count + *((unsigned __int16 *)new_string + 6) )// 最后一位:次数+字串里的7,即8
{
return 1;
}
return 0;
}
}
}
return 0;
}
在sub_401740中,验证第一位是否为“1”,函数将密码连接成“123456789”+从调用次数开始截取到最后的密码部分,比如第一次点OK时密码是“ABCDEFG”,连接后即为“123456789BCDEFG”,验证第一位+第二位是否等于‘c’,所以第二位为‘2’,通过取“123456789BCDEFG”中第(7+验证次数)位与我们输入密码的最后一位比较,来验证最后一位,所以第一次验证时最后一位应该是“8”,但第二次验证则无法验证成功第二位的2,因为2被截去了,所以只能在第一次按OK时输入“1215pb8”才能通过验证。
至此整个分析过程完成,并成功找到密码。
截个图:
4.反调试分析
关于反调试:程序启动有反调试:2处:
1、比较父进程名称,是否为exeplorer.exe
0109152C | 74 04 | je crack_.1091532 |
0109152E | 84 D2 | test dl, dl |
01091530 | 74 1A | je crack_.109154C | 跳去结束程序
01091532 | 57 | push edi |
01091533 | FF 15 60 80 0A 01 | call dword ptr ds:[<&CloseHandle>] |
01091539 | 8B 4D FC | mov ecx, dword ptr ss:[ebp-0x4] |
0109153C | 33 C0 | xor eax, eax |
0109153E | 5F | pop edi |
0109153F | 5E | pop esi |
01091540 | 33 CD | xor ecx, ebp |
01091542 | 5B | pop ebx |
01091543 | E8 7D 17 00 00 | call crack_.1092CC5 |
01091548 | 8B E5 | mov esp, ebp |
0109154A | 5D | pop ebp |
0109154B | C3 | ret |
0109154C | 6A 00 | push 0x0 |
0109154E | E8 5E 36 00 00 | call <crack_.sub_1094BB1> |
2、通过以下代码,比较运行中延时时间,大于2毫秒,则认为被调试,程序退出,这块没明白具体原理,下断结果3毫秒,比判断多了1毫秒,不知道是怎么产生的,猜测是程序被调试响应速度慢。
[Asm] 纯文本查看 复制代码 01091575 | 57 | push edi | 获取时间
01091576 | E8 63 33 00 00 | call <crack_.sub_10948DE> |
0109157B | 8B 35 84 81 0A 01 | mov esi, dword ptr ds:[<&LoadStringW>] |
01091581 | 8B F8 | mov edi, eax |
01091583 | 6A 64 | push 0x64 |
01091585 | 68 20 04 0B 01 | push crack_.10B0420 | 10B0420:L"Crack_Me_4"
0109158A | 6A 67 | push 0x67 |
0109158C | 53 | push ebx |
0109158D | FF D6 | call esi |
0109158F | 6A 64 | push 0x64 |
01091591 | 68 58 03 0B 01 | push crack_.10B0358 | 10B0358:L"CRACK_ME_4"
01091596 | 6A 6D | push 0x6D |
01091598 | 53 | push ebx |
01091599 | FF D6 | call esi |
0109159B | 8B CB | mov ecx, ebx |
0109159D | E8 FE 00 00 00 | call <crack_.sub_10916A0> |
010915A2 | E8 37 33 00 00 | call <crack_.sub_10948DE> |
010915A7 | 2B C7 | sub eax, edi |
010915A9 | 6A 00 | push 0x0 |
010915AB | 83 F8 02 | cmp eax, 0x2 | 比较时间
010915AE | 0F 8F DA 00 00 00 | jg crack_.109168E |
中间也有几处反调试,皆为第2种方法,通过时间差来判断。
附上Crackme程序:
Crack_Me.exe.rar
(124.37 KB, 下载次数: 33)
IDA数据文件:
Crack_Me.idb.rar
(456.83 KB, 下载次数: 21)
|
免费评分
-
查看全部评分
|