IDA Hex-Rays:如何安全地修复不正确的函数声明?

逆向工程 艾达 六线谱 调用约定 堆栈变量
2021-06-21 09:29:08

更新:

问题变得更加复杂和复杂,因为在stdcallcdecl函数调用函数后,Hex-Rays 错误地恢复了堆栈

.text:00403F2F 074                 mov     edx, gameScreenHeight
.text:00403F35 074                 mov     ecx, [eax]
.text:00403F37 **074**             push    10h
.text:00403F39 078                 push    edx
.text:00403F3A 07C                 mov     edx, gameScreenWidth
.text:00403F40 07C                 push    edx
.text:00403F41 080                 push    eax
.text:00403F42 084                 mov     eax, [ecx+54h]
.text:00403F45 084                 call    eax
.text:00403F47 **070**             test    eax, eax
.text:00403F49 070                 jz      short loc_4

结果,一个带有 4 个参数的函数只有三个:

坏的: lpDD->lpVtbl->SetDisplayMode(lpDD, gameScreenWidth, gameScreenHeight, 16, **a1**)

好的: lpDD->lpVtbl->SetDisplayMode(lpDD, gameScreenWidth, gameScreenHeight, 16)

因此,该函数可能不会使用寄存器值,而只是保存和恢复它,但由于调用后将指针移至堆栈,因此被认为在某些问题调用中使用。这会导致整个调用链被标记为使用该寄存器作为参数。

最糟糕的是,在分支函数中会出现这样的问题,这些函数有多个退出点,并且在每个退出点中堆栈都是平衡的 (000)。我不能在错误调用后更改堆栈指针。我还必须找到另一个电话并平衡所做的更改。xx


最初的问题: 我需要检测并安全地修复错误识别的函数签名。我怎么能做到这一点?

例如,此功能保存游戏设置:

BOOL __thiscall sub_410640(HKEY this)
{
  HKEY v1; // ecx
  HKEY v2; // ecx

  sub_431C00(this, "volumeMaster", *(_DWORD *)&phkResult);
  sub_431C00(*(HKEY *)&g_volumeMusic, "volumeMusic", *(_DWORD *)&g_volumeMusic);
  sub_431C00(v1, "volumeFX", *(_DWORD *)&g_volumeFX);
  sub_431C00(v2, "volumeSpeech", *(_DWORD *)&g_volumeSpeech);
  return sub_431C00(*(HKEY *)&dword_4A262C, "volumeMinimum", *(_DWORD *)&dword_4A262C);
}

.text:00410640     sub_410640      proc near               ; CODE XREF: PlayVideo+50↑p
.text:00410640                                             ; sub_416910+1A6↓p
.text:00410640 000                 mov     eax, phkResult
.text:00410645 000                 push    eax             ; Data
.text:00410646 004                 push    offset ValueName ; "volumeMaster"
.text:0041064B 008                 call    sub_431C00

.text:00410650 008                 mov     ecx, g_volumeMusic
.text:00410656 008                 push    ecx             ; Data
.text:00410657 00C                 push    offset aVolumemusic ; "volumeMusic"
.text:0041065C 010                 call    sub_431C00

.text:00410661 010                 mov     edx, g_volumeFX
.text:00410667 010                 push    edx             ; Data
.text:00410668 014                 push    offset aVolumefx ; "volumeFX"
.text:0041066D 018                 call    sub_431C00

.text:00410672 018                 mov     eax, g_volumeSpeech
.text:00410677 018                 push    eax             ; Data
.text:00410678 01C                 push    offset aVolumespeech ; "volumeSpeech"
.text:0041067D 020                 call    sub_431C00

.text:00410682 020                 mov     ecx, dword_4A262C
.text:00410688 020                 push    ecx             ; Data
.text:00410689 024                 push    offset aVolumeminimum ; "volumeMinimum"
.text:0041068E 028                 call    sub_431C00

