IOS逆向-arm64汇编

Posted GY-93

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IOS逆向-arm64汇编相关的知识,希望对你有一定的参考价值。

真机和模拟器上的 汇编指令是不一样的

1. 寄存器

1.1 寄存器介绍

  • 通用寄存器:
    • 64bit的:x0 ~ x28
    • 32bit : w0 ~ w28(属于x0 ~ x28的低32bit)
    • x0-x7通常用来拿来存放函数的参数,更多的是参数使用堆栈来传递
    • x0通常拿来存放函数的返回值
  • 程序寄存器: pc(program Counter)
  • 堆栈指针寄存器:sp(Stack Pointer)、fp(Frame Pointer)
  • 链接寄存器:lr(Link Register), 也就是x30
  • 程序状态寄存器:
    • cpsr:(Cuurent Progame Status Register)
    • spsr: (Saved Program Status Register)

1.2 一些调试LLDB的相关指令

  • LLDB指令:

    • memory read : 读取所有寄存器的值
    • memory write 寄存器 值:给某个寄存器写入值
    • po $x0:打印方法调用者
    • x/s $s1: 打印方法名
    • po $x2:打印参数(以此类推, x3,x4也可能是参数)
    • 如果是非arm64,寄存器就是r0,r1,r2
  • 首先我们创建一个Xcode项目,打个断点,然后通过LLDB调试环境来查看汇编语言的寄存器信息:在这里插入图片描述

我们可以读取并修改寄存器的值:在这里插入图片描述

2. 汇编常用指令

2.1 在Xcode中创建汇编文件

  • 汇编文件的后缀为.asm,这里简写成.s文件
  • 我们以函数的形式来学习汇编的指令

在这里插入图片描述
在这里插入图片描述

  • 但是当我们编译项目后,报如下错误:

在这里插入图片描述

  • 分析错误发现是找不到_test函数,其实我们表面是调用test()函数,但是底层实质是寻找_test()函数 ,所以我们编写汇编代码的时候,需要修改函数名称为_test

在这里插入图片描述
当修改问函数名之后,在次编译发现还是报上述那个错误,找不到_test方法,其实如果是直接在方法前面加上_表示私有函数,我们还需要使用.global关键字来声明下函数

在这里插入图片描述

2.2 mov、ret

  • mov : 赋值
    • 指令格式:mov {条件} {S} 目的寄存器,源操作数
    • mov指令可完成从另外一个寄存器、被移位的寄存器或将一个立即数加载到目的寄存器。其中S选项指令的操作是否影响CPSR中条件标志位的值,当没有S指令不更新CPSR中条件标志位的值
    • 指令示例:
      • mov R1, R0 :将寄存器R0的值传递到寄存器R1
      • mov PC, R14:将寄存器R14的值传送到PC,常用语子程序返回
      • mov R1, R0, LSL#3:将寄存器R0的值左移3位传送到R1
  • ret:函数返回,将lr(x30)寄存器的值赋值给pc寄存器
  • mov的示例代码如下
// 作用 告诉写的函数放在哪个段,.text:表示函数放在代码段
.text
//表示函数是公开的,可以被被人访问,而函数的实现在下面
.global _test
//函数名称 test . 相当于打个标记  
_test:
mov x0, #0x8 // 把立即数8储存到寄存器x0中
mov x1, x0  //把寄存器x0的值传到寄存器x1中
ret

在这里插入图片描述

2.3 add

  • add: 加法
    • 指令格式:add {条件} {S}目的寄存器,操作数1,操作数2
    • add指令用于把两个数相加,并将结果存放到目的寄存器中,操作数据1,应该是一个寄存器,操作数2也可以是一个寄存器,被移位的寄存器,或一个立即数
    • 指令示例:
      • add R0, R1, R2: R0 = R1 + R2
      • add R0, R1, #256: R0 = R1 + 256
      • add R0, R2, R3, LSL#1: R0 = R2 + (R3 << 1)
  • add的示例代码
.text
.global _add
;add指令
_add:
mov x0, #0x1
mov x1, #0x2
add x0, x0, x1
ret

在这里插入图片描述

2.3 sub

  • sub:减法
    • 指令格式:sub {条件} {S}目的寄存器,操作数1,操作数2
    • sub指令用于把操作数1减去操作数2,并将结果存放到目的寄存器中。操作数1应是一个寄存区,操作数2可以是一个寄存器,被移位的寄存器,或是一个立即数。该指令可用于有符号或无符号数的减法运算
    • 指令示例:
      • add R0, R1, R2: R0 = R1 - R2
      • add R0, R1, #256: R0 = R1 - 256
      • add R0, R2, R3, LSL#1: R0 = R2 - (R3 << 1
  • sub代码示例
