查看原文
其他

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

游戏安全攻防 小道安全 2022-11-13

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  LRPC

总结:识别函数的方法就是汇编指令中是否有包含:BBLBLXBXPC的汇编指令。

 

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


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

ATPCS的英文全称是ARM-THUMB procedure call standardARM-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位整数时,可以通过R0R1返回,依此类推.

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

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

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

 

总结:

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

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/popLDMFD/STMFD等。

 

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


当参数个数小于等于4个的时候,使用r0r34个寄存器进行参数传递;如果参数个数大于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 spsp#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位整数时,可以通过R0R1返回,依此类推.

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   参数 5r3=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、传入参数个数及指针、本地变量和临时变量。如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。

 

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


因此得出:栈是通过FPSP寄存器串成一串的,每个单元就是一个栈帧(也就是一个函数调用过程)。又由于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:链接地址寄存器,存放下一条执行指令


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存