【CTF REVERSE】WHCTF2017-CRACKME

时间:2023-03-08 21:58:36
【CTF REVERSE】WHCTF2017-CRACKME

1、前言

假装大学生水一下CTF题目,常规思路。程序没有加壳,是VC写的MFC程序。

2、破题思路

  • 1、MessageBox 下断点
  • 2、找到提示错误字符串的函数B
  • 3、跟踪函数
  • 4、跟踪算法

3、实现过程

PEID查询无壳,进IDA查看字符串。

【CTF REVERSE】WHCTF2017-CRACKME

得到这个字符串的一个存放地址,向上跟踪到有这个字符串的地方,依稀可以看到加密后的Flag轮廓。

.data:00403254 aVSIKTQU        db '注册码要往这里填呀 ! ',0     ; DATA XREF: sub_401230+40o

【CTF REVERSE】WHCTF2017-CRACKME

当即想到有字符串就会有弹框的地方,查找所有模块后,在MessageBox断点。

【CTF REVERSE】WHCTF2017-CRACKME

运行-输入Flag的地方输入【ABCDEFG1234567】,然后通过堆栈窗口跟踪到调用弹窗的地方。

【CTF REVERSE】WHCTF2017-CRACKME

拿到弹窗地址00401759,到IDA里的窗口查看对比得到调用Flag输入错误提示信息的地址。

sub_401720 调用提示错误字符串的函数B

【CTF REVERSE】WHCTF2017-CRACKME

在sub_401720处断点,截获到存着flag的寄存器。说明计算离计算flag的距离已经不远了。

继续通过堆栈窗口跳到调用401720这个函数的地方,也就是00401621这个地址

00401621 调用提示字符串B - 错误

调用00401621地址也就是错误提示的地方来自以上的一个判断,这里应该有一个判断。但是IDA识别不出来,自己手动创建函数后程序结构判断一目了然。

int __thiscall sub_4015E0(CWnd *this)
{
CWnd *v1; // esi@1
int result; // eax@3
int v3; // [sp-4h] [bp-Ch]@2
int *v4; // [sp+4h] [bp-4h]@2 v1 = this;
CWnd::UpdateData(this, 1);
if ( *(_DWORD *)(*((_DWORD *)v1 + 165) - 8) == 33
&& (v3 = *((_DWORD *)v1 + 165), v4 = &v3, CString::CString(&v3, (char *)v1 + 660), (unsigned __int8)sub_401630(v3)) )
{
result = sub_4016E0(v1);
}
else
{
result = sub_401720(v1);
}
return result;
}

依赖IDA向上追溯已经没法子了,那么通过程序中可以看到会对比一个33的字符,那么这个判断里非常有可能就是计算flag的地方。计算要求必须要33个字符,那么我们就整个33字符输入进去即可!输入构造好的33个字符的字符串【ABCDEFG123456789qwertyuiopzxcvbnm】

反汇编的注释如下:


004015E0 push ecx ; 调用错误提示的函数
004015E1 push esi
004015E2 mov esi,ecx
004015E4 push 0x1
004015E6 call <jmp.&MFC42.#6334> ; MFC更新数据函数UpdateData()
004015EB mov ecx,dword ptr ds:[esi+0x294]; 将FLAG字符串存储到ECX寄存器中
004015F1 lea eax,dword ptr ds:[esi+0x294]
004015F7 cmp dword ptr ds:[ecx-0x8],0x21 ; 对比是否有33个字符
004015FB jnz short WHCTF201.0040161F
004015FD push ecx
004015FE mov ecx,esp
00401600 mov dword ptr ss:[esp+0x8],esp
00401604 push eax
00401605 call <jmp.&MFC42.#535> ; MFC_CString函数
0040160A mov ecx,esi
0040160C call WHCTF201.00401630 ; 这个函数出来之后就比较是否跳到提示错误弹窗函数
00401611 test al,al ; 再一次比较是否到调用提示错误的弹窗函数
00401613 je short WHCTF201.0040161F
00401615 mov ecx,esi
00401617 call WHCTF201.004016E0 ; 调用提示成功的弹窗函数
0040161C pop esi ; 0012FEB4
0040161D pop ecx ; 0012FEB4
0040161E retn
0040161F mov ecx,esi ; 调用提示错误的弹窗函数
00401621 call WHCTF201.00401720
00401626 pop esi ; 0012FEB4
00401627 pop ecx ; 0012FEB4
00401628 retn

反汇编代码中首先对比了一个输入的字符串是否有33个字符,如果没有就调用提示错误的弹窗函数。计算Flag通常会有两次判断,一次是判断字符串长度是否正确,然后判断输入的字符串是否正确。以上汇编中从00401630这个函数出来后会进行结果的判断然后根据结果是否跳转到调用提示错误弹窗函数的地方。

跟进00401630函数,OD反汇编代码如下:

00401630 push ecx                               ;  Flag计算函数
00401631 push ebx ; msvcrt.srand
00401632 mov ebx,dword ptr ds:[<&MSVCRT.srand>] ; msvcrt.srand
00401638 push ebp
00401639 push esi
0040163A push edi
0040163B xor edi,edi
0040163D mov ebp,ecx ; -CRACKME
0040163F mov byte ptr ss:[esp+0x13],0x1
00401644 mov edx,0xA
00401649 xor esi,esi
0040164B push edx
0040164C call ebx ; msvcrt.srand
0040164E add esp,0x4
00401651 call dword ptr ds:[<&MSVCRT.rand>] ; [rand
00401657 cdq
00401658 mov ecx,0xA
0040165D idiv ecx ; 有符号除法
0040165F mov ecx,dword ptr ss:[esp+0x18] ; flag的值存储到ecx中
00401663 mov cl,byte ptr ds:[edi+ecx]
00401666 lea eax,dword ptr ds:[esi+edx]
00401669 cmp cl,byte ptr ds:[eax+ebp+0x60] ; flag{出现的地方
0040166D je short WHCTF201.00401674
0040166F mov byte ptr ss:[esp+0x13],0x0
00401674 add esi,0xA ; esi+10
00401677 inc edi ; 目标操作数+1
00401678 cmp esi,0x14A ; 330
0040167E jl short WHCTF201.0040164B
00401680 lea ecx,dword ptr ss:[esp+0x18]
00401684 call <jmp.&MFC42.#800>
00401689 mov al,byte ptr ss:[esp+0x13]
0040168D pop edi
0040168E pop esi
0040168F pop ebp
00401690 pop ebx ; msvcrt.srand
00401691 pop ecx
00401692 retn 0x4

Flag出现的地方,就是0040165F这里。

ABCDEFG123456789qwertyuiopzxcvbnm
flag{The-Y3ll0w-turb4ns-Upri$ing}

思路小结:

  • 1、MessageBox 下断点
  • 2、找到提示错误字符串的函数B
  • 3、有个CMP对比字符串是不是等于0x21(33)个字符
  • 4、调用两个CALL,MFC自带的跳过
  • 5、跟进00401630 计算flag函数
  • 6、00401663 读取flag字符
  • 7、0040165F 转换为Key值

4、附件下载

https://github.com/zprogram/zprogram.github.io/blob/master/Blog_attachment/CTF/WHCTF2017-CRACKME.zip