用 IDA 反编译的 C 语言编写的哈希算法

逆向工程 艾达 C++ 反编译器 散列函数 CRC
2021-07-05 20:35:50

我一直在重写一个程序,虽然它使用哈希来对文件进行指纹识别,但我已经使用 IDA 来查找执行哈希的函数以及它在将文件发送到哈希函数之前对文件执行的操作。

我只是对正在发生的事情有几个问题,我知道我可以像在 DLL 中一样简单地调用它,但我也想了解发生了什么。

unsigned int __cdecl newhash(int a1, unsigned int a2, int zero)
{
  int content; // ebx@1
  int v4; // ecx@1
  int v5; // edx@1
  int i; // eax@1
  int v7; // ecx@2
  unsigned int v8; // eax@2
  int v9; // edx@2
  int v10; // ecx@2
  int v11; // eax@2
  int v12; // edx@2
  int v13; // ecx@2
  int v14; // eax@2
  unsigned int v15; // eax@3
  int v16; // edx@15
  int v17; // ecx@15
  int v18; // eax@15
  int v19; // edx@15
  int v20; // ecx@15
  int v21; // eax@15
  int v22; // edx@15
  unsigned int contentLength; // [sp+Ch] [bp-4h]@1

  content = a1;
  contentLength = a2;
  v4 = -1640531527;
  v5 = -1640531527;
  for ( i = zero; contentLength >= 12; contentLength -= 12 )
  {
    v7 = (*(_BYTE *)(content + 7) << 24)
       + (*(_BYTE *)(content + 6) << 16)
       + (*(_BYTE *)(content + 5) << 8)
       + *(_BYTE *)(content + 4)
       + v4;
    v8 = (*(_BYTE *)(content + 11) << 24)
       + (*(_BYTE *)(content + 10) << 16)
       + (*(_BYTE *)(content + 9) << 8)
       + *(_BYTE *)(content + 8)
       + i;
    v9 = (v8 >> 13) ^ ((*(_BYTE *)(content + 3) << 24)
                     + (*(_BYTE *)(content + 2) << 16)
                     + (*(_BYTE *)(content + 1) << 8)
                     + *(_BYTE *)content
                     + v5
                     - v7
                     - v8);
    v10 = (v9 << 8) ^ (v7 - v8 - v9);
    v11 = ((unsigned int)v10 >> 13) ^ (v8 - v9 - v10);
    v12 = ((unsigned int)v11 >> 12) ^ (v9 - v10 - v11);
    v13 = (v12 << 16) ^ (v10 - v11 - v12);
    v14 = ((unsigned int)v13 >> 5) ^ (v11 - v12 - v13);
    v5 = ((unsigned int)v14 >> 3) ^ (v12 - v13 - v14);
    v4 = (v5 << 10) ^ (v13 - v14 - v5);
    i = ((unsigned int)v4 >> 15) ^ (v14 - v5 - v4);
    content += 12;
  }
  v15 = a2 + i;
  switch ( contentLength )
  {
    case 0xBu:
      v15 += *(_BYTE *)(content + 10) << 24;
      goto LABEL_5;
    case 0xAu:
LABEL_5:
      v15 += *(_BYTE *)(content + 9) << 16;
      goto LABEL_6;
    case 9u:
LABEL_6:
      v15 += *(_BYTE *)(content + 8) << 8;
      goto LABEL_7;
    case 8u:
LABEL_7:
      v4 += *(_BYTE *)(content + 7) << 24;
      goto LABEL_8;
    case 7u:
LABEL_8:
      v4 += *(_BYTE *)(content + 6) << 16;
      goto LABEL_9;
    case 6u:
LABEL_9:
      v4 += *(_BYTE *)(content + 5) << 8;
      goto LABEL_10;
    case 5u:
LABEL_10:
      v4 += *(_BYTE *)(content + 4);
      goto LABEL_11;
    case 4u:
LABEL_11:
      v5 += *(_BYTE *)(content + 3) << 24;
      goto LABEL_12;
    case 3u:
LABEL_12:
      v5 += *(_BYTE *)(content + 2) << 16;
      goto LABEL_13;
    case 2u:
LABEL_13:
      v5 += *(_BYTE *)(content + 1) << 8;
      goto LABEL_14;
    case 1u:
LABEL_14:
      v5 += *(_BYTE *)content;
      break;
    default:
      break;
  }
  v16 = (v15 >> 13) ^ (v5 - v4 - v15);
  v17 = (v16 << 8) ^ (v4 - v15 - v16);
  v18 = ((unsigned int)v17 >> 13) ^ (v15 - v16 - v17);
  v19 = ((unsigned int)v18 >> 12) ^ (v16 - v17 - v18);
  v20 = (v19 << 16) ^ (v17 - v18 - v19);
  v21 = ((unsigned int)v20 >> 5) ^ (v18 - v19 - v20);
  v22 = ((unsigned int)v21 >> 3) ^ (v19 - v20 - v21);

  return (((v22 << 10) ^ 
           (unsigned int)(v20 - v21 - v22)) >> 15) ^ 
           (v21 - v22 - ((v22 << 10) ^ (v20 - v21 - v22)));
}

