首页
社区
课程
招聘
[原创]使用frida在A64下内存读写断点的简单实现
发表于: 2023-9-15 16:32 14281

[原创]使用frida在A64下内存读写断点的简单实现

2023-9-15 16:32
14281

很早之前看过一篇帖子,使用frida的API

实现内存读写断点,但是由于mprotect函数只能对一整页的内存属性修改,并不能具体到具体地址,所以使用意义不大。之前逆向游戏的时候很久都没有找到核心代码,尝试实现这个功能。

整个脚本不涉及物理内存和内核,只依靠arm指令特性实现

此函数本身有着异常类型type,发生异常的地址memory,以及context,它记录了当前所有寄存器的数据,最关键的是它允许我们修改寄存器数据和内存。
具体说明请自行查看frida的文档。

armv8架构下有着通用寄存器X0-X31,浮点/SIMD 寄存器V0-V31,程序计数器PC。X29,X30,X31有着特殊含义

参数1-参数8 分别保存到 X0~X7 寄存器中 ,剩下的参数从右往左依次入栈,被调用者实现栈平衡,返回值存放在 X0 中。
在BL或BLR的时候,将下一条指令的地址放入lr寄存器中,然后再跳转到目的地址。
而后被调用方需要保存fp和lr在栈中,在RET指令前从栈里取出。

Armv8架构有着两种指令集,AArch64和AArch32。
在AArch64运行状态下使用A64指令集,在AArch32运行状态下使用A32指令集。
A64指令集和A32 指令集是不兼容的,它们是两套完全不一样的指令集,它们的指令编码也是不一样的。
其A64指令集的指令宽度是32位等长。本脚本的读写断点功能实现只在A64下。

以下资料都来自

arm架构下对于内存的访问都基于Load/Store指令,并不能像X86一样任何指令都能直接访问内存,如

它只能通过Load/Store系列的指令来访问和修改内存

所以我们要想实现读写断点,只需要关注Load/Store系列的指令

Load/Store下的寻址模式只有5种,在参考手册的C1.3.3

分别是

通过寄存器的地址直接读取数据

寄存器基址加上偏移得到访问的地址,该指令并不会使X1的变动

寄存器基址加上偏移得到访问的地址,该指令会先为SP赋值,然后再对该地址赋值,一般用在入栈操作上

先取得数据,再将SP的值更新,一般用在出栈操作上

原文如下

大概意思是,该指令的地址加上一个有符号的#imm19的偏移量,寻址范围为±1MB。

用LDR (immediate)的Unsigned offset举例
这是对于通用寄存器的

这是对于SIMD寄存器的


可以看到,两个指令前面0-21位都是一致的,
前面这个LDR以size来判断取值大小是32位还是64位
后面的LDR(SIMD)用size和opc来判断取值大小为8,16,32,64,128位
而区分Rt是通用寄存器或是浮点寄存器的只是第26位。

通过以上的理论知识,想象一下,对于Load和Store,除开pc偏移寻址,其他的寻址模式都是寄存器间接寻址,换句话说,
这些指令只要寄存器里的值不变,该指令在内存的任何地方进行加载和存储的结果都是一致的,包括浮点/SIMD 寄存器

在参考手册上C4.1.4的Load register (literal)

可以看到,整个A64使用pc偏移寻址的只有3个Load系列的指令,LDR(literal),LDRSW(literal),PRFM(literal).没有Store系列的指令


这三个指令,只有opc(代表取值大小),V(取值的寄存器类型)是不一致的。
而imm19与Rt都是在变化的。
分割出来这三个指令很简单

这样就分割开了pc偏移寻址和其他Load指令

如果是pc偏移寻址,
将要读取的数据事先存入事先准备好的一块内存里,(最好是128位数据防止加载的是128位浮点数)

还记得setExceptionHandler吗?它可以直接修改寄存器数据,包括PC
直接粘代码吧,没有多难

可以看到代码量并没有多少,也非常简单
解释一些可能会疑惑的地方

更科学的方法应该是将mycode写入x16,然后将该条指令修改成BLR X16
但是这样做是有问题的,因为该内存页不止一个地方在访问,而且一条一样的指令也会因为寄存器数据不同而去访问不一样的地址,而我们每次捕获异常之后才会改动该code,所以下次执行跳转后执行的不一定是原先的code,这个问题也有办法解决,在我们的code里把它再该回去就行

