Kernel Symbols where are you


#1

着手看iOS内核,遇到的第一个问题就是需要恢复符号。
例如我看的这个iOS6.1.6(aka. Darwin Kernel Version 13.0.0 xnu-2107.7.55.2.2~1/RELEASE_ARM_S5L8930X)有39401个符号,其中在内核TEXT段中的(非kext)有13033个,总共有3677个是有符号的。这些有符号的函数,主要是内核Exports出来给kext用的。

针对这个问题,《IOS Hackers Handbook》里提到使用zynamics BinDiff,以及IDA的python脚本:idaiostoolkit
https://github.com/stefanesser/IDA-IOS-Toolkit
《SysScan-Singapore-Targeting_The_IOS_Kernel.pdf》里进一步说,是使用OSX的内核和iOS的内核进行比对。

BinDiff工具还是很智能的,从匹配算法上能看到有下面这几种:
MD index matching (flowgraph MD index, top down)
address sequence
call reference matching
call sequence matching(exact)
call sequence matching(sequence)
edges flowgraph MD index
hash matching
instruction count
loop count matching
name hash matching
prime signature matching
relaxed MD index matching
string references

实际操作一下OSX和iOS的内核比对,效果并不好。
Bindiff只找出767个函数,被认为90%以上可以确认是一致的。其中的一部分函数还是来自于name hash的匹配。

对于不同处理器架构(X86_64 vs armv7)进行比对有点外星科技了。加上本身xnu内核版本偏差的比较大,一个是13.4.0,一个是13.0.0
这样的结果也可以理解。
Idaiostoolkit的效果就更差,基本无法提供有用信息。如果去看《Wrox Press Mac OS X and iOS Internals, To the Apple’s Core》
会发现里面提到一个叫joker的工具。这个在OSX上运行的命令行工具可以从http://newosxbook.com/index.php
里的downloads中获取。
这个小工具可以代替findSyscallTable.py帮我们获取到详细的系统调用列表。
执行结果:https://github.com/jerryxjtu/joker/blob/master/logs/kernel_2107.7.55.2.2.txt
为了解这个小工具的神奇之处,我重写了这个工具,来一探究竟。
源码在:https://github.com/jerryxjtu/joker/blob/master/src/joker.c
使用mingw编译,可以在windows上运行。
代码里对hardcode的kernel signatures来进行查找,这种做法不优雅,对之后的内核版本的检测可能也会存在问题。
又写了一个ida插件,为了能在IDA中自动添加这些系统调用符号。


https://github.com/jerryxjtu/joker/blob/master/src/joker4ida.c
这样恢复了364个符号。从数量上看这些符号真不算多,但不要小看这364个符号,每一次的越狱都离不开他们,除非可以不利用用户态程序来触发内核漏洞。
他们重要是因为他们是用户程序和内核通讯的唯一方法。
有的同学可能要说:那我通过设备节点也可以和内核通讯啊,例如linux下还有/proc、/sys伪文件系统的方法,还有netlink等众多机制啊,但不要忘了这些方法所使用的open、read、write、ioctl等都是系统调用。

Xnu系统调用和linux系统不太一样的地方在于他有两张系统调用表。
这里从libsystem_kernel.dylib中抽出两个syscall包裹函数来说明他们的区别:
kern_return_t task_for_pid(mach_port_name_t target_tport, int pid, mach_port_name_t *t)
__text:39296F38_task_for_pid
__text:39296F38 MOV R12, #0xFFFFFFD3
__text:39296F3C SVC 0x80
__text:39296F40 BX LR

int getrlimit(int resource, struct rlimit *rlp)
__text:392A6640___getrlimit
__text:392A6640 MOV R12, #0xC2
__text:392A6644 SVC 0x80
系统调用的实现,都是将系统调用号放入约定好的寄存器,然后产生软件异常(SVC 0x80)。
这时处理器进入特权模式,pc指向异常向量表基地址+8。
arm_syscall_handler处理函数,会根据系统调用号,执行内核中相应的函数,操作完成后再返回用户态程序,继续执行。

