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

iOS 底层原理 + 逆向 文章汇总

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

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

常识

  • 栈:是一种具有特殊的访问方式的存储空间(即先进后出 Last In First Out, LIFO
    入栈出栈图示
    • 高地址往低地址存数据(存:高-->低

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

SP和FP寄存器

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

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

注意:

  • 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)的汇编代码

//开辟栈空间
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

内存读写指令

  • str(store register)指令(能和内存和寄存器交互的专门的指令):将数据从寄存器中读出来,存到内存中 (即一个寄存器是8字节-64位

  • ldr(load register)指令:将数据从内存中读出来,存到寄存器中

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

注意:

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

  • 写数据:先拉伸栈空间,再拿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                     ;返回

栈的操作如下图所示


栈的操作

调试查看栈

  • 重写x0、x1的值


    调试查看栈-01
  • register read sp【查看栈的存储情况:debug - debug workflow - view Memory

    调试查看栈-02

  • 然后单步往下执行,发现x0、x1已经变成我们写入的值


    调试查看栈-03

    )
    查看内存变化,发现sp拉伸了32字节


    调试查看栈-04
  • stp x0, x1, [sp, #0x10]:将x0、x1写入fp偏移0x10的位置,继续往下执行一步

    调试查看栈-05

    调试查看栈-06

    此时sp的值并没有变化,还是指向40
    调试查看栈-07

  • ldp x1, x0, [sp, #0x10]:读取x0,x1的数据并交换,继续往下执行一步,此时内存并没有变化

    调试查看栈-08

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

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

    调试查看栈-10

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

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

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

<!--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指令

  • b 标号 :跳转

  • bl标号

    • 将下一条指令的地址放入lr(x30)寄存器(lr保存的是回家的路)(即l)
    • 转到标号处执行指令(即b)


      bl图示

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

  • ret

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

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

练习

下面通过汇编代码来演示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

  • 执行mov x0. #0xaaaa:x0变成aaaa,此时此刻lr寄存器保存的是5f34

    演示bl、ret指令-03

  • 验证lr是否保存的是5f34,通过查看寄存器发现结果与预期是一致的

    演示bl、ret指令-04

  • 继续执行bl _B,跳转到B,此时的lr会变成A中bl的下一条指令的地址5eb8

    演示bl、ret指令-05

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

    演示bl、ret指令-06

  • 执行B中的ret,会回到A中5eb8

    演示bl、ret指令-07

  • 继续执行A中的ret,会再次回到5eb8

    演示bl、ret指令-08

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

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

  • 需要在A的bl之前保护lr寄存器
    • 疑问2:是否可以保存到其他寄存器上?
      答案是不可以,原因是不安全,因为你不确定这个寄存器会在什么时候被别人使用
    • 正确做法:保存到栈区域

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

void d(){
}
void c(){
    d();
    return;
}
- (void)viewDidLoad{
    [super viewDidLoad];
    printf("A");
    c();
    printf("B");
}
  • 查看汇编,断点断在c函数


    函数嵌套调试-01
  • 进入c函数的汇编


    函数嵌套调试-02
    • stp x29,x30,[sp,#-0x10]!:边开辟栈,边写入,其中 x29就是fp,x30是lr!表示将这里算出来的结果,赋值给sp

    • lsp x29,x30,[sp],#0x10:读取sp指向地址的数据,放入x29、x30,然后,,#0x10表示将sp+0x10,赋值给sp

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

    函数嵌套调试-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:改成简写形式

  • 其中lrx30的一个别名
_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

断点调试

  • 查看此时sp寄存器的地址


    函数嵌套调试-04
  • 执行str x30, [sp, #-0x10],继续查看sp,发现sp变化了,但是此时lr没变

    函数嵌套调试-05

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

  • 执行A中的mov x0, #0xaaaa:x0变成aaaa

    函数嵌套调试-07

  • 执行A中的bl _B,跳转到B,此时lr变成 1e94,x0变成bbbb

    函数嵌套调试-08

  • 执行B的ret:从B回到A,此时lr还是 1e94

    函数嵌套调试-09

  • 执行A中的ldr x30, [sp], #0x10

    函数嵌套调试-10

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

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

  • 写入没问题

  • 读取时会崩溃:因为sp中,对栈的操作必须是16字节对齐的,所以会在做栈的操作时就会崩溃

    函数嵌套调试-12

x30寄存器

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

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

  • lr是x30的别名

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

总结

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

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

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

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

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

  • 汇编练习

    • 指令
      • sub sp,sp,$0x10 ;拉伸栈空间18字节

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

    • 简写
      • str x0,x1,[sp,$-0x10]!(!就是将[]里面的结果赋值给sp)
  • bl指令

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

    • B代表着跳转

    • L表示lr(x30)寄存ios_reverse_02器

  • ret指令

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

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

推荐阅读更多精彩内容