安卓逆向:这是一篇逆向基础函数在ARM32中的刨根问底。

2021-01-18 11:22:44 浏览数 (1)

ARM32中函数需要关注的点有哪些?

1. 怎么去识别ARM的函数?

2. 函数采用什么样的调用约定?

3. 函数的参数是怎样进行传递的?

4. 函数的返回值是通过怎样接收存储?

5. 栈帧是什么东西?

6. 栈帧有什么作用?

7. 栈帧在函数中的使用?

1. 怎样在ARM汇编中去识别和定位出函数

1.使用专门的跳转指令。

1.1 B 跳转指令

1.2 BL 带返回的跳转指令

1.3 BLX 带返回和状态切换的跳转指令

1.4 BX 带状态切换的跳转指令

2.直接向程序计数器 PC 写入跳转地址值。

通过向程序计数器 PC写入跳转地址值,可以实现在 4GB 的地址空间中的任意跳转,在跳转之前结合使用MOV LR,PC

总结:识别函数的方法就是汇编指令中是否有包含:B、BL、BLX、BX、PC的汇编指令。

2. 函数采用什么样的调用约定?

ARM函数调用约定采用的是:ATPCS

ATPCS的英文全称是ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)

总结:参数1~参数4 分别保存到 R0~R3 寄存器中 ,剩下的参数从右往左一次入栈,被调用者实现栈平衡,返回值存放在 R0 中。

ATPCS主要设定的以下几个使用规则:

堆栈使用规则

ATPCS规定堆栈为FD类型,即满递减堆栈。并且堆栈的操作是8字节对齐。

而对于ARM汇编来说,如果目标文件中包含了外部调用,必须满足以下条件:

1.外部接口的数据栈一定是8位对齐的,也就是要保证在进入该汇编代码后,直到该汇编程序调用外部代码之间,数据栈的栈指针变化为偶数个字;

2.在汇编程序中使用PRESERVE8伪操作告诉连接器,本汇编程序是8字节对齐的.。

参数传递规则

根据参数个数是否固定,可以将子程序分为参数个数固定的子程序和参数个数可变的子程序.这两种子程序的参数传递规则是不同的.

1.参数个数可变的子程序参数传递规则

对于参数个数可变的子程序,当参数不超过4个时,可以使用寄存器R0~R3来进行参数传递,当参数超过4个时,还可以使用数据栈来传递参数. 在参数传递时,将所有参数看做是存放在连续的内存单元中的字数据。然后,依次将各名字数据传送到寄存器R0,R1,R2,R3;如果参数多于4个,将剩余的字数据传送到数据栈中,入栈的顺序与参数顺序相反,即最后一个字数据先入栈. 按照上面的规则,一个浮点数参数可以通过寄存器传递,也可以通过数据栈传递,也可能一半通过寄存器传递,另一半通过数据栈传递.

2.参数个数固定的子程序参数传递规则

对于参数个数固定的子程序,参数传递与参数个数可变的子程序参数传递规则不同,如果系统包含浮点运算的硬件部件。浮点参数将按照下面的规则传递:

(1)各个浮点参数按顺序处理;

(2)为每个浮点参数分配FP寄存器; 分配的方法是,满足该浮点参数需要的且编号最小的一组连续的FP寄存器.第一个整数参数通过寄存器R0~R3来传递,其他参数通过数据栈传递.

子程序结果返回规则

1.结果为一个32位的整数时,可以通过寄存器R0返回.

2.结果为一个64位整数时,可以通过R0和R1返回,依此类推.

3.结果为一个浮点数时,可以通过浮点运算部的寄存器f0,d0或者s0来返回.

4.结果为一个复合的浮点数时,可以通过寄存器f0-fn或者d0~dn来返回.

5.对于位数更多的结果,需要通过调用内存来传递.

总结:

r0–r3:存储传递给函数的参数值,多余的参数通过压栈传递。

r4 -r11:存储函数的局部变量,Thumb模式不会使用r8以后的寄存器

r12:是内部过程调用暂时寄存器(intra-procedure-call scratch register)。

r13:存储栈指针(sp)。在计算机中,栈非常重要。这个寄存器保存着栈顶的指针。这里可以看到更多关于栈的信息。

r14:链接寄存器(link register)。存储着当被调用函数返回时,将要执行的下一条指令的地址。

r15:用作程序计数器(program counter)。存储着当前执行指令的地址。每条执行被执行后,该计数器会进行自增( 1)。

函数的返回值放到r0中。