.text:00410693 028                 add     esp, 28h
.text:00410696 000                 retn
.text:00410696     sub_410640      endp

如果我们查看里面的函数,我们可以看到通过寄存器传递的参数并没有以任何方式使用。

另外,很明显,对同一个函数的调用应该是统一的,这样的参数传递没有任何意义。

BOOL __usercall sub_431C00@<eax>(HKEY a1@<ecx>, LPCSTR lpValueName, ...)
{
  LONG v3; // esi
  HKEY phkResult; // [esp+0h] [ebp-4h]
  va_list Data; // [esp+Ch] [ebp+8h]

  va_start(Data, lpValueName);
  phkResult = a1;
  if ( RegOpenKeyExA(HKEY_LOCAL_MACHINE, SubKey, 0, 1u, &phkResult) )
    return 0;
  v3 = RegSetValueExA(phkResult, lpValueName, 0, 4u, (const BYTE *)Data, 4u);
  RegCloseKey(phkResult);
  return v3 == 0;
}

.text:00410600     sub_410600      proc near               ; CODE XREF: sub_4073F0+2B0↑p
.text:00410600                                             ; WinMain(x,x,x,x)+5F4↓p ...
.text:00410600
.text:00410600     arg_0           = dword ptr  4
.text:00410600
.text:00410600 000                 mov     eax, [esp+arg_0]
.text:00410604 000                 test    eax, eax
.text:00410606 000                 mov     ecx, 1
.text:0041060B 000                 mov     dword_4AE978, ecx
.text:00410611 000                 mov     dword_4AF074, eax
.text:00410616 000                 jz      short locret_410632
.text:00410618 000                 mov     dword_4A267C, 69h
.text:00410622 000                 mov     dword_4AE920, 0
.text:0041062C 000                 mov     dword_4AF03C, ecx
.text:00410632
.text:00410632     locret_410632:                          ; CODE XREF: sub_410600+16↑j
.text:00410632 000                 retn
.text:00410632     sub_410600      endp

现在我需要调整这些函数的声明,使其符合现实。

但是出现了两个问题:

  1. 如何理解问题所在?
  2. 如何安全地进行更正,使一个函数中的一个错误不会导致整个应用程序数据库的堆栈不平衡和反编译错误?

预期结果:

BOOL __usercall sub_431C00(LPCSTR lpValueName, _DWORD value);

sub_431C00("volumeMaster", *(_DWORD *)&phkResult);
sub_431C00("volumeMusic", *(_DWORD *)&g_volumeMusic);
sub_431C00("volumeFX", *(_DWORD *)&g_volumeFX);
sub_431C00("volumeSpeech", *(_DWORD *)&g_volumeSpeech);

链接到此 PE 文件

哦,是的,有趣的是,在这种情况下,IDA 正确定义了函数签名,但由于某种原因,Hex-Rays 炸毁了屋顶:

国际开发协会: int __cdecl sub_431C00(LPCSTR lpValueName, BYTE Data)

六角射线: BOOL __usercall sub_431C00@<eax>(HKEY a1@<ecx>, LPCSTR lpValueName, ...)

2个回答

欢迎来到 RE.SE!

如何理解问题所在?

当函数被反编译时,IDA 通过数据流分析检查其依赖关系。基本上,它询问必须定义哪些值才能使该函数工作。

当它遇到函数 ( push ecx)的第一行时,IDA 注意到 ecx 的值在此函数中定义之前就已使用,因此它将其建立为依赖项,尽管 IDA 很清楚它不是函数的参数RegOpenkeyExA通过其签名调用。在任一函数中, ecx 值被恢复。

这让 IDA 认为,ecx 是函数签名的一部分:它实际上是

有趣的 IDA 事实:当你先反编译函数sub_410640而不看 时sub_431C00,签名就像你想要的一样(由于缺乏信息)

