首页
社区
课程
招聘
[原创] KCTF 2023 第三题 wp - 98k
2023-9-6 12:53 10838

[原创] KCTF 2023 第三题 wp - 98k

2023-9-6 12:53
10838

逆向体验极好,最后一步的猜谜体验极差。

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);

创建了一个定时器队列,将四个函数添加进去(上面分析的是第一个函数),这四个都是反调试。中间两个是常用的 NtSetInformationThreadNtQueryInformationProcess 检测调试器,最后一个和第一个类似也是利用调试器存在时才会触发的异常进行反调试:

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 判断,就能直接解,这样题目就会好很多。


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞2
打赏
分享
最新回复 (10)
雪    币: 2259
活跃值: (1070)
能力值: ( LV8,RANK:129 )
在线值:
发帖
回帖
粉丝
Zero*/ 2 2023-9-7 18:07
2
1
师傅是怎么做到在ida中讲soui的符号表恢复出来的,我经常调了半天发现是个库函数
雪    币: 6545
活跃值: (2222)
能力值: ( LV12,RANK:549 )
在线值:
发帖
回帖
粉丝
GreatIchild 3 2023-9-7 19:56
3
0
调试看效果猜的,就重命名做个标记而已,也没恢复符号表
雪    币: 5568
活跃值: (3016)
能力值: ( LV12,RANK:394 )
在线值:
发帖
回帖
粉丝
htg 4 2023-9-8 11:22
4
0
请问如何在IDA里生成arm的伪码?
在IDA77里反编译arm代码,使用F5生成伪码时,弹出处理器不支持,只支持arm64、x86、x64等。
雪    币: 6545
活跃值: (2222)
能力值: ( LV12,RANK:549 )
在线值:
发帖
回帖
粉丝
GreatIchild 3 2023-9-8 15:45
5
0
htg 请问如何在IDA里生成arm的伪码? 在IDA77里反编译arm代码,使用F5生成伪码时,弹出处理器不支持,只支持arm64、x86、x64等。
这没详细信息我也不知道为啥。正常流程就是 ida 32位,拖进去的时候选择 arm little endian,后面的可以直接默认,反汇编、创建函数后就能 F5
雪    币: 5568
活跃值: (3016)
能力值: ( LV12,RANK:394 )
在线值:
发帖
回帖
粉丝
htg 4 2023-9-8 16:43
6
0

按下F5,弹出Warning消息框“

Sorry, you do not have a decompiler for the current file.

You can decompile code for the following processor(s):

ARM64, x64, x86


雪    币: 6545
活跃值: (2222)
能力值: ( LV12,RANK:549 )
在线值:
发帖
回帖
粉丝
GreatIchild 3 2023-9-10 23:41
7
0
htg 按下F5,弹出Warning消息框“Sorry, you do not have a decompiler for the current file.You can decompile code fo ...
不知道,应该你用的这个 ida 加载插件不全吧
雪    币: 261
活跃值: (2040)
能力值: ( LV12,RANK:610 )
在线值:
发帖
回帖
粉丝
ylp1332 15 2023-9-11 01:05
8
0
htg 按下F5,弹出Warning消息框“Sorry, you do not have a decompiler for the current file.You can decompile code fo ...
只有Arm64插件,没有Arm插件?
不应该啊,绿色版里都有
雪    币: 19431
活跃值: (29097)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-9-11 09:08
9
1
感谢分享
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
NOOB@ToT 2023-10-9 20:04
10
0
猜测符号这么精准有什么技巧吗,给simulation中各个成员变量一个符号,给出ctor,vector相关函数的符号
雪    币: 6545
活跃值: (2222)
能力值: ( LV12,RANK:549 )
在线值:
发帖
回帖
粉丝
GreatIchild 3 2023-10-13 11:27
11
0
NOOB@ToT 猜测符号这么精准有什么技巧吗,给simulation中各个成员变量一个符号,给出ctor,vector相关函数的符号
当然是看逻辑了,可以看出来他做了什么操作,就起一个相对应的名字。至于vector这种,见多了就知道了
游客
登录 | 注册 方可回帖
返回