iOS 底层面试

iOS逆向:函数本质(上)

2021-05-04  本文已影响0人  iOS鑫

本文的主要目的是理解函数栈以及涉及的相关指令

在讲函数的本质之前,首先需要讲下以下几个概念栈、SP、FP

常识

- 栈空间开辟:往低地址开辟(`开辟:高-->低`)
复制代码

SP和FP寄存器

注意:

  • arm64开始,取消了32位的LDM、STM、PUSH、POP指令,取而代之的是 ldr/ldp、str/stp(r和p的区别在于处理的寄存器个数,r表示处理1个寄存器,p表示处理两个寄存器)
  • arm64中,对栈的操作是16字节对齐的!!!

以下是arm64之前和arm64之后的一个对比 !


函数调用栈

以下是常见的函数调用开辟 (sub)以及恢复栈空间 (add)的汇编代码

//开辟栈空间
sub    sp, sp, #0x40             ; 拉伸0x40(64字节)空间
stp    x29, x30, [sp, #0x30]     ;x29\x30 寄存器入栈保护
add    x29, sp, #0x30            ; x29指向栈帧的底部
... 
ldp    x29, x30, [sp, #0x30]     ;恢复x29/x30 寄存器的值
//恢复栈空间
add    sp, sp, #0x40             ; 栈平衡
ret

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130595548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

内存读写指令

注意:

  • 读/写数据都是往高地址读/写
  • 写数据:先拉伸栈空间,再拿sp进行写数据,即先申请空间再写数据

练习

使用32个字节空间作为这段程序的栈空间,然后利用栈将x0和x1的值进行交换

sub sp, sp, #0x20       ;拉伸栈空间32个字节
stp x0, x1, [sp, #0x10] ;sp往上加16个字节,存放x0和x1
ldp x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来,放入x1和x0,内存是temp(寄存器里面的值进行交换了)
add sp, sp, #0x20       ;栈平衡
ret                     ;返回

栈的操作如下图所示 !


调试查看栈

调试查看栈-01 调试查看栈-02 调试查看栈-03 ) 查看内存变化,发现sp拉伸了32字节 调试查看栈-04 调试查看栈-05 调试查看栈-06 此时sp的值并没有变化,还是指向40 调试查看栈-07 调试查看栈-08

疑问:再来看sp是否有变化? 从结果来看,也没有变化。所以这里只是读出来进行的交换,并不会导致内存变化

调试查看栈-09 调试查看栈-10

疑问:栈空间不断开辟,死循环,会不会崩溃?

在这里我们将会处理上篇(逆向初识汇编)文章中文末遗留的问题

下面我们通过一个汇编代码来演示

<!--asm.s-->
.text
.global _B

_B:
    sub sp,sp,#0x20
    stp x0,x1,[sp,#0x10]
    ldp x1,x0,[sp,#0x10];寄存器里面的值进行交换
    bl _B
    add sp,sp,#0x20
    ret

<!--调用-->
int B();

int main(int argc, char * argv[]) {
    B();
}

运行结果发现:死循环会崩溃,会导致堆栈溢出

死循环崩溃图示

bl 、ret指令

bl图示

等到B函数ret时,通过lr获取回家的路(注:lr就是保存回家的路)

练习

下面通过汇编代码来演示bl、ret指令

.text
.global _A, _B

_A:
    mov x0\. #0xaaaa
    bl _B
    mov x0, #0xaaaa
    ret

_B:
    mov x0, #0xbbbb
    ret
演示bl、ret指令-01

疑问:发现A和print之间你还有几个汇编操作,这个是什么意思呢?

演示bl、ret指令-02 演示bl、ret指令-03 演示bl、ret指令-04 演示bl、ret指令-05 演示bl、ret指令-06 演示bl、ret指令-07 演示bl、ret指令-08

走到这里,发现死循环了,主要是因为lr一直是5eb8,ret只会看lr 。其中pc是指接下来要执行的内存地址,ret是指让CPU将lr作为接下来执行的地址(相当于将lr赋值给pc)

演示bl、ret指令-09

疑问1:此时B回到A没问题,那么A回到viewDidload怎么回呢?

系统中函数嵌套是如何返回? 下面我们来看下系统是如何操作的,例如:d -> c -> viewDidLoad

void d(){
}
void c(){
    d();
    return;
}
- (void)viewDidLoad{
    [super viewDidLoad];
    printf("A");
    c();
    printf("B");
}
函数嵌套调试-01 函数嵌套调试-02
- `lsp x29,x30,[sp],#0x10`:读取sp指向地址的数据,放入x29、x30,然后`,,#0x10`表示将sp+0x10,赋值给sp
复制代码
函数嵌套调试-03

自定义汇编代码完善:_A中保存回家的路 所以根据系统的函数嵌套操作,最终在_A中增加了如下汇编代码,用于保存回家的路

<!--导致死循环的汇编代码-->
_A:
    mov x0\. #0xaaaa
    bl _B
    mov x0, #0xaaaa
    ret

<!--增加lr保存:可以找到回家的路-->
_A:
    sub sp, sp, #0x10  //拉伸
    str x30, [sp]     //存
    mov x0, #0xaaaa
    //保护lr寄存器,存储到栈区域
    bl _B
    mov x0, #0xaaa
    ldr x30, [sp]      //修改lr,用于A找到回家的路
    add sp, sp, #0x10 //栈平衡
    ret
复制代码

修改_A、_B:改成简写形式

_A:
    sub sp, sp, #0x10  //拉伸
    str x30, [sp]     //存
    mov x0, #0xaaaa
    //保护lr寄存器,存储到栈区域
    bl _B
    mov x0, #0xaaa
    ldr x30, [sp]      //修改lr,用于A找到回家的路
    add sp, sp, #0x10 //栈平衡
    ret

_B:
    mov x0, #0xbbbb
    ret

<!--改成简写形式-->
_A:
    //sub sp, sp, #0x10  //拉伸
    //str x30, [sp]     //存
    str x30, [sp, #-0x10]
    mov x0, #0xaaaa
    //保护lr寄存器,存储到栈区域
    bl _B
    mov x0, #0xaaa
    //ldr x30, [sp]      //修改lr,用于A找到回家的路
    //add sp, sp, #0x10 //栈平衡
    ldr x30, [sp], #0x10 //将sp的值读取出来,给到x30,然后sp += 0x10
    ret

_B:
    mov x0, #0xbbbb
    ret
复制代码

断点调试

函数嵌套调试-04 函数嵌套调试-05

查看0x16f5a1c50的memory,此时放入的是lr的值 861f2c,即ViewDidLoad中的bl下一条指令的地址,目前只放了8个字节(1个寄存器)

函数嵌套调试-06 函数嵌套调试-07 函数嵌套调试-08 函数嵌套调试-09 函数嵌套调试-10

发现此时sp也变了,从0x16f5a1c50->0x16f5a1c60。从这里可以看出,A找到了回家的路

函数嵌套调试-11

疑问:为什么是拉伸16字节,而不是8字节呢? 通过手动尝试,有以下说明:

函数嵌套调试-12

x30寄存器

总结

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130595548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

上一篇 下一篇

猜你喜欢

热点阅读