iOS逆向学习-005汇编中block及OC方法调用

2,814 阅读6分钟

OC方法调用在汇编中的展示

项目中新建一个Person类:

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;

+ (instancetype)person;

@end

@implementation Person

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

@end

我们直接在main函数里调用工厂方法person,断点看下调用方法的汇编

Demo`main:
    ...
    0x1025b6038 <+24>:  adrp   x8, 7
    0x1025b603c <+28>:  add    x8, x8, #0x3c8            ; =0x3c8 
->  0x1025b6040 <+32>:  ldr    x0, [x8]
    0x1025b6044 <+36>:  adrp   x8, 7
    0x1025b6048 <+40>:  add    x8, x8, #0x3c0            ; =0x3c0 
    0x1025b604c <+44>:  ldr    x1, [x8]
    0x1025b6050 <+48>:  bl     0x1025b63a0               ; symbol stub for: objc_msgSend
    ...

我们知道OC调用方法的本质就是消息发送,所以我们看到汇编里调用的objc_msgSend方法。objc_msgSend方法有两个参数,分别是idSEL类型,所以我们查下寄存器,这两个值分别是什么:

image.png

我们通过动态调试,很容易就能发现是Person调用了person方法。但如果静态分析 的话,只能自己算地址,然后找到对应的调用对象和方法名,不过好在有很多静态分析的工具。目前推荐两个Hopper Disassembler v4IDAIDA非常贵,买断的话要1w块钱,Hopper可以试用,如果只是研习的话,还是用Hopper吧。

我们看下Hopper的试用:

image.png

工具自动帮我们标明了adrp到的地址到底指的是什么。

allocinit的优化

我们跳转到刚才person方法的函数内部:

image.png

我们发现allocinit的方法并不是想象中的消息发送,而是直接优化成了一个objc_alloc_init的函数,这个是在iOS 11以后才优化的,以前还是调用的objc_msgSend

block在汇编中的展示

我们知道block有3种:

  • NSGlobalBlock:放在常量区的代码块。如果block不包含外部变量(不包括全局变量和静态变量,因为全局变量和静态变量在编译时期就能确定相对固定的地址),那么会生成NSGlobalBlock

  • NSStackBlock:放在栈区的代码块。如果block包含外部变量,那么会生成NSStackBlock

  • NSMallocBlock:放在堆区的代码块。如果NSStackBlock调用copy函数,那么会将NSStackBlock拷贝到堆区,生成NSMallocBlock。在ARC环境下,只要NSStackBlock被强引用了,那么编译器自动帮你拷贝到了堆区,所以我们平常使用的block大多数都是NSMallocBlock

我们看下从源码中看到block的结构:

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

typedef void(*BlockInvokeFunction)(void *, ...);

接下来我们从汇编的角度探索下3种block,以及逆向比较关注的如何找到block的实现,也就是上方结构中的block

NSGlobalBlock汇编构成

我们写一个简单的block

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

我们动态调试一下:

image.png

我们可以看到,这个block是一个__NSGlobalBlock__

接下来我们找下block的实现,根据block的结构看,invoke在内存中处于17-24字节处,我们打印下内存看下:

image.png

我们反汇编找到了NSlog打印,说明我们没有找错地方。

我们同样用汇编工具看下Hopper看下:

image.png

工具直接帮我们标记出来是字面量(常量)block,我们可以直接点注释跳转过去:

image.png

我们根据block的结构,找到对应的实现invoke,再次双击进去看下:

image.png

我们可以找到block的实现,打印了block字符串。

NSStackBlock汇编构成

我们给刚才的block里添加一个局部变量:

int a = 10;
    void(^block)(void) = ^(void) {
        NSLog(@"block--%d", a);
    };

我们动态调试下汇编:

image.png

我们看到adrp的3次,分别为blockisainvokedescriptor,然后 str到了栈上,换而言之,汇编代码在栈上生成了一个block。我们打印了isa所在的地址(sp寄存器 + 0x8),内存内容正好能和block的结构对上,而且从isa中也可以看出,这个block__NSStackBlock__

我们瞟一眼反汇编工具Hopper

image.png

我们可以直接拿到block的实现,所以如果我们实操反汇编别人的APP,那么直接从这边跳转看block的实现就行了,不用看block的完整结构了。

我们对比下前面的NSGlobalBlockNSGlobalBlock在编译完直接存放在可执行文件内的,所以我们拿该block,直接在常量区拿block的首地址就行了。而NSStackBlock类型的block需要在栈上生成,但是block结构中的有些变量的值也是存放在可执行文件内的。

NSMallocBlock汇编构成

我们继续跑刚才的汇编代码,会发现一个函数objc_retainBlock: image.png

我分别打印了这个函数的参数与返回值,参数根据地址值知道就是刚才的NSStackBlock,而函数的返回值得到了NSMallocBlock,而且我们看到他们的函数实现invoke地址是一样的。

这里之所以调用了objc_retainBlock,是因为被栈引用了一下,我们可以看到stur x0, [x29, #-0x20],把返回值NSMallocBlock的首地址存到了栈上,所以基本上我们平时写代码拿到的block都是NSMallocBlock

我们看下源码中函数objc_retainBlock到底干了点什么:

//
// The -fobjc-arc flag causes the compiler to issue calls to objc_{retain/release/autorelease/retain_block}
//

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

objc_retainBlock中调用了_Block_copy函数,但我们看不到底层_Block_copy做了什么。根据注释,我们了解到_Block_copy是创建一个基于堆的块的副本或简单地添加一个引用到现有的一个,而且必须要和Block_release成对使用。

但是我们在汇编代码中并没有看到Block_release的调用,那么是如何销毁堆上的block的呢?其实我们可以看到汇编代码最后调用了objc_storeStrong,我们看下这个方法干了什么:

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

这个函数把新的对象obj赋给变量*location,并且obj引用计数加一(objc_retain),而老的对象prev引用计数减一(objc_release),而刚才汇编代码中新的变量obj是:

0x100b71ffc <+116>: mov    x9, #0x0
0x100b72000 <+120>: mov    x1, x9

#0x0nil,新的objnil,从而达到了block的销毁。

block在逆向中的实用

判断一个block的参数及返回值,可以查看block的签名,如何查看签名,还是看block的结构:

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

我们可以看到签名signature,在结构体Block_descriptor_3中,Block_descriptor_1Block_descriptor_2Block_descriptor_3在内存中是紧紧靠在一起的,所以找到Block_descriptor_1就能拿到Block_descriptor_3,而Block_descriptor_1Block_layout是一个指针,寻址过去就能拿到了。

拿到了signature后,我们可以靠lldb分析:

po [NSMethodSignature signatureWithObjCTypes:"v16@?0@8"]

最后可以查看官网的Type Encodings来看具体是什么类型的参数或者返回值