fp叫做frame pointer寄存器,即栈帧指针寄存器;sp叫做stack pointer寄存器,即栈指针寄存器。

在ARM指令系统中是地址递减栈,入栈操作的参数入栈顺序是从右到左依次入栈,而参数的出栈顺序则是从左到右的你操作。包括push/pop和LDMFD/STMFD等。

3.函数的参数是怎样进行传递的?

当参数个数小于等于4个的时候,使用r0到r3这4个寄存器进行参数传递;如果参数个数大于4个,余下的参数就通过sp所指向的数据栈进行参数传递。

比如有3个参数的话,那么r0代表函数的第一个参数,r1代表函数的第二个参数,r2代表函数的第三个参数。

比如有6个参数的话,那么r0-r3表示前面4个参数,然后余下的两个参数通过在栈上开辟8字节的空间进行参数传递。

看如下函数参数定义和调用实例:

  1. 参数小于四个的函数实现及ARM汇编实现

C 源代码:

看汇编前先熟记以下几个指令。

fp:栈底寄存器

sp:栈顶寄存器

ldr r0, [pc,#4] ; 把pc 4内存地址的值给r0

str r1, [pc,#4] ; 把r1的值写入pc 4内存地址

入栈指令:push、STM

Push {fp}

STMFD sp!{r4-r7,lr}

出栈指令:pop、LDM

Pop(fp)

LDMFD sp! {r4-r7,lr}

上图函数源码对应的ARM的汇编代码

以上汇编指令表示:

sub sp,sp,#28 开辟28栈空间

ADD指令表示用于相加操作:add r2, r2,r3 等于r2=r2 r3

BX表示带返回值的函数

以上代码中 bl 826C前面的三个ldr表示的是传递到bl826C的三个函数参数。

  1. 参数大于四个的情况

以上ARM汇编中在bx前面有再开辟完栈空间后有对应的六个ldr指令,表示这个函数有六个参数

4.函数的返回值是通过怎样接收存储?

1.结果为一个32位的整数时,可以通过寄存器R0返回.

2.结果为一个64位整数时,可以通过R0和R1返回,依此类推.

3.结果为一个浮点数时,可以通过浮点运算部件的寄存器f0,d0或者s0来返回.

4.结果为一个复合的浮点数时,可以通过寄存器f0-fN或者d0~dN来返回.

5.对于位数更多的结果,需要通过调用内存来传递.

先简单回归下函数中参数和返回值用的指令。

int armTest(int a, int b, int c, int d),

输入的参数:r0 = a, r1 = b, r2= c, r3 = d.

函数的返回:r0 = 类型为int的返回值。

int *armTest(char a, double b, int c, char d),

输入参数:r0 = a, r1用于对齐(double 要求8字节对齐), b = {r2, r3},c放在堆栈的sp[0]位置,d放在堆栈的sp[4]位置,这里的sp是指进入函数时的sp。

返回:r0 = 类型为int *的返回值。

函数调用完毕后,如果函数有返回值,函数一般把返回值保存在r0寄存器中,因此一般我们通过bl指令调用一个函数后,就可以通过在汇编里面访问r0得到返回值。

往下看分析下实例代码。

源代码情况

Main函数对应汇编

0x00010418< 0>: push{r11, lr}

0x0001041c< 4>: add r11,sp,#4

0x00010420< 8>: sub sp,sp,#8 为局部变量开辟空间

0x00010424< 12>: mov r3,#5 参数 5;r3=5

0x00010428< 16>: str r3, [sp]

0x0001042c< 20>: mov r3,#6 参数6 r3=6

0x00010430< 24>: str r3, [sp, #4]

0x00010434< 28>: mov r0, #1 参数1

0x00010438< 32>: mov r1, #2 参数2

0x0001043c< 36>: mov r2, #3 参数3

0x00010440< 40>: mov r3, #4 参数4

---->0x00010444< 44>:bl 0x103c8<func>

0x00010448< 48>: mov r3, #0 r3=0

0x0001044c< 52>: mov r0, r3 返回值r0=0

0x00010450< 56>: sub sp, r11, #4

0x00010454< 60>: pop {r11, pc}

Func函数对应的ARM汇编

0x000103c8 < 0>: push{r11} ; (str r11, [sp, #-4]!)

0x000103cc < 4>: add r11, sp, #0

0x000103d0 < 8>: sub sp, sp, #20 开辟栈空间#20

0x000103d4< 12>: str r0, [r11, #-8] r0写入到[r11, #-8]的位置

0x000103d8 < 16>: str r1, [r11, #-12]

0x000103dc < 20>: str r2, [r11, #-16]

0x000103e0 < 24>: str r3, [r11, #-20] ; 0xffffffec

0x000103e4 < 28>: ldr r2, [r11, #-8] 取出[r11,#-8]空间的值写入到r2寄存器中

0x000103e8 < 32>: ldr r3, [r11, #-12]

0x000103ec < 36>: add r2, r2, r3 r2=r2 r3

0x000103f0 < 40>: ldr r3, [r11, #-16]

0x000103f4 < 44>: add r2, r2, r3 r2=r2 r3

0x000103f8 < 48>: ldr r3, [r11, #-20] ; 0xffffffec

----> 0x000103fc < 52>:add r2,r2, r3

0x00010400 < 56>: ldr r3, [r11, #4]

0x00010404 < 60>: add r3, r2, r3 r3= r2 r3

0x00010408 < 64>: mov r0, r3 存储返回值r0= r3

0x0001040c < 68>: sub sp,r11, #0 恢复栈空间

0x00010410 < 72>: pop {r11} ; (ldr r11, [sp], #4)

0x00010414 < 76>: bx lr 函数结束跳转出来

5.栈帧是什么东西?

1.栈是一种具有后进先出的数据组织方式,也就是说后存放的先取出,先存放的后取出。栈底是第一个进栈的数据所处位置,栈顶是最后一个数据进栈所处的位置。

栈帧(stack frame):就是一个函数所使用的那部分栈,所有函数的栈帧串起来就组成了一个完整的栈。栈帧的两个边界分别由fp(r11)和sp(r13)来限定。

栈帧是如何形成的呢? 当栈顶指针 sp小于栈底指针 fp时, 就形成了栈帧。

下图是ARM的栈帧布局

上图描述的是ARM的栈帧布局方式,main stack frame为调用函数的栈帧,func1stack frame为当前函数(被调用者)的栈帧,栈底在高地址,栈向下增长。图中FP就是栈基址,它指向函数的栈帧起始地址;SP则是函数的栈指针,它指向栈顶的位置。ARM压栈的顺序依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。

函数调用过程中涉及四个重要的寄存器:PC、LR、SP和FP。注意,每个栈帧中的PC、LR、SP和FP都是寄存器的历史值,并非当前值。PC寄存器和LR寄存器均指向代码段, 其中PC代表代码当前执行到哪里了,LR代表当前函数返回后,要回到哪里去继续执行。SP和FP用来维护栈空间,其中SP指向栈顶,FP指向上一个栈帧的栈顶。

因此得出:栈是通过FP和SP寄存器串成一串的,每个单元就是一个栈帧(也就是一个函数调用过程)。又由于LR是指向调用函数的(即PC寄存器的历史值)。那么,如果能得到每个栈帧中的LR值,就能得到整个的函数调用链。

6.栈帧的作用?

1、返回地址

函数完成后要返回的程序内部地址

2、局部数据存储

为局部变量分配的内存

3、参数存储

为函数参数分配的内存

4、栈指针和基指针

运行时系统用来管理栈的指针

栈指针通常指向栈的顶部,基指针(帧指针)通常存在并指向栈帧内部的地址。

下例中栈就是用于存储保存局部变量的。

栈用于存储返回值的

Str r0,[r7,#12]意思是将返回值R0写入到栈[r7,#12]的位置

7. 栈帧在函数中的使用?

ARM压栈的顺序很是规矩,依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。

ARM进行函数内压栈和出栈往往使用如下的语句:

stmfd sp!,{r0-r9, lr} ; 满递减入栈,给寄存器r0-r9,lr压栈,sp不断减4。

ldmfd sp!,{r0-r9, pc} ; 满递减出栈,给寄存器r0-r9出栈,并使程序跳转回函数的调用点,sp不断增4。

最后总结:

函数指令:B、BL、BX、BLX、PC

函数调用约定:ATPCS

函数参数:小于等于4个:R0-R3; 大于4个用SP栈表示。

函数返回值:R0

栈可用于:局部变量,返回值,参数传递

函数传递及调用常用指令

fp:栈底寄存器

sp:栈顶寄存器

ldr r0, [pc,#4] ; 把pc 4内存地址的值给r0

str r1, [pc,#4] ; 把r1的值写入pc 4内存地址

push,pop

lr:链接地址寄存器,存放下一条执行指令

0 人点赞