编写完自己的反汇编程序后,我现在希望使其汇编列表更易于阅读,例如摘自(人工)例子

push    ebp
mov     ebp, esp
sub     esp, 10h
mov     eax, dword ptr [55431824h]
imul    eax, dword ptr [ebp+8]
add     eax, dword ptr [ebp+0ch]
mov     dword ptr [ebp-4], eax
mov     eax, dword ptr [ebp-4]
leave
ret     10h


(通过>标记为自动识别的序言/结尾):

>push    ebp
>mov     ebp, esp
>sub     esp, 10h

 mov     eax, dword ptr [_global_55431824]
 imul    eax, dword ptr [arg_0]
 add     eax, dword ptr [arg_4]
 mov     dword ptr [local_4], eax
 mov     eax, dword ptr [local_4]

>leave
>ret     10h


朝着

eax = 10
eax *= arg_0
eax += arg_4
local_4 = eax
eax = local_4
return


此时,每个伪指令仍与原始反汇编表示相关。打印时,我可以扫描某些序列,因此我将前两行替换为

eax = 10 * arg_0


,然后跳过下一行。但是,我可以做到这一点有一个极限。串联下一个操作,将导致

eax = 10 * arg_0 + arg_4


需要先看两条指令,并且仅适用于movimuladd的特定组合,而确保目标寄存器仍然是目标寄存器,并且中间指令对其他寄存器没有持久影响。

目标是最终得到这样的结果:

local_4 = 10 * arg_0 + arg_4
eax = local_4
return


其中显然在eax之前为return专门分配了一个值,所以最后一行可以是

return eax


,最后整个结构可以折叠为

return 10 * arg_0 + arg_4


(这与我在原始的琐碎C程序中开始的内容非常接近)。我正在为诸如最后一条这样的复合线的内部表示而苦苦挣扎。否则,非常可靠的反编译器页面backerstreet.com/creating_statements会在原始汇编程序和类似C的复合语句之间随意切换:

if(!expr1 && !expr2)
  ...


,而没有解释中间步骤如何存储在内存中并可以创建输出字符串。

我应该查看哪种类型的中间存储形式,(a)可以根据原始的反汇编指令来构造,(b)可以对其元素进行组合和重新排列,并且(c)可以转换为人类可读的输出,例如最后一行?

值得一提的是,我并不是要创建通用通用反编译器:)我的测试输入是用Delphi Pascal的较旧版本(很可能是1993年前)编译的,但是,尽管它的程序集并没有真正经过优化和可读性,但我还是想加倍努力,让我的计算机尽其所能,并使其更易于理解代码。


一个更长的真实示例

下面是一些实际的反汇编。基本块编号后跟一个支配器位(loops十六进制数;我生成了它们,但尚未使用该数据)。 ENTRYEXIT编号再次到达基本块,并用于生成函数的.dot视图。

; ## BLOCK 0; loops: 01; EXIT to 1, 4
401966A8    (  0)  8B 15 42201590       mov     edx,dword ptr [42201590]
401966AE    (  0)  8B 12                mov     edx,dword ptr [edx]
401966B0    (  0)  80 BA 39 01 00 00 02 cmp     byte ptr [edx+139h],2
401966B7    (  0)  75 11                jnz     label_4

; ## BLOCK 1; loops: 03; ENTER from 0; EXIT to 2, 4
401966B9    (  0)  83 78 24 02          cmp     dword ptr [eax+24h],2
401966BD    (  0)  7D 0B                jge     label_4

; ## BLOCK 2; loops: 07; ENTER from 1; EXIT to 3=RET
401966BF    (  0)  BA 02 00 00 00       mov     edx,2
401966C4    (  0)  2B 50 24             sub     edx,dword ptr [eax+24h]
401966C7    (  0)  8B C2                mov     eax,edx

; ## BLOCK 3 (epilog); loops: 0F; ENTER from 2
401966C9          >C3                   retn    

; ## BLOCK 4; loops: 11; ENTER from 0, 1; EXIT to 5, 7
                        label_4:
401966CA    (  0)  8B 15 42201590       mov     edx,dword ptr [42201590]
401966D0    (  0)  8B 12                mov     edx,dword ptr [edx]
401966D2    (  0)  80 BA 39 01 00 00 01 cmp     byte ptr [edx+139h],1
401966D9    (  0)  75 0D                jnz     label_7

; ## BLOCK 5; loops: 31; ENTER from 4; EXIT to 6, 7
401966DB    (  0)  83 78 24 00          cmp     dword ptr [eax+24h],0
401966DF    (  0)  75 07                jnz     label_7

