iOS RE 4 beginners 1 - MachO && class-dump
roadmap
之前在 iosre看到一张比较系统的iOS逆向学习路线图,因为接触过一段时间macOS上服务的漏洞挖掘,所以对*OS安全还是挺有兴趣的,也一直想系统性地学习下iOS逆向,之前的一直不成体系,也很零碎,正好对着这个图重构下知识体系。
macho file format
类似Windows/Linux平台逆向学习,首先要学习正向开发的基础知识,以及涉及的文件格式(指可执行文件):
- Windows - PE
- Linux - ELF
- *OS - MachO
根据roadmap中的app分析流程,第一步就是“砸壳“,就是在根据文件格式做文章,因为macho文件是加密的,被加载到内存执行的时候才会解密,所以我们做静态分析,需要把内存中解密之后的可执行文件dump出来,并修复文件才可以拖入hopper/IDA正常分析。
Overview
我感觉这些可执行文件大同小异的味道,基本都是文件头+各种节区。 在macOS上你可以使用:
- MachOView
- MachOExplorer
来查看一个macho文件的结构,推荐前者,后者不知道为什么总是卡卡的,而且很容易崩溃 :(
总体上来看,macho文件格式可以看做:
Header
Load Commands
- LC_SEGMENT
- TEXT
- DATA
- LINKEDIT
- LC_CODESIGNATURE
- LC_DYLD_INFO_ONLY
- LC_XXXX_DYLIB
- LC_SEGMENT
Data
- Segment(1-n)
Header
只关注几个基本字段
- magic number : 表示macho的类型,FAT, ARMv7,ARM64,x86_64
- FAT 就是 “胖文件”,表示这个文件里包含了多个架构的MachO文件,可以使用
lipo
分离
- FAT 就是 “胖文件”,表示这个文件里包含了多个架构的MachO文件,可以使用
- CPU Type, CPU SubType : arch
- Number of load commands : Load commands的数量
- flags:表示一些标识位,比如是否开了PIE,checksec可以从这里获取一些信息。
- reversed:64位保留字段
Load Commands
即告诉操作系统,该如何加载文件中的数据。
- LC_SEGMENT_64:定义一个段,加载后被映射到内存中,包括里面的节。 比如代码段 数据段 :
- TEXT 代码段
- DATA 数据段
- LC_DYLD_INFO_ONLY:记录了有关链接的重要信息,包括在_LINKEDIT中动态链接 相关信息的具体偏移和大小。ONLY表示这个加载指令是程序运行所必需的,如果旧的 链接器无法识别它,程序就会出错。
- LC_SYMTAB:为文件定义符号表和字符串表,在链接文件时被链接器使用,同时也用于调试器映射符号到源文件。符号表定义的本地符号仅用于调试,而已定义和未定义的external符号被链接器使用。
- LC_DYSYMTAB:将符号表中给出符号的额外符号信息提供给动态链接器。
- LC_LOAD_DYLINKER:默认的加载器路径。
/usr/lib/dyld
- LC_UUID:用于标识MachO文件的ID,也用于崩溃堆栈和符号文件的对应解析。
- LC_VERSION_MIN_IPHONEOS:系统要求的最低版本。
- LC_SOURCE_VERSION:构建二进制文件的源代码版本号。
- LC_MAIN:程序的入口。dyld获取该地址,然后跳转到该处执行。
- LC_ENCRYPTION_INFO_64:文件是否加密的标志,加密内容的偏移和大小。
- lldb dump 砸壳修复文件之后,需要修改该标识位以确保正常反汇编文件。
- LC_LOAD_DYLIB:依赖的动态库,包括动态库名称、当前版本号、兼容版本号。
- “otool -L xxx”命令查看
- LC_RPATH: Runpath Search Paths, @rpath 搜索的路径。
- LC_FUNCTION_STARTS:函数起始地址表,使调试器和其他程序能很容易地看到一个地址是否在函数内。
- LC_DATA_IN_CODE:定义在代码段内的非指令的表。
- LC_CODE_SIGNATURE:代码签名信息。
- codesign -d [filename]
Data-Segments
各种节区,比如代码段,数据段,只读数据段等:
这里可以看到很多__DATA, __objc__?
节区,Symbol Table
String Table
也单独列了出来。
- __objc_protolist
- __objc_classlist
- __objc_catlist section
- …
这些节区保存了OC中类名,函数名等信息,这就为从MachO中dump出来头文件打下了基础。
Get class info from macho file
__DATA, __objc_protolist
节区:
存储的都是指针,指向一个又一个protocol的结构,可以参考objc的代码 :
1 | struct protocol_t : objc_object { |
所以我们可以按照结构体索引 __DATA, __objc_protolist
里指针指向的位置的数据,就可以解析出来protocol的类型,名字,方法等信息。
class-dump read notes
env
macos11.4 + xcode12
compile
Q : openssl/aes.h
not found
A : add header file path
1 | export LDFLAGS="-L/usr/local/opt/openssl/lib" |
XCode中的配置是:
Q : Library not found for -lcrypto
A : add the missing dylib
raed && debug
核心逻辑就看
1 | - (void)processObjectiveCData; |
1. symbolTable loadSymbols
Load Commands 里找到 LC_SYMTAB,然后找到 __DATA(依赖属性 RW)。
然后利用 LC_SYMTAB 初始化了cursor开始遍历找符号。
strtab 从 string table 开始 : 一个 symbol起始位置,一个string起始位置。
然后根据 arm 还是 x64 走不同的逻辑(这里目标是ARM64的Binary) :
开始解析 symbol table,item by item
1 | string table index --> 在string table里找到对应的 string |
然后根据string table index里找到对应的string,放到symbols数组里,
根据 string 的 value 判断是不是 class,这里是根据字符串的开头是不是 @"*OBJC_CLASS*$_"
。
对于解析出来class name,添加到 class symbols dict里,这样处理之后,symbols, classSymbols都有了。
2. dynamicSymbolTable loadsymbols
类似1
3. loadProtocols
从 __DATA , __objc_protolist
读取 对应的value
比如得到地址0x1009ccc58
走到 - (CDOCProtocol *)protocolAtAddress:(uint64_t)address
初始化对应的CDOCProtocol
对象
依赖这个地址,从文件对应地址读取出来 这个 proto
的相关信息:
1 | struct cd_objc2_protocol objc2Protocol; |
name protocols这些字段是一个地址,指向对应的值(字符串/数组)
最后参照objc2Protocol的值,分别获取protocol 的 name, 各种methods,属性等,初始化了protocol对象
所以protocols就都处理出来了,最后得到了
_protocolsByAddress __NSDictionaryM * 6781 key/value pairs 0x0000000112f93820
4. protocolUniquer createUniquedProtocols
依赖3中找到的 _protocolsByAddress
name -> protocol 对应关系的dict addr -> protocol 对应关系的dict
p1->protocols 里还有protocol,merge进来(adopted protocols)
p1 : _name __NSCFString * @”AWEFriendsActivityWidgetConfigurationIntentHandling” 0x0000000112fbc710
p2 : _name NSTaggedPointerString * @”NSObject” 0x07518ee6ed78d7f9
@interface AWEFriendsActivityWidgetConfigurationIntentHandling : NSObject { //blablabla… }
这种情况
5. loadClasses
解析section : __DATA __objc_classlist
和3类似的套路,先得到 一个 地址,然后根据地址,去文件中索引对应的结构:
CDOCClass *aClass = [self loadClassAtAddress:val]
只调试一次过程分析即可: val uint64_t 4335166480 In [2]: hex(4335166480) Out[2]: '0x102656410'
这个0x102656410,使用machoview也能看到,调试+machoview对比看,更容易理解。
loadClassAtAddress
方法分析:
1 | struct cd_objc2_class objc2Class; |
也是读取对应的class结构,这个过程其实很眼熟,如果读过iOS逆向的书,比如庆神的书,有一章介绍oc方法调用过程的,会把oc->cpp代码,那里面这个 oc object的结构分析的很清楚。
然后解析 class->data
字段
1 | struct cd_objc2_class_ro_t objc2ClassData; |
然后得到class 的 name,methods,protocol, property信息 然后返回这个class
展开说下 获取 methods && property的时候
(NSArray *)loadMethodsAtAddress:(uint64_t)address; { return [self loadMethodsAtAddress:address extendedMethodTypesCursor:nil]; }
loadMethodsAtAddress :
1 | objc2Method.name = [cursor readPtr]; |
一样的套路,都是解析出来对应的字段,然后按照这些字段读取信息(string) CDOCMethod *method = [[CDOCMethod alloc] initWithName:name typeString:types address:objc2Method.imp]; [methods addObject:method];
最后获得methods数组,给前面填充class的地方使用
loadIvarsAtAddress ,loadPropertiesAtAddress , loadMethodsOfMetaClassAtAddress
同理
至此,class解析完毕
6. loadCategories
关于Categories 可以看 https://zhuanlan.zhihu.com/p/24925196
处理 __DATA __objc_catlist section
:
- (CDOCCategory *)loadCategoryAtAddress:(uint64_t)address;
一样的处理方法
1 | struct cd_objc2_category objc2Category; |
可以看到和对objc2Class的处理有点像,就是因为是category的原因,所以字段有不同, 简单的理解成 处理一种特殊的class,并且提取出相应的 methods 和 properties就行
至此整个 process函数的处理结束
7. 处理 or 输出
这部分主要是处理输出了,如果没什么参数就直接stdout输出,如果有指定文件目录,就遍历之前process得到的信息,写文件(.h)到指定的目录。
Reference
https://zhuanlan.zhihu.com/p/24925196
https://en.wikipedia.org/wiki/Mach-O
https://evilpan.com/2020/09/06/macho-inside-out/
iOS应用逆向与安全 (刘培庆著)