IDA遇到浮点数计算时F5解析失败,如何复现计算过程

996.icu LICENSE

  • 问题背景
  • 解决方法
  • 总结

问题背景

问题背景也是写这篇文章的原因,起因于我之前在论坛发的贴子:求助:大量浮点数运算IDA的F5操作无法解析

总结下贴子中内容,也就是说,IDA在遇到大量浮点数运算时,按Fn+F5解析为伪代码时,是不会把这些运算给解析出来的。而Hopper则会解析为:在伪代码中保留汇编运算。

对于需要完整复现一套算法的逆向工程师来说,这种无法解析的情况是必须迈过去的一道坎,也是一个追求卓越的逆向工程师的必经之路。

参考贴子中大佬给的建议,以下是我个人的解决方法,也算是抛砖引玉,还望各位不吝赐教,欢迎提出更好的解决方法或者改进建议(或许可以编写一套自己的汇编解析方法,来自动生成伪代码)。

解决方法

我们知道在arm64架构中,存在着x0-x28的通用寄存器,以及v0-v31的浮点型寄存器(如下图,图片来自Xcode的调试信息窗口/iPhone真机调试)。


而大量的浮点数运算,就是靠这些浮点型寄存器完成的。也就是上图中的Floating Point Registers

正如函数传递通用参数(非浮点数)的调用约定是按x0-x7的顺序来的,传递浮点数时,则是按照v0-v7的顺序来的。因此,首先我们可以根据这个参数传递规则来确认输入的参数。

接着,就是本文的核心了,如何复现函数中的浮点数计算过程。

浮点型寄存器的结构其实很简单,从v0到v31,每一个浮点型寄存器都是16个字节组成的,形象的理解下,就是4个float数,即一个浮点型寄存器=4个float数。

因此,在复现的代码中,我们可以用float数组来模拟浮点型寄存器,比如v0浮点型寄存器,就可以用如下数组定义来表示

float v0[4] = {0};//模拟v0寄存器并初始化为0
float v1[4] = {0};//模拟v1寄存器并初始化为0
...

其余以此类推。
我们以一条非常简单的指令为例,来看看浮点数运算是怎么被复现的:

FSUB		S1, S3, S4

即S1 = S3 - S4,这里的S代表什么呢?我们可以从《ios应用逆向与安全》这本书中找到答案,第230页有如下说明:

128位寄存器,Q0-Q31
64位寄存器,D0-D31
32位寄存器,S0-S31

或者更直接的,通过动态调试,在调试信息窗口查看浮点型寄存器来进行确认。这样我们就知道了:上面那个S,就是代表1个float,并且是浮点型寄存器的第1个float。因此可复现为以下代码:

v1[0] = v3[0] - v4[0];

如法炮制,对于以下指令:

FMUL		D6, D6, D7

则可复现为:

*(double *)v6 = (*(double *)v6) * (*(double *)v7);

当然,除了以上这些非常简单的运算指令,我们还会遇到其他的arm64汇编指令,这种情况我们就可以直接在armDeveloper这个网站搜索指令,了解意思后再进行复现即可。

虽然上面的网站可以直接搜索指令,但国内网络体验并不好,因此也可以参考《ios应用逆向与安全》书中第229页的方法,在arm InfoCenter网站下载官方手册。不过目前官网也给出了以下说明(技术内容被移动到了上面那个体验并不好的armDeveloper网站= =),好在手册还能下载:

总结

一句话总结:浮点数计算由浮点型寄存器完成,因此理解浮点型寄存器的组成结构,用float数组复现对应计算就行!
最后一点建议,arm64汇编并不难,就是对一堆寄存器的简单操作:把内存中的数据载入寄存器->计算->存入内存。正是因为其操作过于简单,也导致了汇编的单调乏味和容易出错的特性,也是后来C、C++等高等语言出现的契机。对于我们搞逆向的来说,耐心点、细心点就问题不大哈哈。

2 Likes