.text
.global _sub
;sub指令
_sub:
sub x0, x0, x1 //把x0 - x1相减的值放到x0中
ret
  • 调用sub(6, 2);
    在这里插入图片描述
    由上述断点调试图可知, 这次我们并没直接使用汇编指令给x0、x1寄存器赋值,当我们传递两个参数进去之后,会自动赋值到x0、x1寄存器上,所以我直接使用sub指令把x0寄存器上的值减去x1寄存器上面的值,然后保存到x0寄存器上面 , 通过LLDB调试指令可以看出相见后的结果储存在x0寄存器中

2.4 cmp(compare:比较)

2.4.1 cmp基本使用

  • cmp: 将2个寄存器相减,将相减的结果会影响cpsr寄存器的标志位

    • 指令格式:cmp {条件}操作数1,操作数2
    • cmp指令用于把一个寄存器的内容和另外一个寄存器的内容或立即数进行比较,同时更新CPSR中的标志位的值。该指令进行一次减法运算,但是不储存结果,只更改条件标志位。标志位表示是操作数1和操作数2的关系(大、小、相等),例如,当操作数1大于操作数2,则此后的有GT的后缀的指令将可以执行。
    • 指令示例:
      • cmp R1, R0 : 将寄存器R1的值与寄存器R0的值相减,并根据结果设置CPSR的标志位
      • cmp R1, #100:将寄存器R1的值与立即数100相减,并根据结果设置CPSR的标志位
  • 代码示例:

_test:
;cmp 指令
mov x0, #0x3
mov x1, #0x1
// 和sub指令相似,都表示x0 - x1,但是cmp是把相减的值,会影响cpsr寄存器中的标志位
cmp x0, x1  
ret

在这里插入图片描述

随着汇编代码的不断执行,我们可以看到cpsr寄存器的值在不断改变, 我们可以通过计算器来查看cpsr的值,看看cmp指令的对cpsr寄存器的影响

2.4.2 cpsr寄存器

在这里插入图片描述

  • cpsr寄存器标志位解释
    在这里插入图片描述

2.5 b

2.5.1 B指令的基本使用

  • b:跳转指令,可以带条件跳转,一般跟cmp配合使用
    • 指令格式:b{条件} 目标地址
    • b指令是最简单的跳转指令。一旦遇到一个b指令,ARM处理器将立即跳转到给定的目标地址,从哪里继续执行。注意储存在跳转指令中的实际值是相对当前PC值的一个偏移量,而不是一个绝对的地址值,它的值由汇编器来计算(参考寻址方式中的相对寻址)。它是24位有符号数,左移两位后有符号扩展为32位,表示的有效偏移为26位(前后32MB的地址空间)。
    • 指令示例:
      • B Lable:程序无条件跳转到标号Lable处执行
      • cmp R1 , #0: 当CPSR寄存器中的z条件码置位时,程序跳转到标号Lable处执行
      • BEQ Lable: 当相等时,跳转到标号Lable处执行
  • 示例代码:
_test:

;b指令
mov x1, #0x7
b mycode
mov x0, #0x5
mycode:
mov x0, #0x6
ret 

在这里插入图片描述

当我执行si指令之后,我们可以看到,程序直接跳转到mycode标记之后继续执行代码,并没有执行mov x0, #0x5这句汇编代码
在这里插入图片描述

2.5.2指令的条件域

当处理器工作在ARM状态时,几乎所有的指令均根据CPSR中条件码和指令的域有条件的执行。当指令的执行条件满足时,指令被执行,否则指令被忽略。

每一条ARM指令包含4位的条件码,位于指令的最搞4位【31:28】。条件码共有16种,每条条形码可用两个字符表示,这两个字符可以添加在指令助计符的后面和指令同时使用 。例如跳转指令B可以上后缀EQ变为BEQ表示"相等则跳转"

在这里插入图片描述

  • ARM指令及功能描述:
    在这里插入图片描述
    在这里插入图片描述

  • 代码示例:

_test:
;b指令带条件的跳转
mov x0, #0x1
mov x1, #0x1
cmp x0,x1
beq mycode  ;如果上述条件不相等,就不会跳转会按照顺序指向下列代码
mov x2, #0x2
; 如果想达到上面语句和mycode:后面代码只执行其中一行时,可以选择在改行加上 ret 语句
mycode:
mov x3, #0x3 ;条件不想等时,也回执行到这句代码
ret

