ARM汇编逆向iOS 实战

我们先讲一些ARM汇编的基础知识。(我们以ARMV7为例,最新iPhone5s上的64位暂不讨论)

基础知识部分:

首先你介绍一下寄存器:

R0-R3:用于函数参数及返回值的传递

R4-R6, R8,R10-R11:没有特殊规定,就是普通的通用寄存器

R7:栈帧指针(Frame Pointer).指向前一个保存的栈帧(stack frame)和链接寄存器(link register, lr)在栈上的地址。

R9:操作系统保留

R12:又叫IP(intra-procedure scratch), 要说清楚要费点笔墨,后续再详细介绍

R13:又叫SP(stack pointer),是栈顶指针

R14:又叫LR(link register),存放函数的返回地址。

R15:又叫PC(program counter),指向当前指令地址。

CPSR:当前程序状态寄存器(Current Program State Register),在用户状态下存放像condition标志中断禁用等标志的。

   在其它系统状态中断状等状态下与CPSR对应还有一个SPSR,在这里不详述了。

另外还有VFP(向量浮点运算)相关的寄存器,在此我们略过,感兴趣的可以从后面的参考链接去查看。

基本的指令:

add 加指令

sub 减指令

str 把寄存器内容存到栈上去

ldr 把栈上内容载入一寄存器中

.w是一个可选的指令宽度说明符。它不会影响为此指令的行为,它只是确保生成 32 位指令。Infocenter.arm.com的详细信息

bl 执行函数调用,并把使lr指向调用者(caller)的下一条指令,即函数的返回地址

blx 同上,但是在ARM和thumb指令集间切换。

bx bx lr返回调用函数(caller)。

接下来是函数调用的一些规则。

一. 在iOS中你需要使用BLX,BX这些指令来调用函数,不能使用MOV指令(具体意义下面会说)

二. ARM使用一个栈来来维护函数的调用及返回。ARM中栈是向下生长(由高地址向低地址生长的)。

函数调用前后栈的布局如图一(引用的苹果iOS ABI Reference):

              图(一)

SP(stack pointer)指向栈顶(栈低在高地址)。栈帧(stack frame)其实就是通过R7及存在栈上的旧R7来标识的栈上的一块一块的存储空间。栈帧包括:

参数区域(parameter area),存放调用函数传递的参数。对于32位ARM,前4个参数通过r0-r3传递,多余的参数通过栈来传递,就是存放在这个区域的。

链接区域(linkage area),存放调用者(caller)的下一条指令。

栈帧指针存放区域(saved frame pointer),存放调用函数的栈帧的底部,标识着调用者(caller)栈帧的结束及被调用函数(callee)的栈帧开始。

局部变量存储区(local storage area)。用于存被调函数(callee)的局部变量及在被调用函数(callee)结束后反回调用函数(call)之前需要恢复的寄存器内容。

寄存器存储区(saved registers area)。Apple的文档中是这样说的。但我认为这个区域和local storage area相邻且干的事也是存放需要恢复的寄存器内容,因此我觉得要不就把这个区域在概念上不区分出来,要不就把存放需要恢复的寄存器这项功能从local storage area中分出来。 当然这些都只是概念上的,其实实质上是没有区别的。

接下来看看在调用子函数开始及结尾时所要做的事情。(官方叫序言和结语, prologs and epilogs)

调用开始:

LR入栈R7入栈R7 = SP地址。在经过前面两条入栈指令后,SP指向的地址向下移动,再把SP赋值给R7, 标志着caller栈帧的结束及callee的栈帧的开始将callee会修改且在返回caller时需要恢复的寄存器入栈。分配栈空间给子程序使用。由于栈是从高地址向低地址生长,所以通常使用sub sp, #size来分配。

调用结尾:

释放栈空间。add sp, #size指令。恢复所保存的寄存器。恢复R7将之前存放的LR从栈上弹出到PC,这样函数就返回了。

-----------------------------------------------------------华丽的分割线-------------------------------------------------------------

