看雪2023 KCTF年度赛 | 第三题设计思路及解析

WriteUp 1年前 (2023) admin
338 0 0

看雪2023 KCTF年度赛 | 第三题设计思路及解析

这是一场人类与超智能AI的“生死”较量

请立刻集结,搭乘SpaceX,前往AI控制空间站

智慧博弈  谁能问鼎


看雪·2023 KCTF 年度赛于9月1日中午12点正式开赛!比赛基本延续往届模式,设置了难度值、火力值和精致度积分。由此来引导竞赛的难度和趣味度,使其更具挑战性和吸引力,同时也为参赛选手提供了更加公平、有趣的竞赛平台。

*注意:签到题持续开放,整个比赛期间均可提交答案获得积分


今天中午12:00第三题《秘密计划》已截止答题,该题持续了48小时,共有6支战队成功提交flag,他们分别是:

看雪2023 KCTF年度赛 | 第三题设计思路及解析


想必还是有一点点难度的,接下来一起看下该题的设计思路和解析吧。


出题团队简介


出题战队:嗷来吼

战队成员:yimingqpa

看雪2023 KCTF年度赛 | 第三题设计思路及解析

设计思路



1.验证核心在解密出来的ARM代码里面

2.因为想出的题目简单一些,ARM代码及其简单没有混淆和加密算法

3.为了再次降低难度,直接在ARM代码里面给出了明文

如果直接去掉ARM代码中明文,相当于根据SHA256值反推出明文:

看雪2023 KCTF年度赛 | 第三题设计思路及解析



赛题解析


本题解析由看雪专家 GreatIchild 提供:

看雪2023 KCTF年度赛 | 第三题设计思路及解析

GUI 程序先拖到 Resource Hacker 里看资源,提取出一个压缩包,最重要的部分就是其中的 dlg_main.xml ,即主界面布局:
<?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 ,后面要用):
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); ,实际上用异常隐藏了信息:

看雪2023 KCTF年度赛 | 第三题设计思路及解析

如果触发异常就会导致程序退出。实际上程序正常执行的时候是不会触发这个异常的,在有调试器时调试器捕获到这个异常之后传递给应用程序处理就会导致程序退出,所以这是用于反调试的。

这个函数 (0x407880) 查找引用,来到函数 0x405F10 :
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 检测调试器,最后一个和第一个类似也是利用调试器存在时才会触发的异常进行反调试:
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);  }}

看雪2023 KCTF年度赛 | 第三题设计思路及解析

绕过反调试只要把 4 个 CreateTimerQueueTimer 前的 if 判断去掉就行, 0x406102 处的 jz loc_406192 改为 jmp loc_406192 ,这就可以愉快调试了。

最后还有一行设置定时器函数 0x406820 ,这个函数的作用是发送消息 (0x47C) 使界面上的图像闪烁,与调试无关。

sub_405F10 查找引用来到函数 sub_405C50 ,前面处理三个系统消息(接收到 WM_CLOSE 就会退出):
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;}

后面处理自定义的消息:
  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 处理,进入后即可看到提示的字符串:
看雪2023 KCTF年度赛 | 第三题设计思路及解析

为了保证逻辑连贯性,后面从按下按钮开始的过程开始分析,遇到哪种类型的自定义消息再分析其处理过程。

通过布局文件中按钮的名字字符串 check_va 可以定位到函数 sub_405AF0
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 ,跟入查看:
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才会进入后面的逻辑:
  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 一定成立,循环里一定有一轮会将输入放进去。
  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) :
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) :
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) :
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 :
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 个函数:
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 :
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 中都是预定义好的值,一定是有一个正确的密钥的,解密成功后进入后面的逻辑。
    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 哈希字节值)。

后面的逻辑稍微整理一下:
v47 = 0;ptr = VirtualAlloc(0, 0xA00000u, 0x3000u, PAGE_EXECUTE_READWRITE); // MEM_COMMIT | MEM_RESERVEuc_open(1, 0, &uc); // UC_ARCH_ARM, UC_MODE_ARMuc_ctl(uc, 0x44000007u, 17); // (UC_CTL_IO_WRITE, 1, UC_CTL_CPU_MODEL), UC_CPU_ARM_CORTEX_A15tohex = std::string::tohex(&decrypted);uc_mem_map_ptr(uc, 0i64, 0xA00000u, 7u, ptr); // rwxuc_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) :
*(_OWORD *)(this + 0x274) = *(_OWORD *)param1;*(_OWORD *)(this + 0x284) = *((_OWORD *)param1 + 1);

将传入的参数指向的 32 个字节复制到偏移 0x274 处。
0x47B 的处理:
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 形式反编译:
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 是有关系的,做逆向的时候基本不会去关心跟主逻辑无关的东西。

可能有人说第二种解法,就是花钱,可能是某 md5 网站查 hash 某些条目需要花钱购买,也可能是查不到就可以花钱用破这一个 hash 。先说后一种情况,只要查不到就基本不可能暴破出来的。 sha256 要是那么容易破区块链早就不安全了,就现在的服务器配置暴 16 字节( 32 个十六进制数)都能暴到地球毁灭了。再前一种情况,现实是我试过的网站都查不到,假设某网站能查到,那怎么保证每个做到这一步的师傅都能找到这个网站,这一步就跟能力无关了。

不过肯定的是,这题逆向部分设计的是比较好的,有很多东西。如果把题目改成没有求 sha256 而是直接将原始输入加密、解密、传入 unicorn 判断,就能直接解,这样题目就会好很多。

看雪2023 KCTF年度赛 | 第三题设计思路及解析

看雪2023 KCTF年度赛 | 第三题设计思路及解析

今天中午12:00
第四题《AI控制空间站》已开赛!

看雪2023 KCTF年度赛 | 第三题设计思路及解析

欢迎参赛

在这个充满变数的赛场上,没有人能够预料到最终的结局。有时,优势的领先可能只是一时的,一瞬间的失误就足以颠覆一切。而那些一直默默努力、不断突破自我的人,往往会在最后关头迎头赶上,成为最耀眼的存在。


谁能保持领先优势?谁能迎头赶上?谁又能突出重围成为黑马?




看雪2023 KCTF年度赛 | 第三题设计思路及解析


看雪2023 KCTF年度赛 | 第三题设计思路及解析

球分享

看雪2023 KCTF年度赛 | 第三题设计思路及解析

球点赞

看雪2023 KCTF年度赛 | 第三题设计思路及解析

球在看


看雪2023 KCTF年度赛 | 第三题设计思路及解析

点击阅读原文进入比赛

原文始发于微信公众号(看雪学苑):看雪2023 KCTF年度赛 | 第三题设计思路及解析

版权声明:admin 发表于 2023年9月7日 下午6:02。
转载请注明:看雪2023 KCTF年度赛 | 第三题设计思路及解析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...