在这里插入图片描述
在这里插入图片描述

2.6 bl

  • bl:带返回的跳转指令,执行的操作:**将下一条指令的地址存储到lr(x30)寄存器中,跳转到标记处开始执行代码,当跳转函数执行完时(执行到ret语句时),会将lr(x30)寄存器中存储的地址值赋值给pc寄存器,达到了返回的目的 ** 有点类似函数调用
    • 指令格式:BL {条件} 目标地址
    • BL是另一个跳转指令,单是跳转之前,会在寄存器R14中保存PC当前的内容,因此可以通过将R14的内容重新加载到PC中,来返回到跳转指令之后的那个指令执行处。该指令是实现子程序调用的一个基本但常用的手段。
    • BL Lable:当程序无条件跳转到标号Lable处执行时,同时将当前的PC值保存到R14中

在这里插入图片描述
执行si直接跳转到mycode
在这里插入图片描述

mycode执行完之后,在返回继续之心后面代码
在这里插入图片描述
在这里插入图片描述

编写函数调用的本质,底层汇编使用bl指令来实现

3. 内存操作

3.1 寻址方式

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.2 load,从内存中读取数据

3.2.1 ldr

  • ldr(从内存中读取数据)
    • 格式:LDR{条件} 目的寄存器, <储存器地址>
    • LDR指令用于从储存器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从储存器中读取32位的字数据到通用寄存器,然后对数据进行处理。
    • 指令示例:

在这里插入图片描述

  • 代码示例:
.text
.global _test

_test:
; ldr指令: 从内存冲读取数据
ldr x0, [x1] ;找到x1寄存器中存储的那个地址值对应的那块内存
ret
  • ldr x0, [x1]:从x1寄存器的地址开始读取8个字节数据到 x0
  • ldr w0 , [x1]:从x1寄存器的地址读取4个字节数据到w0

在这里插入图片描述
在这里插入图片描述

  • 从上述断点结果来看, 我们可以发现x0的值和x1的值不相等, 造成这个结果的原因是,因为ldr x0, [x1]是从x1的地址往后读取8个字节的内容到x0,但是变量a只占4个字节,所以还多读了4个字节的内容

3.2.2 ldur

  • 相同点:和ldr指令作用类似,都是取内存中的数据放到某个寄存器中
  • 区别: ldur:一般用于负数,ldr一般用于正数
  • 下图的用法可以参考上面的 寻址方式

在这里插入图片描述

3.2.3 ldp

  • ldp : 从内存中读取数据放到一对寄存器中
  • 示例代码:
ldp w0, w1, [x2, #0x10]

首先从[x2, #0x10]得到的新地址中,往高字节(高地址方向)读取4个字节赋值给w0,然后再接着读取4个字节赋值给w1
在这里插入图片描述

3.2 store往内存中写入数据(str、stur、stp)

  • str:往内存中写入数据str w0, [x1] : 把w0寄存器的值写入x1中
  • stur: 往内存中写入负数,一般用于便宜量是负数
  • stp:成对写入数据stp w0, w1 [x1]: 先把w0数据写入x1中,然后在将w1中内容写入x1中

在这里插入图片描述

  • 零寄存区器: 里面存储的值是0
    • wzr(32bit)
    • xzr(64bit)

想要把某个内存地址的值清零,可以直接使用str指令把wzrxzr寄存器值写入内存地址,因为wzrxzr的寄存器的储存的值就是0
在这里插入图片描述
断点调试如下:
在这里插入图片描述

3.3 pc、lr

  • pc: 程序计数器,用来记录CPU当前指令是那一条指令,存储当前cpu正在执行指令的地址,类似于8086汇编的ip寄存器

在这里插入图片描述

  • lr:链接寄存器,其实也是x30寄存器,存储着函数的返回地址
    在这里插入图片描述

在这里插入图片描述

我们可以看到当test函数执行完成之后,执行下一句代码的时候,我们可以看到lr寄存器存储的地址就是这句代码的地址,所以lr寄存器存储的是函数的返回的地址

4. 函数的堆栈

  • 把C语言文件转换成汇编文件:xcrun -sdk iphones clang -arch arm64 -S C语言文件 -o 输出文件,如果不写-o也会自动输出
    在这里插入图片描述

4.1 叶子函数(内部不会调用其他函数的函数)

  • 叶子函数分析
void haha()
{
    int a = 2;
    int b = 3;
}

//对应的核心汇编源码
	
	
	//核心代码
	//sp指针-16 
	sub	sp, sp, #16             ; =16
	
	.cfi_def_cfa_offset 16
	mov	w8, #2 // 把立即数2赋值给w8寄存器
	str	w8, [sp, #12] // 把w8寄存器的值, 写入到 【sp + 12】的内存空间中
	mov	w8, #3  // 把立即数3赋值给w8寄存器
	str	w8, [sp, #8] // 把w8寄存器的值, 写入到 【sp + 8】的内存空间中

	add	sp, sp, #16             ; =16 // 当函数之心完成时,需要把sp指针(寄存器)恢复到初始化位置, 这样就完成函数执行过程中的内存分配, 其次我们需要知道, 我们只是把sp指针的位置回复到初始位置,并没有清楚那段内存空间的数据, 这些数据被称为垃圾数据, 等下次其它函数的数据覆盖掉这些内存空间
	
	ret

在这里插入图片描述

4.2 非叶子函数

void haha()
{
    int a = 2;
    int b = 3;
}

void hehe()
{
    int a = 4;
    int b = 5;
    haha();
}


对应的转换核心汇编代码

	//hehe函数的汇编代码 
	sub	sp, sp, #32             ; =32
	stp	x29, x30, [sp, #16]     ; 16-byte Folded Spill. //存储fp(x29)、lr(x30)的值
	add	x29, sp, #16            ; =16

	mov	w8, #4
	stur	w8, [x29, #-4] //写入数据, 移动fp指针
	mov	w8, #5
	str	w8, [sp, #8]  // 写入数据,移动 sp指针
	bl	_haha //跳转到haha 函数
	
	ldp	x29, x30, [sp, #16]     ; 16-byte Folded Reload // 读取 fp、lr的值
	add	sp, sp, #32             ; =32 // 回复sp指针的位置 
	ret //返回

在这里插入图片描述

非叶子函数不仅需要开辟空间储存数据, 还需要开辟空间储存 fp(x29)、lr(x30)

4.3 sp、fp 堆栈指针

  • sp(Stack Pointer): 通过sp指针(寄存器)指定一块存储空间给函数使用
  • fp (Frame Pointer):也就是x29寄存器

5 实战

5.1 破解(Mac程序)命令行工具

  • 首先创建一个mac命令行项目,编写一段简单的命令行测试代码:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int password = 0;
        while (password != 123456) {
            printf("请输入正确的密码:");
            scanf("%d",&password);
        }
        printf("密码输入正确,欢迎使用XX管理系统!\\n");
    }
    return 0;
}

  • 然后编译,在找到Products下的可执行文件,拷贝到一个路径下,并使用终端执行该文件

在这里插入图片描述

  • 接下来我们使用hopper工具来分析该可执行文件

在这里插入图片描述

我们分析汇编代码, 我们可以通过hopper去修改汇编代码, 这里是去掉while循环的代码
选中汇编代码

在这里插入图片描述

修改之后的汇编代码:在这里插入图片描述

然后再使用hopper工具导出新的可执行文件工具, 在重新执行可执行文件就可以看到破解效果
在这里插入图片描述

5.2 破解iphone程序

破解iPhone流程和破解Mac上程序过程类似, 首先需要分析代码, 然后直接修改可执行文件,或则通过tweak代码来达到修改的目的, 最终把修改好的可执行文件直接替换到手机中该可执行文件即可

6. 问题

  • 如果返回值超过x0寄存器的大小,怎么处理?
    • 如果返回值很大,可能会将返回值放到堆栈, 然后把内存地址赋值到x0上返回
  • x0寄存器又当参数,又是返回值,会不会乱套?
    • 不会,因为操作的时间是分开的,进入函数的时候是参数,返回的时候是返回值
  • 所有的调用方法在汇编中都已下划线_开头的,如果不写_,则表示不是公开的函数,如果是公开的函数一定需要_

以上是关于IOS逆向-arm64汇编的主要内容,如果未能解决你的问题,请参考以下文章

Android逆向基础之ARM汇编语言知识总结

Android 逆向arm 汇编 ( 使用 IDA 解析 arm 架构的动态库文件 | 分析 malloc 函数的 arm 汇编语言 )

Android 逆向arm 汇编 ( 使用 IDA 解析 arm 架构的动态库文件 | 分析 malloc 函数的 arm 汇编语言 )

[转] iOS开发同学的arm64汇编入门

[转] iOS开发同学的arm64汇编入门

iOS逆向工具之hopper的使用