ROM:00000000 B _arm_reset
ROM:00000004 LDR PC,=_off_arm_undefined_handler
ROM:00000008 LDR PC,=_off_arm_syscall_handler
ROM:0000000C LDR PC,=_off_arm_prefetch_abort_handler
ROM:00000010 LDR PC,=_off_arm_data_abort_handler
ROM:00000014 LDR PC,=_off_arm_reserved_handler
ROM:00000018 LDR PC,=_off_arm_irq_handler
ROM:0000001C LDR PC,=_off_arm_fiq_handler

Note:上面这个异常向量表我是从bootrom里拿出来的。当内核运行起来的时候异常向量表会在0xFFFF0000这个地址,通过设置cpu寄存器VE位来实现。

参考《ARM Architecture Reference Manual(ARMv7-A and ARMv7-R edition)》
B1.8Exception handling通过设置SCTLR(System Control Register)寄存器的VE位,可以改变异常向量表的地址。
VE== 0 Exception base address = 0x00000000.
This setting is referred to as normalvectors, or as low vectors.
VE== 1 Exception base address = 0xFFFF0000.
This setting is referred to as highvectors, or Hivecs.

我们看到task_for_pid系统调用号是#0xFFFFFFD3,是一个负数(-45)。而getrlimit调用号是0xC2,是一个正数(194)。
这两张表的区别就在于这一正和一负。
参考https://theiphonewiki.com/wiki/Kernel_Syscalls
传递正数的叫做POSIX calls,也就是符合POSIX标准。而传递负数的叫做Mach traps,相当于mach私有的。

虽然叫做POSIX syscall,但同为POSIX系统的xnu和linux来对比,随着系统调用号的增大,差别越来越大。

只找到系统调用的符号,数量上是少了点。
既然Xnu提供了源码,我们是否可以利用源码来恢复符号呢?

我想到的第一种办法就是通过静态库签名,IDA SDK里的flair专门来做这个事情。
利用这种方法还是比较复杂的,复杂不在于原理,而是条件非常苛刻。因为静态库签名属于精确比对,所以要求用于生成签名的机器码需要和目标分析文件尽可能一致。
条件在于:

  1. 和执行文件一致的源代码(包括内核配置,哪些功能启用,哪些关闭)
  2. 相同的编译器版本
  3. 相同的编译参数

为验证可行性,使用winocm维护的darwin_on_arm的xnu版本,这样满足最理想条件
https://github.com/darwin-on-arm/xnu
做release版本编译。编译产生mach_kernel和mach_kernel.sys
其中mach_kernel不包含符号。IDA看到有18468个函数,基本都是无符号名的。

pmacho在生成签名样式pat文件时,会将目标文件中整个text段的生成一个签名,即使这个目标文件中存在多个函数。因为对于连接器来说最小的链接单位就是一个目标文件,这也就是为什么静态库中总是尽可能将一个函数就放到一个目标文件里,而导致了一大堆的目标文件:


pmacho本身就是针对静态库的,所以一个目标文件中的一点点改动就导致整个目标文件匹配失败。
于是我重写了pmacho-o,目的是将每一个函数生成一个签名。
主要就是处理macho文件中的符号表,根据重定位信息来标定哪些位置的机器码会在链接产生变化,最后根据IDA规定的格式(见flair里的pat.txt)产生样式文件。

对编译过程中生成的目标文件进行扫描,使用pmacho-o工具生成签名样式,再用sigmake生成签名。应用这个签名,从Navigation band上的颜色条可以看出大部分函数都已经被识别出来。

但这个签名放到iOS6.1.6的内核上,基本识别不出什么函数。Navigation band上的颜色条在识别前后看不出有任何变化:

这时再检查之前的三个条件。
iOS设备上运行uname –a可以看到内核版本是13.0.0
jerryteki-iPod:~root# uname -aDarwinjerryteki-iPod 13.0.0 Darwin Kernel Version 13.0.0: Wed Feb 13 21:36:52 PST2013; root:xnu-2107.7.55.2.2~1/RELEASE_ARM_S5L8930X iPod4,1 arm N81AP Darwin

