前言
在内核中有许多关键变量和校验,为获得这些变量和绕过校验就要求我们在内存中定位这些地址。本文将介绍 Undecimus 中基于 String XREF 定位关键内存地址的方法,通过该方法不仅可以准确定位内核中的特定元素,也能为自行设计二进制分析工具带来很好的启发。
定位 Kernel Task
为了获取内核信息,我们需要定位到 Kernel Task 的地址,再通过 tfp0 的 kread 读取内容。要定位 Kernel Task,关键是找到获取 Kernel Task 的代码,然后尝试从内存中定位这段代码,再分析指令解出变量的文件偏移即可。
查找使用 Kernel Task 的函数
在 xnu-4903.221.2 中可以找到访问 Kernel Task 的如下代码:
1 | int |
这里有一段字符串 “shouldn’t be applying exception notification to process!” 可用于辅助定位,它在编译后会被存储在 __TEXT,__cstring
段,通过在内存中搜索 __TEXT,__cstring
段即可找到字符串地址,我们称之为 location_str
。
定位到函数中的 String XREF
由于 ARM 的取址常常需要 2 条指令完成,为了定位使用 location_str
的代码,我们需要对代码段进行静态分析。当发现寄存器中的值等于 location_str
时即发现了一个交叉引用(XREF),通过这种手段我们便能在内存中定位到语句 panic("shouldn't be applying exception notification to process!")
对应的指令地址。
回溯找到 Kernel Task XREF
最快定位到 Kernel Task 的方法是回溯到 task_suspend(p->task)
,在 task_suspend
第一次访问 p->task
时一定会对 task 寻址,我们可以从寻址指令中解出 task 的文件偏移,再加上内核在内存中的基地址即可得到 Kernel Task 的地址。
1 | kern_return_t |
从上面的分析可以看出问题的关键在于 XREF 的定位,下面我们将分析一种 String Based XREF 定位算法来解决上述问题。
在内存中加载 Kernelcache
根据 iPhone Wiki 给出的 Kernelcache 定义[1]:
The kernelcache is basically the kernel itself as well as all of its extensions (AppleImage3NORAccess, IOAESAccelerator, IOPKEAccelerator, etc.) into one file, then packed/encrypted in an IMG3 (iPhone OS 2.0 and above) or 8900 (iPhone OS 1.0 through 1.1.4) container.
即 kernelcache 就是将 kernel 和它的扩展打包在一个文件中并以 IMG3 格式存储(iOS 2 以上)。
在 上一篇文章 中我们介绍了基于 tfp0 的沙盒逃逸方法,通过沙盒逃逸我们可以从 /System/Library/Caches/com.apple.kernelcaches/kernelcache
读取 kernelcache,它既是当前系统加载的镜像。
读者可打开 Undecimus 的 jailbreak.m
文件,搜索 “Initializing patchfinder” 定位到 kernelcache 的加载代码,加载方法和普通的 Mach-O
文件类似,也是先读取 Mach Header
和 Load Commands
,然后逐段记录偏移量,具体代码在 init_kernel
函数中。
这里不再赘述加载过程,只指出几个关键的全局变量:
cstring_base
和cstring_size
是__TEXT,__cstring
段的虚拟地址和长度;xnucore_base
和xnucore_size
是__TEXT,__TEXT_EXEC
段,即代码段的虚拟地址和长度;kerndumpbase
是所有段中最小的虚拟地址,即 kernelcache 加载的虚拟基地址,在普通的Mach-O
文件中这个值一般是__PAGEZERO
段的虚拟地址 0x100000000,在内核中似乎是__TEXT
段的虚拟地址 0xFFFFFFF007004000;kernel
是 kernelcache 在用户空间的完整映射,即一份完整加载的内核镜像。
Find String Based XREF
在 Undecimus 中包含一个 find_strref
函数用于定位字符串的 XREF:
1 | addr_t |
它要求传入字符串 string,引用的序号 n,基准段 string_base,是否完全匹配 full_match
,以及是否位于 __PPLTEXT
段,对于寻找 Kernel Task 的场景,我们的入参如下:
1 | addr_t str = find_strref("\"shouldn't be applying exception notification", 2, string_base_cstring, false, false); |
即以 __TEXT,__cstring
为基准,不要求完全匹配,找到第 2 个交叉引用所在的地址。
定位字符串地址
字符串地址的定位逻辑在 boyermoore_horspool_memmem
函数中:
1 | static unsigned char * |
我们首先根据调用分析入参:
1 | addr_t base = cstring_base; |
- haystack = kernel + base + off,即
__TEXT,__cstring
段的起始地址; - hlen = size - off,即
__TEXT,__cstring
段的长度; - needle = string 即待查找字符串指针;
- nlen = strlen(string) 即待查找字符串的长度。
在函数的开头首先维护了一个 bad_char_skip
数组来记录当匹配失败时,应当跳过多少个字符来避免无意义的匹配。整个算法采用了倒序扫描的方式,不断从 haystack[needle_len - 1]
向前扫描并检查 haystack[i] == needle[i]
,当匹配到 haystack[0]
时如果依然满足条件,说明找到了字符串的地址,否则根据匹配失败的字符查 bad_char_skip
表将 haystack 指针后移继续匹配。
需要注意的是,在匹配成功后得到的字符串地址是相对于用户空间的 kernelcache 映射 kernel
的,并非是字符串在内核中的实际地址。
搜索对字符串所在地址的寻址操作
在获取到字符串在用户空间的地址 str
后,首先需要计算它在 kernelcache 中的虚拟地址:
1 | addr_t str_vmaddr = str - kernel + kerndumpbase; |
内核代码中对 str 的引用一定涉及到对 str_vmaddr
的寻址,主要的寻址方式有以下几种:
1 | ; 1 |
在 find_strref
的尾部调用了 return find_reference(str_vmaddr, n, text_base)
,find_reference
对 __TEXT_EXEC,__text
进行了静态分析,对寻址相关的指令模拟了寄存器运算,主要逻辑在 xref64
函数中,当发现寄存器中的值等于 str_vmaddr
时即找到了一条对 str 的交叉引用。
这里的代码主要是对机器码的解码和运算操作,篇幅较长不再贴出,读者有兴趣可以自行阅读。
通过 String XREF 定位变量地址
上文中我们已经得到了目标函数 proc_apply_resource_actions
中对 str 的引用地址,随后需要向上回溯定位 task_suspend
函数的调用指令:
1 | addr_t find_kernel_task(void) { |
整个过程主要分 3 步:
- 回溯找到
bl _task_suspend
的调用点,解出task_suspend
函数的地址; - 从
task_suspend
函数向后搜寻第一条 adrp 指令,即是对 Kernel Task 的寻址; - 从寻址指令中解出 Kernel Task 地址。
我们再回过头来看 proc_apply_resource_actions
函数片段:
1 | switch(action) { |
编译时不一定会按照 case 的顺序生成机器码,因此我们需要根据 str XREF 找到 kernelcache 中的实际表示,一个简单地办法是在 find_strref("\"shouldn't be applying exception notification", 2, string_base_cstring, false, false)
后打一个断点来获取 str XREF 的文件偏移,再利用二进制分析工具反汇编 kernelcache 中的这个部分。
通过断点调试可知 str XREF 位于 0x0000000000f9f084,这应该是一条 add 指令:
1 | /** |
在 Mach-O
查看器中打开可以发现,0x0000000000f9f084 确实是一条 add 指令:
要定位 task_suspend(p->task)
有两种方式,其一是 p->task
是一个基于偏移量的结构体成员寻址有明显特征,第二个是看函数调用前的参数准备。在 0xf9f074 处有一个 +16 的偏移量寻址,显然这是对 p->task
地址的计算,因此 0xf9f078 处即是 task_suspend(p->task)
的调用。
所以从 add 指令处向前回溯 3 条指令即可,找到这条 CALL 指令后,即可从中解出 task_suspend
的地址:
1 | // find bl _task_suspend |
随后我们从 task_suspend
函数的起始地址开始向后搜寻第一个 adrp 指令即可找到对 Kernel Task 的 adrp 语句,静态分析 adrp & add 即可计算出 Kernel Task 的地址:
1 | addr_t adrp = step64(kernel, task_suspend, 20*4, INSN_ADRP); |
注意这里我们得到的依然是 fileoff,需要加上 kerndumpbase
得到虚拟地址:
1 | return kern_task + kerndumpbase; |
需要注意的是,如果要在内核中读取 Kernel Task,这个地址需要加上 kernel_slide 才可以。计算 kernel_slide 的代码紧跟在 tfp0 之后,读者有兴趣可以自行阅读。
总结
本文详细分析了 Undecimus 中基于 string 的交叉引用在内存中定位代码和变量的技术,通过该技术可以实现内核中变量地址的定位,随后可通过读写实现绕过检测和注入等操作。该技术不仅是完成 Jailbreak 的关键技术,也能给读者带来二进制静态分析的一些启发。