[Reverse] MoeCTF 窗口闪退RC4解密

Reverse / RC4 / Windows窗口 难度:中等

0x01 题目初步分析

尝试运行exe发现不行,那就ida反编译分析一下

0x02 分析 WinMainCRTStartup

从WinMainCRTStartup开始分析

__int64 WinMainCRTStartup()
{
  *(_DWORD *)refptr___mingw_app_type = 1;
  return _tmainCRTStartup();
}

发现会return到_tmainCRTStartup

0x03 分析 _tmainCRTStartup

那就双击过去来分析一下_tmainCRTStartup

int _tmainCRTStartup()
{
  PVOID StackBase; // rsi
  signed __int64 v1; // rax
  _DWORD *v2; // rsi
  int v3; // edi
  int v4; // ebx
  size_t v5; // rdi
  char **v6; // rax
  char **v7; // rbp
  char **v8; // r12
  size_t v9; // rdi
  unsigned __int64 v10; // rbx
  size_t v11; // rsi
  char *v12; // rax
  char *v13; // rdx
  char **v14; // rdi
  const char **v15; // r8
  int v16; // ecx
  int result; // eax

  StackBase = NtCurrentTeb()->NtTib.StackBase;
  while ( 1 )
  {
    v1 = _InterlockedCompareExchange64(
           (volatile signed __int64 *)refptr___native_startup_lock,
           (signed __int64)StackBase,
           0);
    if ( !v1 )
    {
      v2 = refptr___native_startup_state;
      v3 = 0;
      if ( *(_DWORD *)refptr___native_startup_state == 1 )
        goto LABEL_20;
      goto LABEL_6;
    }
    if ( StackBase == (PVOID)v1 )
      break;
    Sleep(0x3E8u);
  }
  v2 = refptr___native_startup_state;
  v3 = 1;
  if ( *(_DWORD *)refptr___native_startup_state == 1 )
  {
LABEL_20:
    amsg_exit(31);
    if ( *v2 == 1 )
      goto LABEL_21;
LABEL_9:
    if ( v3 )
      goto LABEL_10;
    goto LABEL_22;
  }
LABEL_6:
  if ( *v2 )
  {
    has_cctor = 1;
  }
  else
  {
    *v2 = 1;
    initterm(refptr___xi_a, refptr___xi_z);
  }
  if ( *v2 != 1 )
    goto LABEL_9;
LABEL_21:
  initterm(refptr___xc_a, refptr___xc_z);
  *v2 = 2;
  if ( v3 )
    goto LABEL_10;
LABEL_22:
  _InterlockedExchange64((volatile __int64 *)refptr___native_startup_lock, 0);
LABEL_10:
  if ( *refptr___dyn_tls_init_callback )
    ((void (__fastcall *)(_QWORD, __int64, _QWORD))*refptr___dyn_tls_init_callback)(0, 2, 0);
  pei386_runtime_relocator();
  *refptr___mingw_oldexcpt_handler = (__int64)SetUnhandledExceptionFilter(refptr__gnu_exception_handler);
  set_invalid_parameter_handler_0(_mingw_invalidParameterHandler);
  fpreset();
  v4 = argc;
  v5 = 8LL * (argc + 1);
  v6 = (char **)malloc_0(v5);
  v7 = argv;
  v8 = v6;
  if ( v4 <= 0 )
  {
    v14 = v6;
  }
  else
  {
    v9 = v5 - 8;
    v10 = 0;
    do
    {
      v11 = strlen(v7[v10 / 8]) + 1;
      v12 = (char *)malloc_0(v11);
      v8[v10 / 8] = v12;
      v13 = v7[v10 / 8];
      v10 += 8LL;
      memcpy_0(v12, v13, v11);
    }
    while ( v9 != v10 );
    v14 = (char **)((char *)v8 + v9);
  }
  *v14 = 0;
  argv = v8;
  _main();
  v15 = (const char **)envp;
  v16 = argc;
  *(_QWORD *)*refptr___imp___initenv = envp;
  result = main(v16, (const char **)argv, v15);
  mainret = result;
  if ( !managedapp )
    exit_0(result);
  if ( !has_cctor )
  {
    cexit_0();
    return mainret;
  }
  return result;
}

在111行发现

result = main(v16, (const char **)argv, v15);

0x04 分析 main 函数

