Frida开发思想

定位:Objection辅助定位

  Objection在逆向过程中最强大的功能是从海量的代码中定位关键的程序逻辑。Frida需要每次手动编写代码取Hook静态分析的函数,进而观察其参数和返回值是否与需求相符。,Objection将常用的一些功能集成在一起,使得逆向开发和分析人员在分析过程中不需要浪费精力在编写代码上。

  以Junior为例Junior
  使用adb命令将Junior.apk安装并启动,首先使用Objection遍历App所有的activity

1
adb install -t junior.apk


  安装和遍历后发现,这个App共有17个activity,这里选择分析的是activity为计算器的相关活动com.example.junior.CalculatorActivity,并用指令去启动这个活动。

  同时手机上的activity也被成功启动了,打开了计算器页面。
  接下来选取减法作为我们的分析目标,利用Jadx反编译,找到CalculatorActivity,在其onCreate()函数中可以看到注册了很多事件

经过测试发现,每次按“等号”按钮后计算结果都会被打印出来。据此找到等号“按钮”的id为btn_equal,根据id找到对应的点击事件响应onClick函数中属于“等号”的部分。

从代码中发现最终真实的点击“等号”按钮后的主要代码在caculate()函数中。验证想法:为防止原码和真实运行代码不同,使用以下命令验证是否存在caculate()函数。

说明caculate()函数确实存在。
  接下来使用命令Hook这个函数来确认在点击“等号”按钮后这个函数被调用了。在Hook上之后,任意输入一个表达式并点击“等号”按钮,会发现这个函数在点击“等号”按钮后被调用
android hooking watch class_method com.example.junior CalculatorActivity.caculate --dump-args --dump-backtrace --dump-return

caculate()函数内容:

  这个函数中对减法的处理是通过Airth类中的sub()函数来实现的。验证Airth类在内存中真实存在,我们使用如下命令android hooking list classes,但是这个命令通常会列出很多类,超过整个Terminal缓存空间,所有有一些类就找不到了。
  其实Objection本身有一个log文件,用于记录objection运行时产生的所有数据。这个日志数据存放在/.objection目录下的objection.log文件中。
  运行objection注入App之前,首先切换到
/.objection目录下,将之前的objection.log文件删除或者改名字,在windows中该文件位置可通过

删除或者改名log文件后重新注入App,重新遍历所有类

1
2
3
objection -g com.example.junior explore

android hooking list classes

遍历完成后退出Objection注入模式确保log文件刷新成功,重新通过命令查看objection.log文件,同时可以配合相应的命令过滤文本,观察结果是否有输出来判定内存中是否存在目标类Airth

判定内存中确实存在Arith类之后,进一步通过Objection命令判断Arith是否存在sub()函数

在内存中确认这个函数之后,使用如下命令对这个函数进行Hook
android hooking watch class_method com.example.junior.util.Arith.sub --dump- args --dump-backtrace --dump-return

可以确认这个简单计算器的减法是通过sub(java.lang.String, java.lang.String)实现的。

利用:Frida脚本修改参数、主动调用

  在确认Arith类的函数sub(java.lang.String, java.lang.String)是最终计算器的真实执行函数。接下来对这个减法进一步利用。
  先确保整个代码的正确性。采取最终实现和Objection一样的Hook结果的目标来确保整个代码的正确性,初步的Frida脚本代码

1
2
3
4
5
6
7
8
9
10
11
12
13
function main(){
Java.perform(function (){
var Arith=Java.use('com.example.junior.util.Arith')
Arith.sub.implementation = function (str1,str2){
var result = this.sub(str1,str2)
console.log('str1,str2,result=>',str1,str2,result)
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("Java.lang.Throwable".$new)))
return result
}
})
}

setImmediate(main)

运行出现函数重载报错

经过上面的定位已经发现,我们使用的是参数类型都是String类型的overload()函数。将.overload('java.lang.String', 'java.lang.String')添加到.implementation之前,然后保存脚本,重新测试减法,Hook结果如下:

结果和通过Objection Hook出来的结果一致。验证成功,现在通过Frida脚本参数和返回进行修改,如将第二个参数改为123。

1
2
3
4
5
6
7
8
9
10
11
12
13
function main(){
Java.perform(function (){
var Arith=Java.use('com.example.junior.util.Arith')
Arith.sub .overload('java.lang.String', 'java.lang.String').implementation = function (str1,str2){
var result = this.sub(str1,"123")
console.log('str1,str2,result=>',str1,str2,result)
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
return result
}
})
}

