逆向体验极好,最后一步的猜谜体验极差。
GUI 程序先拖到 Resource Hacker 里看资源,提取出一个压缩包,最重要的部分就是其中的 dlg_main.xml ,即主界面布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?
xml
version
=
"1.0"
?>
<
SOUI
alpha
=
"255"
appWnd
=
"1"
bigIcon
=
"ICON_LOGO:32"
height
=
"300"
margin
=
"0,0,0,0"
name
=
"mainWindow"
resizable
=
"0"
smallIcon
=
"ICON_LOGO:16"
translucent
=
"1"
width
=
"600"
>
<
root
cache
=
"1"
ncskin
=
"skin_bg_shadow"
colorBkgnd
=
"#e6e6faff"
>
<
caption
pos
=
"0,0"
size
=
"600, 300"
show
=
"1"
font
=
"adding:0"
>
<
caption
pos
=
"0,0"
size
=
"600,30"
colorBkgnd
=
"#3cb371ff"
>
<
imgbtn
pos
=
"-40,4"
size
=
"27,22"
tip
=
"关闭"
animate
=
"1"
skin
=
"skin_bg_close"
name
=
"btn_close"
/>
<
text
pos
=
"8,5"
colorText
=
"#ffffffff"
font
=
"face:微软雅黑,size:13"
>CTF 2023</
text
>
</
caption
>
<
caption
pos
=
"1,30"
size
=
"598, 269"
skin
=
"skin_bg_main"
>
<
img
pos
=
"201,10"
size
=
"196,196"
skin
=
"skin_img_logo"
name
=
"img_logo"
/>
<
text
pos
=
"95,224"
font
=
"face:微软雅黑,size:14"
>FLAG:</
text
>
<
edit
pos
=
"144,222"
size
=
"296, 24"
colorBkgnd
=
"#FFFFFF"
cueText
=
"请输入你的答案"
colorText
=
"#000000"
font
=
"face:微软雅黑,size:13"
maxBuf
=
"32"
inset
=
"4,2,4,2"
skin
=
"image_check_png"
name
=
"input_va"
/>
<
imgbtn
pos
=
"456,218"
size
=
"80,32"
tip
=
"验证输入"
animate
=
"1"
font
=
"face:微软雅黑,size:14"
skin
=
"image_btn_png"
name
=
"check_va"
>验证</
imgbtn
>
</
caption
>
</
caption
>
</
root
>
</
SOUI
>
|
没其他东西了, ida 启动。 WinMain (0x401FC0) 的部分初始化(主要是 simulation vftable 的偏移为 2a8 ,后面要用):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
memset
(v30, 0, 0x2DCu);
sub_A487A2(L
"LAYOUT:XML_MAINWND"
);
*(_DWORD *)v30 = &main_dlg::`vftable';
*(_DWORD *)&v30[4] = &main_dlg::`vftable';
*(_OWORD *)&v30[0x2AC] = 0i64;
*(_DWORD *)&v30[0x28] = &main_dlg::`vftable';
*(_DWORD *)&v30[0x2C] = &main_dlg::`vftable';
*(_DWORD *)&v30[0x130] = &main_dlg::`vftable';
*(_DWORD *)&v30[0x2A8] = &simulation::`vftable';
*(_DWORD *)&v30[0x2BC] = 0;
*(_DWORD *)&v30[0x2C0] = 15;
v30[0x2AC] = 0;
*(_DWORD *)&v30[0x2C8] = 0;
*(_DWORD *)&v30[0x2CC] = 0;
*(_DWORD *)&v30[0x2D0] = 0;
*(_DWORD *)&v30[0x2C4] = 0;
*(_DWORD *)&v30[0x2D8] = 0;
*(_DWORD *)&v30[0x268] = 0;
*(_DWORD *)&v30[0x26C] = 0;
*(_DWORD *)&v30[0x270] = 0;
*(_OWORD *)&v30[0x274] = 0i64;
*(_DWORD *)&v30[0x2A4] = 0;
*(_OWORD *)&v30[0x284] = 0i64;
*(_OWORD *)&v30[0x294] = 0i64;
|
上调试器,发现附加时就退出了;在调试器中运行,会触发异常 EXCEPTION_INVALID_HANDLE ,调用栈找到异常位置 0x4078C5 , ida 的 F5 只有一句 CloseHandle((HANDLE)0x99999999);
,实际上用异常隐藏了信息:
如果触发异常就会导致程序退出。实际上程序正常执行的时候是不会触发这个异常的,在有调试器时调试器捕获到这个异常之后传递给应用程序处理就会导致程序退出,所以这是用于反调试的。
这个函数 (0x407880) 查找引用,来到函数 0x405F10 :
1
2
3
4
5
6
7
8
9
10
11
|
v13 = CreateTimerQueue();
*(_DWORD *)(
this
+ 0x294) = v13;
if
( v13 )
{
CreateTimerQueueTimer((
PHANDLE
)(
this
+ 0x298), v13, sub_407880, *(
PVOID
*)(
this
+ 28), 500u, 2000u, 0);
CreateTimerQueueTimer((
PHANDLE
)(
this
+ 0x29C), *(
HANDLE
*)(
this
+ 0x294), sub_407900, 0, 600u, 2000u, 0);
CreateTimerQueueTimer((
PHANDLE
)(
this
+ 0x2A0), *(
HANDLE
*)(
this
+ 0x294), sub_4079D0, 0, 700u, 2000u, 0);
CreateTimerQueueTimer((
PHANDLE
)(
this
+ 0x2A4), *(
HANDLE
*)(
this
+ 0x294), sub_407A60, 0, 800u, 2000u, 0);
}
SetForegroundWindow(*(
HWND
*)(
this
+ 28));
*(_DWORD *)(
this
+ 0x270) = SetTimer(*(
HWND
*)(
this
+ 28), 1u, 100u, TimerFunc);
|
创建了一个定时器队列,将四个函数添加进去(上面分析的是第一个函数),这四个都是反调试。中间两个是常用的 NtSetInformationThread
和 NtQueryInformationProcess
检测调试器,最后一个和第一个类似也是利用调试器存在时才会触发的异常进行反调试:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
void
__stdcall sub_407A60(
PVOID
a1,
BOOLEAN
a2)
{
HANDLE
v2;
// eax
void
*v3;
// esi
v2 = CreateMutexW(0, 0, L
"A2D972DA-0A03-41D4-906B-6EFF73D0C937"
);
v3 = v2;
if
( v2 )
{
SetHandleInformation(v2, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
CloseHandle(v3);
}
}
|
绕过反调试只要把 4 个 CreateTimerQueueTimer 前的 if 判断去掉就行, 0x406102 处的 jz loc_406192
改为 jmp loc_406192
,这就可以愉快调试了。
最后还有一行设置定时器函数 0x406820 ,这个函数的作用是发送消息 (0x47C) 使界面上的图像闪烁,与调试无关。
sub_405F10 查找引用来到函数 sub_405C50 ,前面处理三个系统消息(接收到 WM_CLOSE 就会退出):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
switch
( a3 )
{
case
WM_CREATE:
*(_DWORD *)(
this
+ 0x2D4) = 0;
break
;
case
WM_INITDIALOG:
*(_DWORD *)(
this
+ 0x2D4) = 1;
*a6 = sub_405F10(
this
, v14, v16);
goto
LABEL_9;
case
WM_CLOSE:
*(_DWORD *)(
this
+ 0x2D4) = 1;
sub_405E60(
this
);
break
;
default
:
goto
LABEL_10;
}
|
后面处理自定义的消息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
switch
( a3 )
{
case
0x47A:
v15 = a5;
v13 = (
WCHAR
*)a4;
v12 = 0x47A;
goto
LABEL_22;
case
0x47B:
v15 = a5;
v13 = (
WCHAR
*)a4;
v12 = 0x47B;
goto
LABEL_22;
case
0x47D:
v15 = a5;
v13 = (
WCHAR
*)a4;
v12 = 0x47D;
goto
LABEL_22;
case
0x47C:
v15 = a5;
v13 = (
WCHAR
*)a4;
v12 = 0x47C;
LABEL_22:
v17 = 1;
*a6 = sub_4061D0(
this
, v12, v13, (
int
)v15);
return
msg != 0;
}
if
( a3 != 0x47E )
return
0;
v17 = 1;
*a6 = sub_4061D0(
this
, 0x47E, (
WCHAR
*)a4, (
int
)a5);
return
msg != 0;
|
实际上就是 0x47A 到 0x47E 的消息都会传给 sub_4061D0 处理,进入后即可看到提示的字符串:
为了保证逻辑连贯性,后面从按下按钮开始的过程开始分析,遇到哪种类型的自定义消息再分析其处理过程。
通过布局文件中按钮的名字字符串 check_va
可以定位到函数 sub_405AF0 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
BOOL
__thiscall sub_405AF0(
int
this
,
int
a2)
{
int
v2;
// ebx
int
v3;
// eax
int
v4;
// eax
v2 = 0;
if
( (*(
int
(__stdcall **)(
int
))(*(_DWORD *)a2 + 24))(a2) == 10000 )
{
if
( (*(
int
(__stdcall **)(
int
))(*(_DWORD *)a2 + 88))(a2) )
{
v3 = wcscmp((
const
unsigned
__int16
*)(*(
int
(__stdcall **)(
int
))(*(_DWORD *)a2 + 88))(a2), L
"btn_close"
);
if
( v3 )
v3 = v3 < 0 ? -1 : 1;
if
( !v3 )
{
v2 = 1;
(*(
void
(__stdcall **)(
int
, _DWORD))(*(_DWORD *)a2 + 100))(a2, 0);
sub_405E60(
this
);
if
( !(*(
int
(__stdcall **)(
int
))(*(_DWORD *)a2 + 96))(a2) )
return
1;
}
}
if
( (*(
int
(__stdcall **)(
int
))(*(_DWORD *)a2 + 88))(a2) )
{
v4 = wcscmp((
const
unsigned
__int16
*)(*(
int
(__stdcall **)(
int
))(*(_DWORD *)a2 + 88))(a2), L
"check_va"
);
if
( v4 )
v4 = v4 < 0 ? -1 : 1;
if
( !v4 )
{
++v2;
(*(
void
(__stdcall **)(
int
, _DWORD))(*(_DWORD *)a2 + 100))(a2, 0);
sub_406490(
this
);
if
( !(*(
int
(__stdcall **)(
int
))(*(_DWORD *)a2 + 96))(a2) )
return
1;
}
}
}
if
( (*(
int
(__stdcall **)(
int
))(*(_DWORD *)a2 + 96))(a2) )
v2 += sub_A3FBD7(a2) != 0;
return
v2 != 0;
}
|
那么按下按钮后就会进入函数 sub_406490 。跟入查看。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void
__thiscall sub_406490(
int
this
)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v36 =
this
;
v1 = *(_DWORD *)(
this
+ 0x26C);
if
( !v1 )
return
;
v46 = 0;
input_ = 0i64;
(*(
void
(__thiscall **)(
int
,
__int64
*, _DWORD))(*(_DWORD *)(v1 + 12) + 0x230))(v1 + 12, &input_, 0);
v48 = 0;
if
( SOUI::SStringW::empty(&input_) || SOUI::SStringW::size(&input_) != 32 )
goto
LABEL_42;
|
输入长度为 32 才会进入后面的逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
v2 = SOUI::SStringW::size(&input_);
v3 = SOUI::SStringW::data(&input_, v2);
std::wstring::ctor(&w_input, v3);
v4 = __rdtsc();
srand
(v4);
v5 =
rand
() % 32;
v6 = __rdtsc();
v40 = v5 + 4;
srand
(v6);
v7 =
rand
() % (v5 + 4);
v8 = 0;
v9 = 0;
v10 = 0;
v38 = 0;
v47.start = 0;
v9 = 0;
v47.finish = 0;
v42 = 0;
v47.end_of_storage = 0;
v41 = 0;
LOBYTE(v48) = 2;
v41 = 0;
if
( v40 <= 0 )
goto
LABEL_29;
do
{
if
( v41 == v7 )
{
// ...
}
else
{
// ...
}
vector_pair_WCHAR_ptr_int__::push_back(&v47, v9, v14);
v10 = v47.end_of_storage;
v9 = v47.finish;
v42 = v47.end_of_storage;
LABEL_27:
++v41;
}
while
( v41 < v40 );
|
创建一个 vector<pair<WCHAR*, int>>
容器 (v47) ,进入循环,循环变量 v41 与提前随机生成的值 v7 不等时就会创建一个随机的字符串,但是又将字符串第一个值置为 0 ,之后再随机生成一个 int 值,将这两项压入 vector 中;当 v41 与 v7 相等时,vector 中压入输入的字符串和长度。关键在于 v7 的生成,循环轮数是随机产生的 v40 ,但是 v7 = rand() % v40
,这样就保证 v7 < v40
一定成立,循环里一定有一轮会将输入放进去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
v8 = v47.start;
v39 = v47.start;
LABEL_29:
v29 = v8;
if
( v8 != v9 )
{
v30 = v36;
do
{
(*(
void
(__stdcall **)(
int
,
int
,
WCHAR
*,
int
))(*(_DWORD *)v30 + 176))(v30, 0x47A, v29->first, v29->second);
++v29;
}
while
( v29 != v9 );
v10 = v42;
v8 = v39;
}
|
依次将 vector 中的元素取出,调用某个函数。看到 0x47A 自然想到是发送一个 0x47A 的消息,参数是 vector 中的元素。分析 0x47A 的处理逻辑 (0x4061D0) :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
if
( param1 )
{
if
( !*param1 )
{
j_j_j_free(param1);
return
;
}
sha256_digest(&v18, param1, param2);
v24 = 0;
v7 = (
char
*)&v18;
if
( v18.cap >= 0x10 )
v7 = v18.data.lstr;
sub_406D20(&v19, v7);
LOBYTE(v24) = 1;
if
( v19.size )
{
v8 = (
char
*)&v19;
if
( v19.cap >= 0x10 )
v8 = v19.data.lstr;
(*(
void
(__thiscall **)(
int
,
char
*,
size_t
,
int
))(*(_DWORD *)(
this
+ 0x2A8) + 8))(
this
+ 0x2A8,
v8,
v19.size,
*(_DWORD *)(
this
+ 0x1C));
(*(
void
(__thiscall **)(
int
))(*(_DWORD *)(
this
+ 0x2A8) + 12))(
this
+ 0x2A8);
}
else
{
(*(
void
(__stdcall **)(
int
,
int
,
int
,
int
))(*(_DWORD *)
this
+ 176))(
this
, 0x47D, 0, 0);
}
// std::string dtor
}
|
如果传入的第一个参数 (WCHAR*) 为空或者第一个元素为 0 就会直接返回,所以之前随机生成的那些值没有任何用,只有输入会进入后面的处理逻辑。 sha256_digest 计算输入的 sha256 哈希值(字节的形式),之后进入函数 sub_406D20 使用公钥加密 (e = 17, n = 21906585121072429525136501263777504096756081865092042684099138287497672694873834291670997121471129570594152130723534201262878959784904251279658444106234669253576862136338490628016468594828131996514196656561676389378950188066235426847675556311224351660898776963053168935262766064574259854293773964700038348161447837302470663811641426752301151303723561636330562630171909979887875204514399203706102031815258959587171992732031141351891482029327218735402874813783992338509681802890209021222118623824887702576029614546957547984036089601600189689935707774369388278963379757463175497681264877157619309237813256488481685294697, PKCS1_OAEP
) ,得到结果后依次调用 (*(_DWORD *)(this + 0x2A8) + 8)
和 (*(_DWORD *)(this + 0x2A8) + 12)
, 2a8 这个偏移就是上面一开始说过的 simulation vftable 的偏移,则依次调用其 vftable 中第 3 和第 4 个函数。第 3 个函数 (0x40CF10) :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
void
__thiscall sub_40CF10(simulation *
this
,
char
*data,
size_t
Size,
HWND
handle)
{
std::string *v5;
// ecx
char
*v6;
// esi
if
( data && Size && handle )
{
this
->handle = handle;
v5 = &
this
->encrypted;
this
->encrypted.size = 0;
v6 = (
char
*)v5;
if
( v5->cap >= 0x10 )
v6 = v5->data.lstr;
*v6 = 0;
std::string::assign(v5, data, Size);
}
}
|
将加密结果与窗口 handle 保存在 simulation 对象中。
第 4 个函数 (0x40CF60) :
1
2
3
4
5
6
7
8
9
10
11
|
BOOL
__thiscall sub_40CF60(simulation *
this
)
{
BOOL
result;
// eax
if
(
this
->handle )
{
if
(
this
->encrypted.size )
result = QueueUserWorkItem((LPTHREAD_START_ROUTINE)sub_40CF80,
this
, 0);
}
return
result;
}
|
将 sub_40CF80 添加到队列中执行。进入 sub_40CF80 :
1
2
3
4
5
6
7
|
DWORD
__stdcall sub_40CF80(simulation *
this
)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
if
( !
this
)
return
0;
(*(
void
(__thiscall **)(simulation *))
this
->vtable)(
this
);
|
首先会调用 vftable 中第 1 个函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
void
__thiscall sub_40D6A0(simulation *
this
)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v1 =
this
->strs.finish;
v2 = &
this
->strs;
v3 =
this
->strs.start;
if
( v3 != v1 )
{
std::string::array_dtor(v3, v1);
v1 = v2->start;
v2->finish = v2->start;
}
if
( v1 == v2->end_of_storage )
{
std::vector_std::string_::grow_cap_push(v2, v1,
"F33FC7A6-5A29-44E7-921E-1A3E9D88B648"
);
}
else
{
v1->data = 0i64;
v1->size = 0;
v1->cap = 0;
std::string::ctor(v1,
"F33FC7A6-5A29-44E7-921E-1A3E9D88B648"
, 0x24u);
++v2->finish;
}
// push 7 more strings
// ...
_i = 0;
_size = v2->finish - v2->start;
do
{
// exchange 2 random elments in vector
// ...
++_i;
}
while
( _i < 15 );
}
|
将固定的 8 个字符串压入到 simulation 对象的 vector<string> 中,再 15 轮循环随机交换 vector 中的两个值。
此函数执行结束后回到 sub_40CF80 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
extracted.size = 0;
extracted.data = 0i64;
extracted.cap = 15;
extracted.data.sstr[0] = 0;
v55 = 0;
strs_iter =
this
->strs.start;
ptr =
this
->strs.finish;
if
( strs_iter != ptr )
{
while
( 1 )
{
v2 = strs_iter->cap < 16;
v3 = (
char
*)strs_iter;
v47 = 0;
if
( !v2 )
v3 = strs_iter->data.lstr;
v4 = sub_40DA90(&v54, &v47, v3);
if
( &extracted != v4 )
{
// std::string::dtor(&extracted);
extracted = *v4;
v4->size = 0;
v4->cap = 15;
v4->data.sstr[0] = 0;
}
if
( v54.cap >= 0x10 )
{
v6 = v54.data.lstr;
if
( v54.cap + 1 >= 0x1000 )
{
v6 = (
char
*)*((_DWORD *)v54.data.lstr - 1);
if
( (unsigned
int
)(v54.data.lstr - v6 - 4) > 0x1F )
goto
LABEL_76;
}
j_j_j_j_j_free(v6);
}
if
( v47 )
break
;
if
( ++strs_iter == ptr )
goto
LABEL_17;
}
|
遍历该 vector 中的字符串,并作为密钥传入函数 sub_40DA90 对 code.dat 的数据解密,如果密钥正确解密成功就会将 v47 置为 1 同时循环结束。解密后的数据保存在 extracted 中。 vector 中都是预定义好的值,一定是有一个正确的密钥的,解密成功后进入后面的逻辑。
1
2
3
4
5
6
7
8
9
10
11
|
if
( extracted.size )
{
(*((
void
(__thiscall **)(simulation *, std::string *))
this
->vtable + 1))(
this
, &decrypted);
LOBYTE(v55) = 1;
if
( !decrypted.size )
{
SendMessageW(
this
->handle, 0x47Du, 0, 0);
LABEL_68:
// std::string::dtor(&decrypted);
goto
LABEL_18;
}
|
调用 vftable 第 2 个函数,函数内是对之前公钥加密的数据进行私钥解密 (p = 151800295406637185657660953042405417749139697216607628859251336122477567504850721334078661322707043309259975387744155240851465173871868351073728222215736007577965117010898265208250803040509461853998999375852253779832620625838189650944263095658219155634441735163044037983313290619292144767471775089263495418251, q = 144311874113221289713261361370020383289362372276623337764783487115704894762475782336636918040619744269077401891962484775555733509180237070031790604589232999322942408727672541034852132898576665814175107105064246348891716440001483229535995859746199351835116402561390819328452677427755066159440587651365835961947
) ,解密成功后保存在 decrypted 中(得到的是原始输入的 sha256 哈希字节值)。
后面的逻辑稍微整理一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
v47 = 0;
ptr = VirtualAlloc(0, 0xA00000u, 0x3000u, PAGE_EXECUTE_READWRITE);
// MEM_COMMIT | MEM_RESERVE
uc_open(1, 0, &uc);
// UC_ARCH_ARM, UC_MODE_ARM
uc_ctl(uc, 0x44000007u, 17);
// (UC_CTL_IO_WRITE, 1, UC_CTL_CPU_MODEL), UC_CPU_ARM_CORTEX_A15
tohex = std::string::tohex(&decrypted);
uc_mem_map_ptr(uc, 0i64, 0xA00000u, 7u, ptr);
// rwx
uc_mem_write(uc, 0x43000ui64, extracted_data, extracted_size);
uc_mem_write(uc, 0x4033ui64, tohex.data, tohex.size);
if
( uc_emu_start(uc, 0x43000ui64, v20 + 0x43000, 0i64, 0) )
{
v22 = (
void
(__stdcall *)(
HWND
,
UINT
,
WPARAM
,
LPARAM
))SendMessageW;
}
else
{
memset
(bytes, 0, 0x20);
uc_mem_read(uc, 0x14390ui64, bytes, 0x20u);
v22 = (
void
(__stdcall *)(
HWND
,
UINT
,
WPARAM
,
LPARAM
))SendMessageW;
v47 = 1;
SendMessageW(
this
->handle, 0x47Eu, (
WPARAM
)bytes, 0);
}
uc_mem_unmap(uc, 0i64, 0xA00000u);
uc_close(uc);
v24 =
this
->handle;
if
( v47 )
{
v22(v24, 0x47Bu, 0, 0);
}
else
{
v22(v24, 0x47Du, 0, 0);
}
|
使用 unicorn 执行 arm 指令, code.dat 中解密出的数据是 要执行的 arm 指令放在 0x43000 ,输入的 sha256 十六进制哈希值放在 0x4033 , 执行成功后会将 0x14390 处的 32 字节读出并发送消息 0x47E ,之后再发送消息 0x47B 。其中任何一个地方有问题都会发送消息 0x47D (MessageBoxW(*(HWND *)(this + 28), L"验证失败!", L"提示", 0);
) 。
0x47E 的处理 (0x4061D0) :
1
2
|
*(_OWORD *)(
this
+ 0x274) = *(_OWORD *)param1;
*(_OWORD *)(
this
+ 0x284) = *((_OWORD *)param1 + 1);
|
将传入的参数指向的 32 个字节复制到偏移 0x274 处。
0x47B 的处理:
1
2
3
4
5
|
if
( *(_BYTE *)(
this
+ 0x28C) )
MessageBoxW(*(
HWND
*)(
this
+ 28), L
"验证成功!"
, L
"提示"
, 0);
else
(*(
void
(__stdcall **)(
int
,
int
, _DWORD, _DWORD))(*(_DWORD *)
this
+ 176))(
this
, 0x47D, 0, 0);
return
sub_AE4396((unsigned
int
)&v25 ^ v20);
|
并不是直接提示成功,而是对偏移 28C 这里的值做判断,不为 0 才会提示成功,否则会发出 0x47D 消息提示失败。结合 0x47E 的处理,上面 uc_mem_read 得到的数据第 0x18 字节必须为 1 ,即 unicorn 执行结束时 0x143a8 必须为 1 。
最后只剩下 unicorn 执行 arm 代码这部分了。将解密后的代码 dump 出来,拖入 ida 以 arm 形式反编译:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
void
sub_43000()
{
int
v0;
// r1
char
*v1;
// r0
const
char
*v2;
// r2
int
v3;
// r5
int
v4;
// r3
int
v5;
// r4
v0 = 12;
while
( 2 )
{
v1 = input;
v2 =
"4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8"
;
v3 = 0;
--v0;
do
{
v4 = *(_DWORD *)v1;
v5 = *(_DWORD *)v2;
if
( ++v3 >= 16 )
{
if
(
"4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8"
==
"6749dae311865d64db83d5ae75bac3c9e36b3"
"aa6f24caba655d9682f7f071023"
)
{
MEMORY[0x14390] = 1;
MEMORY[0x143A8] = 1;
}
return
;
}
v1 += 4;
v2 += 4;
}
while
( v4 == v5 );
if
( v0 )
continue
;
break
;
}
}
|
中间就能看到 MEMORY[0x143A8] = 1
的赋值,不过 F5 不太准,需要看下汇编,实际上是将 12 个十六进制串压入到栈上,依次和 0x4033 处放的输入的 sha256 哈希值十六进制串比较,遇到相同的才会进入中间的循环将当前的串和 6749dae311865d64db83d5ae75bac3c9e36b3aa6f24caba655d9682f7f071023
比较,相同则会将两个值设为 1 。
所以最后只有一个问题,就是已知 sha256 为 6749dae311865d64db83d5ae75bac3c9e36b3aa6f24caba655d9682f7f071023
求长度为 32 的原始输入。但是这个是完全没法求的,不可逆。到这里就卡住了。
本来这些很早就分析完了,但是破解 sha256 本身就不太现实(几个在线网站搜了下都没有),这东西又不是花钱就能解决的。。。所以一度怀疑是不是逆向还有什么看漏了的。。。做了一晚上无用功。。。晚上睡觉都梦到找到了隐藏的逻辑。。。
第二天醒来又看了下 arm 指令里那些 16 进制串,其中一个是输入的 sha256 ,网站上搜不到对应的明文。尝试性的搜了下第一个串 e0bc614e4fd035a488619799853b075143deea596c477b8dc077e309c0fe42e9
,竟然找到了! ,解出来的明文是 6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b
,正好是第二个串。所以这些串是有关联的,尝试了一下发现 6749dae311865d64db83d5ae75bac3c9e36b3aa6f24caba655d9682f7f071023
对应的明文就是它下面的串的前 32 字节 ea96b41c1f9365c2c9e6342f5faaeab2
,运行程序验证成功。
到这里感觉很无语。如果是卡在逆向过程或者设计算法求解那我没话说,难度大做不出来那是自己菜。但是偏偏最后卡在这猜谜,跟逆向完全没有关系,还浪费我好多时间。我相信很多师傅都卡在这里,都猜不到,就纯拖时间。最后这部分猜 sha256 的设计就不合理,这是逆向题目而不是猜谜题目,这里就没有合理的引导提示这些 sha256 是有关系的,谁做逆向的时候还会去关心那些跟主逻辑无关的东西啊,这要是合理的话之后出题不就可以照样猜 sha256 ,明文也在程序里有体现但是是一个跟主逻辑完全无关的函数,只要执行这个函数就能拿到明文输入;甚至我可以啥都不给,明文的每个字符都可以在二进制程序里找到那为什么不能从 sha256 恢复明文?
或者可能有人说第二种解法,就是花钱,可能是某 md5 网站查 hash 某些条目需要花钱购买,也可能是查不到就可以花钱用破这一个 hash 。先说后一种情况,只要查不到就基本不可能暴破出来的。 sha256 要是那么容易破区块链早就不安全了,就现在的服务器配置暴 16 字节( 32 个十六进制数)都能暴到地球毁灭了。再前一种情况,现实是我试过的网站都查不到,假设某网站能查到,那怎么保证每个做到这一步的师傅都能找到这个网站?这一步就跟能力无关了,在线网站一个一个去抽奖这合理吗?好,再假设每个师傅都能查到这个网站,但是这个 hash 要给钱,那我不就专门可以做一个 hash 破解网站就收录这题的输入但是要花钱,这不就可以来圈钱了?
做完戾气有点大。我觉得谁卡在这一步卡很久,最后不管做没做出来戾气都会很大。不过也该说一下,逆向部分设计的是比较好的,有很多东西。如果把题目改成没有求 sha256 而是直接将原始输入加密、解密、传入 unicorn 判断,就能直接解,这样题目就会好很多。
更多【 KCTF 2023 第三题 wp - 98k】相关视频教程:www.yxfzedu.com