a1 是一个地址位置 a2 是散列零的文件长度我重命名了因为它总是出于任何原因发送零。

现在的问题:

  1. 首先也是最重要的,这是一个像CRC这样的标准算法吗?
  2. v4 和 v5 变量是否有理由为 -1640531527?
  3. (*(_BYTE *)(content + 7) << 24)不是一个字节只有8位的目的什么,所以每次都是0吗?我查了一下操作的顺序,似乎首先是转换,然后是位操作,所以这意味着它将它转换为文件中的第 8 个字节并将其位移 24 位,对吗?为什么?
  4. 为什么有些位是有符号的,有些是无符号的,如果混合会改变结果吗?

这些是我的大部分问题,我知道它正在遍历所有字节并计算出文件的“哈希”,我知道 switch case 正在处理文件不能被完全整除的情况到 12. 我想一旦我理解了按位运算背后的逻辑就会更清楚。

3个回答

-1640531527 是十六进制“0x9e3779b9”。这个数字用于 boost 哈希函数。该代码在这里的功能ub4 hash( k, length, initval)类似于你的,至少在最后一部分。我认为这是开始谷歌搜索的一个好点。

据我所知,它可能是Jenkins Hash 的中间变体(lookup2)

一些更底层的细节:

  1. (*(_BYTE *)(content + 7) << 24) 的目的是什么不是只有8位的字节,所以不会每次都是0吗?

在 C 中,移位隐式地将操作数提升为至少一个 int/unsigned int,因此 _BYTE 值被提升为一个 unsigned int。这可能是因为大多数处理器支持单个字大小而不是字节的移位。

还有另一个问题,结果被分配给一个 int 而不是一个 unsigned int,这将带你到下一个问题......

  1. 为什么有些位是有符号的,有些是无符号的,如果混合会改变结果吗?

有符号左移和无符号左移的汇编程序是相同的,因为移到寄存器外的位就消失了。这意味着反编译器无法判断左移是否无符号,因此它使用 int 的安全猜测。

有符号右移和无符号右移是不同的,因为符号位必须正确填充。这允许反编译器仅针对右移正确猜测无符号。

通常,反编译器无法检测变量是否无符号,因为很多操作与有符号变量相同。

只是对先前答案的一个小补充。

在 3 中询问的以下移位构造是一种广泛使用的将字节流转换为 32 位整数的方法。

   (*(_BYTE *)(content + 7) << 24)
 + (*(_BYTE *)(content + 6) << 16)
 + (*(_BYTE *)(content + 5) << 8)
 + *(_BYTE *)(content + 4)

31    24    23    16    15     8    7      0
AAAAAAAA    BBBBBBBB    CCCCCCCC    DDDDDDDD
  ^^^         ^^^         ^^^         ^^^
content[7]  content[6]  content[5]  content[4]

如果content是字节数组的地址,你可以简单地写

*(_BYTE *)(content+7)

作为

content[7]

但是当然,您应该content以与反编译器不同的方式声明,但是反编译器只看到一个指针,并不知道它真的是一个字节数组。