; ## BLOCK 6; loops: 71; ENTER from 5; EXIT to 8=RET
401966E1    (  0)  B8 01 00 00 00       mov     eax,1
401966E6    (  0)  EB 02                jmp     label_8

; ## BLOCK 7; loops: 91; ENTER from 4, 5; EXIT to 8=RET
                        label_7:
401966E8    (  0)  33 C0                xor     eax,eax

; ## BLOCK 8 (epilog); loops: 0111; ENTER from 6, 7
                        label_8:
401966EA          >C3                   retn    

401966EB                align 4


clear if是黄色的,并且节点包含其控制位集,因此我可以尝试理解它们(尚待做)。这说明了流程,但是您看不到每个节点代表多少代码。



反汇编被解析为以下伪代码。一些注意事项是manually added

; #### PARSED
401966A8    (  0)       edx = Main.UnitList@20551314 ;
401966AE    (  0)       edx = (dword)[edx] ;
401966B0    (  0)       if ((byte)[edx + 139h] != 2) goto label_4 ;

401966B9    (  0)       if ((dword)[eax + 24h] >= 2) goto label_4 ;

401966BF    (  0)       edx = 2 ;
401966C4    (  0)       edx -= (dword)[eax + 24h] ;
401966C7    (  0)       return edx ; MOV EAX,.. where an exit block follows

                        return ;  .. and this line is generated by the actual exit block

                    label_4:
401966CA    (  0)       edx = Main.UnitList@20551314 ;
401966D0    (  0)       edx = (dword)[edx] ;
401966D2    (  0)       if ((byte)[edx + 139h] != 1) goto label_7 ;

401966DB    (  0)       if ((dword)[eax + 24h] != 0) goto label_7 ;

401966E1    (  0)       eax = 1 ;
401966E6    (  0)       return ;  this was a jump-to-exit-block, so missed

                    label_7:
401966E8    (  0)       eax = 0 ; this was a XOR, not a MOV, so missed

                    label_8:
                        return ;


冗长性的损失已经值得努力:从21行代码到16行,即使提前检查EAX是否得到在一次RET两次失败之前被写到右边。反转条件,整个块可以是单个if并支撑到401966B0上,从而删除if
return开始的label_4的操作可以合并为单个edx语句。
此外,可以将401966BFreturn条件组合为一个,并放在if块中。由于在其末尾有一个401966D2,因此在那里不需要if。手动重建:

401966A8    (  0)       edx = Main.UnitList@20551314 ;
401966AE    (  0)       edx = (dword)[edx] ;
401966B0    (  0)       if ((byte)[edx + 139h] == 2 &&
401966B9    (  0)           (dword)[eax + 24h] < 2)
                        {
401966BF    (  0)           return 2 - (dword)[eax + 24h] ;
                        }

401966CA    (  0)       edx = Main.UnitList@20551314 ;
401966D0    (  0)       edx = (dword)[edx] ;
401966D2    (  0)       if ((byte)[edx + 139h] == 1 &&
401966DB    (  0)          (dword)[eax + 24h] == 0)
                        {
401966E1    (  0)           return 1 ;
                        }

401966E8    (  0)       return 0 ;


将原来的21行减少到仅11行。此外,寄存器值传播将对全局类return的引用解析。我手动为其元素创建了名称; else和转发折叠为

401966B0    (  0)       if (Main.UnitList.flag == 2 &&
401966B9    (  0)           Main.UnitList.count < 2)


,这使代码几乎可读。该函数是一个类函数,并且Main.UnitList指向类实例,因此401966A8读取结构的数据成员。 eax是指向全局声明的类实例的指针,因为它指向BSS。

评论

您要尝试做的事情很难。考虑一下Hex-Rays反编译器已经开发了多长时间,以及需要多少手动调整结果。

@GuntramBlohm:好吧,我不得不同意这一点。从寄存器和“局部”变量到“实际”变量的下一步是在我的能力范围内(跟踪每个寄存器的生命周期),但是最后我得到了一个单数操作列表,我想将它们串联起来。至少对于平凡的情况。这需要一个IL,从概念上讲与原始程序集是分开的。

确实,就像@GuntramBlohm所说的那样,反编译不是为了胆小的人。但是,这是可行的,尽管您必须做出艰难的选择。查看McSema(blog.trailofbits.com/2014/08/07/…),它将x86程序集转换为LLVM IR的工作非常出色。请记住,您要执行的操作需要广泛使用启发式和模式匹配。

@yaspr:看起来不错!是的,由于我的目标相当有限(我在其中添加了一段内容),因此我可能会研究模式匹配。所使用的编译器很老套,没有花哨的优化–在这里和那里甚至没有(人为)明显的优化。

