Android逆向入门-Frida

本文最后更新于:3 小时前

安装

使用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:27042adb 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方法

先学会编写代码,安装库

1
npm  i  @types/frida-gum

安装在全局的,使用如下命令找到全局node module的目录,直接把frida-gum拖到项目目录下

1
npm root -g

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();
// console.log(this.this$0.value.m.value);
}
})

直接调用方法

在下图代码能看到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);
// console.log(Process.enumerateModules());
// console.log((1000 + this.calc())*107);
var modules = Process.enumerateModules();
for(var i in modules){
// console.log(modules[i].name);
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);
// console.log("nothing");
}
})

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);
// console.log(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){
// console.log(modules[i].name);
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")
// console.log(addr);
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")
// console.log(cmprAddr);
Interceptor.attach(cmprAddr,{
onEnter(args){
// console.log(args[0])
var str = Memory.readUtf8String(args[0]);
if(str == "abcd"){
console.log(Memory.readUtf8String(args[1]));
}
// console.log(str)
},
onLeave(retval){
// retval.replace(1)
}
})
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");
// console.log(check_flag)
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)
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"])
// console.log(get_flag)
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){

}
})
// console.log(get_flag)
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");
// console.log(addr);
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");
// console.log(addr);
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");
// console.log(addr);
Interceptor.attach(addr,{
onEnter(args){
var baseAddr = Module.findBaseAddress("libfrida0xb.so");
var jnzAddr = baseAddr.add(0x170ce);
var writer = new X86Writer(jnzAddr);
// Memory.protect(jnzAddr,0x1000,'rwx');
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");
// console.log(addr);
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