iOS RE 4 beginners 1 - MachO && class-dump

roadmap

之前在 iosre看到一张比较系统的iOS逆向学习路线图,因为接触过一段时间macOS上服务的漏洞挖掘,所以对*OS安全还是挺有兴趣的,也一直想系统性地学习下iOS逆向,之前的一直不成体系,也很零碎,正好对着这个图重构下知识体系。

ios_re

macho file format

类似Windows/Linux平台逆向学习,首先要学习正向开发的基础知识,以及涉及的文件格式(指可执行文件):

  • Windows - PE
  • Linux - ELF
  • *OS - MachO

根据roadmap中的app分析流程,第一步就是“砸壳“,就是在根据文件格式做文章,因为macho文件是加密的,被加载到内存执行的时候才会解密,所以我们做静态分析,需要把内存中解密之后的可执行文件dump出来,并修复文件才可以拖入hopper/IDA正常分析。

Overview

Untitled

我感觉这些可执行文件大同小异的味道,基本都是文件头+各种节区。 在macOS上你可以使用:

  • MachOView
  • MachOExplorer

来查看一个macho文件的结构,推荐前者,后者不知道为什么总是卡卡的,而且很容易崩溃 :(

总体上来看,macho文件格式可以看做:

  • Header

  • Load Commands

    • LC_SEGMENT
      • TEXT
      • DATA
      • LINKEDIT
    • LC_CODESIGNATURE
    • LC_DYLD_INFO_ONLY
    • LC_XXXX_DYLIB
  • Data

    • Segment(1-n)

只关注几个基本字段

  • magic number : 表示macho的类型,FAT, ARMv7,ARM64,x86_64
    • FAT 就是 “胖文件”,表示这个文件里包含了多个架构的MachO文件,可以使用lipo分离
  • CPU Type, CPU SubType : arch
  • Number of load commands : Load commands的数量
  • flags:表示一些标识位,比如是否开了PIE,checksec可以从这里获取一些信息。
  • reversed:64位保留字段

CleanShot 2021-07-11 at 15.09.41@2x

Load Commands

CleanShot 2021-07-11 at 15.17.40@2x

即告诉操作系统,该如何加载文件中的数据。

  • 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

各种节区,比如代码段,数据段,只读数据段等:

CleanShot 2021-07-11 at 15.19.59@2x

这里可以看到很多__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节区:

CleanShot 2021-07-11 at 15.30.45@2x

存储的都是指针,指向一个又一个protocol的结构,可以参考objc的代码 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

env

macos11.4 + xcode12

compile

Q : openssl/aes.h not found

A : add header file path

1
2
export LDFLAGS="-L/usr/local/opt/openssl/lib"
export CPPFLAGS="-I/usr/local/opt/openssl/include"

XCode中的配置是:

111

Q : Library not found for -lcrypto

A : add the missing dylib

fix_crypto

raed && debug

核心逻辑就看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (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开始遍历找符号。

CleanShot 2021-07-11 at 15.47.33@2x

strtab 从 string table 开始 : 一个 symbol起始位置,一个string起始位置。

然后根据 arm 还是 x64 走不同的逻辑(这里目标是ARM64的Binary) :

CleanShot 2021-07-11 at 15.48.18@2x

开始解析 symbol table,item by item

1
2
3
4
5
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

类似1

3. loadProtocols

__DATA , __objc_protolist 读取 对应的value

比如得到地址0x1009ccc58

走到 - (CDOCProtocol *)protocolAtAddress:(uint64_t)address

初始化对应的CDOCProtocol对象

依赖这个地址,从文件对应地址读取出来 这个 proto的相关信息:

1
2
3
4
5
6
7
8
9
10
11
12
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方法分析:

1
2
3
4
5
6
7
8
9
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 字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 :

1
2
3
4
5
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;

一样的处理方法

1
2
3
4
5
6
7
8
9
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

https://iosre.com/

https://evilpan.com/2020/09/06/macho-inside-out/

iOS应用逆向与安全 (刘培庆著)