看雪CTF2016CrackMe攻防大赛——第一题
Posted roachs
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了看雪CTF2016CrackMe攻防大赛——第一题相关的知识,希望对你有一定的参考价值。
前言
暑假来了,不知道做些什么好,就拿看雪CTF的题来练习练习,学习下大佬们的操作。这是2016年CrackMe攻防赛的第一题,我就被难到了。本来都已经放弃了,但是幸得大佬分享,故跟随大佬的步伐粗略分析了下。
准备
系统:Windows 7 x64 ultimate
工具:IDA pro v7.0(最好用7.0,用6.8会有些问题)
分析
首先运行Crack_Me。
有一个密码输入框,一个OK按钮和一个计数框。
随便输入密码,获得失败提示,提示失败。
载入OD,先运行一下,可是却发现程序直接终止了,看来是有反调试了。
我不会过反调试,所以采用了附加的方式,想着绕过检测,但是它应该有基于延迟时间的检测,所以最终也没动态调试成功。
后来看了大佬的文章,采用了IDA静态分析的办法。
载入IDA中,搜索那个失败提示字符串,快捷键是Alt + T
。
发现失败提示的弹窗调用。
.text:0040241C loc_40241C: ; CODE XREF: sub_402120+2D9↑j
.text:0040241C push 0 ; uType
.text:0040241E push offset Caption ; "Failed"
.text:00402423 push offset Text ; "something you lost!"
.text:00402428 push 0 ; hWnd
.text:0040242A call ds:MessageBoxW
.text:00402430 jmp loc_402775
向下翻,可以看到Successed
字符串,那么这里应该就是调用成功弹窗的代码了。
F5查看此处伪代码。
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:
*(_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 ( v20[0] )
{
v11 = 0;
v12 = v20;
do
{
++v10;
*v12 ^= *(_WORD *)(v11 + lParam);
可以发现是通过一个标志来判断调用不同的弹窗的,看来是通过传递消息来完成的,而成功的消息是0x40B
。
上下翻看,并没有发现检测函数调用,说明这个函数就只是一个消息框函数,先记住该函数是sub_2120
。
既然是传递消息,那么就需要找到消息传递的地方,搜索立即数0x40B
,快捷键是Alt + I
。
有一个压栈操作,有可能就是传参操作。双击点开发现是利用PostMessageW
传递消息的。
v26 = 0;
v27 = &v34;
if ( (_WORD)v34 )
{
do
{
v27 = (__int64 *)((char *)v27 + 2);
++v26;
}
while ( *(_WORD *)v27 );
if ( v26 == 2 )
{
LODWORD(v34) = ‘5 1‘;
HIDWORD(v34) = &unk_420050;
v28 = v3 + 2;
v35 = 0;
v29 = 0;
while ( *((_WORD *)&v34 + v29) == *v28 )
{
++v29;
++v28;
if ( v29 >= 4 )
{
if ( !sub_401740(v2, v3) )
return PostMessageW(*(HWND *)(v2 + 4), 0x111u, 0x40Au, 0);
return PostMessageW(*(HWND *)(v2 + 4), 0x111u, 0x40Bu, 0);
}
}
}
}
那么该函数就是一个检测函数,如果检测通过就传递成功的消息。
该函数是sub_401870
,那么我们就找一下有哪些函数调用了这里。
切到反汇编窗口,在sub_401870函数中右击鼠标,选择Proximity browser
,可以看到函数调用图。
我们向上双击点开父节点,最后可以得到该调用图。
简单分析可以发现只有一条路可以到达最后的消息传递函数,也就是只有一条路可以最后调用成功,那么我们就逐步分析。
sub_402120
查看函数伪代码。
if ( (unsigned __int16)wParam == 0x3EB )
{
sub_402790(String, 10, (const char *)L"%d", ++dword_420318);
v4 = GetDlgItem(hWnd, 0x3E8);
SetWindowTextW(v4, String);
v5 = operator new(0x14u);
v40 = 0;
v6 = v5;
*(_OWORD *)v39 = 0i64;
v7 = GetDlgItem(hWnd, 0x3E8);
GetWindowTextW(v7, v39, 10);
sub_4027C0(v39, (const char *)L"%d", &v19);
*v6 = v19;
v6[1] = hWnd;
v6[3] = GetDlgItem(hWnd, 0x3EA);
v6[2] = GetDlgItem(hWnd, 0x3E8);
v6[4] = v6;
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)sub_4020E0, v6, 0, 0);
return 0;
}
该函数通过创建新线程的方法调用了sub_4020E0函数,而这一块函数正是OK按钮的事件处理函数。因为这里的wParam == 0x3EB,而0x3EB是OK按钮的消息。
CreateWindowExW(0, L"button", L"OK", 0x50000000u, 160, 60, 250, 50, hWnd, (HMENU)0x3EB, hInstance, 0);
CreateThread函数以sub_4020E0函数为线程入口,而v6则作为参数传递。从v6的赋值可以猜测v6是一个结构体,而v6 = v5,那么v5就是一个结构体。这里创建一个结构体赋给v5。
00000000 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 Now_struct dd ?
00000014 struct_count_array ends
这样的话看起来就很有感觉了。
sub_4020E0
该函数的代码很少,主要是调用了两个函数。
int __stdcall sub_4020E0(struct_count_array *lpMem)
{
sub_401CB0((int)lpMem, 0);
sub_402CE4(lpMem);
return 0;
}
将结构体和一个0传给sub_401CB0。
sub_401CB0
查看伪代码。
void __fastcall sub_401CB0(struct_count_array *a1, int a2)
{
struct_count_array *v2; // edi
unsigned int v3; // esi
WCHAR *v4; // eax
void *v5; // ebx
clock_t v6; // [esp+4h] [ebp-D0h]
WCHAR String; // [esp+8h] [ebp-CCh]
v2 = a1;
if ( a1 )
{
if ( a2 )
{
v6 = clock();
memset(&String, 0, 0xC8u);
GetWindowTextW((HWND)v2->passwd_handle, &String, 200);
v3 = 0;
v4 = &String;
if ( String )
{
do
{
++v4;
++v3;
}
while ( *v4 ); //计算输入的密码长度
}
v5 = operator new[](2 * (v3 + 1));
if ( clock() - v6 > 2 )
exit(0);
sub_402870(v5, &String); //将密码改为WORD型保存
if ( v3 >= 7 )
{
if ( v3 <= 7 ) //判断是否为7
{
sub_401A60(v2, v5);
return;
}
SendMessageW((HWND)v2->win_handle, 0x40Du, 0, 0);
}
else
{
SendMessageW((HWND)v2->win_handle, 0x40Eu, 0, 0);
}
j_j_j___free_base(v5);
return;
}
if ( sub_401C00((HWND *)a1) //检测‘b‘
&& (memset(&String, 0, 0xC8u), GetWindowTextW((HWND)v2->passwd_handle, &String, 100), sub_402A50(‘p‘)) ) //检测‘p‘
{
sub_401CB0(v2, 1);
}
else
{
SendMessageW((HWND)v2->win_handle, 0x111u, 0x40Fu, 0);
}
}
}
函数有两块部分,根据传入a2值的不同选择不同的部分。当a2 == 0
时,先检测输入的字符串中是否有b和p。然后再次调用自己,此时传入的a2值为1。
当a2 == 1
时,检测输入的密码长度是否为7,若为7,则调用sub_401A60,并传递结构体与修改后的密码。
sub_401A60
查看伪代码。
BOOL __fastcall sub_401A60(struct_count_array *a1, _WORD *a2)
{
struct_count_array *v2; // edi
_WORD *v3; // ebx
unsigned int v4; // ecx
int *i; // eax
unsigned __int8 v6; // cf
unsigned int v7; // ecx
int *v8; // edi
int j; // ecx
unsigned int v10; // ecx
int *k; // eax
unsigned int v12; // ecx
int *v13; // edi
int l; // ecx
int v15; // ecx
_WORD *v16; // eax
_WORD *v17; // esi
int v18; // eax
__int16 v19; // cx
int v20; // edx
unsigned int v21; // ecx
_WORD *v22; // eax
unsigned int v23; // eax
struct_count_array *v25; // [esp+10h] [ebp-198h]
int v26[50]; // [esp+14h] [ebp-194h]
int lParam[50]; // [esp+DCh] [ebp-CCh]
v2 = a1;
v3 = a2;
v25 = a1;
memset(lParam, 0, 0xC8u);
memset(v26, 0, 0xC8u);
SendMessageW((HWND)v2->passwd_handle, 0xDu, 0xDu, (LPARAM)lParam);
SendMessageW((HWND)v2->win_handle, 0x111u, 0x40Cu, (LPARAM)v26);
v4 = 0;
for ( i = lParam; *(_WORD *)i; ++v4 )
i = (int *)((char *)i + 2);
if ( v4 )
{
v6 = v4 & 1;
v7 = v4 >> 1;
memset(lParam, 0, 4 * v7);
v8 = &lParam[v7];
for ( j = v6; j; --j )
{
*(_WORD *)v8 = 0;
v8 = (int *)((char *)v8 + 2);
}
}
v10 = 0;
for ( k = v26; *(_WORD *)k; ++v10 )
k = (int *)((char *)k + 2);
if ( v10 )
{
v6 = v10 & 1;
v12 = v10 >> 1;
memset(v26, 0, 4 * v12);
v13 = &v26[v12];
for ( l = v6; l; --l )
{
*(_WORD *)v13 = 0;
v13 = (int *)((char *)v13 + 2);
}
}
v15 = 0;
v16 = v3;
if ( v3 && *v3 )
{
do
{
++v16;
++v15;
}
while ( *v16 );
}
v17 = operator new[](2 * (v15 + 1));
v18 = 0;
if ( *v3 )
{
v19 = *v3;
v20 = 0;
do
{
++v18;
v17[v20] = v19;
v20 = v18;
v19 = v3[v18];
}
while ( v19 );
}
v21 = 0;
v17[v18] = 0;
v22 = v17;
if ( v17 )
{
if ( *v17 )
{
do
{
++v22;
++v21;
}
while ( *v22 );
}
v23 = 0;
if ( v21 )
{
do
{
if ( v23 >= 2 )
{
if ( v23 >= 4 )
v17[v23] ^= ‘B‘;
else
v17[v23] ^= ‘P‘;
}
else
{
v17[v23] ^= 0xFu;
}
++v23;
}
while ( v23 < v21 ); //对密码进行加密
}
}
j_j_j___free_base(v3);
return sub_401870((int)v25, v17);
}
这是一个加密函数,对密码进行了简单的异或运算。然后将加密后的密码与结构体传给sub_401870函数。
根据最后传给sub_401870函数的参数来看,该函数的上半部分没有什么用,应该是垃圾代码,用来混淆视听的。
sub_401870
查看伪代码。
BOOL __fastcall sub_401870(struct_count_array *a1, _WORD *a2)
{
struct_count_array *str_1; // ebx
_WORD *en_pass; // edi
char *v4; // ecx
signed int v5; // eax
signed int v6; // eax
__int16 *v7; // ecx
unsigned int v8; // edx
__int16 *i; // eax
unsigned int v10; // ecx
unsigned int v11; // eax
unsigned int v12; // ecx
_WORD *v13; // eax
unsigned int v14; // eax
unsigned int v15; // edx
_WORD *j; // eax
unsigned int v17; // ecx
unsigned int v18; // eax
int v19; // esi
__int16 v20; // cx
__int64 *v21; // ebx
__int16 *v22; // 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 *str_2; // [esp+Ch] [ebp-54h]
__int16 charlist_ATOZ[28]; // [esp+10h] [ebp-50h]
char charlist_1to9; // [esp+48h] [ebp-18h]
__int64 v34; // [esp+50h] [ebp-10h]
__int16 v35; // [esp+58h] [ebp-8h]
str_1 = a1;
en_pass = a2;
str_2 = a1;
memset(charlist_ATOZ, 0, 0x36u);
v4 = &charlist_1to9;
v5 = ‘0‘;
do
{
*(_WORD *)v4 = v5;
v4 += 2;
++v5;
}
while ( v5 <= ‘9‘ ); //填充数字0~9
v6 = ‘a‘;
v7 = charlist_ATOZ;
do
{
*v7 = v6;
++v7;
++v6;
}
while ( v6 <= ‘z‘ ); //填充小写字母a~z
v8 = 0;
for ( i = charlist_ATOZ; *i; ++v8 )
++i;
v10 = 0;
if ( v8 )
{
do
{
v11 = (unsigned __int16)charlist_ATOZ[v10];
if ( v11 >= ‘a‘ && v11 <= ‘z‘ )
charlist_ATOZ[v10] = v11 - 32;
++v10;
}
while ( v10 < v8 ); //将小写字母转为大写字母
}
v12 = 0;
v13 = en_pass;
if ( en_pass )
{
if ( *en_pass )
{
do
{
++v13;
++v12;
}
while ( *v13 ); //计算密码长度
}
v14 = 0;
if ( v12 )
{
do
{
if ( v14 >= 2 )
{
if ( v14 >= 4 )
en_pass[v14] ^= ‘B‘;
else
en_pass[v14] ^= ‘P‘;
}
else
{
en_pass[v14] ^= 0xFu;
}
++v14;
}
while ( v14 < v12 ); //将加密的密码解密
}
v15 = 0;
for ( j = en_pass; *j; ++v15 )
++j;
v17 = 0;
if ( v15 )
{
do
{
v18 = (unsigned __int16)en_pass[v17];
if ( v18 >= ‘a‘ && v18 <= ‘z‘ )
en_pass[v17] = v18 - 32;
++v17;
}
while ( v17 < v15 ); //将密码中的小写字母转为大写字母
}
}
v19 = 0;
v34 = ‘ ‘;
v35 = 0;
if ( *en_pass )
{
v20 = charlist_ATOZ[0];
v21 = &v34;
v22 = en_pass;
do
{
if ( v20 )
{
v23 = *v22;
v24 = charlist_ATOZ;
v25 = 0;
while ( v23 != *v24 )
{
v24 = &charlist_ATOZ[++v25];
if ( !charlist_ATOZ[v25] )
goto LABEL_37;
}
*(_WORD *)v21 = charlist_ATOZ[v25];
v21 = (__int64 *)((char *)v21 + 2);
LABEL_37:
v20 = charlist_ATOZ[0];
}
v22 = &en_pass[++v19];
}
while ( en_pass[v19] );
str_1 = str_2;
} //记录密码中所有字母的位置
v26 = 0;
v27 = &v34;
if ( (_WORD)v34 )
{
do
{
v27 = (__int64 *)((char *)v27 + 2);
++v26;
}
while ( *(_WORD *)v27 ); //统计密码中字母的个数
if ( v26 == 2 ) //密码中的字母个数必须为2个
{
LODWORD(v34) = 0x350031; //设置v34低字为15
HIDWORD(v34) = &unk_420050; //设置v34高字为pb
v28 = en_pass + 2;
v35 = 0;
v29 = 0;
while ( *((_WORD *)&v34 + v29) == *v28 )
{
++v29;
++v28;
if ( v29 >= 4 )
{
if ( !sub_401740(str_1, en_pass) )
return PostMessageW((HWND)str_1->win_handle, 0x111u, 0x40Au, 0);
return PostMessageW((HWND)str_1->win_handle, 0x111u, 0x40Bu, 0);
}
}
}
}
return PostMessageW((HWND)str_1->win_handle, 0x111u, 0x40Au, 0);
}
该函数有些复杂,我一开始也没看懂,最后是看了大佬的文章又想了想,才明白了一点。
大致过程就是i,先生成了两个表,一个数字0~9的表,一个大写字母表。然后解密密码,将密码中的小写字母转为大写字母。随后统计密码中的字母个数并检测密码从第三位到第六位是否为15pb
。通过sub_401740函数检测第一位、第二位和最后一位,通过后传递成功消息。
sub_401740
该函数的参数为结构体和解密后的密码。
查看伪代码。
signed int __fastcall sub_401740(struct_count_array *a1, _WORD *a2)
{
_WORD *passwd; // ebx
struct_count_array *str_1; // edx
__int128 *v4; // ecx
signed int v5; // eax
int passwd_len; // esi
_WORD *v7; // eax
_WORD *v8; // edi
unsigned __int16 *v9; // 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 *str_2; // [esp+8h] [ebp-4Ch]
_WORD *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]
passwd = a2;
v22 = 0;
str_1 = a1;
v23 = 0;
str_2 = a1;
charlist_1to9 = ‘ ‘;
v4 = &charlist_1to9;
v5 = ‘1‘;
v21 = ‘ ‘;
do
{
*(_WORD *)v4 = v5;
v4 = (__int128 *)((char *)v4 + 2);
++v5;
}
while ( v5 <= ‘9‘ ); //填充数字1~9
passwd_len = 0;
v7 = passwd;
if ( passwd && *passwd )
{
do
{
++v7;
++passwd_len;
}
while ( *v7 ); //计算密码长度
}
v8 = sub_4028D0((char *)&charlist_1to9, &passwd[*(_DWORD *)&str_1->Now_count]); //截取密码连接在数字表后面
v9 = &passwd[passwd_len - 1];
v19 = v8;
v10 = 0;
v17 = v9;
if ( (_WORD)charlist_1to9 )
{
v11 = &charlist_1to9;
v12 = *v9 & 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;
}
v8 = v19;
goto LABEL_12;
}
}
else
{
LABEL_12:
v14 = &charlist_1to9;
v15 = ‘1‘;
while ( *(_WORD *)v14 == *(_WORD *)((char *)v14 + (char *)passwd - (char *)&charlist_1to9) ) //第一位是1
{
v15 += 6;
v14 = (__int128 *)((char *)v14 + 2);
if ( v15 > ‘9‘ )
{
if ( (unsigned __int16)*passwd + (unsigned __int16)v8[9] == ‘c‘ //第二位是2
&& *v17 == *(_DWORD *)&str_2->Now_count + (unsigned __int16)v8[6] ) //最后一位7+次数,即8
{
return 1;
}
return 0;
}
}
}
return 0;
}
该函数也很复杂,对我来说也是挺有难度的。大致过程是,先生成1~9的数字表,然后从输入密码次数+1
的位置开始截取密码,即假如输入‘ABCDEFG‘,次数为1,则截取后的字符串为‘BCDEFG‘,然后将其连接在数字表的后面,得到‘123456789BCDEFG‘。
根据循环的条件可以知道第一位为1,第一位+第二位=‘c‘,所以第二位为2,因为次数参与了运算,所以从第二次的验证开始就无法验证第二位了,所以只能一次输对。最后一位是次数+7,所以即8。
最后得到正确密码为:1215pb8。
参考
- 详细分析看雪2016CTF第一题Crack_ME用IDA+X64DBG追码
https://www.52pojie.cn/thread-755611-1-1.html
(出处: 吾爱破解论坛)
以上是关于看雪CTF2016CrackMe攻防大赛——第一题的主要内容,如果未能解决你的问题,请参考以下文章