新手小白也能看懂的LLDB技巧/逆向技巧

引言

在我们日常工作中,难免会遇到一些Crash等疑难问题需要查看系统实现,有时候在做一些高级定制功能时,比如自定义富文本渲染等,也需要查看系统是怎么做的;特别是对于iOS程序员来说,由于Apple的闭源生态,遇到以上问题基本只能靠逆向手段来逐步分析。学会汇编和逆向,可以说是一个程序员从入门走向进阶的第一步。

前面我们通过逆向手段分析了CoreText中的字体级联/Font Fallback机制,正好借此机会聊聊一些常用的逆向思路与技巧;本篇文章更多面向小白入门,不需要你有太多的汇编、LLDB知识,我们将结合一些典型场景来举一反三,当然,如果大家有更好的奇技淫巧欢迎一起交流、学习。

持续更新,最新内容也可以在公众号「非专业程序员Ping」一起交流!!

一、工欲善其事,必先利其器

在开始之前,需要先了解两个常用的逆向工具:Hopper 和 IDA。具体安装和使用我们也有专门的文章介绍:常见逆向工具使用

一般而言我们都是结合两个工具一起看,IDA 的反汇编通常比 Hopper 还原度更高,所以我们主用的还是IDA。

二、查找二进制文件路径

分析的第一步,肯定是找到我们想要分析的入口函数,以CoreText中的字体级联/Font Fallback机制这篇文章的Case为例子,我们要研究的是 CTFontCopyDefaultCascadeListForLanguages 的实现,我们可以在LLDB中通过如下命令找到该符号所在库:

image lookup -rn CTFontCopyDefaultCascadeListForLanguages

在这里插入图片描述

通过这个命令我们可以得到两个信息:

1)CTFontCopyDefaultCascadeListForLanguages 在CoreText库中

2)CoreText 二进制文件的路径

得到路径之后,在访达中可以通过快捷键 Command + Shift + G 可以快速跳转到位置,得到CoreText的二进制文件:
在这里插入图片描述

三、善用AI

得到CoreText的二进制文件之后,我们通过IDA进行反汇编。

之后将 CTFontCopyDefaultCascadeListForLanguages 的主要调用逻辑Copy到单独的文件夹:
在这里插入图片描述

在AI没普及之前,我们一般是结合LLDB硬着头皮逐行分析,有了AI之后,我们可以直接将整个文件夹扔给AI(比如Cursor、GPT等),让AI给我们梳理流程、逐行注释,输出调用流程图等。

如下,是AI生成的函数调用流程;结合AI的逻辑梳理和注释,我们对 CTFontCopyDefaultCascadeListForLanguages 的整体逻辑会有一个大致的理解。

在这里插入图片描述

当然,AI也不是全能的,一些逻辑AI也只是猜测甚至乱说😩,具体的细节还需要我们结合LLDB、IDA一起分析,只是说有了AI之后,可以大大加快我们的分析效率。

下面我们将列举几个典型例子,我们日常分析中能遇到的也基本就这些。

四、典型例子

4.1 函数调用

不管是面向对象,还是面向过程,在汇编的世界里,程序逻辑都是由一个一个独立的函数调用组成的;对函数而言,最重要的就是它的输入输出,基本上我们只要读懂了每个函数的输入输出是什么,那我们就能理解整体的逻辑。

函数入参与返回值,按照数据类型不同可以总结为下面的表格:我们最常接触的,其实只有前两项
在这里插入图片描述

举个🌰:

1)整数/指针类型

// 以如下调用为例
let ctFont = UIFont.systemFont(ofSize: 18)
let languages = ["zh-HK", "zh-Hans"]
CTFontCopyDefaultCascadeListForLanguages(ctFont, languages as CFArray)
入参 返回值
在这里插入图片描述 在这里插入图片描述

2)浮点数类型

// 以getArea为例
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let area = getArea(frame: self.view.frame)
print(area)
}

func getArea(frame: CGRect) -> CGFloat {
return frame.width * frame.height
}
}
入参 返回值
在这里插入图片描述 在这里插入图片描述

4.2 怎么确定一段地址的含义

还是以举一个实际例子,在CoreText中的字体级联/Font Fallback机制文章中,我们要分析 TBaseFont::CreateFallbacks 的实现,签名如下:

__int64 __usercall TBaseFont::CreateFallbacks@<X0>(__int64 result@<X0>, __int64 a2@<X1>, __int64 a3@<X2>, __int64 a4@<X3>, __int64 a5@<X4>, _QWORD *a6@<X8>)
}

其中第一个参数 result@ 通过调用的地方我们知道是 CTFont 中的一个成员变量,但具体是什么不确定。

我们最终结合LLDB做如下尝试分析出了 result@ 的具体含义:
在这里插入图片描述
同理,我们如果拿到一段地址要分析其含义的话,也可以按这个步骤进行尝试。

4.3 如何修改 if 条件

我们以如下函数调用为例:
在这里插入图片描述
我们传入的num是1,正常逻辑会执行 num + 2 返回 3,那有没有办法不修改入参,执行 num + 1 的路径。

我们来看下这段逻辑的汇编代码:
在这里插入图片描述
图片注释说的比较明白;if 条件在汇编层面一般被翻译成 tbz(Test Bit and branch if Zero)、tbnz(Test Bit and branch if Not Zero)等,我们可以通过修改寄存器的值来更改执行分支;这在分析系统API时,是一个很有用的思路。

举一反三,通过LLDB,我们可以实时修改任何寄存器、内存地址的值,这可以非常方便的帮我们浮现一些偶现的Bug、Crash等。

4.4 如果跳过函数执行/修改函数返回值

我们还是以上面的addNum为例,如果我们不修改入参,但是想修改返回值,应该怎么做:

最直接的方式是等addNum执行完后,修改返回值寄存器,如下,返回值存储在w0,我们可以修改成任意想要的值:
在这里插入图片描述

但这种方式有个弊端是,需要等函数执行完,在实际的场景中,我们往往希望不执行函数且能让函数返回我们想要的值,比如:函数中可能会修改其他参数,函数可能有复杂的鉴权逻辑无法通过鉴权等,这些场景我们往往不希望或者不能让函数执行,这种情况下就需要另一个指令:thread return

以addNum为例,按如下步骤操作:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

使用 thread return 时,需要注意,我们的断点需要打在 objc_msgSend 的入口处,如果在 addNum 的入口打断点执行 thread return,最终可能会得到非预期值;原因是 thread return 改的是当前 frame 的返回寄存器,由于Objective-C的runtime特性,函数调用实际上是经过 objc_msgSend 给对象发消息,addNum 的上一个frame其实是 objc_msgSend,而 objc_msgSend 返回时可能会继续修改 w0 寄存器的值,会导致最终上层取到的值非预期。

4.5 善用 watchpoint 命令

在实际场景中,我们可能会遇到某个变量被很多地方修改,但是我们又无法一一断点,比如我们要查看哪些地方修改了 UIView 的 frame;或者某个值的修改链路很深,一般在系统库中比较常见,比如iOS中闭包嵌套闭包调用的场景。

针对以上场景,我们可以使用 watchpoint 命令,来观测变量的修改,watchpoint 的常见用法如下:
在这里插入图片描述

五、总结

本文总结了我们在逆向的通用分析思路,以及LLDB调试汇编逻辑中可能遇到的常见技巧和场景;本文会长期更新,觉得有用的小伙伴可以关注下公众号,及时接收更新;当然,各路大神有更多有用技巧,也欢迎指教!

posted on 2025-10-18 16:07  非专业程序员Ping  阅读(51)  评论(0)    收藏  举报

导航