Showing Posts From

Macho

MachO 代码签名剖析

验证代码的正确性是计算机科学中最难的问题之一,因为不存在普遍意义的正确的算法,所以这一验证通常使用数字签名处理。数字签名主要做两部分工作:验证代码的来源是否合法。 代码是否被修改过。代码签名并非苹果独有技术,Java 和 Android 的 Dalvik 都在使用,但苹果公司是最早开始使用的。大家可以通过阅读下文思考代码来源是否合法和代码是否被修改过的验证是如何实现的。 本篇文章主要参考自 Jonathan Levin 的《最强 iOS 和 macOS 安全宝典》代码签名一章。测试环境:macOS 11.2.3。 测试项目:/bin/ls 在 x86_64 架构下的 MachO 文件。iOS 下的文件与之相差不大。代码签名格式 在了解代码签名机制前,非常有必要了解代码签名的包含的内容。代码签名附着在 MachO 的尾部。加载命令为LC_CODE_SIGNATURE,它指向一个超级二进制块Code Signature,该二进制块又包含了多个其他的子二进制块。之前写过一篇文章,有讲解如何手动解析这个签名二进制块:深入理解MachO数据解析规则。 下面是该二进制块的层级结构:超级二进制块是一个目录性质的结构,用于指定子二进制块的位置,各个子二进制块才是代码签名的主要角色。 子二进制块类型 子二进制块类型通过不同值进行表示:值 二进制块类型0x0000 代码目录0x0002 需求0x0005 授权0x10000 CMS 二进制块0x10001 身份证明(未使用)本篇主要就是对这几个子二进制块的功能和部分实现进行分析。 二进制块的提取 jtool 是 Jonathan Levin 开发的一款主要用于 MachO 分析的高效工具,可以使用 homebrew 进行安装。 $ brew install jtool我们可以使用 jtool 单独提取代码签名部分: $ jtool -arch x86_64 -e signature /bin/ls Extracting Code Signature (5728 bytes) into ls.signature $ od -t x1 -A x ls.signature #原始字节内容 0000000 fa de 0c c0 00 00 14 86 00 00 00 03 00 00 00 00 0000010 00 00 00 24 00 00 00 02 00 00 02 61 00 01 00 00 0000020 00 00 02 9d fa de 0c 02 00 00 02 3d 00 02 01 00 0000030 00 00 00 00 00 00 00 7d 00 00 00 30 00 00 00 02 0000040 00 00 00 0e 00 00 d2 30 20 02 0b 0c 00 00 00 00 0000050 00 00 00 00 63 6f 6d 2e 61 70 70 6c 65 2e 6c 73 # ...也可以使用 MachO 找到 Code Signature 块进行查看。 代码签名的子二进制块 我们可以使用 jtool 查看代码签名内容的分析: $ jtool -arch x86_64 --sig -v /bin/ls Blob at offset: 53808 (5728 bytes) is an embedded signature of 5254 bytes, and 3 blobs Blob 0: Type: 0 @36: Code Directory (573 bytes) Version: 20100 Flags: none (0x0) Platform Binary CodeLimit: 0xd230 Identifier: com.apple.ls (0x30) CDHash: 46cc1da7c874a5853984a286ffecb48daf2f65f023d10258a31118acfc8a3697 (computed) # of Hashes: 14 code + 2 special Hashes @125 size: 32 Type: SHA-256 Requirements blob: a8ccc60c2a5bff15805beb8687c6a899db386d964a5eb3cf3c895753f6879cea (OK) Bound Info.plist: Not Bound Slot 0 (File page @0x0000): e4a537939e00f4974e02b03d36e4dab75f7dc095d2214ba66bc53c73c145ceff (OK) Slot 1 (File page @0x1000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK) Slot 2 (File page @0x2000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK) Slot 3 (File page @0x3000): 4a7cb3e6c1b3a6ac82e3575239ee53d4f0d3bed260fed63438fd21ce0d00392e (OK) Slot 4 (File page @0x4000): 9ec9e4e02292dfda34ef3caa8317e8bfbcc41a46b18d994dba45febe31b8c660 (OK) Slot 5 (File page @0x5000): 037285f744f366210cde48821261d4a5f5b739dcf0b82f94144613e92c4b7c07 (OK) Slot 6 (File page @0x6000): be89c764e52382702918f2db62ff24d9df40410fe894b11d505a4abc1f854340 (OK) Slot 7 (File page @0x7000): a6b322014743965656e796155c1e0bf22e19a3e8770a43f1111cfbc961037d26 (OK) Slot 8 (File page @0x8000): a643fc9485d941019cbdeead1d5c47add9382417ebe4d15768221f3763553b84 (OK) Slot 9 (File page @0x9000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK) Slot 10 (File page @0xa000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK) Slot 11 (File page @0xb000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK) Slot 12 (File page @0xc000): 23304ae11c1ade4411cb63a0955eb644574b8af416e4e3818e382421272ae1b4 (OK) Slot 13 (File page @0xd000): e0ca7b7000d04057e71c49365b1937711b3557f6b91e0fa144791c66de2a7a4d (OK) Blob 1: Type: 2 @609: Requirement Set (60 bytes) with 1 requirement: 0: Designated Requirement (@20, 28 bytes): SIZE: 28 Ident: (com.apple.ls) AND Apple Anchor Blob 2: Type: 10000 @669: Blob Wrapper (4585 bytes) (0x10000 is CMS (RFC3852) signature) CA: Apple Certification Authority CN: Apple Root CA CA: Apple Certification Authority CN: Apple Code Signing Certification Authority CA: Apple Certification Authority CN: Apple Root CA CA: Apple Certification Authority CN: Apple Root CA CA: Apple Certification Authority CN: Apple Code Signing Certification Authority CA: Apple Software CN: Software Signing Time: 201222002625Zi它有三个 Blob,即三个子二进制块,Blob 0 是代码签名 Blob 1是需求,Blob 2 是 CMS,下面是对这几个 Blob 的分析。 代码目录(Code Directory) 代码目录是签名块的主体,它提供了签名资源的散列值(哈希值)。代码签名并非对整个文件进行签名,因为有时二进制文件可能很大,计算全部内容占用资源较多;而且二进制的加载是按需加载,不会一开始就都全部映射到内存中。签名时会将整个 MachO 文件划分成多个页,每个页单独签名。 代码目录部分就是对签名信息的描述,其中包含了各个分页的签名值,签名算法和分页大小等内容。代码签名的数据结构如下: /* * C form of a CodeDirectory. */ typedef struct __CodeDirectory { uint32_t magic; /* magic number (CSMAGIC_CODEDIRECTORY) */ uint32_t length; /* total length of CodeDirectory blob */ uint32_t version; /* compatibility version */ uint32_t flags; /* setup and mode flags */ uint32_t hashOffset; /* offset of hash slot element at index zero */ uint32_t identOffset; /* offset of identifier string */ uint32_t nSpecialSlots; /* number of special hash slots */ uint32_t nCodeSlots; /* number of ordinary (code) hash slots */ uint32_t codeLimit; /* limit to main image signature range */ uint8_t hashSize; /* size of each hash in bytes */ uint8_t hashType; /* type of hash (cdHashType* constants) */ uint8_t platform; /* platform identifier; zero if not platform binary */ uint8_t pageSize; /* log2(page size in bytes); 0 => infinite */ uint32_t spare2; /* unused (must be zero) */ /* Version 0x20100 */ uint32_t scatterOffset; /* offset of optional scatter vector */ /* Version 0x20200 */ uint32_t teamOffset; /* offset of optional team identifier */ /* followed by dynamic content as located by offset fields above */ } CS_CodeDirectory;结合 CodeDirectory 的偏移量,可以从 MachOView 里查看到这部分数据的内容:找到对应数据结构中的含义,我们关注其中三个 uint8_t 类型的值:参数 值 含义hashSize 0x20 hash 值大小,为 0x20 字节。hashType 0x02 表示签名算法,0x01 表示 SHA-1,0x02表示SHA-256。从 macOS10.12 和 iOS11开始,苹果转向使用 SHA-256。pageSize 0x0C 这里是一个计算公式:log2(PageSize) = 0x0C根据公式算出分页大小:PageSize = 2 ^ 0x0C = 4096 = 0x1000 = 4K。这跟系统的内存分页大小是一致的。 由此可知整个 MachO 文件会按照 0x1000 字节的大小进行分页,分页使用 SHA-256 算出散列值。这些计算出的散列值会记录在代码插槽(Code Slots)里。 代码插槽验证 上面Slot 从 0 到 13 的标记对应的都是代码插槽。 有了计算规则我们还可以手动验证代码签名的正确性,我们以前三个代码插槽为例,也即前 0x1000 字节的内容,尝试手动计算其散列值。 $ lipo /bin/ls -thin x86_64 -output /tmp/ls_x86_64 $ dd bs=0x1000 skip=0 count=1 if=/tmp/ls_x86_64 2>/dev/null | openssl sha256 SHA256(stdin)= e4a537939e00f4974e02b03d36e4dab75f7dc095d2214ba66bc53c73c145ceff $ dd bs=0x1000 skip=1 count=1 if=/Users/zhangferry/ls_x86_64 2>/dev/null | openssl sha256 SHA256(stdin)= ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 $ dd bs=0x1000 skip=2 count=1 if=/Users/zhangferry/ls_x86_64 2>/dev/null | openssl sha256 SHA256(stdin)= ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7注意到后面两个插槽计算结果一样,这是因为这两部分数据为补齐位,它们全部为0。跟前三个代码插槽的值进行对比: Slot 0 (File page @0x0000): e4a537939e00f4974e02b03d36e4dab75f7dc095d2214ba66bc53c73c145ceff (OK) Slot 1 (File page @0x1000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK) Slot 2 (File page @0x2000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)发现两边散列值一样,输出内容后面的 OK 是 jtool 验证的结果。 这里可以看到代码插槽有14个,File page 里的内容表示相对起始地址。另外注意到输出部分有一句注释: # of Hashes: 14 code + 2 special其表示有 14 个代码插槽和 2 个特殊插槽。 特殊插槽 特殊插槽的出现是因为App由多个内容组成,并非只有二进制文件,为了保证这些非二进制文件的完整性,对它们也会进行签名,它们的签名值就是特殊插槽。因为代码插槽的索引是从0开始的,而且其大小不固定,为了把特殊插槽也能排列进去,就选用负数来表示特殊插槽的含义。以下是特殊插槽的定义:# 插槽目的-1 绑定的info.plist-2 需求(requirement):二进制块嵌入代码签名-3 资源目录:CodeSignature/CodeResources文件的散列值-4 具体应用:实际上未被使用-5 授权(entitlement):嵌入在代码签名中的授权我们可以在上方 jtool 的输出内容里找到特殊插槽的内容: Requirements blob: a8ccc60c2a5bff15805beb8687c6a899db386d964a5eb3cf3c895753f6879cea (OK) Bound Info.plist: Not Bound因为特殊插槽作用是固定的,也就没用序号表示。 代码签名需求(Requirements) 目前代码签名只是分块取散列值,保存起来,但好像还不够强大。苹果公司已经为代码签名增加了另外一个机制:需求(requirements)。它可以自定义规则以施加特定限制,比如允许哪些动态库加载。 需求有一套特殊的语法规则,其表达由操作数和操作码组成,丰富的操作码集使得构建任何数量的逻辑条件成为可能。可以在requirements.h 文件里查看都有哪些操作码。 enum ExprOp { opFalse, // unconditionally false opTrue, // unconditionally true opIdent, // match canonical code [string] opAppleAnchor, // signed by Apple as Apple's product opAnchorHash, // match anchor [cert hash] opInfoKeyValue, // *legacy* match Info.plist field [key; value] opAnd, // binary prefix expr AND expr opOr, // binary prefix expr OR expr opCDHash, // match hash of CodeDirectory directly opNot, // logical inverse opInfoKeyField, // Info.plist key field [string; match suffix] opCertField, // Certificate field [cert index; field name; match suffix] opTrustedCert, // require trust settings to approve one particular cert [cert index] opTrustedCerts, // require trust settings to approve the cert chain opCertGeneric, // Certificate component by OID [cert index; oid; match suffix] opAppleGenericAnchor, // signed by Apple in any capacity opEntitlementField, // entitlement dictionary field [string; match suffix] exprOpCount // (total opcode count in use) };对需求的编译是由 csreq 进行的,对需求的验证可以使用 codesign -v。 我们这里来尝试解读下已有的需求内容。 大部分二进制文件的需求只是验证签名身份,即使用证书是否为苹果所颁发。在 App Store 里的应用则使用更严格的规则集。我们可以查看 Xcode 的代码签名需求: $ codesign -d -r- /Applications/Xcode.app/Contents/MacOS/Xcode Executable=/Applications/Xcode.app/Contents/MacOS/Xcode designated => (anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = APPLECOMPUTER) and identifier "com.apple.dt.Xcode"注意这里有不少 1.2.840.113635 开头的标识,它代表的是国际通用标准证书中苹果公司的分支(iso.member-body.us.appleOID)。其中100对应了安全相关的一些定义 appleDataSecurity,详细内容可以看这里oidref.com。对照Xcode的签名需求,我们可以大致推断出这些规则的含义:由苹果签名且证书节点包含 6.1.9 即 Mac App Store App。 或由苹果签名且证书节点包含 6.2.6 即 "dev_program"。(推测是开发版本的应用) 其证书节点包含 6.1.13 即 Developer ID Applications。 证书的团队标识符(OU)为 APPLECOMPUTER 且 BundleId 为 com.apple.dt.Xcode。注意其中最后一项的内容,限定了团队标识符和BundleId,这样就能够解决应用被重签名的问题了。 CMS CMS 是Cryptographic Message Syntax的缩写,是一种标准的签名格式,由RFC3852定义。书中并没有提这部分内容,但我认为这部分恰恰是代码签名最关键的步骤。 CMS 格式的签名中,除了包含证书之外,还承载了一些其他的信息,比如签名属性 signedAttrs。 上面说了 CodeDirectory 里保存了 MachO 分页的 Hash 值,只要保证这个 CodeDirectory 不被修改就可以了。所以对代码目录进行 Hash 计算,获得 CDHash,然后对这个 CDHash 进行签名就可以了。 注意这一步才是真正的签名,其开始涉及加密,前面的代码插槽只是提供摘要信息。 注意到 jtool 的签名输出里有这样一句: CDHash: 46cc1da7c874a5853984a286ffecb48daf2f65f023d10258a31118acfc8a3697 (computed)这就是外部计算的 CDHash 值,用于跟 signedAttrs 里的内容进行对比。而更关键的是对 signedAttrs 的加密验证,实际验证流程比较复杂,感兴趣的小伙伴可以阅读这篇细说iOS代码签名(三)。 我结合文中签名校验内容和上面的代码插槽,画出了表示签名校验的整个流程:这里有两处 Hash 对比,一个是对 signedAttrs 的解密,确保其是可信任的。另一处是 CDHash 的对比,确保代码未被修改。 signerInfo 里包含了 signedAttrs 、签名使用的 Hash 算法、加密算法、签名数据等信息。再结合证书里的公钥,我们就可以验证,signedAttrs 的有效性。 授权 除了确保代码的真实性和完整性,代码签名还为苹果公司及其强大的安全机制提供了授权(entitlement)功能。授权文件也被包含在签名里,其散列值放在索引为-5的插槽中。授权文件是一个 XML 格式的文件,我们可以使用 jtool --ent 查看其内容,因为 ls 没有授权文件,我们以 Mac 端微信为例进行查看: $ jtool -arch x86_64 --ent /Applications/WeChat.app/Contents/MacOS/WeChat <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.security.app-sandbox</key> <true/> <key>com.apple.security.application-groups</key> <array> <string>5A4RE8SF68.com.tencent.xinWeChat</string> </array> <key>com.apple.security.device.audio-input</key> <true/> <key>com.apple.security.device.camera</key> <true/> <key>com.apple.security.device.microphone</key> <true/> <key>com.apple.security.files.downloads.read-write</key> <true/> <key>com.apple.security.files.user-selected.read-write</key> <true/> <key>com.apple.security.network.client</key> <true/> <key>com.apple.security.network.server</key> <true/> <key>com.apple.security.personal-information.location</key> <true/> </dict> </plist>这里我们可以看到其中包含了沙盒、application-groups、声音输入、摄像头等一系列权限。在应用访问特定 API 的时候苹果可以根据这些授权判定该行为是否合法。因为苹果公司是应用的终极签名者,所以签名过程中也可以很容易的修改授权,比如 com.apple.security.sandbox.container-required 这一表示沙盒权限的值就会被强制安置到授权文件中。 强制验证代码签名 为了使代码签名真正有效,非常重要的一步就是要保证验证过程顺利执行,没有遗漏。当前签名验证是发生在内核模式下,而非用户模式下。签名的验证发生在两个阶段:加载可执行文件时、实际访问二进制代码时(Page Fault)。分成两个阶段也是出于性能方面的考虑,因为二进制文件是动态加载的,对于还没加载的部分仅当其加载如内存时,也即发生 Page Fault 时再进行签名验证。 可执行文件的加载 可执行文件的加载出现在execve()/mac_execve()或posix_spawn()系统调用被触发的时候。对于MachO,exec_mach_imgact()会被调用,在解析文件时它会找到LC_CODE_SIGNATURE的位置。代码签名二进制块会被加载到内核的统一高速缓存缓冲区中。 Page Fault时的处理 可以查看 osfmk/vm/vm_fault.c 的代码: /* * CODE SIGNING: * When soft faulting a page, we have to validate the page if: * 1. the page is being mapped in user space * 2. the page hasn't already been found to be "tainted" * 3. the page belongs to a code-signed object * 4. the page has not been validated yet or has been mapped for write. */ #define VM_FAULT_NEED_CS_VALIDATION(pmap, page, page_obj) \ ((pmap) != kernel_pmap /*1*/ && \ !(page)->cs_tainted /*2*/ && \ (page_obj)->code_signed /*3*/ && \ (!(page)->cs_validated || (page)->wpmapped /*4*/))当 Page Fault 满足以上条件时将触发签名验证过程: 1、该页面正在用户空间中映射 2、这个页面还没有被发现为 tainted 3、该页属于一个代码签名对象 4、页面还没有被验证,或者还没有被映射为可写状态 代码签名的漏洞 代码签名机制虽然强大,保护着应用的安全,但依然被攻破过,以下讲解几例曾经出现的漏洞。 JIT(即时生成代码) 该情况发生在 Page Fault 过程,如果该页内容是用于 JIT,将会被特殊标记,可以创建和执行任意代码,而无需代码签名。 从 iOS 10 开始,苹果公司开始在64位的设备上加固 JIT。采用专门的 memcpy() 将JIT映射到可执行但不可读的内存上,然后可执行的 JIT 映射为不可写,可写的 JIT 映射为不可执行状态。 Jekyll 应用 Jekyll 应用的含义是应用在提交至 App Store 时表现为无害,但其实它包含恶意功能,只不过这些功能处于休眠状态。过审之后和本地服务器进行合作,自愿公开其地址空间和符号,通过代码注入或者返回导向编程(Return Oriented Programming,ROP),触发预置的恶意程序。 目前还没有可靠的打击 ROP 的方法,但因为沙盒机制的缘故,恶意代码的影响范围是可控的。 苹果公司使用 LLVM BitCode 向 App Store 提交应用的方案,也会使恶意应用难以事先知晓其地址空间。 内存锁定 从上面我们知道发生Page Fault会触发签名验证的流程,那如果没有Page Fault就不会存在签名验证了。按照mmap -> mlock -> memcpy -> mprotect 的调用顺序,应用可以修改可执行内存,以任何看起来合适的方式修补内存。虽然XNU通常会阻止将曾经可写的内存设置为r-x,但当内存锁定时,会绕过该检测。 苹果在iOS 9.3中修复了这个漏洞。 总结 我们再来尝试回答开头上面遗留的问题: 1、如何验证代码的来源是否合法? 主要通过证书来验证来源是否合法,所有的开发者证书都由苹果颁发,且被 Root CA 认证。另外依托于需求(requirements),还可以再扩展一些其他验证方式。 2、如何确认代码是否被修改过。 主要通过代码插槽和 CDHash,再对 CDHash 进行签名,就可确认其是否被修改过。注意实际验证流程有两处关键的 Hash 比对,可以再结合上面的流程图加深理解。

