Hook框架Frida

admin 2022年1月6日01:05:23评论923 views字数 12427阅读41分25秒阅读模式

0x00 概述

Frida是个轻量级别的hook框架

是Python API,但JavaScript调试逻辑

Frida的核心是用C编写的,并将Google的V8引擎注入到目标进程中,在这些进程中,JS可以完全访问内存,挂钩函数甚至调用进程内的本机函数来执行。

使用Python和JS可以使用无风险的API进行快速开发。Frida可以帮助您轻松捕获JS中的错误并为您提供异常而不是崩溃。

关于frda学习路线了,Frida的学习还是蛮简单的,只需要了解两方面的内容:
1)主控端和目标进程的交互(message)
2)Python接口和js接口(查文档)

frida框架分为两部分:
1)一部分是运行在系统上的交互工具frida CLI。
2)另一部分是运行在目标机器上的代码注入工具 frida-serve。

0x01 资源和环境

1
2
3
4
5
Mac Os 10.15.5
Python 3.7
Google Nexus 6P Anroid 6.0.1
Frida官网:https://www.frida.re/
Frida源码:https://github.com/frida

0x02 运行模式

Frida通过其强大的仪器核心Gum提供动态检测,Gum是用C语言编写的。因为这种检测逻辑很容易发生变化,所以通常需要用脚本语言编写,这样在开发和维护它时会得到一个简短的反馈循环。这就是GumJS发挥作用的地方。只需几行C就可以在运行时内运行一段JavaScript,它可以完全访问Gum的API,允许您挂钩函数,枚举加载的库,导入和导出的函数,读写内存,扫描模式的内存等

0x03 Frida安装

1
2
3
pip install frida
pip install frida-tools
frida --version

image-20200922142344067

安卓端安装

1
https://github.com/frida/frida/releases
1
cat /proc/cpuinfo

image-20200922143332932

看到我的cpu是armv8 64位的 所以对应的去下载相应的版本

image-20200922143734995

1
2
3
4
5
6
adb push frida-server-12.11.17-android-arm64 /data/local/tmp
adb shell
su
cd /data/local/tmp
chmod 777 frida-server-12.11.17-android-arm64
./frida-server-12.11.17-android-arm64

在bash下frida-ps -U

image-20200922144633734

0x04 Hook示例的安装与分析

Frida官网给我们了一个ctf的示例,就以此为例子,开始学习frida在android逆向的使用。
rps.apk 下载地址

使用虚拟机或者自己的手机将应用安装好,发现是一个简单的石头剪刀布的游戏应用,简单的玩了一下,没什么特别的,直接分析代码吧,看看到底想干什么。

image-20200923103121714

源代码分析

使用jadx-gui反编译,发现app没有加壳和混淆,当然一来就加壳和混淆的话对我们就太不友好了,接下分析就简单了,直接看java代码。

image-20200923103824726

在MainActivity中找到OnCreate()方法,可以看到只是简单的声明了button控件以及对应的监听器。

1
2
3
4
5
6
7
8
9
10
11
protected void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
setContentView(2130968600);
this.P = (Button)findViewById(2131492941);
this.S = (Button)findViewById(2131492943);
this.r = (Button)findViewById(2131492942);
this.P.setOnClickListener(this);
this.r.setOnClickListener(this);
this.S.setOnClickListener(this);
this.flag = 0;
}

继续查看button的onclick方法,可以看出cpu是通过随机数组出的,其判断输赢的方法在this.showMessageTask中。

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
public void onClick(View paramView) {
if (this.flag == 1)
return;
this.flag = 1;
((TextView)findViewById(2131492946)).setText("");
TextView textView1 = (TextView)findViewById(2131492944);
TextView textView2 = (TextView)findViewById(2131492945);
this.m = 0;
this.n = (new Random()).nextInt(3); //随机数0,1,2
int i = this.n;
(new String[3])[0] = "CPU: Paper";
(new String[3])[1] = "CPU: Rock";
(new String[3])[2] = "CPU: Scissors";
textView2.setText((new String[3])[i]);
if (paramView == this.P) {
textView1.setText("YOU: Paper");
this.m = 0;
}
if (paramView == this.r) {
textView1.setText("YOU: Rock");
this.m = 1;
}
if (paramView == this.S) {
textView1.setText("YOU: Scissors");
this.m = 2;
}
this.handler.postDelayed(this.showMessageTask, 1000L); //输赢判断方法
}

跟进分析showMessageTask,可以看到如果赢了mainActivity.cnt会+1,但是一旦输了cnt就会置0,而获取flag的要求是我们得获胜1000次,…… Hook框架Frida

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

private final Runnable showMessageTask = new Runnable() {
public void run() {
TextView textView = (TextView)MainActivity.this.findViewById(2131492946);
if (MainActivity.this.n - MainActivity.this.m == 1) {
MainActivity mainActivity = MainActivity.this;
mainActivity.cnt++;
textView.setText("WIN! +" + String.valueOf(MainActivity.this.cnt));
} else if (MainActivity.this.m - MainActivity.this.n == 1) {
MainActivity.this.cnt = 0;
textView.setText("LOSE +0");
} else if (MainActivity.this.m == MainActivity.this.n) {
textView.setText("DRAW +" + String.valueOf(MainActivity.this.cnt));
} else if (MainActivity.this.m < MainActivity.this.n) {
MainActivity.this.cnt = 0;
textView.setText("LOSE +0");
} else {
MainActivity mainActivity = MainActivity.this;
mainActivity.cnt++;
textView.setText("WIN! +" + String.valueOf(MainActivity.this.cnt));
}
if (1000 == MainActivity.this.cnt)
textView.setText("SECCON{" + String.valueOf((MainActivity.this.cnt + MainActivity.this.calc()) * 107) + "}");
MainActivity.this.flag = 0;
}
};

简单分析一下获取flag需要的条件,总结有3个办法:

  1. 分析calc()方法能算出答案,但这个方法在so中,得分析汇编代码才行,当然可以尝试使用ida pro,F5查看C代码分析,前提是算法不难。
  2. 获取calc函数的返回值,从而计算答案。
  3. 还有一个方法就是,直接将MainActivity.this.cnt的值构造成1000。

接下来就用frida,使用后两种思路来解这个简单的示例。但在这之前得先了解Frida自带的Messages机制,了解frida怎么从通过一个python脚本发送和接收message消息是一个提升理解frida的好方法。

0x04 Frida自带的Messages机制与进程交互

先来看看一个Messages的模板,这里用到的语言分别是python和javascript,他们之间的关系是python作为载体,javascript作为在android中真正执行代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -*-coding:utf-8-*-
import frida, sys

#hook代码,采用javascript编写
jscode = """
//javascript代码,重点
"""

#自定义回调函数
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

#重点的4行代码
process = frida.get_usb_device().attach('应用完整包名')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()

当然如果是对此简单的使用,只需要编写jscode,以及填写你要hook的应用完整包名就行了,不过如果单纯只会用可能在以后会被模板限制,所以一探究竟还是很有必要。
可以在Bash中,使用python终端的help()函数找到frida库的源代码的绝对路径。

1
2
import frida
help(frida)

image-20200923112932246

接下来就来具体看看这几句代码做了什么事情。

1
2
3
4
5
process = frida.get_usb_device().attach('应用完整包名')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()

首先使用了frida.get_usb_device(),返回了一个_get_device函数,跟进_get_device方法。

1
2
def get_usb_device(timeout = 0):
return _get_device(lambda device: device.type == 'tether', timeout)

在_get_device中,通过get_device_manager()实例化DeviceManager类,并调用该类中的enumerate_devices()方法。

1
2
3
4
5
6
7
8
9
10
def _get_device(predicate, timeout):
mgr = get_device_manager() //获取设备管理
def find_matching_device(): //寻找匹配设备
usb_devices = [device for device in mgr.enumerate_devices() if predicate(device)]
if len(usb_devices) > 0:
return usb_devices[0]
else:
return None
device = find_matching_device()
...省略

get_device_manager()代码

1
2
3
4
5
6
def get_device_manager():
global _device_manager
if _device_manager is None:
from . import core
_device_manager = core.DeviceManager(_frida.DeviceManager())
return _device_manager

DeviceManager中enumerate_devices()方法,可以看到enumerate_devices()方法实际上是返回了一个Device()类的实例化对象List。

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
class DeviceManager(object):
def __init__(self, impl):
self._impl = impl

def __repr__(self):
return repr(self._impl)

