很早之前看过一篇帖子,使用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,
LDR X0 , [X1]
/
/
x0
=
*
x1;
LDR X0 , [X1]
/
/
x0
=
*
x1;
LDR X0, [X1,
STR
X0, [SP,
LDR X0 ,[SP],
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
位,然后只需要
24
,
25
,
27
,
28
,
29
的值即
0x3b
var opcode
=
((thiscode>>
24
)&
0x3b
)
/
/
判断opcode是不是等于
0x18
if
(opcode
=
=
24
){
/
/
获取当前code
var thiscode
=
ptr(details.context.pc).readU32()
/
/
去掉后面
24
位,然后只需要
24
,
25
,
27
,
28
,
29
的值即
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 ,
mycode.add(
0x8
).writeU32(
0xA90077E8
)
/
/
STP X8,X29,[SP]
mycode.add(
0xc
).writeU32(
0xA90107E0
)
/
/
STP X0 ,X1 ,[SP,
mycode.add(
0x10
).writeU32(
0xA9020FE2
)
/
/
STP X2,X3,[SP,
mycode.add(
0x14
).writeU32(
0x58000760
)
/
/
LDR X0 , [mycode,
mycode.add(
0x18
).writeU32(
0x58000781
)
/
/
LDR X1 , [mycode,
mycode.add(
0x1C
).writeU32(
0x580007A2
)
/
/
LDR X2 , [mycode,
mycode.add(
0x20
).writeU32(
0x580007C3
)
/
/
LDR X3 , [mycode,
mycode.add(
0x24
).writeU32(
0xD63F0060
)
/
/
BLR X3
mycode.add(
0x28
).writeU32(
0xA9420FE2
)
/
/
LDP X2, X3,[SP,
mycode.add(
0x2C
).writeU32(
0xA94107E0
)
/
/
LDP X0, X1,[SP,
mycode.add(
0x30
).writeU32(
0xA94077E8
)
/
/
LDP X8, X29,[SP]
mycode.add(
0x34
).writeU32(
0x910943FF
)
/
/
ADD SP, SP,
mycode.add(
0x38
).writeU32(
0x5800075E
)
/
/
LDR X30, [mycode,
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
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 ,
mycode.add(
0x8
).writeU32(
0xA90077E8
)
/
/
STP X8,X29,[SP]
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!