假设我们有一个汇编代码,可以使用哪些已知技术来恢复原始高级代码中使用的变量?
编辑:通过恢复变量,我不是指恢复变量名,而是试图识别用于存储临时结果的内存位置,这些临时结果可以在高级代码中被变量替换。另外,我不是在谈论字节码,而是没有类型信息,也没有嵌入完整名称的真正二进制代码。
假设我们有一个汇编代码,可以使用哪些已知技术来恢复原始高级代码中使用的变量?
编辑:通过恢复变量,我不是指恢复变量名,而是试图识别用于存储临时结果的内存位置,这些临时结果可以在高级代码中被变量替换。另外,我不是在谈论字节码,而是没有类型信息,也没有嵌入完整名称的真正二进制代码。
(我本来打算发表评论,但结果很长,它自己给出了答案)
一些评论提到了 Hex-Rays 反编译器。它的基本思想不是商业机密,实际上在Ilfak Guilfanov 的白皮书中有所描述,该白皮书随附于 2008 年的演讲。
我将在此处粘贴相关部分:
局部变量分配
此阶段使用数据流分析连接来自不同基本块的寄存器,以便将它们转换为局部变量。如果一个寄存器由一个块定义并被另一个块使用,那么我们将创建一个覆盖定义和使用的局部变量。换句话说,一个局部变量由所有可以连接在一起的定义和所有用途组成。虽然基本思想很简单,但由于字节/字/双字寄存器,事情变得复杂了。
表面上很简单,但当然实现必须考虑许多细节。而且总是有改进的余地。有这样一段话:
目前,我们不分析堆栈变量的有效范围(这首先需要很好的别名分析:我们必须能够证明堆栈变量在两个位置之间没有被修改)。我怀疑在不久的将来是否可以对堆栈变量进行完整的实时范围分析。
因此,对于堆栈变量,现在的方法很简单:每个堆栈槽都被视为整个函数的单个变量(有一些小例外)。反编译器在这里依赖于 IDA 在反汇编期间所做的工作,其中为指令的每次访问创建一个堆栈槽。
当前的一个问题是同一变量的多个名称。例如,编译器可能将堆栈变量缓存在一个寄存器中,将其传递给某个函数,然后将其重新加载到另一个寄存器中。反编译器在这里必须是悲观的。如果我们不能证明同一位置在两个时间点包含相同的值,我们就不能合并变量。例如,任何时候代码将变量的地址传递给调用,反编译器必须假设调用可能会破坏该地址之后的任何内容。因此,即使寄存器仍包含与堆栈变量相同的值,我们也不能 100% 确定。因此变量名过多。但是,用户可以使用手动映射覆盖它。
有一些关于引入函数注释的想法,这些注释将准确指定函数如何使用和/或更改其参数(类似于 Microsoft 的 SAL),这将缓解这个问题,但存在一些技术实现问题。
您所描述的正是 Gogul Balakrishnan 在其价值集分析 [1] 的博士论文中解决的问题。特别是,他根据“抽象位置”等概念为 x86 定义了内存模型。这是他对该概念的描述:
如前所述,可执行文件没有可用于分析的源代码变量等内在实体;因此,下一步是从可执行文件中恢复类似变量的实体。我们将这种类似变量的实体称为 a-locs(用于“抽象位置”)。
听起来很熟悉你的问题?你应该阅读这篇论文,但要注意——像大多数关于抽象解释的文件一样——它是简洁和不友好的阅读。
[1] http://pages.cs.wisc.edu/~bgogul/Research/Thesis/thesis.html
Soo..... 这就是二元分析很难,语义信息丢失的原因之一。变量不是计算机体系结构中已知的概念,它让人联想到更高层次的理解。
我能给你的最好的答案是,如果你正在做编译器输出分析(你是),你可以寻找该编译器用来存储变量的约定,可能是寄存器和变量“溢出”到位置的组合在堆栈帧上。
坏消息是它依赖于编译器。好消息是大多数编译器或多或少相似。
您可以尝试通过观察对值起作用的条件操作来确定有符号性(假设开发人员没有犯错误,例如比较有符号和无符号值)。
关于二进制文件中字符串的一个巧妙技巧是命令行工具strings
。值得一提的是,它不搜索“变量”。它只是寻找连续的有效字符并打印它们。因此,这也有助于从任何类型的文件中提取字符串(以明文形式存储时)。
示例程序:
int main(int argc, char* argv[]) {
char pw[]="SecretPW";
if(!strcmp(pw,argv[1])){
printf("Correct!\n");
} else {
printf("False...\n");
}
return 0;
}
使用 string 提取字符串:
$ ./test FalsePW
False...
$ strings test
SecretPW
Correct!
False...
$ ./test SecretPW 139 ↵
Correct!