

iOS RE 4 beginners 1 - MachO && class-dump
source link: https://o0xmuhe.github.io/2021/07/11/iOS-RE-4-beginners-1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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
- segment load(1-n)
-
- 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:定义一个段,加载后被映射到内存中,包括里面的节。 比如代码段 数据段
- LC_DYLD_INF0_0NLY:记录了有关链接的重要信息,包括在_LINKEDIT中动态链接 相关信息的具体偏移和大小。ONLY表示这个加载指令是程序运行所必需的,如果旧的 链接器无法识别它,程序就会出错。
- LC_SYMTAB:为文件定义符号表和字符串表,在链接文件时被链接器使用,同时也用于调试器映射符号到源文件。符号表定义的本地符号仅用于调试,而已定义和未定义的external符号被链接器使用。
- LC_DYSYMTAB:将符号表中给出符号的额外符号信息提供给动态链接器。
- LC_LOAD_DYLINKER:默认的加载器路径。
/usr/lib/dyld
- LC.UUID:用于标识Mach-0文件的ID,也用于崩溃堆栈和符号文件的对应解析。
- LC_VERSION_MIN_IPHONEOS:系统要求的最低版本。
- LC.SOURCE.VERSION:构建二进制文件的源代码版本号。
- LC.MAIN:程序的入口。dykl获取该地址,然后跳转到该处执行。
- 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:代码签名信息。
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的代码 :
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
// Fields below this point are not always present on disk.
const char **_extendedMethodTypes;
......
}
struct objc_object {
private:
isa_t isa;
public:
...
}
所以我们可以按照结构体索引 __DATA, __objc_protolist
里指针指向的位置的数据,就可以解析出来protocol的类型,名字,方法等信息。
class-dump read notes
macos11.4 + xcode12
compile
Q : openssl/aes.h
not found
A : add header file path
export LDFLAGS="-L/usr/local/opt/openssl/lib"
export CPPFLAGS="-I/usr/local/opt/openssl/include"
XCode中的配置是:
Q : Library not found for -lcrypto
A : add the missing dylib
raed && debug
核心逻辑就看
- (void)processObjectiveCData;
{
for (CDMachOFile *machOFile in self.machOFiles) {
CDObjectiveCProcessor *processor = [[[machOFile processorClass] alloc] initWithMachOFile:machOFile];
[processor process];
[_objcProcessors addObject:processor];
}
}
- (void)process;
{
if (self.machOFile.isEncrypted == NO && self.machOFile.canDecryptAllSegments) {
[self.machOFile.symbolTable loadSymbols];
[self.machOFile.dynamicSymbolTable loadSymbols];
[self loadProtocols];
[self.protocolUniquer createUniquedProtocols];
// Load classes before categories, so we can get a dictionary of classes by address.
[self loadClasses];
[self loadCategories];
}
}
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
string table index --> 在string table里找到对应的 string
type
section index
desc
value
然后根据string table index里找到对应的string,放到symbols数组里,
根据 string 的 value 判断是不是 class,这里是根据字符串的开头是不是 @"*OBJC_CLASS*$_"
。
对于解析出来class name,添加到 class symbols dict里,这样处理之后,symbols, classSymbols都有了。
2. dynamicSymbolTable loadsymbols
3. loadProtocols
从 __DATA , __objc_protolist
读取 对应的value
比如得到地址0x1009ccc58
走到 - (CDOCProtocol *)protocolAtAddress:(uint64_t)address
初始化对应的CDOCProtocol
对象
依赖这个地址,从文件对应地址读取出来 这个 proto
的相关信息:
struct cd_objc2_protocol objc2Protocol;
objc2Protocol.isa = [cursor readPtr];
objc2Protocol.name = [cursor readPtr];
objc2Protocol.protocols = [cursor readPtr];
objc2Protocol.instanceMethods = [cursor readPtr];
objc2Protocol.classMethods = [cursor readPtr];
objc2Protocol.optionalInstanceMethods = [cursor readPtr];
objc2Protocol.optionalClassMethods = [cursor readPtr];
objc2Protocol.instanceProperties = [cursor readPtr];
objc2Protocol.size = [cursor readInt32];
objc2Protocol.flags = [cursor readInt32];
objc2Protocol.extendedMethodTypes = 0;
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
方法分析:
struct cd_objc2_class objc2Class;
objc2Class.isa = [cursor readPtr];
objc2Class.superclass = [cursor readPtr];
objc2Class.cache = [cursor readPtr];
objc2Class.vtable = [cursor readPtr];
objc2Class.data = [cursor readPtr];
objc2Class.reserved1 = [cursor readPtr];
objc2Class.reserved2 = [cursor readPtr];
objc2Class.reserved3 = [cursor readPtr];
也是读取对应的class结构,这个过程其实很眼熟,如果读过iOS逆向的书,比如庆神的书,有一章介绍oc方法调用过程的,会把oc->cpp代码,那里面这个 oc object的结构分析的很清楚。
然后解析 class->data
字段
struct cd_objc2_class_ro_t objc2ClassData;
objc2ClassData.flags = [cursor readInt32];
objc2ClassData.instanceStart = [cursor readInt32];
objc2ClassData.instanceSize = [cursor readInt32];
if ([self.machOFile uses64BitABI])
objc2ClassData.reserved = [cursor readInt32];
else
objc2ClassData.reserved = 0;
objc2ClassData.ivarLayout = [cursor readPtr];
objc2ClassData.name = [cursor readPtr];
objc2ClassData.baseMethods = [cursor readPtr];
objc2ClassData.baseProtocols = [cursor readPtr];
objc2ClassData.ivars = [cursor readPtr];
objc2ClassData.weakIvarLayout = [cursor readPtr];
objc2ClassData.baseProperties = [cursor readPtr];
然后得到class 的 name,methods,protocol, property信息 然后返回这个class
展开说下 获取 methods && property的时候
(NSArray *)loadMethodsAtAddress:(uint64_t)address; { return [self loadMethodsAtAddress:address extendedMethodTypesCursor:nil]; }
loadMethodsAtAddress :
objc2Method.name = [cursor readPtr];
objc2Method.types = [cursor readPtr];
objc2Method.imp = [cursor readPtr];
NSString *name = [self.machOFile stringAtAddress:objc2Method.name];
NSString *types = [self.machOFile stringAtAddress:objc2Method.types];
一样的套路,都是解析出来对应的字段,然后按照这些字段读取信息(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;
一样的处理方法
struct cd_objc2_category objc2Category;
objc2Category.name = [cursor readPtr];
objc2Category.class = [cursor readPtr];
objc2Category.instanceMethods = [cursor readPtr];
objc2Category.classMethods = [cursor readPtr];
objc2Category.protocols = [cursor readPtr];
objc2Category.instanceProperties = [cursor readPtr];
objc2Category.v7 = [cursor readPtr];
objc2Category.v8 = [cursor readPtr];
可以看到和对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
iOS应用逆向与安全 (刘培庆著)
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK