iOS逆向实战--008:反汇编

OC的反汇编

案例1:

OC的方法调用

打开Person.h,写入以下代码:

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property(nonatomic,copy)NSString *name;

@property(nonatomic,assign)int age;

+(instancetype)person;

@end

打开Person.m,写入以下代码:

#import "Person.h"

@implementation Person

+(instancetype)person{
   return [[Person alloc] init];
}

@end

打开ViewController.m,写入以下代码:

#import "ViewController.h"
#import "Person.h"

@implementation ViewController

- (void)viewDidLoad {
//    [super viewDidLoad];
   Person *p = [Person person];
}

@end

真机运行项目,来到viewDidLoad方法

  • objc_msgSend函数有两个默认参数,id类型的selfSEL类型的_cmd
  • 两个参数分别对应x0x1

查看x0寄存器

   0x1046de46c <+20>: adrp   x8, 2
   0x1046de470 <+24>: add    x8, x8, #0xec8            ; =0xec8 
   0x1046de474 <+28>: ldr    x0, [x8]
  • 使用adrp + add指令,计算地址为0x1046e0ec8,赋值x8
  • x8进行寻址,写入x0
  • x0对应id类型参数,id类型本质上是结构体指针,占8字节

查看内存中的数据

x 0x1046e0ec8
-------------------------
0x1046e0ec8: 38 0f 6e 04 01 00 00 00 b0 0f 6e 04 01 00 00 00  8.n.......n.....
0x1046e0ed8: 08 00 00 00 10 00 00 00 10 00 00 00 00 00 00 00  ................
  • 读取8字节数据,小端模式,从右往左读取0x01046e0f38

打印x0

po 0x01046e0f38
-------------------------
Person
  • 打印的Person是类对象

查看x1寄存器

   0x1046de478 <+32>: adrp   x8, 2
   0x1046de47c <+36>: add    x8, x8, #0xeb0            ; =0xeb0 
   0x1046de480 <+40>: ldr    x1, [x8]
  • 使用adrp + add指令,计算地址为0x1046e0eb0,赋值x8
  • x8进行寻址,写入x1
  • x1对应SEL类型参数,占8字节

查看内存中的数据

x 0x1046e0eb0
-------------------------
0x1046e0eb0: 10 c8 88 e2 01 00 00 00 18 f6 15 e3 01 00 00 00  ................
0x1046e0ec0: 98 88 fd e2 01 00 00 00 38 0f 6e 04 01 00 00 00  ........8.n.....
  • 读取8字节数据,小端模式,从右往左读取0x01e288c810

打印x1

po (SEL)0x01e288c810
-------------------------
"person"
  • 打印的"person"是方法名称

当汇编代码中出现objc_msgSend,表示调用了一个OC方法,读取x0x1便可知道调用了哪个对象的哪个方法

案例2:

allocinit函数

使用iOS12.2系统调试,来到Person对象的person方法

  • 调用objc_alloc_init函数

iOS12.2开始,系统优化allocinit函数,不再使用消息发送(objc_msgSend),直接调用优化后的objc_alloc_init函数


使用iOS12.1系统调试

  • 分别调用objc_alloc函数和objc_msgSend函数

调用objc_msgSend函数,实际上就是在调用init函数

  • 调用objc_msgSend函数,但这里没有对x0进行赋值。这说明在上一个调用函数objc_alloc中返回的x0,正是objc_msgSend函数将要使用的参数

iOS10.0开始,系统优化alloc函数,直接调用优化后的objc_alloc函数。但对于init函数,依然使用objc_msgSend进行消息发送


使用iOS9.1系统调试

allocinit函数,调用两次objc_msgSend进行消息发送

案例3:

objc_storeStrong函数

Person初始化完毕,x0返回一个实例对象。之后会调用objc_storeStrong函数

  • OC中,使用strong修饰的对象,都会调用此函数
  • viewDidLoad方法中,定义的局部变量p,就是一个强引用
  • 调用objc_storeStrong函数,并不一定会让对象的引用计数+1,也可能调用后直接销毁。例如:局部变量p,它没有被外部代码使用,出栈后就会销毁

来到objc源码

打开NSObject.mm文件,找到objc_storeStrong函数的实现

void
objc_storeStrong(id *location, id obj)
{
   id prev = *location;
   if (obj == prev) {
       return;
   }
   objc_retain(obj);
   *location = obj;
   objc_release(prev);
}
  • 参数location,二级指针,指向对象指针的指针
  • 参数obj,对象指针
  • 局部变量prev,将location指向的对象指针赋值给prev
  • 判断对象相等,直接return
  • 如果不等,当前对象obj引用计数+1
  • location指向obj
  • location指向的老对象释放