深入理解MachO数据解析规则

我们知道Apple设备可执行文件的存储格式是MachO,一个二进制文件。通常在做逆向或者静态分析的时候都会用到这个文件,分析MachO的常用工具是MachOView。今天借助于MachOView,主要分析Code Signature的存储规则。 本篇文章同时也是围绕这几个问题展开的: 1、MachOView是如何确认MachO内容的。 2、二进制数据是如何存储的,如何确认位置。 3、字节码含义如何解析。前置准备 1、二进制文件其实简单理解就是通过二进制形式进行存储内容的文件,它可以原封不动的读到内存中用于完成各种处理。比如数值3.1415927,文本文件需要9个字节进行存储:3 . 1 4 1 5 9 2 7 这 9 个 ASCII 值,而如果是二进制的话4个字节就够了:DB 0F 49 40。 2、二进制文件读到内存中通常是连续存储的,它不需要额外的处理,原本怎样,在内存里就是怎样的。 3、每个进程都会被分配一个虚拟地址空间,进程寻址的范围就是在这个虚拟地址空间进行的,虚拟地址到物理地址之间有一个映射表进行管理。 4、可以简单理解:虚拟地址 = 随机基址(ASLR)+ 逻辑地址(段内偏移)。 后面的内容也会出现很多偏移量(offset)的概念,它的含义很简单就是相对某一位置偏移多少字节。关键是需要确认它是相对哪个位置进行的偏移,在不同的数据段,这个相对的锚点是不一样的。但通常来说偏移量都是相对于当前的数据段来说的。 5、FAT格式的MachO可以理解为多个架构的顺序组合,所以分析某个架构时,还需要加上对应架构的偏移量。 6、uint32_t占4个字节,uint8_t占1个字节,char占一个字节。 Mach-O格式 格式分析 可以简单看下Mach-O的数据结构:Mach-O文件大致分为三部分: Header 表示当前的Mach-O文件整体信息,包含CPU架构、子版本、文件类型、加载命令数等内容。数字内容好表示,那CPU架构这样的类别是如何表示的呢?二进制数据说到底也是数字,这些类别信息也只能通过数字表示,但需要一个具有特殊含义的数字,这个数字通常叫magic(魔数)。比如0xCAFEBABE表示FAT,0xFEEDFACF表示ARM64。 Header的定义地址:https://opensource.apple.com/source/xnu/xnu-792/EXTERNAL_HEADERS/mach-o/loader.h.auto.html Load Commands 记录各个数据段的信息和位置,只是类别和标记的介绍,包含一些信息的偏移地址、文件大小等内容。 Data 记录具体的内容信息。不同类别的信息对应不同的数据含义。注意上图右侧由Load Commands到Data的箭头,Data的位置是由Load Commands指定的。 他们三者的关系如果用一本书表示的话就是:Header是封面,Load Commands是目录,Data是书的内容。 寻找Code Signature 本节的重点是找到Code Signature(代码签名)这部分内容,它没被MachOView解析,还是原始的数据形态,是一个比较好的分析案例。 分析文件是系统的ls,它的路径在/bin/ls,把它放到MachOView里。ls是一个FAT文件,它包含两个架构,Fat Header里记录了各个架构的类别、偏移量、大小等信息。我们只关注X86_64架构下的内容,展开这个架构下的Load Commands,找到代表代码签名的LC_CODE_SIGNATURE信息:右侧是真实的数据内容,MachOView已经帮我们对应好了字段描述: Data Offset:代表数据偏移 53808,换成16进制就是0xD230 Data Size:代表文件大小 5728,换成16进制就是0x1660 这俩16进制值其实就是Data对应的内容,Value是MachOView帮我们做的处理。 这里的偏移跟上面Fat Header的偏移含义已经不一样了,Fat Header说的是总文件偏移,这里的偏移则是针对X86文件的偏移。所以实际的偏移应该是:0xD230 + 0x4000 = 0x11230。 找到Data部分的Code Signature内容:这里pFile就是相对当前文件的偏移量(也可以理解为逻辑偏移量),它的起始位置正是上面计算得的:0x11230。由大小0x1660,我们还可以计算得出Code Signature最后一个字节所在位置是:0x11230 + 0x1660 - 0x1 = 0x1288F。 解析Code Signature CS_SuperBlob 我们已经找到了代码签名位置,现在开始解析它吧。解析的第一步就是需要找到数据定义,有了定义才能分析出数据含义。Code Signature相关内容的定义在这里:https://opensource.apple.com/source/xnu/xnu-3789.51.2/bsd/sys/codesign.h.auto.html 整个签名的头部是一个CS_SuperBlob结构体,它的定义如下: typedef struct __SC_SuperBlob { uint32_t magic; /* magic number */ uint32_t length; /* total length of SuperBlob */ uint32_t count; /* number of index entries following */ CS_BlobIndex index[]; /* (count) entries */ /* followed by Blobs in no particular order as indicated by offsets in index */ } CS_SuperBlob; 这个结构体第一个参数是magic,它的定义如下: /* * Magic numbers used by Code Signing */ enum { CSMAGIC_REQUIREMENT = 0xfade0c00, /* single Requirement blob */ CSMAGIC_REQUIREMENTS = 0xfade0c01, /* Requirements vector (internal requirements) */ CSMAGIC_CODEDIRECTORY = 0xfade0c02, /* CodeDirectory blob */ CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0, /* embedded form of signature data */ CSMAGIC_EMBEDDED_SIGNATURE_OLD = 0xfade0b02, /* XXX */ CSMAGIC_EMBEDDED_ENTITLEMENTS = 0xfade7171, /* embedded entitlements */ CSMAGIC_DETACHED_SIGNATURE = 0xfade0cc1, /* multi-arch collection of embedded signatures */ CSMAGIC_BLOBWRAPPER = 0xfade0b01, /* CMS Signature, among other things */ //... }第二个参数是length,表示整个SuperBlob的长度。 第三个参数是count,表示index实体条目的数量。 第四个参数是为CS_BlobIndex的一个结构体。 大端小端 1、这个是64位架构的二进制数据,其实有两种64位架构,他们分别表示为大端64位和小端64位,上面MachOView分析的X86 Header中的魔数是0xFEEDFACF,代表的就是当前二进制文件是小端64位格式。 2、比如0x1234这个数据,在小端情况下,12会存放在低字节处,34会放于高字节处,大端则相反。 数据解析 我们把Code Signature的第一个行数据拿出来分析:这里注意Data部分,有两个标签:Data LO和Data HI,是用于表示当前的字节序列,前面是低字节,后面是高字节。这样按照小端的规则,我们就可以按自然顺序取数据了,所以可以得出以下内容: magic 为0xFADE0CC0,对应CSMAGIC_EMBEDDED_SIGNATURE,代表嵌入的代码签名数据。 length 是0x1486,我们可以计算得出最后一个字节位置:0x11230 + 0x1486 - 0x1 = 0x126B5红色标记的字节就是Code Signature结束的地方,在这之后的内容全部由0x00填充,就非实体内容了。 count 是3,表示接下来有3个实体内容,这个实体对应的是结构体:CS_BlobIndex。 CS_BlobIndex 我们来看下CS_BlobIndex这个结构体: /* * Structure of an embedded-signature SuperBlob */typedef struct __BlobIndex { uint32_t type; /* type of entry */ uint32_t offset; /* offset of entry */ } CS_BlobIndex;它有两个成员变量,type表示实体类型,offset表示实体偏移量。 一般表示类型的肯定有特殊数字对应的含义,这里的type也是一样的,这个type在上面的magic在一个enum里定义。 CSSLOT_CODEDIRECTORY = 0, /* slot index for CodeDirectory */ CSSLOT_INFOSLOT = 1, CSSLOT_REQUIREMENTS = 2, CSSLOT_RESOURCEDIR = 3, CSSLOT_APPLICATION = 4, CSSLOT_ENTITLEMENTS = 5,CSSLOT_ALTERNATE_CODEDIRECTORIES = 0x1000, /* first alternate CodeDirectory, if any */ CSSLOT_ALTERNATE_CODEDIRECTORY_MAX = 5, /* max number of alternate CD slots */ CSSLOT_ALTERNATE_CODEDIRECTORY_LIMIT = CSSLOT_ALTERNATE_CODEDIRECTORIES + CSSLOT_ALTERNATE_CODEDIRECTORY_MAX, /* one past the last */CSSLOT_SIGNATURESLOT = 0x10000, /* CMS Signature */数据解析 我们再回到数据部分,根据上面结构体进行分析:能够解析出三条CS_BlobIndex数据:type type含义 offset0x00 CSSLOT_CODEDIRECTORY 0x240x02 CSSLOT_REQUIREMENTS 0x2610x10000 CSSLOT_SIGNATURESLOT 0x29D这里又出现了一个offset,这个offset存在于Code Signature的最外部,所以它表示的就是相对Code Signature的偏移量。 这个表相当于又提供了一个目录,它告诉我们,之后的内容有三部分(三个结构体)组成,各个部分的页码是什么。 CS_CodeDirectory 我们先分析CSSLOT_CODEDIRECTORY,它对应的是CS_CodeDirectory结构体: /* * C form of a CodeDirectory. */ typedef struct __CodeDirectory { uint32_t magic; /* magic number (CSMAGIC_CODEDIRECTORY) */ uint32_t length; /* total length of CodeDirectory blob */ uint32_t version; /* compatibility version */ uint32_t flags; /* setup and mode flags */ uint32_t hashOffset; /* offset of hash slot element at index zero */ uint32_t identOffset; /* offset of identifier string */ uint32_t nSpecialSlots; /* number of special hash slots */ uint32_t nCodeSlots; /* number of ordinary (code) hash slots */ uint32_t codeLimit; /* limit to main image signature range */ uint8_t hashSize; /* size of each hash in bytes */ uint8_t hashType; /* type of hash (cdHashType* constants) */ uint8_t platform; /* platform identifier; zero if not platform binary */ uint8_t pageSize; /* log2(page size in bytes); 0 => infinite */ uint32_t spare2; /* unused (must be zero) */ /* Version 0x20100 */ uint32_t scatterOffset; /* offset of optional scatter vector */ /* Version 0x20200 */ uint32_t teamOffset; /* offset of optional team identifier */ /* followed by dynamic content as located by offset fields above */ } CS_CodeDirectory;数据解析 我们先把这段数据拿出来,然后根据结构体进行分析:这里仅挑一些重要的内容进行分析。 magic是0xFADE0C02,作为标记存在,代表CodeDirectory length是0x23D,表示数据段长度 identoffset是0x30,表示identifier字符串的偏移量,这里的identifier对应的就是我们的bundleId 需要提醒的是当前的CodeDirectory是数据SuperBlob的内部结构体,所以这里的offset就变成了结构体内部偏移了,这里的起始位置也即是0xFADE0C02所在的位置是0x11254,所以可以算出indentoffset的文件偏移量是: identoffset地址为:0x11254 + 0x30 = 0x11284 这里你可能会疑惑,只有偏移量怎么确认从哪结束呢,这里并没有提供数据大小。其实字符串是不需要知道大小也可以确认它到哪结束的,字符里面有结束位\0啊,在ASCII码里结束位就是0x00。可以解析得出ls的bundleId是com.apple.ls。 这里再补充一点:MachO里字符串的编码不是通过ASCII,而是使用UTF-8进行编码的,只不过UTF-8兼容了ASCII,所以我们当做ASCII也能解析出正确的内容。 CS_GenericBlob 我们现在来看下证书的解析,查上面记录的偏移表,CSSLOT_SIGNATURESLOT对应的结构体是CS_Generic_Blob: typedef struct __SC_GenericBlob { uint32_t magic; /* magic number */ uint32_t length; /* total length of blob */ char data[]; } CS_GenericBlob;上个表格我们记录了它的offset是0x29D位置,所以它的起始位置就是:0x11230 + 0x29D = 0x114CD,找到这个位置,带入结构体进行解析:magic是0xFADE0B01,对应了CSSLOT_SIGNATURESLOT值。 数据长度是0x11E9(4585字节),这表示的CS_GenericBlob的大小,而在这之后的内容都是data,表示的就是证书部分。 我们可以计算出证书data结束的最后一个字节位置:0x114CD + 0x11E9 - 0x8 - 0x1 = 0x126AD。 说明:根据《iOS应用逆向与安全》一书说明,借助于010 Editor等二进制工具,我们把data部分的数据复制出来(需要借助于Hooper这类工具),保存为cer格式,就能获取到一个证书文件。但对ls的测试并不能成功,推测这里的data可能还有其余内容,需要拆分。 Jtool 只要有了对应数据结构,签名部分的所有信息我们都是可以解析出来的。但每次都逐字节分析,显然很费事,能不能写个程序,用于上述内容解析呢?当然是可以的,已经有这样的工具了,就是Jtool。jtool比otool功能更强大,解析的数据也更详细。可以通过homebrew进行安装: $ brew install jtool如果通过jtool查看上面x86_64架构的签名信息,可以这样: $ jtool -arch x86_64 --sig /bin/ls输出结果为: Blob at offset: 53808 (5728 bytes) is an embedded signature Code Directory (573 bytes) Version: 20100 Flags: none Platform Binary CodeLimit: 0xd230 Identifier: com.apple.ls (0x30) CDHash: 46cc1da7c874a5853984a286ffecb48daf2f65f023d10258a31118acfc8a3697 (computed) # of Hashes: 14 code + 2 special Hashes @125 size: 32 Type: SHA-256 Requirement Set (60 bytes) with 1 requirement: 0: Designated Requirement (@20, 28 bytes): SIZE: 28 Ident: (com.apple.ls) AND Apple Anchor Blob Wrapper (4585 bytes) (0x10000 is CMS (RFC3852) signature) CA: Apple Certification Authority CN: Apple Root CA CA: Apple Certification Authority CN: Apple Code Signing Certification Authority CA: Apple Certification Authority CN: Apple Root CA CA: Apple Certification Authority CN: Apple Root CA CA: Apple Certification Authority CN: Apple Code Signing Certification Authority CA: Apple Software CN: Software Signing Time: 201222002625Zi第一行里的offset 53808 对应16进制是0xD230,就是LC_CODE_SIGNATURE里记录的偏移量。 根据输出信息也能得出code signature由三部分内容组成:Code Diretory、Requeirement Set、Blob Wrapper。证书部分解析出了6个证书,说明这里应该还有别的结构体可以拆分。 回顾 如果你看到这里,可以回顾下开始讲到的三个问题,用于检验你的理解程度。 1、MachOView是如何确认MachO内容的。 2、二进制数据是如何存储的,如何确认位置。 3、字节码含义如何解析。