在内核源码中,有这个文件config/MasterVersion,来标定内核版本。
http://www.opensource.apple.com/tarballs/xnu/
里最匹配的版本是xnu-2422.1.72,凑巧这个版本又是darwin_on_arm的codebase,也就是我之前编译的内核。

xnu-2050.22.13-> 12.3.0 The version winocmshown on http://winocm.com/xnu/projects/2013/07/16/porting-xnu-to-arm/
xnu-2422.1.72-> 13.0.0 winocm’s codebase andthe closest version for iOS 6.1.6

在内核的串参考中找到下面的信息:4.2.1Compatible Apple Clang 4.1 ((tags/Apple/clang-421.1.58))
http://www.opensource.apple.com/tarballs/clang/
最接近的版本是clang-421.11.65.tar.gz

编译参数就只能试了。
我瞄准task_for_pid这个函数,首先用预编译-E,将所有的头文件等信息都生成到这个task4pid_all.c,然后利用不同的编译参数用-S生成汇编代码,进行比对。
/usr/bin/clang-S -target arm-apple-darwin11 -arch armv7 -fomit-frame-pointer -O2./task4pid_all.c -o ./o2.s

发现需要修改内核配置:/xnu/bsd/conf/MASTER.arm,关闭audit:
#options CONFIG_AUDIT # Kernel auditing

然后我发现即使把llvm/tools/clang/include/clang/Frontend/CodeGenOptions.def里定义的编译参数试来试去,仍然被block在无法生成完全一致的机器码。
例如:
/* Always check if pid == 0 */
if (pid == 0) {
(void ) copyout((char *)&t1,task_addr, sizeof(mach_port_name_t));
AUDIT_MACH_SYSCALL_EXIT(KERN_FAILURE);
return(KERN_FAILURE);
}
这个if(pid == 0)在官方release里,会生成BNE,然后优先走copyout,return的这个分支:
8021D216 BNE loc_8021D226
8021D218 loc_8021D218 ;CODE XREF: task_for_pid+36j
8021D218 ADD R0, SP, #0x20+var_18
8021D21A MOV R1, R8
8021D21C MOVS R2, #4
8021D21E BLX _copyout
8021D222 MOVS R4, #5
8021D224 B loc_8021D2F6

而我编译出来的版本,这里对应BEQ,最后去处理copyout。如用O0来编译,是变成BNE了,但stack size又会不一样。
觉得我编译出来的BEQ的这个版本更合理,因为他生成的代码量会比用BNE小一些。

Note:《2012_EN_FindYourOwniOSKernelBug_XuHao_ChenXiaobo》上说:


这个if (pid == 0)同时告诉我们,dump内核并不是直接调用task_for_pid(0)这么简单,如果直接就这么做,一定会看到-5(KERN_FAILURE)。

只能从clang的源码想办法了,这样问题就复杂了,先不说看懂和修改需要多少时间,单这个编译就很耗时,尤其是在链接的时候,看看编译出来的这个调试版本clang的尺寸,大概能明白了。
clang/build/Debug+Asserts/bin#ls -l clang
-rwxr-xr-x1 root root 840867730 Nov 26 21:40 clang

想继续还是先上块SSD吧…

各位同学、看官,如果有办法可以解决这个机器码不一致的问题,还望不吝赐教!

好吧,精确比对fail了。
现在很多芯片厂家都会提供官方porting的代码以及交叉编译器,例如想看看kindle在官方提供的版本上做了什么修改,这种方法还是奏效的。

精确比对失败了,那就换成模糊比对吧,继续用BinDiff。怎么说我们现在换成相同的arch、非常接近的内核版本,总会好很多吧。
他99%的确认task_for_pid对应的就是0x8021d1f8

这个结果和joker从syscall里找到的一致:
45 task_for_pid 8021d1f8 T

他认为两者匹配的原因是:MD index matching (flowgraph MDindex, top down)
对比框图如下:

BEQ和BNE的区别,并没有影响他的判断:

Bindiff说:有6000多个符号,他可以90%以上确认是一致的,这对我们的帮助已经足够大了。

完。


#2

收藏了慢慢看


closed #3