如何确定一段代码是否是加密算法?

逆向工程 恶意软件 开箱 加密 解密 编码
2021-06-22 06:22:36

我正在尝试对恶意软件进行逆向工程,遇到了一段我怀疑是加密/解密程序的代码,但我不确定。我可以识别出它从它自己的.rsrc部分中提取其有效负载我用FindCryptIDAScope扫描了这个算法,两个插件都未能将其识别为加密算法。

我不熟悉密码算法。代码是加密或解密算法的一些好的迹象有哪些,哪些迹象排除了它?

提取算法是(由IDA生成):

int __stdcall sub_9001320(int a1, unsigned int a2, int a3)
{
// a1 = a3 = 0x09003000
// a2 = 0x7600



  int result; // eax@2
  char v4; // al@23
  unsigned int l; // [sp+4h] [bp-4Ch]@33
  unsigned int k; // [sp+10h] [bp-40h]@19
  unsigned int v7; // [sp+14h] [bp-3Ch]@19
  int v8; // [sp+18h] [bp-38h]@40
  signed int j; // [sp+1Ch] [bp-34h]@13
  unsigned int v10; // [sp+20h] [bp-30h]@5
  unsigned int i; // [sp+24h] [bp-2Ch]@5
  unsigned int v12; // [sp+28h] [bp-28h]@5
  unsigned int v13; // [sp+2Ch] [bp-24h]@5
  signed int v14; // [sp+30h] [bp-20h]@13
  unsigned int v15; // [sp+34h] [bp-1Ch]@5
  unsigned int v16; // [sp+38h] [bp-18h]@5
  unsigned __int8 v17; // [sp+3Fh] [bp-11h]@5
  signed int v18; // [sp+40h] [bp-10h]@5
  _DWORD *lpAddress; // [sp+44h] [bp-Ch]@11
  unsigned int v20; // [sp+48h] [bp-8h]@13
  unsigned int v21; // [sp+4Ch] [bp-4h]@16

  if ( a2 > 0xC )
  {
    if ( *(_DWORD *)a1 == 1083581807 )
    {
      v16 = sub_9001300(a1);
      v18 = 12;
      v17 = 0;
      v15 = 9;
      v12 = 256;
      v13 = 256;
      v10 = 8 * a2 - 96;
      for ( i = 9; v10 >= i; v10 -= i )
      {
        if ( i != 12 && v13 == 1 << i )
          ++i;
        ++v13;
      }
      lpAddress = VirtualAlloc(0, 0xC000u, 0x1000u, 4u);
      if ( lpAddress )
      {
        v20 = 0;
        v14 = 256;
        for ( j = 0; j <= 255; ++j )
        {
          lpAddress[3 * j] = 0;
          lpAddress[3 * j + 2] = 0;
          lpAddress[3 * j + 1] = 1;
        }
        v21 = 0;
        while ( v21 < v16 && v12 < v13 )
        {
          v7 = 0;
          for ( k = 0; k < v15; ++k )
          {
            if ( (1 << (7 - (v17 + 8 * v18) % 8)) & *(_BYTE *)(a1 + (v17 + 8 * v18) / 8) )
              v7 |= 1 << k;
            v4 = (v17 + 1) % 8;
            v17 = (v17 + 1) % 8;
            if ( !v4 )
              ++v18;
          }
          if ( v7 > 0xFF )
          {
            if ( v20 < 0xF00 && v7 > v20 + 255 )
              return 0;
            if ( v21 + lpAddress[3 * v7 + 1] > v16 )
              return 0;
            for ( l = 0; l < lpAddress[3 * v7 + 1]; ++l )
              *(_BYTE *)(a3 + l + v21) = *(_BYTE *)(a3 + l + lpAddress[3 * v7]);
          }
          else
          {
            *(_BYTE *)(v21 + a3) = v7;
          }
          if ( v20 == 3840 )
          {
            if ( (unsigned int)++v14 >> 12 )
              v14 = 256;
          }
          if ( v20 >= 0xF00 )
          {
            v8 = v14 - 1;
            if ( v14 == 256 )
              v8 = 4095;
          }
          else
          {
            v8 = v20 + v14;
          }
          lpAddress[3 * v8] = v21;
          lpAddress[3 * v8 + 1] = lpAddress[3 * v7 + 1] + 1;
          lpAddress[3 * v8 + 2] = 0;
          if ( v20 < 0xF00 )
            ++v20;
          v21 = v21 + lpAddress[3 * v8 + 1] - 1;
          ++v12;
          if ( v15 < 0xC && 1 << v15 == v12 )
            ++v15;
        }
        if ( v21 >= v16 )
        {
          // sub_9001000 is only called onece and always return 0
          if ( *(_DWORD *)(a1 + 8) == sub_9001000(a3, v16) )
          {
            VirtualFree(lpAddress, 0, 0x8000u);
            result = 0;
          }
          else
          {
            result = 0;
          }
        }
        else
        {
          result = 0;
        }
      }
      else
      {
        result = 0;
      }
    }
    else
    {
      result = 0;
    }
  }
  else
  {
    result = 0;
  }
  return result;
}