那就要从main下手看看算法逻辑了 那就双击过去

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v3; // si
  char *v4; // rbx
  unsigned int v5; // ecx
  char v6; // al
  int wShowWindow; // r9d
  struct _STARTUPINFOA StartupInfo; // [rsp+20h] [rbp-88h] BYREF

  v3 = 0;
  _main(argc, argv, envp);
  v4 = *_p__acmdln_0();
  if ( v4 )
  {
    while ( 1 )
    {
      v5 = *v4;
      if ( *v4 <= 32 )
      {
        if ( !(_BYTE)v5 )
          goto LABEL_15;
        if ( (v3 & 1) == 0 )
        {
          do
            v6 = *++v4;
          while ( v6 && v6 <= 32 );
          goto LABEL_15;
        }
        v3 = 1;
      }
      else if ( (_BYTE)v5 == 34 )
      {
        v3 ^= 1u;
      }
      if ( ismbblead_0(v5) )
        v4 += -(v4[1] == 0) + 1;
      ++v4;
    }
  }
  v4 = (char *)&unk_140005010;
LABEL_15:
  memset(&StartupInfo, 0, sizeof(StartupInfo));
  GetStartupInfoA(&StartupInfo);
  wShowWindow = 10;
  if ( (StartupInfo.dwFlags & 1) != 0 )
    wShowWindow = StartupInfo.wShowWindow;
  return WinMain((HINSTANCE)refptr___ImageBase, 0, v4, wShowWindow);
}

在47行发现一个线索

return WinMain((HINSTANCE)refptr___ImageBase, 0, v4, wShowWindow);

0x05 分析 WinMain 窗口逻辑

真正的算法其实在一个叫WinMain的函数

那就双击过去

int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
  MSG Msg; // [rsp+60h] [rbp-20h] BYREF
  WNDCLASSA WndClass; // [rsp+90h] [rbp+10h] BYREF
  CHAR ClassName[24]; // [rsp+E0h] [rbp+60h] BYREF
  HWND hWnd; // [rsp+F8h] [rbp+78h]

  strcpy(ClassName, "Sample Window Class");
  memset(&WndClass, 0, sizeof(WndClass));
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.hInstance = hInstance;
  WndClass.lpszClassName = ClassName;
  RegisterClassA(&WndClass);
  hWnd = CreateWindowExA(0, ClassName, "moectf 2025", 0xCA0000u, 0x80000000, 0x80000000, 400, 100, 0, 0, hInstance, 0);
  if ( !hWnd )
    return 0;
  ShowWindow(hWnd, nShowCmd);
  UpdateWindow(hWnd);
  Sleep(1u);
  DestroyWindow(hWnd);
  memset(&Msg, 0, sizeof(Msg));
  while ( GetMessageA(&Msg, 0, 0, 0) > 0 )
  {
    TranslateMessage(&Msg);
    DispatchMessageA(&Msg);
  }
  return 0;
}

可以发现代码在创建并显示窗口后紧接着执行了Sleep(1u); 然后直接DestroyWindow(hWnd);

这意味着这个程序运行后窗口只会闪烁一下(或者根本看不见)就被销毁了

但这并不意味着线索断了

看到第10行

WndClass.lpfnWndProc = (WNDPROC)WndProc;

这里的WndProc就是负责处理这道题核心逻辑的函数

既然窗口被秒关,那么出题人极大概率把生成Flag或者验证的逻辑写在了WndProc

收到WM_CREATE(窗口创建)或者WM_DESTROY(窗口销毁)消息时的处理代码中

那就双击到WndCProc去看看有什么线索

0x06 核心函数 WndProc 分析

