前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >安卓逆向:这是一篇逆向基础函数在ARM32中的刨根问底。

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

作者头像
小道安全
发布2021-01-18 11:22:44
3.3K1
发布2021-01-18 11:22:44
举报
文章被收录于专栏:游戏安全攻防游戏安全攻防

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:链接地址寄存器,存放下一条执行指令

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 游戏安全攻防 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档