int sub_410640()
{
  sub_431C00("volumeMaster", phkResult);
  sub_431C00("volumeMusic", dword_4D55C8);
  sub_431C00("volumeFX", dword_4D55CC);
  sub_431C00("volumeSpeech", dword_4D55D0);
  return sub_431C00("volumeMinimum", dword_4A262C);
}

核心问题是这个函数中 ecx 寄存器的奇怪用法。即使从优化的角度来看,它也没有任何意义。

如何安全地进行更正,使一个函数中的一个错误不会导致整个应用程序数据库的堆栈不平衡和反编译错误?

例如,您可以 NOP-out 在函数中推送和弹出 ecx 的代码,因为调用函数似乎不依赖于这个保持不变的值。我很快就测试了它,它似乎有效。

您还可以解决指针别名问题。如果您能找到一种方法来证明ecx堆栈上值未被写入或读取,则可以省略此参数。可悲的是,这很难。

我想您也可以尝试使用不同的启发式方法提出更好的反编译器。您也可以尝试使用 RetDec 或 Snowman 以获得更好的结果,但它们会执行类似于 Hex-Rays 的数据流分析,最终可能会得到类似的结果。

反编译器的工作方式有点像单向函数。当您调用它时,会将当前函数和元数据(如变量名称和函数签名)传递给它。尽管它允许进行一些更正,但它只是元数据,根本不应更改汇编代码。

反编译器决定ecx使用 bysub_431C00因为push ecx在函数的开头填充了变量稍后使用的堆栈槽phkResult,所以看起来好像 的初始值phkResult取自ecx

.text:00431C00                 push    ecx
.text:00431C01                 lea     eax, [esp+4+phkResult]
.text:00431C04                 push    eax             ; phkResult
.text:00431C05                 push    1               ; samDesired
.text:00431C07                 push    0               ; ulOptions
.text:00431C09                 push    offset SubKey   ; "SOFTWARE\\Valkyrie Studios\\Septerra Co"...
.text:00431C0E                 push    80000002h       ; hKey
.text:00431C13                 call    ds:RegOpenKeyExA
.text:00431C19                 test    eax, eax
.text:00431C1B                 jz      short loc_431C21
.text:00431C1D                 xor     eax, eax
.text:00431C1F                 pop     ecx
.text:00431C20                 retn

事实上,这push ecx只是一个较短的版本sub esp, -4- 的值ecx不需要在函数调用之间保留,因此此推送用于为phkResult变量保留堆栈的 4 个字节不幸的是,自动算法很难区分真正的推送来保存寄存器或将参数传递给函数调用和虚拟推送来操作堆栈。如果您确定 ecx 用法是误报,只需修复函数原型 ( Ykey) 并删除不必要的参数:

int __cdecl sub_431C00(LPCSTR lpValueName, DWORD Data)
{
  LSTATUS v3; // esi
  HKEY phkResult; // [esp+0h] [ebp-4h]

  if ( RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Valkyrie Studios\\Septerra Core", 0, 1u, &phkResult) )
    return 0;
  v3 = RegSetValueExA(phkResult, lpValueName, 0, REG_DWORD, (const BYTE *)&Data, 4u);
  RegCloseKey(phkResult);
  return v3 == 0;
}

现在父子也有很好的反编译:

int sub_410640()
{
  sub_431C00("volumeMaster", g_volumeMaster);
  sub_431C00("volumeMusic", g_volumeMusic);
  sub_431C00("volumeFX", g_volumeFX);
  sub_431C00("volumeSpeech", g_gvolumeSpeech);
  return sub_431C00("volumeMinimum", g_volumeMinimum);
}

所以,总结一下:

  1. 该问题是由于编译器优化混淆了反编译器而导致它决定使用寄存器值而它只是一个虚拟填充器。
  2. 没有一种真正的解决方案总是有效,您需要进行试验并准备回滚并重试。有了经验,这将变得更容易。

PS IDA 没有添加ecx到函数原型中,因为它只分析堆栈参数。反编译器进行数据流分析,因此它也可以恢复寄存器参数,但有时会导致误报,例如此处。