背景
到目前为止 iOS 13 的越狱方案中,只有 tfp0 予以公开,后续步骤均为闭源( jakeajames 的 jelbrekLib 最近刚刚增加了 iOS 13 的支持)。在研究 iOS 13 从 tfp0 到 jailbreak 的过程中发现了诸多困难,iOS 12 的 setuid 提权以及劫持虚函数的 kexec 都有所变化。考虑到从 XNU Source 和 kernelcache 中寻找线索需要消耗大量时间,我决定去逆一波 unc0ver 找线索(抄作业)。
然而 unc0ver 为了防止黑恶势力利用,对控制流和字符串都做了混淆,混的亲妈都不认识了 (╯°□°)╯︵ ┻━┻。
寻找 kexec 的线索
为了找到 unc0ver 处理 iOS 13 kexec 的代码,我从 IOConnectTrap6 的 XREF 找到了许多调用点,打算从这些调用点去寻找 init_kexec 的代码,但是发现字符串都做了异或处理,为了找线索就不得不还原这些字符串:
简单看一眼代码可知是最基本的异或混淆,在编译期通过异或修改了静态字符串的值,在函数开始时再重新异或一次来还原:
那么解决方案也就简单了,把这个异或操作重新运算到这些字符串的值上面即可。
自动化解密
思路
要实现自动化解密,一个简单的方法是对执行运行时异或解密的代码进行静态分析,并模拟寄存器状态和内存的值,我们观察一段相关代码:
__text:000000010007A928 ADRP X8, #aA_4@PAGE ; "A�h�������is�a�̙��|���.�����=�������pT"...
__text:000000010007A92C ADD X8, X8, #aA_4@PAGEOFF ; "A�h�������is�a�̙��|���.�����=�������pT"...
__text:000000010007A930 ADRP X9, #asc_10019DDF0@PAGE ; "\x1B��B� Uu�����\x1B�B������� �����aI�"...
__text:000000010007A934 ADD X9, X9, #asc_10019DDF0@PAGEOFF ; "\x1B��B� Uu�����\x1B�B������� �����aI�"...
__text:000000010007A938 ADRP X10, #byte_10019DDB0@PAGE
__text:000000010007A93C ADD X10, X10, #byte_10019DDB0@PAGEOFF
__text:000000010007A940 ADRP X11, #byte_10019DD70@PAGE
__text:000000010007A944 ADD X11, X11, #byte_10019DD70@PAGEOFF
__text:000000010007A948 ADRP X12, #str_IOGraphicsAccelerator2@PAGE
__text:000000010007A94C ADD X12, X12, #str_IOGraphicsAccelerator2@PAGEOFF
__text:000000010007A950 LDRB W13, [X12]
__text:000000010007A954 MOV W14, #0x64
__text:000000010007A958 EOR W13, W13, W14
__text:000000010007A95C AND W13, W13, #0xFF
__text:000000010007A960 STRB W13, [X12]
可以发现这里的代码非常简单,只用到了少量指令,要模拟这些指令是非常简单的。
实现
状态模拟
我目前只分析了 sub_10007A8F8,其中只用到了以下这些指令:
handlers = {
'ADRP': handle_adrp,
'ADD': handle_add,
'LDRB': handle_ldrb,
'STRB': handle_strb,
'MOV': handle_mov,
'EOR': handle_eor,
'AND': handle_and,
'STUR': handle_stur,
'LDUR': handle_ldur
}
其中要注意的是 ST*R
和 LD*R
可能会操作 stack,这时候我们需要特殊判断并且模拟栈内存,我采用了一个很偷懒的模拟办法,那就是检测 [x29, var_xx]
的表达式,然后读写一个模拟 memory 的 dict,其中 key=xx。
寄存器只需要处理 X 和 W,这里用 X 来存储;栈上内存用 dict 来存储:
x = [None] * 31
for i in range(31):
x[i] = RegX(i)
x[29].writeX(0)
# simulator stack memory
memory = {}
静态分析
通过 IDAPython 提供的 API 从特定地址读取 asm 然后分析即可:
def u0_xorpatch(startAddr, endAddr):
if endAddr < startAddr:
raise Exception('invalid input params')
print('xorpath from {} to {}'.format(hex(startAddr), hex(endAddr)))
cursor = startAddr
for _ in range(1 + (endAddr - startAddr) // 4):
mnem = GetMnem(cursor)
print('[*] {} {}'.format(hex(cursor), GetDisasm(cursor)))
if mnem in handlers:
handlers[mnem](cursor)
else:
print('[-] Warn: unresolved mnem ' + mnem + ' at ' + str(hex(cursor)))
raise Exception('unresolved mnem ' + mnem + ' at ' + str(hex(cursor)))
cursor += 4
if __name__ == '__main__':
u0_xorpatch(0x10007A928, 0x10007BC7C)
执行效果
在完成脚本后,我尝试解密了 sub_10007A8F8 中的所有字符串,解密段的起始和终止位置非常好找,它是函数开场白后很长的一段代码块:
找到了起始地址和结束地址,将他们写入脚本的入参,然后通过 File → Script File 加载,这里实验前建议先备份 idb,以防止 binpatch 后写坏数据库,最终效果为:
完整实现
https://github.com/Soulghost/ida_scripts/blob/master/ida_u0_xor_decrypt.py