OC逆向02:函数本质(上)

张建 lol

前言

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

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

常识

  • 栈:是一种 具有特殊的访问方式的存储空间(即 后进先出 Last In First Out,LIFO

  • 高地址往低地址存数据(存:高-->低

  • 栈空间开辟:往低地址开辟(开辟:高-->低

SP和FP寄存器

  • SP寄存器:在任意时刻会 保存我们栈顶的地址

  • FP寄存器(也称为 x29 寄存器):属于通用寄存器,但是在某些时刻(例如函数嵌套调用时)可以 利用它保存栈底的地址

1
2
3
4
注意:

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

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

  • 在arm64之前,栈顶指针是压栈时一个数据移动一个单元
  • 在arm64开始,首先是 从高地址往低地址开辟一段空间(由编译器决定),然后再放入数据,所以不存在push、pop操作。这种情况可以通过内存读写指令(ldr/ldp、str/stp)对其进行操作

函数调用栈

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

1
2
3
4
5
6
7
8
9
// 开辟栈空间
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

内存读写指令

注意:

  • 读/写数据 都是往 高地址 读/写

  • 写数据:先拉伸栈空间,在拿sp进行写数据,即 先申请空间再写数据

str(store register)指令:

  • 将数据从寄存器中读出来,存到 内存 中(即一个寄存器是 8字节=64位

ldr(load register)指令:

  • 将数据从内存中读出来,存到 寄存器

  • 此时 ldrstr 的变种 ldp和stp 还可以操作 2个 寄存器(即 16字节=128位

堆栈练习

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

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

栈的操作如下图所示:

调式查看栈

  • 在 asm文件中写入如下代码:
1
2
3
4
5
6
7
8
9
.text
.global _A,_B

_B:
sub sp, sp, #0x20
stp x0, x1, [sp, #0x10]
ldp x1, x0, [sp, #0x10]
add sp, sp, #0x20
ret
  • 在 VC 中调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface ViewController ()

@end

@implementation ViewController

// 函数的声明
int B();

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

}
  • 断点到 sub sp, sp, #0x20: 进行调试,如下图

由上图可知,sp的初始地址是 0x000000016f491a80,正常由高地址往低地址 拉伸(开辟) #0x20个地址后,sp的最新地址为 0x000000016f491a80 - 0x20 = 0x000000016f491a60

  • 按住 control Step into,往下走一步查看结果

  • 此时 x0,x1 的默认地址如下图

  • stp x0, x1, [sp, #0x10]:将 sp 的偏移 #0x10 写入 x0,x1,结果如下图:

  • ldp x1, x0, [sp, #0x10]:将 sp 的内存地址加上 #0x10,读取 x0,x1 的数据并交换结果如下;

  • add sp, sp, #0x20:走到栈平衡,最后将 sp 指针地址加上 #0x20,即 sp恢复了,此时的a和b仍然在内存中,等待着下一轮栈拉伸后数据的写入覆盖。如果此时读取,读取到的是垃圾数据

  • 如果上面默认的地址不方便看,你可以重写 register write x0 0xaregister write x1 0xb 的来断点调试,如下图:

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

在这里我们将会处理上篇 OC逆向01:初始汇编 文章中文末遗留的问题

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--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:转到标号处执行指令(即b)
  • 下一条指令的地址 放入通用寄存器 lr(x30)

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

ret

  • 默认使用 lr(x30)寄存器 的值,通过底层指令提示CPU此处作为下条指令地址

  • arm64平台的特色指令,它面向硬件做了优化处理的

练习

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

  • asm文件下代码如下
1
2
3
4
5
6
7
8
9
10
11
12
.text
.global _A, _B

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

_B:
mov x0, #0xbbbb
ret
  • vc中执行如下代码
1
2
3
4
5
6
7
8
9
10
11
// 函数的声明
int A();
int B();

- (void)viewDidLoad {
[super viewDidLoad];
printf(@"A");
A();
printf(@"B");
}

  • 断点 mov x0,#0xaaaa 运行:发现 lr 保存了下一条指令的地址是 1f78

  • 往下执行 bl _B :此时 x0 的地址为 0xaaaa

  • 继续执行 bl _B,跳转到 B,此时的 lr 会变成 Abl 的下一条指令的地址 1f20

  • 执行完B中的 mov x0, #0xbbbb,x0 变成 bbbb

  • 执行 B 中的 retlr 会回到 A1f20

  • 继续执行 A 中的 ret,会再次回到 1f20

  • 走到这里,发现 死循环了,主要是因为 lr 一直是 1f20ret 只会看 lr

    • 其中 pc 是指 接下来要执行的内存地址ret 是指让CPU将 lr 作为接下来执行的地址(相当于将lr赋值给pc)

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

  • 需要在A的bl之前 保护lr寄存器

    • 疑问2:是否可以保存到其他寄存器上?

      • 答案是 不可以,原因是不安全,因为你不确定这个寄存器会在什么时候被别人使用

      • 正确做法:lr保存到栈区

系统中函数嵌套是如何返回的?

下面我们来看一下系统是如何操作的,例如:d -> c -> viewDidLoad

1
2
3
4
5
6
7
8
9
10
11
12
void d(){
}
void c(){
d();
return;
}
- (void)viewDidLoad{
[super viewDidLoad];
printf("A");
c();
printf("B");
}
  • 查看汇编,断点在 c() 函数

  • stp x29,x30,[sp,#-0x10]:边开辟栈,边写入,其中 x29就是fp(栈底)x30是lr! 的含义是,将[]中计算的结果给sp,即 sp-=0x10

  • ldp x29, x30, [sp], #0x10:读取sp执行地址的数据,放入x29、x30;[sp], #0x10 表示 sp=sp+0x10,以此保证 栈平衡

【结论】:当有函数嵌套调用时,将上一个函数的地址通过 lr(即x30) 放在栈中保存,保证可以找到回家的路,如下图所示

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--导致死循环的汇编代码-->
_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:改成简写形式

  • 其中lr是x30的一个别名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
_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] // 存
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

【断点调试】:

  • str x30, [sp, #-0x10]:查看此时 sp寄存器 的地址 sp = 0x000000016b411a80lr 的地址 lr = 0x00000001003d9f90

  • 执行到 mov x0, #0xaaaa:sp变了,lr未变

  • 执行 mov x0, #0xaaaa,x0 为 aaaa

  • 执行A中的 bl _B:跳转到B,此时 lr = 0x0000000100929f40x0 变成 bbbbsp = 0x000000016f4d9a70

  • 执行B的ret:从B回到A,此时lr还是 lr = 0x0000000100929f40

  • 执行A中的 ldr x30, [sp], #0x10,发现此时 lr变了为 lr = 0x0000000100929f90,sp也变了为 sp = 0x000000016f4d9a80。从这里可以看出,A找到了回家的路

x30寄存器

  • x30 寄存器 存放 的是 函数的返回地址,当ret指令执行时刻,会寻找x30寄存器保存的地址值

  • 注意:在函数嵌套调用时,需要将x30入栈

  • lrx30 的别名

  • sp 栈里面的操作必须是 16字节对齐,崩溃是在栈的操作时挂的

总结

  • :是一种具有特殊的访问方式的存储空间(先进后出,Last In First Out,FIFO

    • ARM64里面对 栈的操作16字节对齐
  • SP和FP寄存器

    • SP寄存器在任意时刻会 保存栈顶的地址
    • FP寄存器也称为 x29寄存器,属于 通用寄存器,但是在某些时刻利用它 保存栈底的地址
  • 栈的读写指令

    • 读:ldr(load register)指令 LDR、LDP

    • 写:str(store register)指令 STR、STP

  • 汇编练习

    • 指令

      • sub sp,sp,$0x10:拉伸栈空间 16 字节

      • stp x0,x1,[sp]:sp所在位置存放x0、x1

  • bl 指令

    • 跳转指令:bl 标号,表示程序执行到标号处。将下一条指令的地址保存到lr寄存器

    • B 代表着 跳转

    • L 表示 lr(x30)寄存器

  • ret 指令

    • 类似函数的 return
    • 让CPU执行lr寄存器所指向的指令
  • 避免嵌套函数无法回去:需要保护bl(即 lr寄存器,存放回家的路 ),保存在当前函数自己的栈空间

  • Post title:OC逆向02:函数本质(上)
  • Post author:张建
  • Create time:2022-04-20 15:51:12
  • Post link:https://redefine.ohevan.com/2022/04/20/OC逆向/OC逆向02:函数本质(上)/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.