交互式反编译工具rellic-xref解析 - 深入LLVM模块与C代码的关联分析

本文详细介绍Trail of Bits开发的交互式反编译工具rellic-xref,该工具通过可视化界面展示LLVM模块与反编译C代码的映射关系,并支持自定义代码优化流程,为逆向工程提供强大支持。

交互式反编译工具rellic-xref解析

Rellic是一个将LLVM模块分析和反编译为C代码的框架,实现了Dream反编译器及其后继者Dream++论文中描述的概念。最近我在博客中介绍了rellic-headergen工具,它能从LLVM模块提取调试元数据并生成可编译的C头文件。本文将重点介绍我开发的另一个工具——rellic-xref,用于探索原始LLVM模块与其反编译C版本之间的关系。

rellic-xref的交互界面

我的任务是改进Rellic生成的"溯源信息"(即关于每条LLVM指令如何关联最终反编译代码的信息)。然而,Rellic的主要前端工具rellic-decomp采用批处理方式生成C源代码,几乎不展示内部处理过程,特别是完全不输出我感兴趣的溯源信息。

为解决这个问题,我开发了rellic-xref作为Web界面,使Rellic能以交互图形方式使用。该工具会启动本地Web服务器作为底层反编译器的接口。用户上传LLVM模块后,右侧面板会显示模块的文本表示。用户可以对LLVM位码运行各种预处理步骤,或直接进行反编译步骤(当模块包含反编译引擎尚不支持的指令时,预处理是必需的)。

反编译完成后,界面会并排显示原始模块和反编译源代码:

此时工具的核心功能已可体验:将鼠标悬停在反编译代码部分会高亮显示导致其生成的指令,反之亦然。

代码优化流程

直接从反编译器输出的源代码可读性欠佳。为此,Rellic提供了一系列优化步骤使代码更易读。这些优化步骤通常以迭代方式执行,在rellic-decomp中执行顺序是硬编码的。例如点击"Use default chain"按钮会将rellic-decomp的默认优化配置加载到rellic-xref中。

默认优化链包含只执行一次的步骤和迭代计算以寻找固定点的步骤。但rellic-xref允许用户改变优化步骤的运行顺序和方式:可以移除步骤(使用上图所示的"x"按钮)或随意插入新步骤。

虽然Rellic的优化步骤操作的是Clang抽象语法树(AST),但它们并不适用于任意C程序,因为这些步骤依赖于反编译过程生成AST的特定假设。特别是反编译代码最初采用类似静态单赋值(SSA)的形式,这直接反映了它是由SSA形式的LLVM位码生成的特性。

优化步骤详解

对于不熟悉Rellic优化步骤的读者,我们提供这些步骤的描述及代码优化示例:

死代码消除

移除AST中不产生副作用的多余语句。例如单独的分号和各种表达式都可以安全移除。

原始代码:

1
2
3
4
5
6
void foo() {
    int a;
    a = 3;
    a;
    bar(a);
}

优化后:

1
2
3
4
5
void foo() {
    int a;
    a = 3;
    bar(a);
}

Z3条件简化

使用Z3 SMT求解器优化if和while条件,通过递归检查每个条件的语法树并修剪确定为真或假的分支来尽可能缩减条件大小。

原始代码:

1
2
3
4
5
6
7
if(1U && x == 3) {
    foo(x);
}

if(x != 3 || x == 3) {
    foo(x);
}

优化后:

1
2
3
4
5
6
7
if(x == 3) {
    foo(x);
}

if(1U) {
    foo(x);
}

嵌套条件传播

将父语句中的条件传播到子语句。这意味着父if/while语句中的条件在嵌套if/while语句中被假定为真。

原始代码:

1
2
3
4
5
if(x == 0) {
    if(x == 0 && y == 1) {
        bar();
    }
}

优化后:

1
2
3
4
5
if(x == 0) {
    if(1U && y == 1) {
        bar();
    }
}

嵌套作用域合并

简单地将出现在复合语句或trivially true的if语句中的语句提取到父作用域。该步骤假设所有局部变量已在函数开头声明。

原始代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void foo() {
    int x;
    int y;
    {
        x = 1;
    }
    if(1U) {
        y = 1;
    }
}

优化后:

1
2
3
4
5
6
void foo() {
    int x;
    int y;
    x = 1;
    y = 1;
}

基于条件的优化

识别作用域中包含相反条件的相邻if语句,将它们合并为if-else结构。

原始代码:

1
2
3
4
5
6
if(a == 42) {
    foo();
}
if(!(a == 42)) {
    bar();
}

优化后:

1
2
3
4
5
if(a == 42) {
    foo();
} else {
    bar();
}

基于可达性的优化

类似基于条件的优化,但识别的是互斥但不完全相反的连续if语句,将它们重组为if-else-if结构。

原始代码:

1
2
3
4
5
6
if(a == 42) {
    foo();
}
if(!(a == 42) && b == 13) {
    bar();
}

优化后:

1
2
3
4
5
if(a == 42) {
    foo();
} else if(b == 13) {
    bar();
}

循环优化

识别负责终止无限while循环的if语句,重构代码创建带条件的循环。

原始代码:

1
2
3
4
5
6
while(true) {
    if(a == 42) {
        break;
    }
    foo();
}

优化后:

1
2
3
while(!(a == 42)) {
    foo();
}

表达式组合

执行多种简化,如将指针算术转为数组访问、移除多余的类型转换等。

原始代码:

1
2
3
*&x
!(x == 5)
(&expr)->field

优化后:

1
2
3
x
x != 5
expr.field

条件规范化

这是默认优化链中唯一不包含的步骤,目的是将条件转为合取范式以揭示更多简化机会。但由于可能产生指数级膨胀的条件,建议谨慎使用且仅作为Z3最终简化前的最后一步。

如何使用?

结语

rellic-xref将传统的批处理式Rellic转变为交互式工具,深入展示了反编译和优化过程。这也揭示了Rellic框架的更多可能性。例如,如果用户能对底层Clang AST有更多控制会怎样?允许自定义变量重命名和类型修改等功能将使Rellic更像逆向工程套件的标准组件。未来对rellic-xref的改进(或开发类似工具)可以在这方面赋予用户更多控制权。

Rellic的优化步骤直接在C AST层级操作并大量使用Clang API。这在我使用Rellic时既是优势也是障碍。例如,称Clang AST为"抽象"有些用词不当,因为它同时具有抽象和具体语法树的特征。我的经验表明Clang AST接口并非设计为可变资源,更像是"一次写入,多次读取"的语义。我们计划将Rellic迁移到使用MLIR的新项目中,这可能有助于解决此问题,但这已超出本文讨论范围。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计