@Jongware好,我有点注意到大多数现代编译器会避免的堆栈操作;我的想法是这是-O0代码还是旧的编译器。

#1 楼

为了创建“人类”可读的表达式,我将使用算术解析树。实际上,每个涉及到的寄存器和内存位置都有一个单独的解析树。

每次分配都会将目标树的根复制到目标树。

每个算术运算会创建一个包含该运算的新根,并且操作数是左右子元素。结果树将分配给算术运算的目标。

您的示例如下构建:



从那里发出得到的公式只是一个ltr树漫步。您可能首先要简化表达;您可以将具有两个常数子节点的运算符替换为运算结果,并且可能希望通过尽可能向下移动常数来变换树,例如:

Google表示“表达式树”和“常量折叠”,以获取更多信息。循环控制变量将出现在树的顶部节点以及树的下方。实际上,这是识别循环变量的好方法。这些基本块的表达式树。如果运行良好,则可以继续查找跨越多个块但在其中一些块中恒定的东西(例如循环),以找出整个函数的内容。 :另一个示例,显示了如何处理寄存器移动

原始寄存器移动只是将树从源寄存器复制到目标寄存器。这是来自旧的watcom C程序,该程序在imul速度慢而一系列的shift / add指令速度更快时进行编译。编译器在此处计算var * 45:

              eax
               |
              add
               |
     +---------+--------+
     |                  |
    add                 5
     |                  
+----+-------+          
|            |
2           ebx


              eax
               |
              add
               |
     +---------+--------+
     |                  |
    add                ebx
     |                  
+----+-------+         
|            |
2            5

              eax
               |
              add
               |
     +---------+--------+
     |                  |
     7                 ebx




评论


感谢您的出色示例。我已经有了基本块(自上一个问题以来得到了改进),它们在确定变量范围方面有很大帮助。由于无论如何都需要清除变量,是否可以通过将原始寄存器移动存储在同一棵树中然后清除分支来完成?

–杂件
2015年5月7日19:31



添加了另一个示例。原始寄存器移动转换为从源复制到目标的树。最后,每个寄存器都有一个具有寄存器值的树(edx,在最后一步中未显示,仍然具有var * 5),并且每个树仅取决于输入变量(eax的最后一个表示中没有edx,即使在计算中使用了它)。清理eax树后,您将获得var mul 45。

–贡特拉姆·布洛姆(Guntram Blohm)
15年5月8日在6:37



#2 楼

您在此处描述的内容与使用二进制语言(例如C)作为输出目标而不是特定体系结构的静态二进制翻译器基本相同。它不像反编译那样先进,但是已经证明在街机游戏等受限领域非常有效。 (例如,http://vide.malban.de/vectrex-programs/vecfever)。我有一些正在进行的代码正在执行,例如,代码可用于6809、6502,z80和Cinematronic CPU(尽管可能无法立即使用-它由四个独立的后台项目共同组成,没有紧急性)。无论如何,请随意浏览一下,看看这里是否有什么对您有帮助的:http://gtoal.com/SBTPROJECT/(如果您只是浏览而不是实际运行它,那么6809可能是最干净的代码)

在http://gtoal.com/sbt/

上有一些关于整个方法的文章:
            //        LDA   $FFFF,X                 ; 0431: A6 1F         
      A = memory[(UINT16)(X + 0xffff)];
  //  N = A;
  //  Z = A;
  //  V = 0;

            //        LEAU  ,X                      ; 0433: 33 84         
      U = X;

            //        LDX   #q4312078qEC8                  ; 0435: 8E 0E C8      
      X = 0x0ec8;
  //  Z = X;
  //  N = (X) >> 8;
  //  V = 0;


(注释出的行是由于冗余存储的优化所致)

除了输出用于执行的功能代码之外,您还可以通过输出描述详细介绍了操作的工作。

其他实用的翻译由David Welch,Norbert Keher,Thomas Sontowski和其他人撰写。还有一个Orion项目,它是由Neil Bradley(受尊敬的模拟器作者)和Orion邮件列表的其他几位成员领导的多人多目标翻译器,该翻译器具有适用于6502和z80的工作前端,以及68000和6809也正在开发中,并构建了一个内部AST(抽象语法树,与该线程先前的回答之一中所描述的一样),以便将更干净,更高效的代码输出到包括C在内的各种后端。 Christina Cifuentes等人撰写的一些学术著作)

Graham

PS当输出是诸如C或LLVM的内部IR之类的通用IR(内部表示)时,而不是直接生成二进制文件或通过输出特定体系结构的asm间接生成二进制文件时,该技术被正确地称为“编译指令集仿真” -但是大多数人都将其称为静态二进制翻译。