//返回了一个Device()类的实例化。
def enumerate_devices(self):
return [Device(device) for device in self._impl.enumerate_devices()]

def add_remote_device(self, host):
return Device(self._impl.add_remote_device(host))

def remove_remote_device(self, host):
self._impl.remove_remote_device(host)

def get_device(self, device_id):
devices = self._impl.enumerate_devices()
if device_id is None:
return Device(devices[0])
for device in devices:
if device.id == device_id:
return Device(device)
raise _frida.InvalidArgumentError("unable to find device with id %s" % device_id)

def on(self, signal, callback):
self._impl.on(signal, callback)

def off(self, signal, callback):
self._impl.off(signal, callback)

继续跟进Device类中的,就找到了attach()方法。在attach方法这是设置断点,看看传入的数据。

image-20200923213529047

接下来提供的“应用完整名”是通过self._pid_of()函数去找到对应的进程号pid,然后将pid后通过Session类初始化。到此第一句代码过程就算是明白了,最终得到的是一个对应进程号pid的Session实例化对象process。

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
32
33
34
35
36
37
class Device(object):
def __init__(self, device):
self.id = device.id
self.name = device.name
self.icon = device.icon
self.type = device.type
self._impl = device

def __repr__(self):
return repr(self._impl)

...节省空间删除部分方法,详细内容可自行查看源码

def kill(self, target):
self._impl.kill(self._pid_of(target))

//返回了一个Session的实例化对象
def attach(self, target):
return Session(self._impl.attach(self._pid_of(target)))

def inject_library_file(self, target, path, entrypoint, data):
return self._impl.inject_library_file(self._pid_of(target), path, entrypoint, data)

def inject_library_blob(self, target, blob, entrypoint, data):
return self._impl.inject_library_blob(self._pid_of(target), blob, entrypoint, data)

def on(self, signal, callback):
self._impl.on(signal, callback)

def off(self, signal, callback):
self._impl.off(signal, callback)

def _pid_of(self, target):
if isinstance(target, numbers.Number):
return target
else:
return self.get_process(target).pid

第二句,紧接着process.create_script(jscode),可以看到它返回一个Script类的实例化,参数不确定。

1
2
def create_script(self, *args, **kwargs):
return Script(self._impl.create_script(*args, **kwargs))

跟进Script类,可以找到on()方法,在on方法中可以设置自定义回调函数。

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
class Script(object):
def __init__(self, impl):
self.exports = ScriptExports(self)

self._impl = impl
self._on_message_callbacks = []
self._log_handler = self._on_log

self._pending = {}
self._next_request_id = 1
self._cond = threading.Condition()

impl.on('destroyed', self._on_destroyed)
impl.on('message', self._on_message)

...节省空间删除部分类方法,详细内容可自行查看源码

def load(self):
self._impl.load()

//设置自定义回调函数
def on(self, signal, callback):
if signal == 'message':
self._on_message_callbacks.append(callback)
else:
self._impl.on(signal, callback)

在IDE中可以看到_on_message_callbacks中存放的on_message函数地址。

Hook框架Frida
接下来调用load()方法,在服务端就启动javascript脚本了,至于在frida-server服务端怎么执行的,可逆向研究一下frida-server,它才是真正的核心。

0x05 Javascript代码构造与执行

方法一:获取calc()返回值

第一种思路就是直接获取calc的返回值,从native函数定义上知道它的返回值是int类型,当然直接获取calc函数的返回值是解出问题最简单的方法。

1
public native int calc();

那怎么获取calc()函数的返回值呢,这个函数在MainActivity类中,直接引用该类下的calc()方法,不就ok了吗,原理是这样,下面就来构造一下Javascript代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Java.Perform 开始执行JavaScript脚本。
Java.perform(function () {
//定义变量MainActivity,Java.use指定要使用的类
var MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity');
//hook该类下的onCreate方法,重新实现它
MainActivity.onClick.implementation = function () {
send("Hook Start...");
//调用calc()方法,获取返回值
var returnValue = this.calc();
send("Return:"+returnValue);
var result = (1000+returnValue)*107;
//解出答案
send("Flag:"+"SECCON{"+result.toString()+"}");
}
});

JavaScript代码就是这样,如果不是很理解,学习一下JavaScript基础即可,下面看看完整的python脚本。

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
# -*-coding:utf-8-*-
import frida, sys