实战部分(一):

用XCode创建一个Test工程,新建一个.c文件,添加如下函数:

#include <stdio.h>

int func(int a, int b, int c, int d, int e, int f)
{
  int g = a + b + c + d + e + f;
  return g;
}

查看汇编语言:

在XCode左上角选中targe 在真机下编译,这样产生的才是ARM汇编,不然在模拟器下生成的是x86汇编。

点击 XCode => Product => Perform Action => Assemble file.c 生成汇编代码。

代码很多,有很多"."开头的".section", ".loc"等,这些是汇编器需要的,我们不用去管。把这些"."开头的及注释增掉后,代码如下:

_func:
  .cfi_startproc
Lfunc_begin0:
  add r0, r1
Ltmp0:
  ldr.w  r12, [sp]
  add r0, r2
  ldr.w  r9, [sp, #4]
  add r0, r3
  add r0, r12
  add r0, r9
  bx lr
Ltmp2:
Lfunc_end0:

_func:表示接下来是func函数的内容。Lfunc_begin0及Lfunc_end0标识函数定义的起止。函数起止一般是"xxx_beginx:"及"xxx_endx:"

下面来一行行代码解释:

add r0, r1 将参数a和参数b相加再把结果赋值给r0ldr.w r12, [sp] 把最的一个参数f从栈上装载到r12寄存器add r0, r2 把参数c累加到r0上ldr.w r9, [sp, #4] 把参数e从栈上装载到r9寄存器add r0, r3 累加d累加到r0add r0, r12 累加参数f到r0add r0, r9 累加参数e到r0

至此,全部的a到f 共6个值全部累加到r0寄存器上。前面说了r0是存放返回值的。

bx lr: 返回调用函数。

-----------------------------------------------------------华丽的分割线-------------------------------------------------------------

实战部分(二):

为了让大家看清楚函数调用时栈上的变化,下面以一个有三个函数,两个调用的C代码的汇编代码为例讲解一下。

上代码:

#include <stdio.h>

__attribute__((noinline))
int addFunction(int a, int b, int c, int d, int e, int f) {
  int r = a + b + c + d + e + f;
  return r;
}

__attribute__((noinline))
int fooFunction(int a, int b, int c, int d, int f) {
  int r = addFunction(a, b, c, d, f, 66);
  return r;
}

int initFunction()
{
  int r = fooFunction(11, 22, 33, 44, 55);
  return r;
}

由于我们是要看函数调用及栈的变化的,所以在这里我们加上__attribute__((noinline))防止编译器把函数内联(如果你不懂内联,请google之)。

在XCode左上角选中targe 在真机下编译,这样产生的才是ARM汇编,不然在模拟器下生成的是x86汇编。

点击 XCode => Product => Perform Action => Assemble file.c 生成汇编代码, 如下:

为了能更符合我们人的思考方式,我们从调用函数讲起。

initFunction:

_initFunction:
  .cfi_startproc
Lfunc_begin2:
@ BB#0:
  push  {r7, lr}
  mov r7, sp
  sub sp, #4
  movs  r0, #55
  movs  r1, #22
Ltmp6:
  str r0, [sp]
  movs  r0, #11
  movs  r2, #33
  movs  r3, #44
  bl _fooFunction
  add sp, #4
  pop {r7, pc}
Ltmp7:
Lfunc_end2:

还是一行行的解释:

1.push {r7, lr} 就是前面基础知识部分说的函数调用的序言(prologs)部分的1, 2两条,将lr, r7 存到栈上去

2.mov r7, sp 序言(prolog)之3。

3.sub sp, #4 在栈上分配一个4字节空间用来存放局部变量, 即参数。前面我们说过,r0-r3可以传递4个参数,但超过的只能通过栈来传递。

4.movs r0, #55 把立即数55存入r0

5.movs r1, #22 把22存入r1

6.str r0, [sp] 把r0的值存入栈指针sp指向的内存。即栈上存了参数55

7.接下来三条指令moves r0, #11 moves r2, #33 moves r3, #44 把相应的立即数存入指定的寄存器。 到目前为止,r0-r3分别存放了11, 22, 33,44共4个立即数参数,栈上存放了55这一个参数。

8.bl _fooFunction 调用fooFunction, 调用后跳转到fooFunction中的情况下面再分析。

9.add sp, #4 栈指针向上移动4个字节,回收第3个指令sub sp, #4分配的空间。

10.pop {r7, pc} 恢复第一条指令push {r7, lr}到栈中的值, 把之前的lr值赋给pc。注意:在进入initFunction的时候lr是调用initFunction的函数的下一条指令,所以现在把当时的lr中的值赋给pc程序计数器,这样执行lr指向的这一条指令,函数就反回了。

指令1,2, 3是函数序言(prologs),指令9, 10是结语(epilogs)。这基本上是一个套路,看多了自然就知道了,都不用停下来一条条分析。

为了方便和栈的变化联系起来,我们画出指令8, bl __fooFunction时的栈布局如图二:

          图(二)

在上面的initFunction调用第8条指令bl _fooFunction之后,进入fooFunction, 其它汇编如下:

fooFunction:

_fooFunction:
  .cfi_startproc
Lfunc_begin1:
  push  {r4, r5, r7, lr}
  add r7, sp, #8
  sub sp, #8
  ldr r4, [r7, #8]
  movs  r5, #66
  strd  r4, r5, [sp]
  bl _addFunction
  add sp, #8
  pop {r4, r5, r7, pc}
Lfunc_end1:

一样,我们一行行来看:

1.push {r4, r5, r7, lr}             你应该发现了,这次和initFunction不同,除了lr和r7也把r4, r5 push到栈上去了,这是因为我们下面会用到r4, r5,所以我们先把r4,r5存到栈上,这样我们在退出fooFunction返回initFunction的时候好恢复r4, r5的值。push到栈上的顺序是lr, r7, r4, r5。
2.add r7, sp, #8                     在initFunction中我们没有push r4, r5所以sp指向的位置正好是新的r7的值,但是这里我们把r4, r5也push到栈上了,现在sp指向栈上的r4的位置,而栈是向下生长的,所以我们把sp + #8个字节就是存放旧r7的位置。
3.sub sp, #8                          在栈上分配8个字节。
4.ldr r4, [r7, #8]                    r7加8个字节,在栈上的位置正好是在initFunction中我们存放的参数55的位置。因此,这里是把55赋值给r4
5.movs  r5, #66                     立即数赋值,不解释了
6.strd r4, r5, [sp]                   把r4, r5中的值存到栈上。我们在initFunction中已经把11,22,33,44这4个参数存放到了r0-r3,现在55,66我们存放在栈上
7.bl _addFunction                   参数已经准备好了,因此现在调用addFunction。
8.add sp, #8                          回收栈空间
9.pop {r4, r5, r7, pc}              这最后两条指令和 initFunction类似,只是多了个恢复r4,r5。不过也是一个指令就完事。

在指令bl _addFunction 调用addFunction后,栈的布局如图(三):

            图(三)

上面的fooFunction第7条指令bl _addFunction之后,进入addFunction。汇编代码如下:

addFunction:

_addFunction:
  .cfi_startproc
Lfunc_begin0:
  add r0, r1
  ldr.w  r12, [sp]
  add r0, r2
  ldr.w  r9, [sp, #4]
  add r0, r3
  add r0, r12
  add r0, r9
  bx lr
Lfunc_end0:

逐行解释之:

add r0, r1         r0 += r1
ldr.w r12, [sp]      把sp指向的内容load到r12寄存器。从图(三)我们知道sp指向66,因此r12存的66
add r0, r2         r0 += r2
ldr.w r9, [sp, #4]    从图(三) sp加4个字节存的是55, r9存的55
add r0, r3         r0 += r3
add r0, r12        r0 += r12
add r0, r9        r0 += r9。 至此r0-r4存的11,22,33,44,及栈上存的55,66想加存到了r0上。
bx lr             返回。

大家应该有注意到因为addFunction没有调用其它的函数,序言和结语与initFunction和fooFunction不一样。因为我们不调用其它函数,就不会有bl, blx这样的指令,所以不会个性lr, 所以我们没有push lr。

在这里我们用了r9, r12为什么不需要保存与恢复,我还没大搞明白,大侠们若能赐教,将不胜感激。

iOS ABI Reference上是这样说的:

关于R9:

In iOS 2.x, register R9 is reserved for operating system use and must not be used by application code. Failure to do so can result in application crashes or aberrant behavior. However, in iOS 3.0 and later, register R9 can be used as a volatile scratch register. These guidelines differ from the general usage provided for by the AAPCS document.

关于R12

R12is the intra-procedure scratch register, also known as IP. It is used by the dynamic linker and is volatile across all function calls. However, it can be used as a scratch register between function calls.

这是C函数的汇编。下篇讲obj-c函数的汇编,包括objc block。

(0)

相关推荐

  • iOS逆向工程使用LLDB的USB连接调试第三方App

    LLDB是Low Level Debugger的简称,在iOS开发的调试中LLDB是经常使用的,LLDB是Xcode内置的动态调试工具.使用LLDB可以动态的调试你的应用程序,如果你不做其他的额外处理,因为debugserver缺少task_for_pid权限,所以你只能使用LLDB来调试你自己的App.那么本篇博客中就要使用LLDB来调试从AppStore下载安装的App,并且结合着Hopper来分析第三方App内部的结构.LLDB与Hopper的结合,会让你看到不一样的东西,本篇博客就会和你

  • iOS逆向工程之Hopper中的ARM指令详解

    虽然前段时间ARM被日本软银收购了,但是科技是无国界的,所以呢ARM相关知识该学的学.现在看ARM指令集还是倍感亲切的,毕竟大学里开了ARM这门课,并且做了不少的实验,当时自我感觉ARM这门课学的还是可以的.虽然当时感觉学这门课以后似乎不怎么用的上,可曾想这不就用上了吗,不过之前学的都差不多忘了,还得捡起来呢.ARM指令集是精简指令集,从名字我们就能看出指令的个数比那些负责指令集要少一些.当然本篇所涉及的ARM指令集是冰山一角,不过也算是基础,可以阅读Hopper中的汇编了,实践出真知,看多了自

  • 使用ARM汇编破解iOS程序基础知识分享

    一.Thumb指令与ARM指令 Thumb指令为16位,因此存储代码的密度高,节省存储空间.但是功能不全,它只是ARM指令(32位)集的补充,是ARM指令集下的一个子集.在初级阶段我们不需要了解这些知识,只要有个概念知道有这么个东西就可以. 二.ARM的寄存器初步了解 R0-R3: 用于函数参数及返回值的传递,超过4个参数,其它参数存在栈中,在ARM中栈是向下生长的,R0还可以作为返回值. R4-R6, R8,R10-R11: 没有特殊规定,就是普通的通用寄存器 R7: 栈帧指针,指向母函数与被

  • ARM汇编逆向iOS 实战

    我们先讲一些ARM汇编的基础知识.(我们以ARMV7为例,最新iPhone5s上的64位暂不讨论) 基础知识部分: 首先你介绍一下寄存器: R0-R3:用于函数参数及返回值的传递 R4-R6, R8,R10-R11:没有特殊规定,就是普通的通用寄存器 R7:栈帧指针(Frame Pointer).指向前一个保存的栈帧(stack frame)和链接寄存器(link register, lr)在栈上的地址. R9:操作系统保留 R12:又叫IP(intra-procedure scratch),

  • GNU ARM汇编语法原理及操作解析

    这篇文章主要介绍了GNU ARM汇编语法原理及操作解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 ARM汇编源程序有两种风格: ARM官方推荐的风格,所有的指令都大写.常用在windows下的IDE中. GNU风格的汇编风格,所有的指令都小写.常用在与Linux相关的工具中. 注释符号: GNU arm汇编所采用的注释符号是@符号,同样c语言中的 // 和 /* */ 两种类型的注释方法在GNU arm汇编中也被支持. 宏定义: 在GNU

  • ARM汇编解决阶乘及大小写转换的问题

    环境以及硬件 一.硬件仿真基于 SAMSUNG's S3C44B0X 16/32-bit RISC microprocessor 芯片,仿真器为 J-LINK 二.编写指令软件为 Integrated Development Environment ,软件仿真为 ARMulate.dll 三.需要基于ARM7硬件平台的C语言启动代码,用于分配中断向量表,初始化ISR地址,初始化堆栈空间,初始化应用程序执行环境,配置存储器系统,设定时钟周期,呼叫主应用程序. 四.这里仅有关键算法代码 ARM汇编求

  • ARM汇编判断之如何用汇编判断数组中正负数个数

    目录 [ARM汇编]如何判断数组中正负数个数? 写在前面: 所有判断后缀 如何用汇编实现判断数组中正负数的个数? 1.编前准备 1.1用C的伪代码表示 1.2参照正向遍历的框架 2.编写ARM汇编 2.1先给定一串数组.输出格式 2.2在循环中加入判断.计数 2.3在主函数中初始化使用的寄存器 2.4输出累计之和 2.5源代码 2.6运行结果 3.总结 [ARM汇编]如何判断数组中正负数个数? 写在前面: 在很多算法中都必须要用到if语句进行判断,前面我们提到了数组,也练习了用汇编的循环框架遍历

  • IOS实战之自定义转场动画详解

    转场动画这事,说简单也简单,可以通过presentViewController:animated:completion:和dismissViewControllerAnimated:completion:这一组函数以模态视图的方式展现.隐藏视图.如果用到了navigationController,还可以调用pushViewController:animated:和popViewController这一组函数将新的视图控制器压栈.弹栈. 下图中所有转场动画都是自定义的动画,这些效果如果不用自定义动

  • iOS汇编入门教程之ARM64汇编基础教程

    前言 对于应用层开发人员而言,仅仅掌握Objective-C和系统框架即可较好的完成开发,但在涉及到应用加固.逆向分析等内容时仅有应用层开发技能就会显得非常的无力,因此掌握汇编对于突破iOS开发水平的瓶颈十分有效. 一个例子 以反调试为例,我们知道,通过调用ptrace函数可以阻止调试器依附. ptrace(31, 0, 0, 0) 这种方式能够被函数hook轻易破解,例如使用facebook的fishhook.为了防止函数被hook,我们可以将函数调用转为通过汇编发起系统调用,即使用下面的代码

  • iOS汇编入门教程之在Xcode工程中嵌入汇编代码的方法

    简介 上一篇文章ARM64汇编基础中介绍了汇编在iOS开发中的应用以及ARM汇编基础知识,本文将介绍在C或Objective-C构成的工程中如何嵌入汇编代码. 注意 在调试ARM汇编时,Xcode的Build对象必须为真机,如果对象为模拟器则是x86汇编. 内联汇编 汇编与C间接通信 在函数中可以直接插入汇编代码来影响函数的运行逻辑,使用的语法为编译指令 __asm__ ,注意插入汇编有可能会被编译器忽略,因此需要加入 __volatile__ 修饰符保证汇编代码有效. 下面给出一个简单的例子,

  • Android 逆向学习详解及实例

    断断续续的总算的把android开发和逆向的这两本书看完了,虽然没有java,和android开发的基础,但总体感觉起来还是比较能接收的,毕竟都是触类旁通的.当然要深入的话还需要对这门语言的细节特性和奇技淫巧进行挖掘. 这里推荐2本书,个人觉得对android开发入门和android逆向入门比较好的教材: <google android 开发入门与实战> <android 软件安全与逆向分析> 1. 我对android逆向的认识 因为之前有一些windows逆向的基础,在看andr

随机推荐