objc_storeStrong函数的目的,对一个strong修饰的对象retain,对老对象release,等价于以下代码:

Person *p = p1;
p = p2;
  • p2赋值给pp2引用计数+1p1释放

回到汇编代码

查看location参数

   0x10058a3ac <+56>: add    x8, sp, #0x8              ; =0x8 
   0x10058a3b0 <+60>: str    x0, [sp, #0x8]
   0x10058a3b4 <+64>: mov    x0, x8
  • sp + #0x8地址写入x8
  • x0,即:局部变量p,入栈到sp + #0x8
  • x8写入x0,此时x0是指向局部变量p的指针地址

查看obj参数

   0x10058a3b8 <+68>: mov    x8, #0x0
   0x10058a3bc <+72>: mov    x1, x8
  • #0x0,即:nil,写入x8
  • x8写入x1,此时x1nil

此刻调用objc_storeStrong函数触发的逻辑:

  • prev被赋值为局部变量p
  • 判断对象不相等
  • 调用objc_retain函数,传入nil
  • location指向nil
  • 调用objc_release函数,传入局部变量p,将其释放
工具反汇编

真实场景下,对Mach-O进行分析,只能使用静态分析,无法使用lldb动态调试。所以要学会运用工具进行反汇编

案例1:

打开ViewController.m,写入以下代码:

#import "ViewController.h"
#import "Person.h"

@implementation ViewController

- (void)viewDidLoad {
//    [super viewDidLoad];
   Person *p = [Person person];
   p.name = @"Zang";
   p.age = 18;
}

@end

使用真机编译项目,将Mach-O文件拖到Hopper中,找到viewDidLoad函数

  • Hopper可以解析出方法、属性等名称,作为注释标记给开发者
  • 对于bl指令,也将跳转地址解析为对应的函数名称

案例2:

Mach-O中,找到Hopper转义出的注释

找到objc_cls_ref_PersonMach-O中的位置

0000000100006350         adrp       x8, #0x10000c000
0000000100006354         add        x8, x8, #0xd58            ; >objc_cls_ref_Person
  • 使用adrp + add指令,计算地址为0x10000cd58,赋值x8

打开Mach-O,找到0x10000cd58


找到personselector,在Mach-O中的位置

000000010000635c         adrp       x8, #0x10000c000
0000000100006360         add        x8, x8, #0xd40             ; @selector(person)
  • 使用adrp + add指令,计算地址为0x10000cd40,赋值x8

打开Mach-O,找到0x10000cd40


找到person等字符串,在Mach-O中的位置

打开Mach-O,找到0x100006a2c,存储了方法名称的字符串


找到imp___stubs__objc_msgSendMach-O中的位置

0000000100006368         bl         imp___stubs__objc_msgSend

HopperviewDidLoad方法中,双击imp___stubs__objc_msgSend

                    imp___stubs__objc_msgSend:
000000010000685c         nop             ; CODE XREF=-[ViewController viewDidLoad]+44, -[ViewController viewDidLoad]+92, -[ViewController viewDidLoad]+116
0000000100006860         ldr        x16, #0x10000c028
0000000100006864         br         x16
                       ; endp

打开Mach-O,找到0x10000685c,存储的正是_objc_msgSend函数


找到Zang字符串,在Mach-O中的位置

000000010000638c         adrp       x2, #0x100008000
0000000100006390         add        x2, x2, #0x8              ; @"Zang"
  • 使用adrp + add指令,计算地址为0x100008008,赋值x8

HopperviewDidLoad方法中,双击Zang

       ; Section __cfstring
       ; Range: [0x100008008; 0x100008028[ (32 bytes)
       ; File offset : [32776; 32808[ (32 bytes)
       ;   S_REGULAR

                    cfstring_Zang:
0000000100008008         dq         ___CFConstantStringClassReference, 0x7c8, 0x10000755d, 0x4 ; "Zang", DATA XREF=-[ViewController viewDidLoad]+84

打开Mach-O,找到0x100008008,存储字符串常量Zang的相关信息

案例3:

使用Hopper查看代码的流程图

打开ViewController.m,写入以下代码:

#import "ViewController.h"

@implementation ViewController

void test1(bool b){
   if(b){
       test2(b);
   }
   else{
       test3();
   }
}

void test2(bool b){
   if(b){
       test3();
   }
   else{
       test4();
   }
}

void test3(){ NSLog(@"test3"); }
void test4(){ NSLog(@"test4"); }

- (void)viewDidLoad {
//    [super viewDidLoad];
   
   int a = 1;
   int b = 2;
   
   if(a == b){
       test1(YES);
   }
   else{
       test2(NO);
   }
}

@end

使用真机编译项目,将Mach-O文件拖到Hopper中,找到viewDidLoad函数

Hopper中,点击CFG mode按钮

切换到viewDidLoad函数的流程图,清晰的展示出不同的代码分支

双击test1函数,切换到test1函数中的代码分支

还原高级代码时,遇到复杂的逻辑分支,可以使用代码的流程图进行分析

案例4:

使用Hopper查看伪码模式

Hopper中,点击Pseudo-code mode按钮

查看Hopper帮我们还原的viewDidLoad函数的伪码

int -[ViewController viewDidLoad]() {
   r31 = r31 - 0x30;
   *(r31 + 0x20) = r29;
   *(r31 + 0x28) = r30;
   *(r31 + 0x18) = r0;
   *(r31 + 0x10) = r1;
   *(r31 + 0xc) = zero_extend_64(0x1);
   *(r31 + 0x8) = zero_extend_64(0x2);
   if (*(r31 + 0xc) == *(r31 + 0x8)) {
           r0 = _test1(zero_extend_64(0x1) & 0x1);
   }
   else {
           r0 = _test2(zero_extend_64(0x0) & 0x1);
   }
   return r0;
}
Block的反汇编

日常开发中,Block有时作为参数传递,有时作为返回值,但大多数情况下,Block会作为方法的参数回调

在逆向分析中,需要定位Blockinvoke。只有找到invoke,才能找到回调方法中的代码逻辑

案例1:

查看GlobalBlock的汇编代码

打开ViewController.m,写入以下代码:

#import "ViewController.h"

@implementation ViewController

- (void)viewDidLoad {
//    [super viewDidLoad];

   void(^block)(void) = ^() {
       NSLog(@"block");
   };
   
   block();
}

@end

真机运行项目,来到viewDidLoad方法

使用register read x0命令,查看x0寄存器

x0 = 0x00000001029d8028  002--OC`__block_literal_global
  • x0存储的是一个GlobalBlock
  • Block内部不使⽤外部变量,或者只使⽤静态变量和全局变量,会形成一个GlobalBlock,它位于全局区Block

使用po 0x00000001029d8028命令,打印内存中的数据

<__NSGlobalBlock__: 0x1029d8028>
signature: "v8@?0"
invoke   : 0x1029d6384 (/private/var/containers/Bundle/Application/2B6AC788-34A7-46BB-A44B-2237AFD48A2A/002--OC.app/002--OC`__29-[ViewController viewDidLoad]_block_invoke)
  • NSGlobalBlockBlockisa
  • invoke : 0x1029d6384是代码实现的所在位置

来到libclosure源码

打开Block_private.h文件,找到Block的定义

struct Block_layout {
   void *isa;
   volatile int32_t flags;
   int32_t reserved;
   BlockInvokeFunction invoke;
   struct Block_descriptor_1 *descriptor;
};
  • Block是一个结构体
  • isa8字节flagsreserved共占8字节
  • 16字节之后,就是invoke,最后是descriptor描述

回到汇编代码

使用x/8g 0x1029d8028,查看内存中的数据

  • 16字节之后,即:invoke,地址和上面打印的0x1029d6384一致

Hopper中,找到Blockinvoke

使用真机编译项目,将Mach-O文件拖到Hopper中,找到viewDidLoad函数

  • Hopper直接标记出___block_literal_global

双击___block_literal_global,找到Block的结构体对象

双击invoke,找到invoke中的代码逻辑

                    ___29-[ViewController viewDidLoad]_block_invoke:
0000000100006384         sub        sp, sp, #0x20               ; Objective C Block defined at 0x100008028, DATA XREF=0x100008038
0000000100006388         stp        x29, x30, [sp, #0x10]
000000010000638c         add        x29, sp, #0x10
0000000100006390         str        x0, [sp, #0x8]
0000000100006394         str        x0, sp
0000000100006398         adrp       x0, #0x100008000            ; argument #1 for method imp___stubs__NSLog
000000010000639c         add        x0, x0, #0x48               ; @"block"
00000001000063a0         bl         imp___stubs__NSLog
00000001000063a4         ldp        x29, x30, [sp, #0x10]
00000001000063a8         add        sp, sp, #0x20
00000001000063ac         ret
                       ; endp

案例2:

查看StackBlock的汇编代码

打开ViewController.m,写入以下代码:

- (void)viewDidLoad {
//    [super viewDidLoad];

   int a = 1;
   void(^block)(void) = ^() {
       NSLog(@"block:%i", a);
   };
   
   block();
}

真机运行项目,来到viewDidLoad方法


标记的第1段代码

   0x1021e62dc <+28>:  add    x9, sp, #0x8              ; =0x8 
   0x1021e62e0 <+32>:  adrp   x10, 2
   0x1021e62e4 <+36>:  ldr    x10, [x10]
   0x1021e62e8 <+40>:  str    x10, [sp, #0x8]
  • sp + #0x8的地址,赋值给x9
  • adrp指令,计算地址为0x1021e8000,赋值给x10
  • x10寻址,将值再写入x10
  • x10入栈sp + #0x8的位置,此时x9成为指针,指向x10

使用x/8g 0x1021e8000,查看内存中的数据

0x1021e8000: 0x00000001f0a50830 0x000000019b84ef94
0x1021e8010: 0x0000000000000000 0x0000000000000024
0x1021e8020: 0x00000001021e6a26 0x00000001021e6ad2
0x1021e8030: 0x00000001f101a280 0x00000000000007d0

使用po 0x00000001f0a50830命令,打印第一个8字节数据

__NSStackBlock__
  • 存储的是一个StackBlock
  • 在内部使⽤局部变量或者OC属性,但是不能赋值给强引⽤或者
    Copy修饰的变量,它位于栈区Block

标记的第2段代码

   0x1021e62f8 <+56>:  adrp   x10, 0
   0x1021e62fc <+60>:  add    x10, x10, #0x358          ; =0x358
  • 使用adrp + add指令,计算地址为0x1021e6358,赋值x10
  • 此处疑似是Blockinvoke

使用dis -s 0x1021e6358指令,查看invoke的汇编代码

002--OC方法的本质`__29-[ViewController viewDidLoad]_block_invoke:
   0x1021e6358 <+0>:  sub    sp, sp, #0x30             ; =0x30 
   0x1021e635c <+4>:  stp    x29, x30, [sp, #0x20]
   0x1021e6360 <+8>:  add    x29, sp, #0x20            ; =0x20 
   0x1021e6364 <+12>: stur   x0, [x29, #-0x8]
   0x1021e6368 <+16>: str    x0, [sp, #0x10]
   0x1021e636c <+20>: ldr    w8, [x0, #0x20]
   0x1021e6370 <+24>: mov    x0, x8
   0x1021e6374 <+28>: adrp   x9, 2

Hopper中,查看StackBlock

使用真机编译项目,将Mach-O文件拖到Hopper中,找到viewDidLoad函数

  • Hopper直接标记出invoke的位置
  • invoke下面是descriptor

双击_block_invoke,找到invoke中的代码逻辑

                    ___29-[ViewController viewDidLoad]_block_invoke:
0000000100006358         sub        sp, sp, #0x30                ; DATA XREF=-[ViewController viewDidLoad]+60
000000010000635c         stp        x29, x30, [sp, #0x20]
0000000100006360         add        x29, sp, #0x20
0000000100006364         stur       x0, [x29, #-0x8]
0000000100006368         str        x0, [sp, #0x10]
000000010000636c         ldr        w8, [x0, #0x20]
0000000100006370         mov        x0, x8
0000000100006374         adrp       x9, #0x100008000
0000000100006378         add        x9, x9, #0x30                ; cfstring_b
000000010000637c         str        x0, [sp, #0x8]
0000000100006380         mov        x0, x9
0000000100006384         mov        x9, sp
0000000100006388         ldr        x10, [sp, #0x8]
000000010000638c         str        x10, x9
0000000100006390         bl         imp___stubs__NSLog
0000000100006394         ldp        x29, x30, [sp, #0x20]
0000000100006398         add        sp, sp, #0x30
000000010000639c         ret
                       ; endp

通过汇编代码和invoke的位置来看,StackBlock案例1中的GlobalBlock区别很大

总结

OC的反汇编

  • 出现objc_msgSend,表示调用了一个OC方法,读取x0x1便可知道调用了哪个对象的哪个方法
  • 初始化的allocinit函数,在iOS12.2及更高版本,优化为objc_alloc_init函数
  • objc_storeStrong函数,对一个strong修饰的对象retain,对老对象release。有些情况下,也会直接释放对象

工具反汇编

  • 运用Hopper工具,查看汇编代码、流程图、伪码模式
  • Hopper可以解析出方法、属性等名称,作为注释标记给开发者,本质上也是通过Mach-O读取相应数据

Block的反汇编

  • Block的类型:全局区、堆区、栈区。重点掌握全局区和栈区Block的分析
  • 在逆向分析中,需要定位Blockinvoke。只有找到invoke,才能找到回调方法中的代码逻辑

了解更多汇编指令

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容