def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)


jscode = """
Java.perform(function () {
var MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity');
MainActivity.onClick.implementation = function () {
send("Hook Start...");
var returnValue = this.calc();
send("Return:"+returnValue);
var result = (1000+returnValue)*107;
send("Flag:"+"SECCON{"+result.toString()+"}");
}
});
"""

process = frida.get_usb_device().attach('com.example.seccon2015.rock_paper_scissors')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()

不知道为什么hook的为应用的onCreate方法始终不行所以hook对应的onClick方法进行结果如下

image-20200924110841383

方法二:修改cnt的值为1000

第二种思路也比较简单,我们需要修改cnt的值,但如果直接修改cnt的初始值为1000的话,在游戏中可能存在不确定因素,比如输了会置0,赢了cnt值就变成1001了,所以还得控制一下输赢,而输赢的条件是电脑出什么,所以最终hook的方法就在onClick中。
从onClick()中可以知道,控制输赢的在于修改this.n 和 this.m的值,再来看看源代码。

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
32
private final Runnable showMessageTask = new Runnable() {
public void run() {
TextView tv3 = (TextView) MainActivity.this.findViewById(R.id.textView3);
MainActivity mainActivity;
//我方:布 CPU:石头 or 我方:石头 CUP:剪刀 ,则为赢
if (MainActivity.this.n - MainActivity.this.m == 1) {
mainActivity = MainActivity.this;
mainActivity.cnt++;
tv3.setText("WIN! +" + String.valueOf(MainActivity.this.cnt));
//反过来当然是输咯
} else if (MainActivity.this.m - MainActivity.this.n == 1) {
MainActivity.this.cnt = 0;
tv3.setText("LOSE +0");
//一样则打平
} else if (MainActivity.this.m == MainActivity.this.n) {
tv3.setText("DRAW +" + String.valueOf(MainActivity.this.cnt));
//我布 cup:剪刀
} else if (MainActivity.this.m < MainActivity.this.n) {
MainActivity.this.cnt = 0;
tv3.setText("LOSE +0");
} else {
mainActivity = MainActivity.this;
mainActivity.cnt++;
tv3.setText("WIN! +" + String.valueOf(MainActivity.this.cnt));
}
//获胜1000次则能够获取flag
if (1000 == MainActivity.this.cnt) {
tv3.setText("SECCON{" + String.valueOf((MainActivity.this.cnt + MainActivity.this.calc()) * 107) + "}");
}
MainActivity.this.flag = 0;
}
};

JavaScript代码编写如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java.perform(function () {
var MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity');
//hook onClick方法,此处要注意的是onClick方法是传递了一个View参数v
MainActivity.onClick.implementation = function (v) {
send("Hook Start...");
//调用onClick,模拟点击事件
this.onClick(v);
//修改参数 满足第一个if 或者 最后一个else即可
this.n.value = 0;
this.m.value = 2;
this.cnt.value = 999;
send("Success!")
}
});

完整python代码

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
import frida, sys

def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

jscode = """
Java.perform(function () {
var MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity');
MainActivity.onClick.implementation = function (v) {
send("Hook Start...");
this.onClick(v);
this.n.value = 0;
this.m.value = 2;
this.cnt.value = 999;
send("Success!")
}
});
"""

process = frida.get_usb_device().attach('com.example.seccon2015.rock_paper_scissors')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()

执行python脚本,任意点击按钮,答案就出来了。

image-20200924112142535

方法三:分析calc()方法算出答案

calc() 这个方法在so中,对应的分析汇编代码

1
2
3
4
5
6

static {
System.loadLibrary("calc");
}

public native int calc();

直接使用ida pro或者radare2分析汇编代码也是可以的。这里给出用radare2反汇编出来的代码。可以看到,calc()函数就单纯的返回了int值7

image-20200924113024992

image-20200924114127105

0x06 总结

一般分析流程

1
2
3
1.反编译apk,分析代码寻找hook点。
2.编写js代码,调用类的方法或者替换。
3.在python中执行即可。

0x07 参考链接

https://bbs.pediy.com/thread-227232.htm

FROM :ol4three.com | Author:ol4three

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:05:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Hook框架Fridahttp://cn-sec.com/archives/720979.html

发表评论

匿名网友 填写信息