这样做是可行的,
具体思路是先在mycode+4里按存顺序4个nop和一个ret
然后在 onEnter: function(args)把内存页修改回去
在修改pc前把lr也修改了
Interceptor.attach完成后是会将mycode的内存页属性改为‘r-x’,我们要重新修改回去。
没这样做是有两个方向的考虑
1,过滤我们不关心的内存本身是会多次进入js虚拟机里的,这样做一个code得两次进入js虚拟机,速度应该很感人。。。
2,我们的目的不是争对莫个函数,而是具体的指令,所以不应该修改任何寄存器的值,attach本身会使用BLR X16跳转到frida.so里,而对于原本的X16 frida是直接丢弃掉的

有的函数会使用SP的负偏移存储数据,为保护栈设计

应该将所有的寄存器全部保存,但是测试发现只有X8在call mprotect后被修改了,而X30最后我们必然会改动,懒得保存

首先使用ce找到我们关心的地址

然后readwritebreak
参数1是地址,2是长度,3要修改的属性(1读,2写)

通过多次异常捕获后最终匹配到我们关心的地址

可以看到从触发异常再进入编写的代码最后返回,只有lr寄存器被修改了,只要符合A64的调用约定,修改它并不影响程序的运行,但是假设有什么逆天的代码用lr做局部变量,或者该段代码实现功能是pc劫持,那就只能崩溃了哈哈。
并没有过多的测试,或许还有我没有考虑到的地方存在bug,大佬们有空拿去测试测试呗。
还有一些问题我自己都还不知道,有大佬知道吗?
1,frida是如何改变pc的值的?我记得arm中是不支持直接对pc进行修改的
2,A64指令中存在预加载指令PRFM,该指令会不会触发异常啊?

