通过 IDA Script 自动化解密 unc0ver-431 的字符串异或混淆

背景

到目前为止 iOS 13 的越狱方案中,只有 tfp0 予以公开,后续步骤均为闭源jakeajamesjelbrekLib 最近刚刚增加了 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*RLD*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 后写坏数据库,最终效果为:


完整实现

4 Likes

这不是一个真正做技术的人心态

5 Likes

大佬批评的是,以后不这么干了。踏下心来学 XNU 去 :stuck_out_tongue:

和我做的思路差不多,不过在解异或过程中我还遇到过MVN这种指令。我感觉你做复杂了,因为最后只是计算出寄存器的值然后STR,所以直接维护一个寄存器的runtime表就行,而且更通用。

嗯嗯,但是某些情况下在异或操作之间会出现寄存器和堆栈 swap 的情况,比如下面这段:

__text:000000010007AEF4                 LDRB            W8, [X10,#(unk_10019DDB3 - 0x10019DDB0)]
__text:000000010007AEF8                 STUR            W8, [X29,#var_5C]
__text:000000010007AEFC                 MOV             W8, #0xD7
__text:000000010007AF00                 STUR            W8, [X29,#var_60]
__text:000000010007AF04                 LDUR            W8, [X29,#var_5C]
__text:000000010007AF08                 STUR            X9, [X29,#var_68]
__text:000000010007AF0C                 LDUR            W9, [X29,#var_60]
__text:000000010007AF10                 EOR             W8, W8, W9

这里的 W8 在 7AEF4 处读取到了要解密的 Byte 值,并且存储在栈上,随后 7AEFC ~ 7AF00 期间遭到污染,然后在 7AF04 处还原,所以我在解析时额外模拟了一份栈内存。这种方式有点黑,通用性确实下降不少,大佬有没有更好的思路可以分享下?

这种情况很少吧,我的话直接把var_60当做寄存器处理了(其实和你处理方式也差不多

1 Like

这个看起来可以用ida ctree api做.对ast进行解释执行,不需要关心机器指令和栈.

多谢指点,我去了解下。