setImmediate(main)

测试发现结果改变了

  其实这里123的直接传递实际上是不对的,正确的传入字符串参数的方式应该是使用Java中相应字符串类新建一个字符串实例传参

1
2
var JavaString = Java.use('java.lang.String')
var result = this.sub(str1,JavaString.$new('123'))

  这里构造新的参数的类型是根据实际函数的第二个参数类型为java.lang.String决定的。直接传递字符串不报错,是因为Frida本身对JavaScript的字符串进行了转换,将JavaScript的字符串在内部转换为了Java的String类型。如果是复杂的参数,就要按照如上代码程序逻辑先调用Java.use()API去获取对应的类对象,然后通过$new()函数构造一个新的参数。
  对于Frida脚本中Java函数的主动调用(区分静态函数和实例函数)。如果是静态函数,只需要获取类对象即可直接完成函数的主动调用;如果是实例函数,只需要优先获取到类的实例对象即可完成函数的主动调用。
  观察sub()函数的调用,发现Java中是直接通过Arith类对象来完成对sub()函数的调用。如果还是无法确认,可以通过Objection注入到应用中,再使用命令打印Arith类的所有函数:
android hooking list class_methods com.example.junior.util.Arith
观察发现,sub()函数是一个被static关键词修饰的函数,因此sub()函数只需要通过获取Arith类的对象即可进行主动调用。

主动调用

1
2
3
4
5
6
7
8
function callsub(){
Java.perform(function (){
var Arith = Java.use('com.example.junior.util.Arith')
var JavaString = Java.use('java.lang.String')
var result = Arith.sub(JavaString.$new("123"),JavaString.$new("111"))
console.log("123 - 111 = ",result)
})
}

规模化利用:Python规模化利用

  在完成关键函数的定位与主动调用后,如果想大规模地对关键函数进行调用,此时就会用到RPC。
  假设App核心的函数算法是sub()函数,经过上面的测试,一个完整的定位+主动调用的链条已经形成,最后一步就是完成RPC实现关键函数的批量调用。
  先修改原有的主动调用脚本call.js的内容,将原本只调用一次的sub()函数修改为可以调用多次的格式,并且需要将完成主动调用的函数修改为导出的rpc函数。代码如下:

1
2
3
4
5
6
7
8
9
10
11
function callsub(){
Java.perform(function (){
var Arith = Java.use('com.example.junior.util.Arith')
var JavaString = Java.use('java.lang.String')
var result = Arith.sub(JavaString.$new("123"),JavaString.$new("111"))
console.log("123 - 111 = ",result)
})
}
rpc.exports={
sub: callsub
}

修改后对脚本进行测试,并在确认脚本无误后使用Python进行RPC调用,以调用sub()函数100次为例,这里Python的调用脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import frida,sys
def on_message(message,data):
if message['type']=='send':
print("* {}".format(message['payload']))
else:
print(message)

device = frida.get_usb_device()
process = device.attach('com.example.junior')
with open ('E:\JetBrains\WebstormProjects\HookProject\src\chap04call.js', "r", encoding="utf-8") as f:
jscode=f.read()
script=process.create_script(jscode)

script.on('message',on_message)
script.load()

for i in range(20,30):
for j in range(0,10):
script.exports.sub(str(i),str(j))

js脚本还要修改

1
2
3
4
5
6
7
8
9
10
11
12
function callsub(i,j){
Java.perform(function (){
var Arith = Java.use('com.example.junior.util.Arith')
var JavaString = Java.use('java.lang.String')
var result = Arith.sub(JavaString.$new(i),JavaString.$new(j))
console.log(i+" - "+j+" = ",result)
})
}

rpc.exports={
sub: callsub
}

  逆向App过程中,如果目标接口的某个参数是由一个复杂的密码函数完成的,其加密逻辑过于复杂而不易进行逆向过程,那么Frida的主动调用就派上用场了,如果想大规模调用就要RPC。
  如果只能在测试手机使用USB模式时才能使用Python进行规模化调用,且一个计算机的接口数量是固定的,那么这里的规模化就不是真正的规模化。而Frida的网络模式完美地解决了对USB的依赖。
  只需要在Android运行Frida-sever时加上-l参数指定监听IP接口和端口即可。如果想通过Python语言连接处于网络模式下的frida-sever,只需要将loader.js脚本中get_usb_device()函数更改为get_device_manager().add_remote_device(‘:‘)即可断开adb连接,通过网络模式对App进行后续的注入和Hook工作。这样可以真正的大规模利用。