int __stdcall sub_9001300(int a1)
{
  int result; // eax@2

  if ( *(_DWORD *)a1 == 1083581807 )
    result = *(_DWORD *)(a1 + 4);
  else
    result = 0;
  return result;
}

文件信息:文件格式 pei-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00001000  09001000  09001000  00001000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rdata        00001000  09002000  09002000  00002000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .rsrc         00005000  09003000  09003000  00003000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .reloc        00001000  09008000  09008000  00008000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
1个回答

TL;DR:我们这里所拥有的可能不是加密算法,从外观上看,它更可能是一个解压循环。它根本不做任何可以被认为与加密类似的事情。

加密算法分为两类。首先是流密码来自维基百科:

流密码是一种对称密钥密码,其中明文数字与伪随机密码数字流(密钥流)相结合。在流密码中,每个明文数字一次用密钥流的相应数字加密一个,以给出密文流的数字。由于每个数字的加密取决于密码的当前状态,因此它也称为状态密码。在实践中,一个数字通常是一个位,而组合操作是一个异或(XOR)。

其次是块密码来自维基百科:

在密码学中,分组密码是一种确定性算法,它对固定长度的位组(称为块)进行操作,并具有由对称密钥指定的不变变换。分组密码在许多密码协议的设计中作为重要的基本组件运行,并被广泛用于实现批量数据的加密。

分组密码的现代设计基于迭代乘积密码的概念。在 1949 年的开创性出版物《保密系统的通信理论》中,克劳德·香农分析了乘积密码,并建议将它们作为通过组合替换和置换等简单操作来有效提高安全性的一种手段。1迭代乘积密码在多轮中执行加密,每轮使用从原始密钥派生的不同子密钥。这种密码的一种广泛实现,以 Horst Feistel 命名为 Feistel 网络,特别是在 DES 密码中实现。2分组密码的许多其他实现,例如 AES,被归类为替代置换网络。

我们这里拥有的可能不是加密算法。它根本不做任何可以被认为与加密类似的事情。一些经常在加密算法中发现但在这里没有的东西(不是一个完整的列表,只是我想出的前几件事):

  1. 没有像流密码那样基于内部状态和种子生成的伪随机流
  2. 使用 XOR(或任何其他操作)生成的流与输入缓冲区没有顺序字符组合。
  3. 没有块结构 - 一个序列在输入消息的每个很好的分块部分上执行多次。
  4. 没有长而复杂的排列(也没有排列表)。
  5. 没有对分组密码通用的置换码进行多次迭代。
  6. 没有密钥序列初始化(用于流密码和分组密码)。
  7. 对输入消息或流/状态的替换不够。

加密算法通常很长(有人可能会说难看)。通常结构化和细致地对长重复序列中的所有字节执行操作。您经常会看到执行单个或几个操作的固定长度的多次迭代。

这更类似于压缩/解压缩算法,因为字节序列在某些条件下被复制,而字节在其他条件下被构建到解压缓冲区中。

让我们回顾一下代码,看看:

int __stdcall sub_9001320(int a1, unsigned int a2, int a3)
{
// a1 = a3 = 0x09003000
// a2 = 0x7600

  int result; // eax@2
  char v4; // al@23
  unsigned int l; // [sp+4h] [bp-4Ch]@33
  unsigned int k; // [sp+10h] [bp-40h]@19
  unsigned int v7; // [sp+14h] [bp-3Ch]@19
  int v8; // [sp+18h] [bp-38h]@40
  signed int j; // [sp+1Ch] [bp-34h]@13
  unsigned int v10; // [sp+20h] [bp-30h]@5
  unsigned int i; // [sp+24h] [bp-2Ch]@5
  unsigned int v12; // [sp+28h] [bp-28h]@5
  unsigned int v13; // [sp+2Ch] [bp-24h]@5
  signed int v14; // [sp+30h] [bp-20h]@13
  unsigned int v15; // [sp+34h] [bp-1Ch]@5
  unsigned int v16; // [sp+38h] [bp-18h]@5
  unsigned __int8 v17; // [sp+3Fh] [bp-11h]@5
  signed int v18; // [sp+40h] [bp-10h]@5
  _DWORD *lpAddress; // [sp+44h] [bp-Ch]@11
  unsigned int v20; // [sp+48h] [bp-8h]@13
  unsigned int v21; // [sp+4Ch] [bp-4h]@16

功能定义

  if ( a2 > 0xC )
  {

断言某个长度至少为 96 个字节。尽管有一些分组密码接受此作为块大小,但这并不常见,并且大多数广泛接受的分组密码不支持此块大小。

    if ( *(_DWORD *)a1 == 1083581807 )
    {

断言缓冲区的某些第一个字节是固定的,这在没有上下文的情况下看起来很奇怪。

      v16 = sub_9001300(a1);

设置v16为输入缓冲区的第二个双字

      v18 = 12;
      v17 = 0;
      v15 = 9;
      v12 = 256;
      v13 = 256;
      v10 = 8 * a2 - 96;

一些更多的变量初始化

      for ( i = 9; v10 >= i; v10 -= i )
      {
        if ( i != 12 && v13 == 1 << i )
          ++i;
        ++v13;
      }

v13按原长度增加这里没有过多的细节,这看起来不像任何密码种子/初始化。

      lpAddress = VirtualAlloc(0, 0xC000u, 0x1000u, 4u);
      if ( lpAddress )
      {

分配一个硬编码的长度缓冲区作为临时数据处理

        v20 = 0;
        v14 = 256;
        for ( j = 0; j <= 255; ++j )
        {
          lpAddress[3 * j] = 0;
          lpAddress[3 * j + 2] = 0;
          lpAddress[3 * j + 1] = 1;
        }

初始化所述缓冲区,2/3 为 0,1/3 为 1。这可能是一个布尔缓冲区。

        v21 = 0;
        while ( v21 < v16 && v12 < v13 )
        {

循环v16次数。记住v16是用户在提供的缓冲区中作为第二个双字提供的。魔法之后的第二个双字可能是一个长度。

          v7 = 0;
          for ( k = 0; k < v15; ++k )
          {

另一个循环,这次是 9 次迭代。这意味着我们对猜测的长度参数中的每个字符进行了 9 次迭代。大多数分组密码将有更多的迭代,并且这些不会是每个字符的。大多数流密码不会处理数据,而是生成字节流以进行异或。我们在这里也没有看到 XOR。

            if ( (1 << (7 - (v17 + 8 * v18) % 8)) & *(_BYTE *)(a1 + (v17 + 8 * v18) / 8) )
              v7 |= 1 << k;

位仅添加到v7,对于任何类型的加密算法也无效。

            v4 = (v17 + 1) % 8;
            v17 = (v17 + 1) % 8;

这两个是字节大小的计数器。

            if ( !v4 )
              ++v18;
          }

增加另一个计数器,用于确定v7将设置多少位整个循环决定v7将设置哪些位,具体取决于固定序列以及输入缓冲区的某些位

          if ( v7 > 0xFF )
          {
            if ( v20 < 0xF00 && v7 > v20 + 255 )
              return 0;
            if ( v21 + lpAddress[3 * v7 + 1] > v16 )
              return 0;
            for ( l = 0; l < lpAddress[3 * v7 + 1]; ++l )
              *(_BYTE *)(a3 + l + v21) = *(_BYTE *)(a3 + l + lpAddress[3 * v7]);
          }

如果v7高于 0xff 做一些理智(并返回无效状态/数据),然后复制一个字节序列,直到从 a3 到 a3 达到空终止符,偏移量由 确定lpAddress的特定偏移量处的值确定v7

          else
          {
            *(_BYTE *)(v21 + a3) = v7;
          }

如果v7小于或等于 255,只需将其分配到指定位置即可。

          if ( v20 == 3840 )
          {
            if ( (unsigned int)++v14 >> 12 )
              v14 = 256;
          }
          if ( v20 >= 0xF00 )
          {
            v8 = v14 - 1;
            if ( v14 == 256 )
              v8 = 4095;
          }
          else
          {
            v8 = v20 + v14;
          }

重置v8v4基于偏移变量的值,即v20和 本身。

          lpAddress[3 * v8] = v21;
          lpAddress[3 * v8 + 1] = lpAddress[3 * v7 + 1] + 1;
          lpAddress[3 * v8 + 2] = 0;

lpAddress基于硬编码的值 (0) 和单个值的副本中设置一些其他值

          if ( v20 < 0xF00 )
            ++v20;
          v21 = v21 + lpAddress[3 * v8 + 1] - 1;
          ++v12;
          if ( v15 < 0xC && 1 << v15 == v12 )
            ++v15;
        }

基于输入和溢出复位的更多计数器更新。

        if ( v21 >= v16 )
        {
          // sub_9001000 is only called onece and always return 0
          if ( *(_DWORD *)(a1 + 8) == sub_9001000(a3, v16) )
          {
            VirtualFree(lpAddress, 0, 0x8000u);
            result = 0;
          }
          else
          {
            result = 0;
          }
        }
        else
        {
          result = 0;
        }
      }
      else
      {
        result = 0;
      }
    }
    else
    {
      result = 0;
    }
  }
  else
  {
    result = 0;
  }
  return result;
}

如果输入无效,则返回 0

int __stdcall sub_9001300(int a1)
{
  int result; // eax@2

  if ( *(_DWORD *)a1 == 1083581807 )
    result = *(_DWORD *)(a1 + 4);
  else
    result = 0;
  return result;
}

断言缓冲区以硬编码的双字开头(由于前面的 if 语句,我们已经知道这种情况)并返回第二个双字中的值。