本文最后更新于:1 个月前
安装
使用pip install frida-tools
来进行安装

adb安装:https://dl.google.com/android/repository/platform-tools-latest-windows.zip
在mumu模拟器右上角菜单打开-》问题诊断可以看到adb调试端口

随后执行adb connect 127.0.0.1:16384

随后输入adb devices
可看到已连接设备

执行adb shell getprop ro.product.cpu.abi
查看系统型号

下一步便是下载对应版本的frida-server
github地址:https://github.com/frida/frida/releases

下载下来解压后,运行adb push frida-server-16.6.6-android-x86_64 /data/local/tmp
,将frida-server放到模拟器机子里
adb shell
进入模拟器
su
以root身份操作(你可能遇到的问题:https://www.cnblogs.com/wutou/p/18255999;或者回去看一眼模拟器,可能会让你勾选确认之类的)
cd /data/local/tmp
chmod 755 frida-server-16.6.6-android-x86_64
./frida-server-16.6.6-android-x86_64
运行frida-server
启动完frida-server之后,保持server窗口,另起一个,执行frida-ps -U
,应当显示的是当前USB连接设备(这里指的是模拟器)的进程情况,或者frida-ps -R
(remote),表示通过网络远程连接的设备的进程情况,若-R不行,则需要进行一次端口转发adb forward tcp:27042 tcp:27042
;adb forward tcp:27043 tcp:27043
,将模拟器的27042端口转发到windows上,

至此配置方面已经准备完毕,接下来通过实战来感受一下
实战
来个经典石头剪刀布游戏:
链接
frida-ps -U
找到进程号

frida -U -p 3897
连接进程

使用jadk进行反编译

一边玩游戏一边看代码,其实主要逻辑很简单
n是玩家出的,m是机器出的,当玩家出的选项的值比机器大1时(游戏设定中假设石头-剪刀=1,剪刀-布=1,若判断游戏石头玩家布的时候就得走到下面那个else,直接判断玩家胜利),cnt为胜利局数,若数值相等则判断平局,若玩家输一把,全部的cnt清零。最终胜利条件需要玩家连续胜利1000局,理论上几乎不可能正常通关

主要逻辑弄明白了,接下来主场就来到了frida,这个东西语法就是写js

安装apktool
https://apktool.org/docs/install
执行apktool d game.apk
,会在目录下生成一个game目录

查看反编译后查看AndroidManifest.xml查看程序的入口Activity,下面hook用

hook方法
先学会编写代码,安装库
安装在全局的,使用如下命令找到全局node module的目录,直接把frida-gum拖到项目目录下
vscode在项目目录下创建一个js,就能在js里编写代码了,并能够自动补全frida相关
拿个例子来看,use主类
1 2 3 4 5 6
| Java.perform(function () { var MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity'); MainActivity.onClick.implementation = function (v) { send("hook start"); } });
|
开启hook之后,玩一下游戏,回显

对onClick进行hook,打印出this.m的值,也就是玩家的输入的值,也就能看到对应的石头剪刀布对应的值了
调用this.onClick()是为了确保程序的正常运行
1 2 3 4 5 6 7 8 9
| Java.perform(function(){ var MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity"); MainActivity.onClick.implementation = function(v){ this.onClick(v); console.log(this.m.value); } })
|
先了解一下现在主要三个方法中在一次进程周期中的执行顺序:
1
| onCreate(Bundle savedInstanceState)->onClick(View v)->run()
|
onCreate()方法是在Activity被创建时调用的,这个方法是安卓应用生命周期的一部分,当用户启动应用或者切换到MainActivity时,onCreate()最先被调用,负责UI的初始化,设置按钮的监听器等等,并为按钮的点击事件定义了行为
执行内容:
- 设置布局
setContentView()
- 获取布局中按钮控件的引用
- 为每个按钮设置点击事件监听器
setOnClickListener
,因为MainActivity实现了View.onClickListenser,所以可以作为按钮的点击事件处理程序,当用户点击其中一个按钮,onClick()方法将被触发
- 初始化一些变量
当用户按下按钮后,onClick将被触发,根据用户的输入获取到对应的m的数值
run()是Handler中的Runnable任务的一部分,在onClick()中调用handler.postDelayed(showMessageTask, 1000L)
来启动一个延时任务,让showMessageTask中的run()方法在1秒后执行,然后根据m和n,判断玩家的胜负,再计算cnt
在判断逻辑中,有n-m为1的情况下会win,并且cnt加1

那就写出如下代码,hook一下onClick(),让n-m的值始终为1
1 2 3 4 5 6 7 8 9 10 11
| Java.perform(function(){ var MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity"); MainActivity.onClick.implementation = function(v){ this.onClick(v); this.n.value = 2; this.m.value = 1; console.log(this.m.value); } })
|
这样不管最开始你点击的是什么,最后都是win,

当然,如果这么点击1000次当然可以,但是并没有必要
直接对cnt的值也一起修改
1 2 3 4 5 6 7 8 9 10 11 12
| Java.perform(function(){ var MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity"); MainActivity.onClick.implementation = function(v){ this.onClick(v); this.n.value = 2; this.m.value = 1; this.cnt.value = 999; console.log(this.m.value); } })
|
获取到flag

调试的时候除console.log还能使用send(),让服务端向python端发送消息,使用script.on()来监听事件并调用回调函数
常见监听事件:
- message:接收Frida脚本发送到主机的消息,可以使用send()函数触发该事件
- destroyed:脚本被销毁时触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 。。。。 def on_message(message, data): print(message) 。。。。 jscode = """
Java.perform(function(){ var MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity");
MainActivity.onClick.implementation = function(v){ this.onClick(v); this.n.value = 2; this.m.value = 1; this.cnt.value = 999; send(this.m.value);//这里调用send() }
}) """ 。。。。 script.on('message', on_message)
|
附py脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import frida, sys
def on_message(message, data): print(message)
jscode = """
Java.perform(function(){ var MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity");
MainActivity.onClick.implementation = function(v){ this.onClick(v); this.n.value = 2; this.m.value = 1; this.cnt.value = 999; send(this.m.value); }
}) """
device = frida.get_usb_device(1000)
process = device.attach(4358) script = process.create_script(jscode) script.on('message', on_message) script.load()
sys.stdin.read()
|
两种启动模式
https://frida.re/docs/modes/
也可以使用如下代码,-f表示Embedded模式启动,此时不论app是否启动,都会重新启动app
1
| frida -D 127.0.0.1:16384 -l test.js -f com.example.seccon2015.rock_paper_scissors
|
若不加-f参数,则为Injected模式,此时frida需要通过已启动的进程来注入
hook内部类
在frida中,内部类用$来表示
在jadx反编译的时候,jadx提示:

因此showMessageTask类可以表示为
1
| var showMessageTask = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity$1");
|
但是这时候获取m和n就不能像上面hook MainActivity时一样用this.m了,因为showMessageTask是内部类,
Java 会为每个内部类自动创建一个指向外部类实例的字段,通常命名为 this$0
console.log(this.this$0);
时,回显结果

我们需要的正是其中的value的部分
因此通过console.log(this.this$0.value.m.value);
获取即可,但是注意这里对属性值的修改需要在run调用之前
1 2 3 4 5 6 7 8 9 10 11
| Java.perform(function(){ var showMessageTask = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity$1"); showMessageTask.run.implementation = function(){ this.this$0.value.m.value = 1; this.this$0.value.n.value = 2; this.this$0.value.cnt.value = 999; this.run(); } })
|
直接调用方法
在下图代码能看到flag内容的逻辑

因此:
1 2 3 4 5 6 7 8
| Java.perform(function(){ var MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity"); MainActivity.onClick.implementation = function(v){ this.onClick(v); console.log((1000 + this.calc())*107); } })
|

调用共享库
Process.enumerateModules()
用于列举当前进程的所有调用模块
Modules.enumerateExports()
用于列举一个已加载模块中的所有符号,包括函数、变量、类等的导出
更多的用法vscode会自动补全,在灵恩据意思用就行
编写以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Java.perform(function(){ var MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity"); MainActivity.onClick.implementation = function(v){ this.onClick(v); var modules = Process.enumerateModules(); for(var i in modules){ if (modules[i].name == "libcalc.so"){ var exports = modules[i].enumerateExports(); for(var j in exports){ console.log(exports[j].name); } } } } })
|
控制台会输出,很经典的jni格式

后续补充:
在jadx的资源里也能看到对应库的名称

使用Module.findExportByName()
能够获取到对应方法的地址

1 2 3 4 5 6 7 8
| Java.perform(function(){ var MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity"); MainActivity.onClick.implementation = function(v){ this.onClick(v); var addr = Module.findExportByName("libcalc.so","Java_com_example_seccon2015_rock_1paper_1scissors_MainActivity_calc"); console.log(addr); } })
|
使用NativeFunction对地址进行调用,NativeFunction的定义如下:
必须接受的参数有三个,一个是地址,第二个是返回值(”void”,”int”,”string”,”pointer”等等),第三个是传入一个数组,表示函数的参数类型

从jadx的反编译结果能够看出calc()方法的返回值为int,且无参数传入

因此编写payload:
同样可以输出调用calc()的结果
1 2 3 4 5 6 7 8 9 10
| Java.perform(function(){ var MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity"); MainActivity.onClick.implementation = function(v){ this.onClick(v); var addr = Module.findExportByName("libcalc.so","Java_com_example_seccon2015_rock_1paper_1scissors_MainActivity_calc"); console.log(addr); var func = new NativeFunction(addr,"int",[]); console.log(func()); } })
|

靶场
做个靶场巩固巩固
https://github.com/DERE-ad2001/Frida-Labs
0x1
是一个猜数字游戏

jadx反编译一下,注意到判断结果的check()函数,胜利条件为i*2 + 4 == i2,
都看到这里了,应该很容易想到怎么搞吧
劫持check()方法,然后手动传入两个特定的值满足i*2 + 4 == i2即可

1 2 3 4 5 6
| Java.perform(function(){ var MainActivity = Java.use("com.ad2001.frida0x1.MainActivity"); MainActivity.check.implementation = function(a,b){ this.check(0,4); } })
|

0x2
emmm,暂时不清楚做什么的,反编译一下再说吧

有这么一个函数,传入a为4919,aes解密之后输出一个字符串,从方法名称猜测应该是输出flag

但是这里的代码环境中并没有直接输出flag的渠道,也没有调用get_flag()的地方
那我们就直接hook这个方法,并把结果用console.log()打印出来
1 2 3 4 5 6
| Java.perform(function(){ var MainActivity = Java.use("com.ad2001.frida0x2.MainActivity"); MainActivity.get_flag.implementation = function(a,b){ console.log(this.get_flag(4919)); } })
|
然后一片风平浪静

前面提到过,onCreate()在创建视图的时候调用,但是这时候程序已经跑起来了,页面中也没有其他地方让我们重新加载视图的,怎么办呢
到处翻资料的时候我找到了一个api:Java.choose(),用于查找所有指定类的实例:
它的onMatch()回调会在匹配到存在这样的实例的时候触发

于是我写下下面代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Java.perform(function(){ var MainActivity = Java.use("com.ad2001.frida0x2.MainActivity"); MainActivity.onCreate.implementation = function(a,b){ console.log(this.get_flag(4919)); } Java.choose("com.ad2001.frida0x2.MainActivity",{ onMatch(obj) { console.log(obj); console.log(obj.get_flag(4919)); }, onComplete(obj){ console.log("gg") } }) })
|
但是为什么结果为undefined呢?

我们回到get_flag这个方法,这玩意实际上是一个void的,,好吧大意了

正当我思考怎么把decryptedText输出的时候,我回去看了眼模拟器:
6666666

再仔细看一眼:
ok,认栽,,先睡觉,明天继续

后续看看怎么输出局部变量decryptedText,后续:有空了学学smail语法,然后用ide可以远程调试,肯定能看到中间变量,或者这里可以用后面学的拦截器对调用decryptedText的函数进行劫持,打印出参数
后续:这道题是静态函数啊,,直接获取到类hook函数就行了!!
0x3
打开游戏,点击click me,显示try again,没了

看看反编译
这里的点击事件是通过在onCreate()里创建了一个内部类,内部类里进行了onClick的重写
判断条件是Checker.code为512

Checker.code是另一个类里的一个静态变量

很容易就能hook到这个onClick方法
1 2 3 4 5 6
| Java.perform(function(){ var innerClass = Java.use("com.ad2001.frida0x3.MainActivity$1"); innerClass.onClick.implementation = function(){ console.log("nothing"); } })
|
对于静态变量直接修改值,然后调用原来的onClick()即可
1 2 3 4 5 6 7 8 9
| Java.perform(function(){ var innerClass = Java.use("com.ad2001.frida0x3.MainActivity$1"); innerClass.onClick.implementation = function(v){ var checker = Java.use("com.ad2001.frida0x3.Checker"); checker.code.value = 512; this.onClick(v); } })
|

0x4
没有什么交互

看反编译
MainActivity里没什么重要信息

但是在Check类中发现了get_flag()方法

但是这里没什么交互,就没办法通过Java.use来对MainActivity进行hook
和0x2类似的,我们用Java.choose()
1 2 3 4 5 6 7 8 9 10 11 12
| Java.perform(function(){ Java.choose("com.ad2001.frida0x4.MainActivity",{ onMatch(obj){ console.log(obj); var check = Java.use("com.ad2001.frida0x4.Check"); console.log(check.get_flag(1337)); }, onComplete(){ } }) })
|
出现报错:
哦~get_flag()这里不是静态方法忘记实例化了

使用$new()进行实例化
1 2 3 4 5 6 7 8 9 10 11 12 13
| Java.perform(function(){ Java.choose("com.ad2001.frida0x4.MainActivity",{ onMatch(obj){ console.log(obj); var check = Java.use("com.ad2001.frida0x4.Check"); var checkInstance = check.$new(); console.log(checkInstance.get_flag(1337)); }, onComplete(){ } }) })
|

0x5
一样的,没有交互

MainActivity中,逻辑如下,好像比前面的还简单
Java.choose获取到mainActivity实例后,直接调用实例的flag()方法似乎就行了

于是乎
1 2 3 4 5 6 7 8 9 10
| Java.perform(function(){ Java.choose("com.ad2001.frida0x5.MainActivity",{ onMatch(obj){ obj.flag(1337); }, onComplete(){
} }) })
|

0x6
无交互

看源码和上一题几乎一样,唯一的不同就是get_flag()方法传入的是一个Checker对象,然后对这个对象的值进行判断

一气呵成
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Java.perform(function(){ Java.choose("com.ad2001.frida0x6.MainActivity",{ onMatch(obj){ var checker = Java.use("com.ad2001.frida0x6.Checker"); var checkerInstance = checker.$new(); checkerInstance.num1.value = 1234; checkerInstance.num2.value = 4321; obj.get_flag(checkerInstance); }, onComplete(){
} }) })
|

0x7
无交互

是先在onCreate()内调用了一次的flag(),并传入不会输出flag的一个Checker对象
那其实和0x6没区别

不过这里的区别在于Checker这次有构造方法了,所以可以直接在$new()里赋值
1 2 3 4 5 6 7 8 9 10 11 12 13
| Java.perform(function(){ Java.choose("com.ad2001.frida0x7.MainActivity",{ onMatch(obj){ var checker = Java.use("com.ad2001.frida0x7.Checker"); var checkerInstance = checker.$new(513,513); obj.flag(checkerInstance); }, onComplete(){
} }) })
|

0x8
给了个文本框,随便输入点什么提示try again

这里胜利的条件为令cmpstr的返回值res为1,cmpstr传入我们输入的文本

但是很不幸,这里的cmpstr是通过jni从共享库中导入的,在不通过ida的情况下,我们并不清楚这个方法的内部逻辑,即使能hook到,怎样控制返回值呢?

翻到了这么个api(一点搜索技巧,搜return或者retval)
称之拦截器,作用根据描述,对一个函数进行拦截,传入两个参数,第一个为一个NativePointer对象,也就是我们上文在石头剪刀布中提到的那个地址。
并在onEnter()内重定义进入函数的操作,在onLeave()内重定义返回的操作

先这样在hook onClick()的时候获取到MainActivity对象
1 2 3 4 5 6 7
| Java.perform(function(){ var innerClass = Java.use("com.ad2001.frida0x8.MainActivity$1"); innerClass.onClick.implementation = function(v){ console.log(this.this$0); this.onClick(v); } })
|
获取所有加载模块
1 2 3 4 5 6 7 8 9 10 11
| Java.perform(function(){ var innerClass = Java.use("com.ad2001.frida0x8.MainActivity$1"); innerClass.onClick.implementation = function(v){ console.log(this.this$0); var modules = Process.enumerateModules(); for(var i in modules){ console.log(modules[i].name) } this.onClick(v); } })
|

获取导出函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Java.perform(function(){ var innerClass = Java.use("com.ad2001.frida0x8.MainActivity$1"); innerClass.onClick.implementation = function(v){ console.log(this.this$0); var modules = Process.enumerateModules(); for(var i in modules){ if(modules[i].name == 'libfrida0x8.so'){ var exports = modules[i].enumerateExports(); for(var j in exports){ console.log(exports[j].name) } }
} this.onClick(v); } })
|

照着文档的说明,对返回值进行替换

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Java.perform(function(){ var innerClass = Java.use("com.ad2001.frida0x8.MainActivity$1"); innerClass.onClick.implementation = function(v){
var addr = Module.findExportByName("libfrida0x8.so","Java_com_ad2001_frida0x8_MainActivity_cmpstr") Interceptor.attach(addr,{ onEnter(args){ var str = Java.cast(args[2], Java.use('java.lang.String')); console.log(str) }, onLeave(retval){ retval.replace(1) } }) this.onClick(v); } })
|
那我flag呢)

再回去看了眼代码逻辑
如果我们的输入和flag的值相同,他会输出flag

思索了很久没有想法,跑去看了一眼官方solution
震惊我,,直接对libc中strcmp进行hook,,看来之后思路要打开了
用ida打开共享库,分析Java_com_ad2001_frida0x8_MainActivity_cmpstr
这个方法,其中strcmp的两个参数,一个直接就是我们输入的内容了,另一个经过简单的加密后和s1进行比较,虽然可以直接用凯撒解密,将GSJEB|OBUJWFMBOE~
这个字符串ascii依次减1来获得flag,但是我们还是用frida来尝试完成

对strcmp进行hook,第一个参数是输入字符串,第二个参数是被比较字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Java.perform(function(){ var innerClass = Java.use("com.ad2001.frida0x8.MainActivity$1"); innerClass.onClick.implementation = function(v){
var cmprAddr = Module.findExportByName("libc.so","strcmp") Interceptor.attach(cmprAddr,{ onEnter(args){ var str = Memory.readUtf8String(args[0]); if(str == "abcd"){ console.log(Memory.readUtf8String(args[1])); } }, onLeave(retval){ } }) this.onClick(v); } })
|
当我们传入abcd的时候就能看到被比较的字符串,也就是flag了

0x9
点击按钮提示Try Again

这道题似乎只需要简单劫持一下返回值就行了

获取导出表中函数名
1 2 3 4 5 6 7 8 9 10 11 12
| Java.perform(function(){ var innerClass = Java.use("com.ad2001.a0x9.MainActivity$1"); innerClass.onClick.implementation = function(v){ console.log("hook"); var module = Process.getModuleByName("liba0x9.so") console.log(module) var exports = module.enumerateExports() for(var i in exports){ console.log(exports[i].name) } } })
|

然后直接拦截返回值就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Java.perform(function(){ var innerClass = Java.use("com.ad2001.a0x9.MainActivity$1"); innerClass.onClick.implementation = function(v){ console.log("hook"); var check_flag = Module.findExportByName("liba0x9.so","Java_com_ad2001_a0x9_MainActivity_check_1flag"); Interceptor.attach(check_flag,{ onEnter(args){
}, onLeave(retval){ retval.replace(1337) } }) this.onClick(v) } })
|

0xA
无交互

看一眼反编译代码,我们实际上是可以直接调用这个native方法的

1 2 3 4 5 6 7 8 9 10 11
| Java.perform(function(){ Java.choose("com.ad2001.frida0xa.MainActivity",{ onMatch(obj){ console.log(obj.stringFromJNI()); }, onComplete(obj){
} }) })
|
好吧居然是这个玩意

(偷偷拿出ida看一眼,确实就是这样了并没有暗藏玄机))

问题来了,flag呢?
再仔细找了一下,我找到了另一个函数,get_flag,显然这里就是关键(这里共享库拿c++写的,所以一堆的各种函数,)

但是我们并没有在java定义中引入这个函数,因此不能直接用,不过上面的石头剪刀布的例子里我们可以直接用NativeFunction()来调用
导出表看的眼睛花了,,直接全部复制出来记事本里搜get_flag

成功拿到对应的NativePointer的地址
1 2 3 4 5 6 7 8 9 10 11
| Java.perform(function(){ Java.choose("com.ad2001.frida0xa.MainActivity",{ onMatch(obj){ var addr = Module.getExportByName("libfrida0xa.so","_Z8get_flagii"); console.log(addr) }, onComplete(obj){
} }) })
|

再回到get_flag代码看一眼
输出flag相关的需要满足条件a1+a2=3,但是输出呢?这里的flag输出在安卓日志中,
这里其实除了是有个想法的,就是直接根据内存偏移,通过a1a2的内存地址直接推出v4的地址并打印出来,临时变量存放在栈上
(未成功,,二进制相关暂时不是很熟悉)

回到android日志相关的方法
在网上看到了adb shell内集成了工具logcat
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Java.perform(function(){ Java.choose("com.ad2001.frida0xa.MainActivity",{ onMatch(obj){ var addr = Module.getExportByName("libfrida0xa.so","_Z8get_flagii");
var get_flag = new NativeFunction(addr,"void",["int","int"]) get_flag(0,3) }, onComplete(obj){
} }) })
|

那我能不能hook这个__android_log_print
函数呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| Java.perform(function(){ Java.choose("com.ad2001.frida0xa.MainActivity",{ onMatch(obj){ var addr = Module.getExportByName("libfrida0xa.so","_Z8get_flagii");
var logAddr = Module.findExportByName("liblog.so", "__android_log_print"); console.log(logAddr); var get_flag = new NativeFunction(addr,"void",["int","int"]); var log = new NativeFunction(logAddr,"int",["int","int","char"]); Interceptor.attach(logAddr,{ onEnter(args){ console.log(Memory.readCString(args[2])); }, onLeave(retval){
} }) get_flag(0,3) }, onComplete(obj){
} }) })
|
但是没有格式化23333

0xB
这回更是重量级,点都没反应的

有一个native的getFlag()方法

去ida看看
我函数体呢?

从反汇编代码里,注意到函数的开头,先将0x0DEADBEEF传入rbp+var_24的地址上,随后将该值和0x539进行比较,随后判断二者是否相等,不相等直接执行跳转到loc_171A6的代码

而0x0DEADBEEF肯定不能等于0x539,所以跳转无法避免,而这个loc_171A6的地方,直接跳到函数尾了,,自行体会,所以ida没办法正确反编译出函数体

将jnz的地方直接nop掉

于是乎就能看到函数体辣
可以看到基本就是做了下异或之后,直接在日志中输出了flag了,并没有额外的条件判断

到这里问题就转化成了,如何才能在内存中跳过那个恒假的判断?
frida中直接操作内存的api不多,这个X86Writer很快就吸引住了我,根据官方描述是能够直接往指定内存上写入机器码(若是arm架构的也有ArmWriter)
https://frida.re/docs/javascript-api/#x86writer

在ida获取到jnz指令的偏移0x170ce

在frida可以通过Module.findBaseAddress()来获取基址,并通过add()来加上偏移,得到jnz指令所在的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Java.perform(function(){ Java.choose("com.ad2001.frida0xb.MainActivity",{ onMatch(obj){ var addr = Module.getExportByName("libfrida0xb.so","Java_com_ad2001_frida0xb_MainActivity_getFlag"); Interceptor.attach(addr,{ onEnter(args){ var baseAddr = Module.findBaseAddress("libfrida0xb.so"); var jnzAddr = baseAddr.add(0x170ce) }, onLeave(retval){
} }) obj.getFlag(); }, onComplete(obj){
} }) })
|
X86Writer提供了很多写入指令的接口,类似于上面ida的处理方式,可以在jnz处写入nop

调用putNop写入nop指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| Java.perform(function(){ Java.choose("com.ad2001.frida0xb.MainActivity",{ onMatch(obj){ var addr = Module.getExportByName("libfrida0xb.so","Java_com_ad2001_frida0xb_MainActivity_getFlag"); Interceptor.attach(addr,{ onEnter(args){ var baseAddr = Module.findBaseAddress("libfrida0xb.so"); var jnzAddr = baseAddr.add(0x170ce); var writer = new X86Writer(jnzAddr); writer.putNop() }, onLeave(retval){
} }) obj.getFlag(); }, onComplete(obj){
} }) })
|
欸不是怎么炸了

网上寻找资料无果后,尝试询问gpt,其中一个方案引起了我的兴趣

在官方api文档中,是这么描述Memory.protect()这个接口的:
可以改变一段内存区域上的读写权限

但是还是炸了,怎么回事呢?
看了眼官方的题解恍然大悟,这里得手动写入6次nop!
于是改了下,但是我发现我这里不需要设置读写权限也行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| Java.perform(function(){ Java.choose("com.ad2001.frida0xb.MainActivity",{ onMatch(obj){ var addr = Module.getExportByName("libfrida0xb.so","Java_com_ad2001_frida0xb_MainActivity_getFlag"); Interceptor.attach(addr,{ onEnter(args){ var baseAddr = Module.findBaseAddress("libfrida0xb.so"); var jnzAddr = baseAddr.add(0x170ce); var writer = new X86Writer(jnzAddr); writer.putNop() writer.putNop() writer.putNop() writer.putNop() writer.putNop() writer.putNop() console.log(jnzAddr) }, onLeave(retval){
} }) obj.getFlag(); }, onComplete(obj){
} }) })
|

或者写入jmp直接跳到下面的指令中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| Java.perform(function(){ Java.choose("com.ad2001.frida0xb.MainActivity",{ onMatch(obj){ var addr = Module.getExportByName("libfrida0xb.so","Java_com_ad2001_frida0xb_MainActivity_getFlag"); Interceptor.attach(addr,{ onEnter(args){ var baseAddr = Module.findBaseAddress("libfrida0xb.so"); var jnzAddr = baseAddr.add(0x170ce); var target = baseAddr.add(0x170d4) var writer = new X86Writer(jnzAddr); Memory.protect(jnzAddr,0x1000,'rwx'); writer.putJmpAddress(target) console.log(jnzAddr) }, onLeave(retval){
} }) obj.getFlag(); }, onComplete(obj){
} }) })
|

至此,经过两天的奋斗,frida应该算是入门了(?)
前路漫漫仍需努力啊
参考文章:
APP逆向神器之Frida【Android初级篇】
ADB安装及使用详解
MuMu模拟器12如何连接adb?
frida jsapi
https://cn-sec.com/archives/1801747.html