LRESULT __fastcall WndProc(HWND a1, UINT a2, WPARAM a3, LPARAM a4)
{
  struct tagPAINTSTRUCT Paint; // [rsp+30h] [rbp-50h] BYREF
  char Destination[14]; // [rsp+80h] [rbp+0h] BYREF
  __int16 v7; // [rsp+8Eh] [rbp+Eh]
  __int64 v8; // [rsp+90h] [rbp+10h]
  __int64 v9; // [rsp+98h] [rbp+18h]
  __int64 v10; // [rsp+A0h] [rbp+20h]
  __int64 v11; // [rsp+A8h] [rbp+28h]
  __int64 v12; // [rsp+B0h] [rbp+30h]
  __int64 v13; // [rsp+B8h] [rbp+38h]
  __int64 v14; // [rsp+C0h] [rbp+40h]
  __int64 v15; // [rsp+C8h] [rbp+48h]
  __int64 v16; // [rsp+D0h] [rbp+50h]
  __int64 v17; // [rsp+D8h] [rbp+58h]
  __int64 v18; // [rsp+E0h] [rbp+60h]
  __int64 v19; // [rsp+E8h] [rbp+68h]
  __int64 v20; // [rsp+F0h] [rbp+70h]
  __int64 v21; // [rsp+F8h] [rbp+78h]
  struct tagRECT Rect; // [rsp+100h] [rbp+80h] BYREF
  char v23[13]; // [rsp+113h] [rbp+93h] BYREF
  char Str[8]; // [rsp+120h] [rbp+A0h] BYREF
  __int64 v25; // [rsp+128h] [rbp+A8h]
  _QWORD v26[2]; // [rsp+130h] [rbp+B0h]
  int v27; // [rsp+140h] [rbp+C0h]
  int v28; // [rsp+144h] [rbp+C4h]
  HDC hdc; // [rsp+148h] [rbp+C8h]

  if ( a2 == 2 )
  {
    PostQuitMessage(0);
    return 0;
  }
  else if ( a2 == 15 )
  {
    hdc = BeginPaint(a1, &Paint);
    strcpy(Destination, "Your flag is ");
    v7 = 0;
    v8 = 0;
    v9 = 0;
    v10 = 0;
    v11 = 0;
    v12 = 0;
    v13 = 0;
    v14 = 0;
    v15 = 0;
    v16 = 0;
    v17 = 0;
    v18 = 0;
    v19 = 0;
    v20 = 0;
    v21 = 0;
    *(_QWORD *)Str = 0x7F1B3E885EF9160LL;
    v25 = 0x2CD336BCB0464A89LL;
    v26[0] = 0xEF5FC91642917EE1uLL;
    *(_QWORD *)((char *)v26 + 6) = 0x739D40A4E356EF5FLL;
    strcpy(v23, "mylittlepony");
    v28 = 12;
    v27 = strlen(Str);
    RC4Crypt((unsigned __int8 *)Str, v27, (const unsigned __int8 *)v23, 12);
    strcat_0(Destination, Str);
    GetClientRect(a1, &Rect);
    DrawTextA(hdc, Destination, -1, &Rect, 0x40005u);
    EndPaint(a1, &Paint);
    return 0;
  }
  else
  {
    return DefWindowProcA(a1, a2, a3, a4);
  }
}

0x07 核心逻辑解析

可以看到后半部分有一个条件语句if 我们来分析一下

当a2 == 15时(15在Windows API中对应的是WM_PAINT消息,也就是窗口尝试绘制自己的画面时触发),程序执行了真正的核心逻辑

1. 37行准备输出格式strcpy(Destination, "Your flag is ");

2. 53-56行通过一大堆赋值语句,把一串看似随机的16进制数塞进了Str及其相邻的内存空间(v25, v26)

3. 57行strcpy(v23, "mylittlepony"); —— 你的密钥是mylittlepony,长度是 12

4. 60行RC4Crypt((unsigned __int8 *)Str, v27, (const unsigned __int8 *)v23, 12); —— 调用了标准的RC4算法,使用密钥对Str中的密文进行解密

5. 61行strcat_0(Destination, Str); 将解密出来的Flag拼接到"Your flag is "后面,然后调用DrawTextA画在那个"闪退"的窗口上

0x08 密文提取与小端序处理

那么现在逆向完了 那就分析一下flag怎么来的把

在逆向工程中,提取这种在代码里硬编码的数据要注意小端序的问题

数据在内存中是倒着存的

把密文从这些__int64常量中提取出来

0x7F1B3E885EF9160LL (补齐8字节为 0x07F1B3E885EF9160) -> 60 91 EF 85 E8 B3 F1 07

0x2CD336BCB0464A89LL -> 89 4A 46 B0 BC 36 D3 2C

0xEF5FC91642917EE1uLL -> E1 7E 91 42 16 C9 5F EF

注意v26 + 6这里发生了内存重叠覆盖:0x739D40A4E356EF5FLL -> 5F EF 56 E3 A4 40 9D 73 (刚好覆盖了上面的5F EF,完美衔接)。

完整的密文 (Hex格式) 为

6091ef85e8b3f107894a46b0bc36d32ce17e914216c95fef56e3a4409d73

0x09 RC4 解密脚本

那就直接rc4 decode解码一波

写python

from Crypto.Cipher import ARC4
cipher = ARC4.new(b"mylittlepony")
print(cipher.decrypt(bytes.fromhex("6091ef85e8b3f107894a46b0bc36d32ce17e914216c95fef56e3a4409d73")).decode('utf-8'))

0x0A 最终 Flag

python直接输出

moectf{Just_dyn@mic_d3bugg1ng}