Process.setExceptionHandler(callback)
Process.setExceptionHandler(callback)
X29 / fp 帧指针
X30 / lr 链接寄存器
X31 / sp 栈指针
X29 / fp 帧指针
X30 / lr 链接寄存器
X31 / sp 栈指针
ARMv8 Architecture Reference Manual
ARMv8 Architecture Reference Manual
mov eax,[test.exe+0x5b5c2]
mov eax,[test.exe+0x5b5c2]
LDR  X0,[SP,#0x10]
LDR  X0,[SP,#0x10]
LDR   X0 , [X1] // x0 = *x1;
LDR   X0 , [X1] // x0 = *x1;
LDR   X0,  [X1,#8] //X0=*(X1+8)
LDR   X0,  [X1,#8] //X0=*(X1+8)
STR  X0, [SP,#-0x20] //sp=sp-0x20 , *(sp-0x20)=X0
STR  X0, [SP,#-0x20] //sp=sp-0x20 , *(sp-0x20)=X0
LDR  X0 ,[SP],#0x20 //x0=*sp  sp=sp+0x20
LDR  X0 ,[SP],#0x20 //x0=*sp  sp=sp+0x20
Literal addressing means that the address is the value of the 64-bit program counter for this instruction plus
a 19-bit signed word offset. This means that it is a 4 byte aligned address within ±1MB of the address of this
instruction with no offset. Literal addressing can only be used for loads of at least 32 bits and for prefetch
instructions. The PC cannot be referenced using any other addressing modes. The syntax for labels is specific
to individual toolchains.
Literal addressing means that the address is the value of the 64-bit program counter for this instruction plus
a 19-bit signed word offset. This means that it is a 4 byte aligned address within ±1MB of the address of this
instruction with no offset. Literal addressing can only be used for loads of at least 32 bits and for prefetch
instructions. The PC cannot be referenced using any other addressing modes. The syntax for labels is specific
to individual toolchains.
//获取当前code
var thiscode=ptr(details.context.pc).readU32()
//去掉后面24位,然后只需要2425272829的值即0x3b
 var opcode=((thiscode>>24)&0x3b)
//判断opcode是不是等于0x18
if(opcode==24){
//获取当前code
var thiscode=ptr(details.context.pc).readU32()
//去掉后面24位,然后只需要2425272829的值即0x3b
 var opcode=((thiscode>>24)&0x3b)
//判断opcode是不是等于0x18
if(opcode==24){
//mempoint即为details.memory.address
//直接把里面的数据当成指针来读
mycode.add(0x128).writePointer(ptr(mempoint.readPointer()))
mycode.add(0x130).writePointer(ptr(mempoint.add(8).readPointer()))
//mempoint即为details.memory.address
//直接把里面的数据当成指针来读
mycode.add(0x128).writePointer(ptr(mempoint.readPointer()))
mycode.add(0x130).writePointer(ptr(mempoint.add(8).readPointer()))
const malloc = new NativeFunction(Module.findExportByName('libc.so', 'malloc'), 'pointer', ['size_t']);
const memset = new NativeFunction(Module.findExportByName('libc.so', 'memset'), 'pointer', ['pointer', 'size_t', 'int']);
const mprotect = new NativeFunction(Module.findExportByName('libc.so', 'mprotect'), 'int', ['pointer', 'size_t', 'int']);
const free = new NativeFunction(Module.findExportByName('libc.so', 'free'), 'void', ['pointer']);
function readwritebreak(addr, size, pattern){
 
    var point1= addr-(addr%0x1000)
    console.log("set memcpy break : ",ptr(point1))
 
    const mycode = malloc(0x1000)
    mprotect(mycode,0x1000,7)
    memset(mycode,0x1000,0)
 
    //构建code
    mycode.add(0x4).writeU32(0xD10943FF)//SUB SP ,SP ,#0x250
    mycode.add(0x8).writeU32(0xA90077E8)//STP X8,X29,[SP]
    mycode.add(0xc).writeU32(0xA90107E0)//STP X0 ,X1 ,[SP,#0x10]
    mycode.add(0x10).writeU32(0xA9020FE2)//STP X2,X3,[SP,#0x20]
    mycode.add(0x14).writeU32(0x58000760)//LDR X0 , [mycode,#0x100]
    mycode.add(0x18).writeU32(0x58000781)//LDR X1 , [mycode,#0x108]
    mycode.add(0x1C).writeU32(0x580007A2)//LDR X2 , [mycode,#0x110]
    mycode.add(0x20).writeU32(0x580007C3)//LDR X3 , [mycode,#0x118]
    mycode.add(0x24).writeU32(0xD63F0060)//BLR X3
    mycode.add(0x28).writeU32(0xA9420FE2)//LDP X2, X3,[SP,#0x20]
    mycode.add(0x2C).writeU32(0xA94107E0)//LDP X0, X1,[SP,#0x10]
    mycode.add(0x30).writeU32(0xA94077E8)//LDP X8, X29,[SP]
    mycode.add(0x34).writeU32(0x910943FF)//ADD SP, SP, #0x250
    mycode.add(0x38).writeU32(0x5800075E)//LDR X30, [mycode,#0x120]
    mycode.add(0x3C).writeU32(0xD65F03C0)//RET
 
    //将point1,0x1000,pattern放入mycode+0x100
    mycode.add(0x100).writePointer(ptr(point1))
    mycode.add(0x108).writeU64(0x1000)
    mycode.add(0x110).writeU64(pattern)
    //mprotect函数存入0x118
    mycode.add(0x118).writePointer(ptr(mprotect))
    //修改目标内存页属性
    mprotect(ptr(point1),0x1000,pattern)
    
    Process.setExceptionHandler(function(details){
        if(details.type.indexOf("access-violation") >= 0){
            var mempoint=ptr(details.memory.address)
            //判断是否是由自己修改内存导致的异常
            if(point1<=mempoint&&mempoint<point1+0x1000){
                //是否命中我们关心的地址
                var off=ptr(mempoint).sub(addr)
                if(off>=0&&off<size){
                    console.warn("命中 :" ,ptr(addr)," pc pointer : ",details.address)
                    var module = Process.findModuleByAddress(ptr(details.context.pc));
                    console.warn("pc - - > ",module.name," -> ",ptr(details.context.pc).sub(module.base))
                    mprotect(ptr(point1),0x1000,7)
                    free(mycode)
                    //console.error('RegisterNatives called from:\n' +Thread.backtrace(details.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');
                    console.warn("readwritebreak exit")
                    return true
 
                }else{
                    console.log(details.memory.operation,"exce ;mpoint :",mempoint,";pc -> ",ptr(details.context.pc),"; opcode :",ptr(ptr(details.context.pc).readU32()),"\n"
                    //将内存页属性改回来
                    mprotect(ptr(point1),0x1000,7)
 
                    //将下一个code地址写入mycode+0x120处作为返回地址
                    mycode.add(0x120).writePointer(ptr(details.context.pc).add(4))
 
                    var thiscode=ptr(details.context.pc).readU32()
                    var opcode=((thiscode>>24)&0x3b)
                    //三个pc偏移寻址共同位
                    if(opcode==24){
                        //将需要读取的数据存入mycode+0x128
                        mycode.add(0x128).writePointer(ptr(mempoint.readPointer()))
                        mycode.add(0x130).writePointer(ptr(mempoint.add(8).readPointer()))
                        //LDR Rn #pc+0x128
                        var n_code=(thiscode&0xFF00001F)|0x940
                        mycode.writeU32(n_code)
                        details.context.pc = mycode
                    }else{
                       //将当前code写入mycode
                        mycode.writeU32(ptr(details.context.pc).readU32())
                        //直接修改pc
                        details.context.pc = mycode
                    }
                    return true
                }
            }
            return false
        }
        return false
    })
}
const malloc = new NativeFunction(Module.findExportByName('libc.so', 'malloc'), 'pointer', ['size_t']);
const memset = new NativeFunction(Module.findExportByName('libc.so', 'memset'), 'pointer', ['pointer', 'size_t', 'int']);
const mprotect = new NativeFunction(Module.findExportByName('libc.so', 'mprotect'), 'int', ['pointer', 'size_t', 'int']);
const free = new NativeFunction(Module.findExportByName('libc.so', 'free'), 'void', ['pointer']);
function readwritebreak(addr, size, pattern){
 
    var point1= addr-(addr%0x1000)
    console.log("set memcpy break : ",ptr(point1))
 
    const mycode = malloc(0x1000)
    mprotect(mycode,0x1000,7)
    memset(mycode,0x1000,0)
 
    //构建code
    mycode.add(0x4).writeU32(0xD10943FF)//SUB SP ,SP ,#0x250
    mycode.add(0x8).writeU32(0xA90077E8)//STP X8,X29,[SP]

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 6
支持
分享
最新回复 (12)
雪    币: 893
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
牛逼,看不懂
2023-10-13 16:59
0
雪    币: 7
活跃值: (496)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
firda 改pc:
mov x16,xx
br  x16
2023-10-16 20:33
2
雪    币: 3303
活跃值: (30941)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2023-10-17 09:18
1
雪    币: 1441
活跃值: (5670)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
5

frida 也有这个类似的api实现,不过楼主的思路也是指的学习的 


参考:

memoryaccessmonitor API

impl in gum



你上面frida关于写指令部分也可以考虑使用 Arm64Writer

impl 参考 这里


最后于 2023-10-18 17:46 被唱过阡陌编辑 ,原因:
2023-10-17 10:21
0
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
唱过阡陌 你肯定没有完整的看过frida文档frida 本来就有这个api实现也和你这个差不多,在arm64下还可以,arm32容易崩参考:memoryaccessmonitor APIimpl in gum你 ...
额 虽然实现思路差不多 但是效果其实还是不一样的 官方的实现首先不精准 正如楼主所说 mprotect改变的是整页的内存属性 在这一页中对任何一个地方的访问都会抛出异常 而官方实现并不会对其进行过滤 其次官方实现触发后一次后就会自动失效 所以总体来说体验及其不好 楼主的实现是一个强有力的拓展和补充 很有意义
2023-10-18 16:12
1
雪    币: 3269
活跃值: (3039)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
7
很好的思路。
2023-10-21 20:16
0
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
//LDR Rn #pc+0x128
var n_code=(thiscode&0xFF00001F)|0x940
这个地方我有一点小小的疑惑 为啥会是按位或0x940(即0x128<<3) 而不是 0x128<<5呢?
2024-10-5 17:30
0
雪    币: 1841
活跃值: (1290)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
9
万里星河 //LDR Rn #pc+0x128 var n_code=(thiscode&0xFF00001F)|0x940 这个地方我有一点小小的疑惑 为啥会是按位或0x940(即0x128
不是(0x128 <<3),arm指令默认4字节对齐,所以是0x4a << 5
2024-10-9 22:05
1
雪    币: 2477
活跃值: (4586)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
frida现已支持硬件断点和观察点
2024-10-10 08:47
0
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
yezheyu 不是(0x128
懂了 大佬这篇文章着实精彩 受教了
2024-10-10 17:26
0
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
默NJ frida现已支持硬件断点和观察点
我用过 但似乎不支持安卓 官方的例子是windows的
2024-10-10 17:27
0
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
大佬,对一个apk的协议分析那篇文章的样本,网上现在都找不到了,都是sgmain6.6以后的, 能发个那篇文章的样本吗
2024-11-10 19:02
0
游客
登录 | 注册 方可回帖
返回
//