From:https://eternalsakura13.com/2020/07/04/frida/
Frida 是目前几乎最好跨平台hook工具,深受广大牢友的喜爱。因此整理下 frida 用法,以备随时查看。
开发环境配置 ( IDE 智能提示 frida ):
见过的较好的frida笔记:
官方api:https://frida.re/docs/javascript-api/
练习 apk 源码及下载地址
优雅的 frida-rpc
查看手机架构:adb shell getprop ro.product.cpu.abi
Android 逆向之分析基操:https://mp.weixin.qq.com/s/IovgsqprLYSnKsH61EP-EQ
细说 So 动态库的加载流程:https://bbs.pediy.com/thread-255674.htm
.init 和 .initarray 和 JNIOnload:https://www.cnblogs.com/runope/p/14789784.html
Frida hook Java/Native与init_array 自吐最终方案:https://bbs.pediy.com/thread-267430.htm
dlopen -> CallConstructors -> init,init_array(反调试,检测,ollvm字符串加解密)
dlsym(JNI_ONLOAD)对 app 自身 so文件的自定义svc进行hook
- 1,so加载进内存,完成重定位
- 2,init,init_array之前
so dump之后修复:SOFIXER
应用以32位在64位终端环境下运行:adb install --abi armeabi-v7a
so分析(trace)
- ida-ins trace
- frida-stalker。目前只支持arm64,why:arm32终将过去式,arm64才是主流。( sktrace:https://github.com/huaerxiela/sktrace.git )。示例:python sktrace.py -m attach -l libhello-jni.so -i 0x1CFF0 Hel
- unicorn-unidbg-kingtrace
目录
1、r0ysue 大佬
2、Frida 环境
2.1 pyenv
2.2 frida 安装
2.3 安装 objection
2.4 frida 使用
frida 帮助 和 frida-server 帮助
2.5 frida 开发环境搭建
3、FRIDA 基础
3.1 frida 查看当前存在的进程
3.2 frida 打印参数和修改返回值
3.3 frida 寻找 instance,主动调用。
3.4 frida rpc
3.5 frida 动态修改
3.6 API List
4、Frida 动静态结合分析
4.1 Objection
objection -----> 启动、并注入内存
objection -----> memory 命令
memory list modules ( 查看加载的 so 库 )
memory list exports ( 查看 so 库的导出函数表 )
memory dump(dump 内存空间)
memory search(搜索 内存空间)
objection -----> android 命令
android heap search instances 类名
android heap execute 实例ID 实例方法
android hooking list activities/services
android intent launch_activity/launch_service activity/服务
android hooking list classes
android hooking list class_methods 类名
android hooking search classes 关键字
android hooking search methods 关键字
hook 类的方法(hook 类里的所有方法 / 具体某个方法)
grep trick 和 文件保存( objection log )
4.2 案例学习
案例学习 1:《仿VX数据库原型取证逆向分析》
案例学习 2:主动调用爆破密码
5、Frida hook 基础(一)
5.1 Frida hook : 打印参数、返回值 / 设置返回值 / 主动调用
5.2 Frida hook : 主动调用静态/非静态函数 以及 设置静态/非静态成员变量的值
5.3 Frida hook : 内部类,枚举类的方法 并 hook,trace原型1
5.4 Frida hook : hook 动态加载的 dex,与查找 interface,
5.5 Frida hook : 枚举 class,trace原型2
5.6 Frida hook : 搜索 interface 的具体实现类
枚举 实现 接口的类
6、Frida hook 基础(二)
6.1 spawn / attach
6.2 Frida hook : hook构造函数/打印栈回溯
6.3 Frida hook : 打印栈回溯
6.4 Frida hook : 手动加载 dex 并调用
6.5 Frida Hook Android 加固应用 方法
Android 加固应用 Hook 方式
获取 构造函数 的 参数
获取 动态加载 的 类
Java.cast 处理 泛型方法
getDeclaredConstructor 获取构造函数
getDeclaredMethods() 获取类中的方法
调用 Method.invoke() 去执行方法
read-std-string
where_is_native
脱壳
基本原理
开始 hook 加固应用
7、Frida 打印 与 参数构造
打印 [object object]
gson 打印 Java 对象的内容
char[] / [Object Object]
byte[] 类型 转 String
修改传递的参数 ( 示例: java array 构造 )
类的多态:强制类型转换 Java.cast
interface / Java.registerClass
成员内部类 / 匿名内部类
遍历一个类的所有方法
hook enum
HashMap 和 Map 类型 转 String
获得 context
打印 non-ascii
使用 objection 打印混淆的方法名
Hook 数据库
8、Frida native hook : NDK 开发入门
Android JNI (一) --- NDK 与 JNI 基础
Android JNI学习 (二) --- 实战 JNI 之 "hello world"
Android JNI学习 (三) --- Java 与 Native 相互调用
Android JNI学习 (四) --- JNI 的常用方法的中文 API
Android JNI学习 (五) --- Demo 演示
9、Frida native hook : JNIEnv 和 反射
9.1 以 jni字符串 来掌握基本的 JNIEnv用法
9.2 Java 反射
10、Frida 反调试 与 反反调试
11、Frida native hook : 符号 hook JNI、art&libc
11.1 Native函数的Java Hook及主动调用
11.2 jni.h 头文件导入
11.3 JNI 函数符号 hook
11.4 JNI 函数参数、返回值打印和替换
12、Frida native hook : JNI_Onload / 动态注册 / inline_hook / native层调用栈打印
12.1 JNI_Onload / 动态注册原理
12.2 Frida hook RegisterNative
12.3 native 层调用栈打印
12.4 主动调用去进行方法参数替换
12.5 inline hook ( so库里面的函数 )
13、Frida native hook : Frida hook native app 实战
14、Frida trace 四件套
14.1 jni trace : trace jni
14.2 strace : 追踪进程的系统调用
14.3 frida-trace : trace libc (or more)
frida_hook_libart
14.4 hook_artmethod : trace java 函数调用
14.5 修改AOSP源码打印
15、Frida native hook : init_array 开发和自动化逆向
15.1 init_array原理 ( so 加载、启动、执行 )
15.2 IDA静态分析 init_array
15.3 IDA 动态调试 so
15.4 init_array && JNI_Onload "自吐"
JNI_Onload
init_array
15.5 native层未导出函数主动调用(任意符号和地址)
16、C/C++ hook
16.1 Native/JNI层参数打印和主动调用参数构造
16.2 C/C++编成 so 并引入 Frida 调用其中的函数
17、使用 frida-net 简单玩转 frida-rpc
方法 1:frida -FU -l _agent.js
方法 2 ( 推荐 ):./frida-inject -n com.xxx.xxx -s _agent.js
这篇文章完全来源于 r0ysue 的知识星球,推荐下大佬的星球
github 地址:https://github.com/frida/frida
python 全版本随机切换,这里提供 macOS上的配置方法
brew update
brew install pyenv
echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n eval "$(pyenv init -)"\nfi' >> ~/.bash_profile
下载一个3.8.2,下载真的很慢,要慢慢等
pyenv install 3.8.2
pyenv versions
sakura@sakuradeMacBook-Pro:~$ pyenv versions
system
* 3.8.2 (set by /Users/sakura/.python-version)
切换到我们装的
pyenv local 3.8.2
python -V
pip -V
原本系统自带的
python local system
python -V
另外当你需要临时禁用 pyenv 的时候
把这个注释了然后另开终端就好了。关于卸载某个 python 版本
Uninstalling Python Versions
As time goes on, you will accumulate Python versions in your $(pyenv root)/versions directory.
To remove old Python versions, pyenv uninstall command to automate the removal process.
Alternatively, simply rm -rf the directory of the version you want to remove.
You can find the directory of a particular Python version with the pyenv
prefix command, e.g. pyenv prefix 2.6.8.
如果直接按下述安装则会直接安装 frida 和 frida-tools 的最新版本。
pip install frida
pip install frida-tools 安装 frida-tools 会自动安装最新版的 frida
也可以通过 版本号 安装旧版本的 frida,例如 12.8.0
pip install frida==16.0.8
pip install frida-tools==12.1.0
frida --version
frida-ps --version
老版本 frida 和 对应关系,对应关系很好找:
frida Cll
frida -U 包名 调试连接到电脑上设备中的应用
frida '*进程名*' 打开进程
frida -l 脚本名 '*进程名*' 注入进程脚本
frida-ps
frida-ps -U 列举出来设备上的所有进程
frida-ps -Ua 列举出来设备上的所有应用程序
frida-ps -Uai 列举出来设备上的所有已安装应用程序和对应的名字
frida-ps -D PID 连接到指定设备
frida-ps -D-a 检查进程是否被杀死
frida-trace
frida-trace -i "recv*" -i "send*" Name 向应用程序中发送APIs
frida-trace -m "ObjC" Name 在应用程序中跟踪ObjC方法调用
frida-trace -U -f Name -I "call" 在设备中打开应用程序并跟踪函数的调用frida-discover 发现程序内部函数
frida-discover -n name
frida-discover -p pidfrida-kill -D
杀死进程
frida-ls-devices 列举出来所有连接到电脑上的设备
安装命令:
pyenv local 3.8.2
pip install objection
objection -h
pyenv local 3.7.7
pip install objection==1.8.4
objection -h
下载 frida-server 并解压,在这里下载 frida-server :https://github.com/frida/frida/releases
先 adb shell,然后切换到 root 权限,把之前 push 进来的 frida server 改个名字叫 fs,然后运行 frida
adb push /tmp/frida-server-12.8.0-android-arm64 /data/local/tmp
mv frida-server-12.8.0-android-arm64 fs
chmod 777 fs
./fs
如果要监听端口,就
./fs -l 0.0.0.0:8888
./frida_server -l 0.0.0.0:9000
adb forward tcp:9000 tcp:9000
adb forward tcp:9001 tcp:9001
frida-ps -H xxx.xxx.xxx.xxx:9000 -a
frida -H xxx.xxx.xxx.xxx:9000 -f com.xxx.xxx -l test.js --no-pauseobjection -N -h 192.168.18.100 -p 9000 -g 得物 explore
# 远程连接
remote_host_ip_port = '192.168.2.102:6666'
dm = frida.get_device_manager()
remote_device = dm.add_remote_device(remote_host_ip_port)
session = remote_device.attach("com.xxx.xxx")# 通过 USB 方式连接
usb_device = frida.get_usb_device()
session = usb_device.attach("com.xxx.xxx")
将objection注入应用(Attach)
objection -g com.jx885.reward explore将objection注入远程应用(Attach)
objection -N -h 192.168.137.11 -p 12345 -g com.babytree.apps.pregnancy explore将objection注入应用(Spawn),引号中的objection命令会在启动时就注入App
objection -g com.babytree.apps.pregnancy explore --startup-command "android hooking watch class_method com.babytree.business.common.c.b.a --dump-args --dump-backtrace --dump-return"当前Hook任务
jobs list关闭Hook任务
jobs kill job_id查看内存中加载的so
memory list modules查看so的导出函数
memory list exports libssl.so将so的导出函数保存到json文件中
memory list exports libart.so --json libart.json列出内存中所有的类
android hooking list classes查看当前可用的activities
android hooking list activities列出类中所有的方法(根据查出来的activity列出类下的方法)
android hooking list class_methods com.babytree.business.common.c.bhook类
android hooking watch class com.babytree.business.common.c.b生成hook类代码
android hooking generate simple com.babytree.business.common.c.bhook类的所有重载
android hooking watch class_method com.babytree.business.common.c.b.$init --dump-args --dump-backtrace --dump-returnhook方法,打印参数、调用栈、返回值
android hooking watch class_method com.babytree.business.common.c.b.a --dump-args --dump-backtrace --dump-return内存堆搜索类的实例
android heap search instances com.babytree.business.common.c.b调用实例的方法
android heap execute 0x2526 a在内存中所有已加载的类中搜索包含特定关键词的类
android hooking search classes 关键词在内存中所有已加载的类的方法中搜索包含特定关键词的方法
android hooking search methods 关键词启动activity
android intent launch_activity com.autonavi.map.activity.NewMapActivity
frida --help
frida-server --help
npm run watch
会监控代码修改自动编译生成 js 文件下面是测试脚本
s1.js
function main() {
Java.perform(function x() {
console.log("sakura")
})
}
setImmediate(main)
loader.py
import time
import frida
device8 = frida.get_device_manager().add_remote_device("192.168.0.9:8888")
pid = device8.spawn("com.android.settings")
device8.resume(pid)
time.sleep(1)
session = device8.attach(pid)
with open("si.js") as f:
script = session.create_script(f.read())
script.load()
input() #等待输入
解释一下:这个脚本就是先通过 frida.get_device_manager().add_remote_device 来找到 device,然后以 spawn 方式启动 settings,然后 attach 到上面,并执行 frida 脚本。
当代码里面没有指定端口时,需要手动转发端口:adb forward tcp:27042 tcp:27042
import sys
import time
import frida
js_code = '''
function main() {
Java.perform(function x() {
console.log("sakura")
})
}
setImmediate(main);
'''
def on_message(msg, data):
if msg['type'] == 'send':
print(f'[*] {msg["payload"]}')
else:
print(msg)
if __name__ == '__main__':
select = 1
if 1 == select:
# ########################### 会自动重启 app ###########################
# 会自动重启 app
device = frida.get_remote_device()
pid = device.spawn(["com.android.settings"])
device.resume(pid)
time.sleep(1)
process = device.attach("com.android.settings")
script = process.create_script(js_code)
script.load()
input('按任意键继续') # 等待输入
elif 2 == select:
# ############ 需要先手动启动 app , 然后才能执行脚本进行 hook #############
# get_remote_device 获取远程设备 (get_usb_device) attach 附加进程
process = frida.get_remote_device().attach('com.android.settings')
script = process.create_script(js_code)
script.on('message', on_message) # 绑定 js 回调
script.load()
sys.stdin.read()
pass
运行结果:
frida-ps 命令:
frida-ps -U
查看通过 usb 连接的 android 手机上的进程。可以通过 grep 过滤就可以找到我们想要的包名。
sakura@sakuradeMacBook-Pro:~$ frida-ps -U
PID Name
----- ---------------------------------------------------
3640 ATFWD-daemon
707 adbd
728 adsprpcd
26041 [email protected]
741 android.hardware.biometrics.fingerprint@
package myapplication.example.com.frida_demo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
private String total = "@@@###@@@";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
fun(50,30);
Log.d("sakura.string" , fun("LoWeRcAsE Me!!!!!!!!!"));
}
}
void fun(int x , int y ){
Log.d("sakura.Sum" , String.valueOf(x+y));
}
String fun(String x){
total +=x;
return x.toLowerCase();
}
String secret(){
return total;
}
}
注入的 js 代码:
function main() {
console.log("Enter the Script!");
Java.perform(function x() {
console.log("Inside Java perform");
var MainActivity = Java.use("myapplication.example.com.frida_demo.MainActivity");
// 重载找到指定的函数
MainActivity.fun.overload('java.lang.String').implementation = function (str) {
//打印参数
console.log("original call : str:" + str);
//修改结果
var ret_value = "sakura";
return ret_value;
};
})
}
setImmediate(main);
查看设备。( -f 是通过 spawn,也就是重启 apk 注入 js )
sakura@sakura:~$ frida-ps -U | grep frida
8738 frida-helper-32
8897 myapplication.example.com.frida_demo
// -f 是通过 spawn,也就是重启 apk 注入 js
sakura@sakura:~$ frida -U -f myapplication.example.com.frida_demo -l frida_demo.js
...
original call : str:LoWeRcAsE Me!!!!!!!!!
12-21 04:46:49.875 9594-9594/myapplication.example.com.frida_demo D/sakura.string: sakura
function main() {
console.log("Enter the Script!");
Java.perform(function x() {
console.log("Inside Java perform");
var MainActivity = Java.use("myapplication.example.com.frida_demo.MainActivity");
// overload 选择被重载的对象
MainActivity.fun.overload('java.lang.String').implementation = function (str) {
//打印参数
console.log("original call : str:" + str);
//修改结果
var ret_value = "sakura";
return ret_value;
};
// 寻找类型为 classname 的实例
Java.choose("myapplication.example.com.frida_demo.MainActivity", {
onMatch: function (x) {
console.log("find instance :" + x);
console.log("result of secret func:" + x.secret());
},
onComplete: function () {
console.log("end");
}
});
});
}
setImmediate(main);
function callFun() {
Java.perform(function fn() {
console.log("begin");
Java.choose("myapplication.example.com.frida_demo.MainActivity", {
onMatch: function (x) {
console.log("find instance :" + x);
console.log("result of fun(string) func:" + x.fun(Java.use("java.lang.String").$new("sakura")));
},
onComplete: function () {
console.log("end");
}
})
})
}
rpc.exports = {
callfun: callFun
};
Python 调用:
import time
import frida
device = frida.get_usb_device()
pid = device.spawn(["myapplication.example.com.frida_demo"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("frida_demo_rpc_call.js") as f:
script = session.create_script(f.read())
def my_message_handler(message, payload):
print(message)
print(payload)
script.on("message", my_message_handler)
script.load()
script.exports.callfun()
执行:
sakura@sakura:~/frida-agent-example/agent$ python frida_demo_rpc_loader.py
begin
find instance :myapplication.example.com.frida_demo.MainActivity@1d4b09d
result of fun(string):sakura
end
即将手机上的 app 的内容发送到 PC 上的 frida python 程序,然后处理后返回给 app,然后 app 再做后续的流程,核心是理解 send/recv
函数。
public class MainActivity extends AppCompatActivity {
EditText username_et;
EditText password_et;
TextView message_tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
password_et = (EditText) this.findViewById(R.id.editText2);
username_et = (EditText) this.findViewById(R.id.editText);
message_tv = ((TextView) findViewById(R.id.textView));
this.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (username_et.getText().toString().compareTo("admin") == 0) {
message_tv.setText("You cannot login as admin");
return;
}
//hook target
message_tv.setText(
"Sending to the server :" +
Base64.encodeToString(
(username_et.getText().toString() + ":" + password_et.getText().toString()).getBytes(),
Base64.DEFAULT
)
);
}
});
}
}
先分析问题,我的最终目标是让 message_tv.setText 可以”发送”username为admin的base64字符串。
那肯定是 hook TextView.setText 这个函数。
console.log("Script loaded successfully ");
Java.perform(function () {
var tv_class = Java.use("android.widget.TextView");
tv_class.setText.overload("java.lang.CharSequence").implementation = function (x) {
var string_to_send = x.toString();
var string_to_recv;
send(string_to_send); // send data to python code
recv(function (received_json_object) {
string_to_recv = received_json_object.my_data
console.log("string_to_recv: " + string_to_recv);
}).wait(); //block execution till the message is received
var my_string = Java.use("java.lang.String").$new(string_to_recv);
this.setText(my_string);
}
});
Python 脚本:
import time
import frida
import base64
def my_message_handler(message, payload):
print(message)
print(payload)
if message["type"] == "send":
print(message["payload"])
data = message["payload"].split(":")[1].strip()
print( 'message:', message)
#data = data.decode("base64")
#data = data
data = str(base64.b64decode(data))
print( 'data:',data)
user, pw = data.split(":")
print( 'pw:',pw)
#data = ("admin" + ":" + pw).encode("base64")
data = str(base64.b64encode(("admin" + ":" + pw).encode()))
print( "encoded data:", data)
script.post({"my_data": data}) # send JSON object
print( "Modified data sent")
device = frida.get_usb_device()
pid = device.spawn(["myapplication.example.com.frida_demo"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("frida_demo2.js") as f:
script = session.create_script(f.read())
script.on("message", my_message_handler)
script.load()
input()
执行和输出:
sakura@sakuradeMacBook-Pro:~/gitsource/frida-agent-example/agent$ python frida_demo_rpc_loader2.py
Script loaded successfully
{'type': 'send', 'payload': 'Sending to the server :c2FrdXJhOjEyMzQ1Ng==\n'}
None
Sending to the server :c2FrdXJhOjEyMzQ1Ng==
message: {'type': 'send', 'payload': 'Sending to the server :c2FrdXJhOjEyMzQ1Ng==\n'}
data: b'sakura:123456'
pw: 123456'
encoded data: b'YWRtaW46MTIzNDU2Jw=='
Modified data sent
string_to_recv: b'YWRtaW46MTIzNDU2Jw=='
参考链接:https://github.com/Mind0xP/Frida-Python-Binding
Java.choose(className: string, callbacks: Java.ChooseCallbacks): void
通过扫描 Java VM 的堆来枚举 className类 的 live instance。
Java.use(className: string): Java.Wrapper<{}>
动态为 className 生成 JavaScript Wrappe r,可以通过调用$new()
来调用构造函数来实例化对象。
在实例上调用 $dispose()
以对其进行显式清理,或者等待JavaScript对象被gc。
Java.perform(fn: () => void): void
附加到 VM 后要运行的 函数。确保当前线程连接到 VM 并调用 fn。
如果应用程序的 " 类加载器 " 还不可用,将延迟调用 fn。
如果不需要访问应用程序的类,请使用 Java.performNow()
send(message: any, data?: ArrayBuffer | number[]): void
将 JSON 序列化后的 message 发送到您的基于Frida 的应用程序,并包含(可选)一些原始二进制数据。The latter is useful if you e.g. dumped some memory using NativePointer#readByteArray().
recv(callback: MessageCallback): MessageRecvOperation
Requests callback to be called on the next message received from your Frida-based application. This will only give you one message, so you need to call recv() again to receive the next one.
wait(): void
堵塞,直到 message 已经 receive 并且 callback 已经执行完毕并返回
命令:objection -d -g package_name explore
sakura@sakura:~$ objection -d -g com.android.settings explore
[debug] Agent path is: /Users/sakura/.pyenv/versions/3.7.7/lib/python3.7/site-packages/objection/agent.js
[debug] Injecting agent...
Using USB device `Google Pixel`
[debug] Attempting to attach to process: `com.android.settings`
[debug] Process attached!
Agent injected and responds ok!
_ _ _ _
___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_| _| _| | . | |
|___|___| |___|___|_| |_|___|_|_|
|___|(object)inject(ion) v1.8.4
Runtime Mobile Exploration
by: @leonjza from @sensepost
[tab] for command suggestions
com.android.settings on (google: 8.1.0) [usb] #
modules 就是指的 so 库
查看内存中加载的 module,命令:memory list modules
com.android.settings on (google: 8.1.0) [usb] # memory list modules
Save the output by adding `--json modules.json` to this command
Name Base Size Path
----------------------- ------------ -------------------- ------------------------------------
app_process64 0x64ce143000 32768 (32.0 KiB) /system/bin/app_process64
libandroid_runtime.so 0x7a90bc3000 1990656 (1.9 MiB) /system/lib64/libandroid_runtime.so
libbinder.so 0x7a9379f000 557056 (544.0 KiB) /system/lib64/libbinder.so
查看库的导出函数:memory list exports libssl.so
com.android.settings on (google: 8.1.0) [usb] # memory list exports libssl.so
Save the output by adding `--json exports.json` to this command
Type Name Address
-------- ----------------------------------------------------- ------------
function SSL_use_certificate_ASN1 0x7c8ff006f8
function SSL_CTX_set_dos_protection_cb 0x7c8ff077b8
function SSL_SESSION_set_ex_data 0x7c8ff098f4
function SSL_CTX_set_session_psk_dhe_timeout 0x7c8ff0a754
function SSL_CTX_sess_accept 0x7c8ff063b8
function SSL_select_next_proto 0x7c8ff06a74
memory dump all 文件名
memory dump from_base 起始地址 字节数 文件
用法:memory search "" (--string) (--offsets-only)
在内存堆上搜索类的实例 ,命令:android heap search instances 类名
sakura@sakura:~$ objection -g myapplication.example.com.frida_demo explore
Using USB device `Google Pixel`
Agent injected and responds ok!
[usb] # android heap search instances myapplication.example.com.frida_demo
.MainActivity
Class instance enumeration complete for myapplication.example.com.frida_demo.MainActivity
Handle Class toString()
-------- ------------------------------------------------- ---------------------------------------------------------
0x2102 myapplication.example.com.frida_demo.MainActivity myapplication.example.com.frida_demo.MainActivity@5b1b0af
调用实例的方法,
命令:android heap execute 实例ID 实例方法
查看当前可用的 activity 或者 service
命令:android hooking list activities/services
com.android.settings on (google: 8.1.0) [usb] # android hooking list services
com.android.settings.SettingsDumpService
com.android.settings.TetherService
com.android.settings.bluetooth.BluetoothPairingService
直接启动 activity 或者服务
命令:android intent launch_activity/launch_service activity/服务
示例:这个命令比较有趣的是用在如果有些设计的不好,可能就直接绕过了密码锁屏等直接进去。
android intent launch_activity com.android.settings.DisplaySettings
列出内存中所有的类 :android hooking list classes
内存中搜索指定类的所有方法
命令:android hooking list class_methods 类名
com.android.settings on (google: 8.1.0) [usb] # android hooking list class_methods java.nio.charset.Charset
private static java.nio.charset.Charset java.nio.charset.Charset.lookup(java.lang.String)
private static java.nio.charset.Charset java.nio.charset.Charset.lookup2(java.lang.String)
private static java.nio.charset.Charset java.nio.charset.Charset.lookupViaProviders(java.lang.String)
在内存中所有已加载的类中搜索 包含特定关键词 的 类
示例:android hooking search classes display // 搜索 类 中包含 display 的 类
在内存中所有已加载的类的方法中搜索包含特定关键词的方法
命令:android hooking search methods display // 搜索方法中包含 display 关键字的 方法
com.android.settings on (google: 8.1.0) [usb] # android hooking search methods display
Warning, searching all classes may take some time and in some cases, crash the target application.
Continue? [y/N]: y
Found 5529 classes, searching methods (this may take some time)...
android.app.ActionBar.getDisplayOptions
android.app.ActionBar.setDefaultDisplayHomeAsUpEnabled
android.app.ActionBar.setDisplayHomeAsUpEnabled
hook 单个方法
objection log 默认是不能用 grep 过滤的,但是可以通过 objection run xxx | grep yyy 的方式,从终端通过管道来过滤。用法如下
sakura@sakura:~$ objection -g com.android.settings run memory list modules | grep libc
Warning: Output is not to a terminal (fd=1).
libcutils.so 0x7a94a1c000 81920 (80.0 KiB) /system/lib64/libcutils.so
libc++.so 0x7a9114e000 983040 (960.0 KiB) /system/lib64/libc++.so
libc.so 0x7a9249d000 892928 (872.0 KiB) /system/lib64/libc.so
libcrypto.so 0x7a92283000 1155072 (1.1 MiB) /system/lib64/libcrypto.so
有的命令后面可以通过 --json logfile
来直接保存结果到文件里。
有的可以通过查看 .objection (位置:C:\Users\用户名\.objection )文件里的输出 log 来查看结果。
sakura@sakurade:~/.objection$ cat *log | grep -i display
android.hardware.display.DisplayManager
android.hardware.display.DisplayManager$DisplayListener
android.hardware.display.DisplayManagerGlobal
案例学习case1:《仿VX数据库原型取证逆向分析》
附件链接:https://www.52pojie.cn/forum.php?mod=viewthread&tid=1082706
android-backup-extractor工具链接:https://github.com/nelenkov/android-backup-extractor
sakura@sakurade:~/Desktop/frida_learn$ java -version
java version "1.8.0_141"
sakura@sakuradeMacBook-Pro:~/Desktop/frida_learn$ java -jar abe-all.jar unpack 1.ab 1.tar
0% 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% 38% 39% 40% 41% 42% 43% 44% 45% 46% 47% 48% 49% 50% 51% 52% 53% 54% 55% 56% 57% 58% 59% 60% 61% 62% 63% 64% 65% 66% 67% 68% 69% 70% 71% 72% 73% 74% 75% 76% 77% 78% 79% 80% 81% 82% 83% 84% 85% 86% 87% 88% 89% 90% 91% 92% 93% 94% 95% 96% 97% 98% 99% 100%
9097216 bytes written to 1.tar.
...
sakura@sakurade:~/Desktop/frida_learn/apps/com.example.yaphetshan.tencentwelcome$ ls
Encryto.db _manifest a db
装个夜神模拟器玩
sakura@sakura:/Applications/NoxAppPlayer.app/Contents/MacOS$ ./adb connect 127.0.0.1:62001
* daemon not running. starting it now on port 5037 *
adb E 5139 141210 usb_osx.cpp:138] Unable to create an interface plug-in (e00002be)
* daemon started successfully *
connected to 127.0.0.1:62001
sakura@sakuradeMacBook-Pro:/Applications/NoxAppPlayer.app/Contents/MacOS$ ./adb shell
dream2qltechn:/ # whoami
root
dream2qltechn:/ # uname -a
Linux localhost 4.0.9+ #222 SMP PREEMPT Sat Mar 14 18:24:36 HKT 2020 i686
肯定还是先定位目标字符串 Wait a Minute,What was happend?
jadx 搜索字符串
重点在 a() 代码里,其实是根据明文的 name 和 password,然后 aVar.a(a2 + aVar.b(a2, contentValues.getAsString("password"))).substring(0, 7)
再做一遍复杂的计算并截取7位当做密码,传入 getWritableDatabase 去解密 demo.db 数据库。
所以我们 hook一下 getWritableDatabase 即可。
// 首先查看要注入的进程
frida-ps -U
...
5662 com.example.yaphetshan.tencentwelcome
...
// 使用 objection 注入
objection -d -g com.example.yaphetshan.tencentwelcome explore
看一下源码
package net.sqlcipher.database;
...
public abstract class SQLiteOpenHelper {
...
public synchronized SQLiteDatabase getWritableDatabase(char[] cArr) {
也可以 objection search 一下这个 method
(samsung: 7.1.2) [usb] # android hooking search methods getWritableDatabase
Warning, searching all classes may take some time and in some cases, crash the target application.
Continue? [y/N]: y
Found 4650 classes, searching methods (this may take some time)...
android.database.sqlite.SQLiteOpenHelper.getWritableDatabase
...
net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase
hook 一下这个 method
[usb] # android hooking watch class_method net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase --dump-args --dump-backtrace --dump-return
- [incoming message] ------------------
{
"payload": "Attempting to watch class \u001b[32mnet.sqlcipher.database.SQLiteOpenHelper\u001b[39m and method \u001b[32mgetWritableDatabase\u001b[39m.",
"type": "send"
}
- [./incoming message] ----------------
(agent) Attempting to watch class net.sqlcipher.database.SQLiteOpenHelper and method getWritableDatabase.
- [incoming message] ------------------
{
"payload": "Hooking \u001b[32mnet.sqlcipher.database.SQLiteOpenHelper\u001b[39m.\u001b[92mgetWritableDatabase\u001b[39m(\u001b[31mjava.lang.String\u001b[39m)",
"type": "send"
}
- [./incoming message] ----------------
(agent) Hooking net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(java.lang.String)
- [incoming message] ------------------
{
"payload": "Hooking \u001b[32mnet.sqlcipher.database.SQLiteOpenHelper\u001b[39m.\u001b[92mgetWritableDatabase\u001b[39m(\u001b[31m[C\u001b[39m)",
"type": "send"
}
- [./incoming message] ----------------
(agent) Hooking net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase([C)
- [incoming message] ------------------
{
"payload": "Registering job \u001b[94mjytq1qeyllq\u001b[39m. Type: \u001b[92mwatch-method for: net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase\u001b[39m",
"type": "send"
}
- [./incoming message] ----------------
(agent) Registering job jytq1qeyllq. Type: watch-method for: net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase
...mple.yaphetshan.tencentwelcome on (samsung: 7.1.2) [usb] #
hook 好之后再打开这个 apk
(agent) [1v488x28gcs] Called net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(java.lang.String)
...
(agent) [1v488x28gcs] Backtrace:
net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(Native Method)
com.example.yaphetshan.tencentwelcome.MainActivity.a(MainActivity.java:55)
com.example.yaphetshan.tencentwelcome.MainActivity.onCreate(MainActivity.java:42)
android.app.Activity.performCreate(Activity.java:6692)
...
(agent) [1v488x28gcs] Arguments net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(ae56f99)
...
...mple.yaphetshan.tencentwelcome on (samsung: 7.1.2) [usb] # jobs list
Job ID Hooks Type
----------- ------- -----------------------------------------------------------------------------
1v488x28gcs 2 watch-method for: net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase
找到参数 ae56f99
剩下的就是用这个密码去打开加密的 db。
然后base64解密一下就好了。
还有一种策略是主动调用,即自己去调用 a 函数以触发 getWritableDatabase 的数据库解密。先寻找 a 所在类的实例,然后 hook getWritableDatabase,最终主动调用 a。这里幸运的是 a 没有什么奇奇怪怪的参数需要我们传入。
[usb] # android heap search instances com.example.yaphetshan.tencentwelcome.MainActivity
Class instance enumeration complete for com.example.yaphetshan.tencentwelcome.MainActivity
Handle Class toString()
-------- -------------------------------------------------- ----------------------------------------------------------
0x20078a com.example.yaphetshan.tencentwelcome.MainActivity com.example.yaphetshan.tencentwelcome.MainActivity@1528f80
[usb] # android hooking watch class_method net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase --dump-args --dump-backtrace --dump-return
[usb] # android heap execute 0x20078a a
(agent) [taupgwkum4h] Arguments net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(ae56f99)
附件链接:https://bbs.pediy.com/thread-257745.htm
因为直接找 Unfortunately,note the right PIN :(找不到,可能是把字符串藏在什么资源文件里了。
review 代码之后找到校验的核心函数,逻辑就是将 input 编码一下之后和密码比较,这肯定是什么不可逆的加密。
public static boolean verifyPassword(Context context, String input) {
if (input.length() != 4) {
return false;
}
byte[] v = encodePassword(input);
byte[] p = "09042ec2c2c08c4cbece042681caf1d13984f24a".getBytes();
if (v.length != p.length) {
return false;
}
for (int i = 0; i < v.length; i++) {
if (v[i] != p[i]) {
return false;
}
}
return true;
}
这里就爆破一下密码。
// 查看 app 进程
frida-ps -U | grep qualification
7660 org.teamsik.ahe17.qualification.easy
// frida 命令执行 js 进行 hook
frida -U -f org.teamsik.ahe17.qualification.easy -l force.js
# 上面命令执行成功后,会进入 frida,再输入 %resume 然后回车,即可让程序继续执行
或者使用 -F 大写F 参数,frida -U -F org.teamsik.ahe17.qualification.easy -l force.js
js 脚本:
function main() {
Java.perform(function x() {
console.log("In Java perform")
var verify = Java.use("org.teamsik.ahe17.qualification.Verifier")
var stringClass = Java.use("java.lang.String")
var p = stringClass.$new("09042ec2c2c08c4cbece042681caf1d13984f24a")
var pSign = p.getBytes()
// var pStr = stringClass.$new(pSign)
// console.log(parseInt(pStr))
for (var i = 999; i < 10000; i++){
var v = stringClass.$new(String(i))
var vSign = verify.encodePassword(v)
if (parseInt(stringClass.$new(pSign)) == parseInt(stringClass.$new(vSign))) {
console.log("yes: " + v)
break
}
console.log("not :" + v)
}
})
}
setImmediate(main)
...
not :9080
not :9081
not :9082
yes: 9083
这里注意 parseInt
demo 就不贴了,还是先定位登录失败点,然后搜索字符串。
public class LoginActivity extends AppCompatActivity {
/* access modifiers changed from: private */
public Context mContext;
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.mContext = this;
setContentView((int) R.layout.activity_login);
final EditText editText = (EditText) findViewById(R.id.username);
final EditText editText2 = (EditText) findViewById(R.id.password);
((Button) findViewById(R.id.login)).setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
String obj = editText.getText().toString();
String obj2 = editText2.getText().toString();
if (TextUtils.isEmpty(obj) || TextUtils.isEmpty(obj2)) {
Toast.makeText(LoginActivity.this.mContext,
"username or password is empty.", 1).show();
} else if (LoginActivity.a(obj, obj).equals(obj2)) {
LoginActivity.this.startActivity(
new Intent(LoginActivity.this.mContext, FridaActivity1.class));
LoginActivity.this.finishActivity(0);
} else {
Toast.makeText(LoginActivity.this.mContext, "Login failed.", 1).show();
}
}
});
}
LoginActivity.a(obj, obj).equals(obj2)
分析之后可得 obj2 来自 password,由从 username 得来的 obj,经过 a 函数运算之后得到一个值,这两个值相等则登录成功。所以这里关键是 hook a 函数的参数,最简脚本如下。
//打印参数、返回值
function Login(){
Java.perform(function(){
Java.use("com.example.androiddemo.Activity.LoginActivity").a.overload('java.lang.String', 'java.lang.String').implementation = function (str, str2){
var result = this.a(str, str2);
console.log("args0:" + str + " args1:" + str2 + " result:" + result);
return result;
}
})
}
setImmediate(Login)
观察输入和输出,这里也可以直接主动调用。
function login() {
Java.perform(function () {
console.log("start")
var login = Java.use("com.example.androiddemo.Activity.LoginActivity")
var result = login.a("1234","1234")
console.log(result)
})
}
setImmediate(login)
输出:
...
start
4e4feaea959d426155a480dc07ef92f4754ee93edbe56d993d74f131497e66fb
然后
adb shell input text "4e4feaea959d426155a480dc07ef92f4754ee93edbe56d993d74f131497e66fb"
接下来是第一关
public abstract class BaseFridaActivity extends AppCompatActivity implements View.OnClickListener {
public Button mNextCheck;
public void CheckSuccess() {
}
public abstract String getNextCheckTitle();
public abstract void onCheck();
/* access modifiers changed from: protected */
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView((int) R.layout.activity_frida);
this.mNextCheck = (Button) findViewById(R.id.next_check);
this.mNextCheck.setOnClickListener(this);
Button button = this.mNextCheck;
button.setText(getNextCheckTitle() + ",点击进入下一关");
}
public void onClick(View view) {
onCheck();
}
public void CheckFailed() {
Toast.makeText(this, "Check Failed!", 1).show();
}
}
...
public class FridaActivity1 extends BaseFridaActivity {
private static final char[] table = {'L', 'K', 'N', 'M', 'O', 'Q', 'P', 'R', 'S', 'A', 'T', 'B', 'C', 'E', 'D', 'F', 'G', 'H', 'I', 'J', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'o', 'd', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'e', 'f', 'g', 'h', 'j', 'i', 'k', 'l', 'm', 'n', 'y', 'z', '0', '1', '2', '3', '4', '6', '5', '7', '8', '9', '+', '/'};
public String getNextCheckTitle() {
return "当前第1关";
}
public void onCheck() {
try {
if (a(b("请输入密码:")).equals("R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=")) {
CheckSuccess();
startActivity(new Intent(this, FridaActivity2.class));
finishActivity(0);
return;
}
super.CheckFailed();
} catch (Exception e) {
e.printStackTrace();
}
}
public static String a(byte[] bArr) throws Exception {
StringBuilder sb = new StringBuilder();
for (int i = 0; i <= bArr.length - 1; i += 3) {
byte[] bArr2 = new byte[4];
byte b = 0;
for (int i2 = 0; i2 <= 2; i2++) {
int i3 = i + i2;
if (i3 <= bArr.length - 1) {
bArr2[i2] = (byte) (b | ((bArr[i3] & 255) >>> ((i2 * 2) + 2)));
b = (byte) ((((bArr[i3] & 255) << (((2 - i2) * 2) + 2)) & 255) >>> 2);
} else {
bArr2[i2] = b;
b = 64;
}
}
bArr2[3] = b;
for (int i4 = 0; i4 <= 3; i4++) {
if (bArr2[i4] <= 63) {
sb.append(table[bArr2[i4]]);
} else {
sb.append('=');
}
}
}
return sb.toString();
}
public static byte[] b(String str) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
GZIPOutputStream gZIPOutputStream = new GZIPOutputStream(byteArrayOutputStream);
gZIPOutputStream.write(str.getBytes());
gZIPOutputStream.finish();
gZIPOutputStream.close();
byte[] byteArray = byteArrayOutputStream.toByteArray();
try {
byteArrayOutputStream.close();
return byteArray;
} catch (Exception e) {
e.printStackTrace();
return byteArray;
}
} catch (Exception unused) {
return null;
}
}
}
关键函数在 a(b("请输入密码:")).equals("R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=")
这里应该直接 hook a,让其返回值为 R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=
就可以进入下一关了。
function ch1() {
Java.perform(function () {
console.log("start")
Java.use("com.example.androiddemo.Activity.FridaActivity1").a.implementation = function (x) {
return "R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL="
}
})
}
总结:
xx.value = yy
,其他方面和函数一样。_
,如 _xx.value = yy
然后是第二关
public class FridaActivity2 extends BaseFridaActivity {
private static boolean static_bool_var = false;
private boolean bool_var = false;
public String getNextCheckTitle() {
return "当前第2关";
}
private static void setStatic_bool_var() {
static_bool_var = true;
}
private void setBool_var() {
this.bool_var = true;
}
public void onCheck() {
if (!static_bool_var || !this.bool_var) {
super.CheckFailed();
return;
}
CheckSuccess();
startActivity(new Intent(this, FridaActivity3.class));
finishActivity(0);
}
}
这一关的关键在于下面的 if 判断要为 false,则 static_bool_var
和 this.bool_var
都要为 true。
if (!static_bool_var || !this.bool_var) {
super.CheckFailed();
return;
}
这样就要调用 setBool_var
和 setStatic_bool_var
两个函数了。
function ch2() {
Java.perform(function () {
console.log("start")
var FridaActivity2 = Java.use("com.example.androiddemo.Activity.FridaActivity2")
// hook 静态函数 直接调用
FridaActivity2.setStatic_bool_var()
// hook 动态函数,找到 instance 实例,从 实例中 调用函数方法
Java.choose("com.example.androiddemo.Activity.FridaActivity2", {
onMatch: function (instance) {
instance.setBool_var()
},
onComplete: function () {
console.log("end")
}
})
})
}
setImmediate(ch2)
接下来是第三关
public class FridaActivity3 extends BaseFridaActivity {
private static boolean static_bool_var = false;
private boolean bool_var = false;
private boolean same_name_bool_var = false;
public String getNextCheckTitle() {
return "当前第3关";
}
private void same_name_bool_var() {
Log.d("Frida", static_bool_var + " " + this.bool_var + " " + this.same_name_bool_var);
}
public void onCheck() {
if (!static_bool_var || !this.bool_var || !this.same_name_bool_var) {
super.CheckFailed();
return;
}
CheckSuccess();
startActivity(new Intent(this, FridaActivity4.class));
finishActivity(0);
}
}
关键是让 if (!static_bool_var || !this.bool_var || !this.same_name_bool_var)
为 false,则三个变量都要为 true
function ch3() {
Java.perform(function () {
console.log("start")
var FridaActivity3 = Java.use("com.example.androiddemo.Activity.FridaActivity3")
FridaActivity3.static_bool_var.value = true
Java.choose("com.example.androiddemo.Activity.FridaActivity3", {
onMatch: function (instance) {
instance.bool_var.value = true
instance._same_name_bool_var.value = true
},
onComplete: function () {
console.log("end")
}
})
})
}
注意:类里有一个 成员函数 和 成员变量 都叫做 same_name_bool_var
,这种时候在成员变量前加一个 _
,修改值的形式为 xx.value = yy
总结:
类名$内部类名
去 use 或者 chooseclazz.class.getDeclaredMethods()
可以得到 类里面声明的所有方法,即 可以枚举类里面的所有函数。接下来是第四关
public class FridaActivity4 extends BaseFridaActivity {
public String getNextCheckTitle() {
return "当前第4关";
}
private static class InnerClasses {
public static boolean check1() { return false;}
public static boolean check2() { return false;}
public static boolean check3() { return false;}
public static boolean check4() { return false;}
public static boolean check5() { return false;}
public static boolean check6() { return false;}
private InnerClasses() {}
}
public void onCheck() {
if (!InnerClasses.check1() || !InnerClasses.check2() || !InnerClasses.check3()
|| !InnerClasses.check4() || !InnerClasses.check5() || !InnerClasses.check6())
{
super.CheckFailed();
return;
}
CheckSuccess();
startActivity(new Intent(this, FridaActivity5.class));
finishActivity(0);
}
}
这一关的关键是让 if (!InnerClasses.check1() || !InnerClasses.check2() || !InnerClasses.check3() || !InnerClasses.check4() || !InnerClasses.check5() || !InnerClasses.check6())
中的所有 check 全部返回 true。
其实这里唯一的问题就是寻找内部类 InnerClasses
,对于内部类的 hook,通过 类名$内部类名 去 use。
function ch4() {
Java.perform(function () {
var InnerClasses = Java.use("com.example.androiddemo.Activity.FridaActivity4$InnerClasses")
console.log("start")
InnerClasses.check1.implementation = function () { return true }
InnerClasses.check2.implementation = function () { return true }
InnerClasses.check3.implementation = function () { return true }
InnerClasses.check4.implementation = function () { return true }
InnerClasses.check5.implementation = function () { return true }
InnerClasses.check6.implementation = function () { return true }
})
}
利用反射,获取类中的所有 method 声明,然后字符串拼接去获取到方法名,例如下面的 check1,然后就可以批量 hook,而不用像我上面那样一个一个写。
var inner_classes = Java.use("com.example.androiddemo.Activity.FridaActivity4$InnerClasses")
var all_methods = inner_classes.class.getDeclaredMethods();
...
public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check1(),
public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check2(),
public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check3(),
public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check4(),
public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check5(),
public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check6()
hook 类方法 的 所有 重载
方法 1:
//目标类
var hook = Java.use(targetClass);
//重载次数
var overloadCount = hook[targetMethod].overloads.length;
//打印日志:追踪的方法有多少个重载
console.log("Tracing " + targetClassMethod + " [" + overloadCount + " overload(s)]");
//每个重载都进入一次
for (var i = 0; i < overloadCount; i++) {
//hook每一个重载
hook[targetMethod].overloads[i].implementation = function() {
console.warn("\n*** entered " + targetClassMethod);
//可以打印每个重载的调用栈,对调试有巨大的帮助,当然,信息也很多,尽量不要打印,除非分析陷入僵局
Java.perform(function() {
var bt = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
console.log("\nBacktrace:\n" + bt);
});
// 打印参数
if (arguments.length) console.log();
for (var j = 0; j < arguments.length; j++) {
console.log("arg[" + j + "]: " + arguments[j]);
}
//打印返回值
var retval = this[targetMethod].apply(this, arguments); // rare crash (Frida bug?)
console.log("\nretval: " + retval);
console.warn("\n*** exiting " + targetClassMethod);
return retval;
}
}
方法 2:
function hookOneClassAllMethod(clsName) {
console.log("开始 hook 一个类的所有方法");
// var NetContent = Java.use("com.xbiao.utils.net.NetContent");
var clazz = Java.use(clsName); // 得到 class
var all_method = clazz.class.getDeclaredMethods(); // 得到类的所有方法
all_method.forEach(function (mth) { // 遍历类的所有方法
var mthName = mth.getName(); // 得到 方法名
var all_overload = clazz[mthName].overloads;
all_overload.forEach(function (olad) {
// hook 重载 argumentTypes
olad.implementation = function () {
console.log("\r")
// printStack()
for (var i = 0; i < arguments.length; i++) {
var arg_type = olad.argumentTypes[i].className;
var arg_val = arguments[i];
var arg_fmt = JSON.stringify(arg_val);
var msg = mthName + " ---> " + "arg[" + i + ":"+ arg_type +"]:" + arg_fmt;
console.log(msg);
}
var retVal = this[mthName].apply(this, arguments);
console.log(mthName + " ---> 返回值:" + retVal);
return retVal;
}
})
})
}
function main() {
Java.perform(() => {
Java.enumeratteClassLoaders({
onMatch: function(loader){
console.log("ClassLoader start");
try {
if(loader.findClass("com.xbiao.utils.AESdedUtil")){
console.log("Successfully found loader")
console.log(loader);
Java.classFactory.loader = loader ;
hookOneClassAllMethod("com.xbiao.utils.net.NetContent")
}
}
catch(error){
console.log("find error:" + error)
}
},
onComplete: function (){
console.log("ClassLoader end");
}
});
});
}
setImmediate(main)
执行结果:
总结:
enumerateClassLoaders
来枚举加载进内存的 classloader,loader.findClass(xxx)
寻找是否包括我们想要的 interface 的实现类,Java.classFactory.loader = loader
来切换 classloader,从而加载该实现类。第五关比较有趣,它的 check 函数是动态加载进来的。
java 里有 interface 的概念,是指一系列抽象的接口,需要类来实现。
package com.example.androiddemo.Dynamic;
public interface CheckInterface { boolean check(); }
...
public class DynamicCheck implements CheckInterface {
public boolean check() { return false; }
}
...
public class FridaActivity5 extends BaseFridaActivity {
private CheckInterface DynamicDexCheck = null;
...
public CheckInterface getDynamicDexCheck() {
if (this.DynamicDexCheck == null) {
loaddex();
}
return this.DynamicDexCheck;
}
/* access modifiers changed from: protected */
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
loaddex();
// this.DynamicDexCheck = (CheckInterface) new DexClassLoader(
// str, filesDir.getAbsolutePath(), (String) null, getClassLoader()
// ).loadClass("com.example.androiddemo.Dynamic.DynamicCheck").newInstance();
}
public void onCheck() {
if (getDynamicDexCheck() == null) {
Toast.makeText(this, "onClick loaddex Failed!", 1).show();
} else if (getDynamicDexCheck().check()) {
CheckSuccess();
startActivity(new Intent(this, FridaActivity6.class));
finishActivity(0);
} else {
super.CheckFailed();
}
}
}
这里有个 loaddex 其实就是先从资源文件加载 classloader 到内存里,再 loadClass DynamicCheck,创建出一个实例,最终调用这个实例的 check。
所以现在我们就要先枚举 class loader,找到能实例化我们要的 class 的那个 class loader,然后把它设置成 Java 的默认 class factory 的 loader。
现在就可以用这个 class loader 来使用 .use 去 import 一个给定的类。
function ch5() {
Java.perform(function () {
// Java.choose("com.example.androiddemo.Activity.FridaActivity5",{
// onMatch:function(x){
// console.log(x.getDynamicDexCheck().$className)
// },onComplete:function(){}
// })
console.log("start")
Java.enumerateClassLoaders({
onMatch: function (loader) {
try {
if(loader.findClass("com.example.androiddemo.Dynamic.DynamicCheck")){
console.log("Successfully found loader")
console.log(loader);
Java.classFactory.loader = loader ;
}
}
catch(error){
console.log("find error:" + error)
}
},
onComplete: function () {
console.log("end1")
}
})
Java.use("com.example.androiddemo.Dynamic.DynamicCheck").check.implementation = function () {
return true
}
console.log("end2")
})
}
setImmediate(ch5)
todo有一个疑问:https://github.com/frida/frida/issues/1049
总结: 通过 Java.enumerateLoadedClasses
来 枚举类,然后 name.indexOf(str)
过滤一下并 hook。
接下来是第六关
import com.example.androiddemo.Activity.Frida6.Frida6Class0;
import com.example.androiddemo.Activity.Frida6.Frida6Class1;
import com.example.androiddemo.Activity.Frida6.Frida6Class2;
public class FridaActivity6 extends BaseFridaActivity {
public String getNextCheckTitle() {
return "当前第6关";
}
public void onCheck() {
if (!Frida6Class0.check() || !Frida6Class1.check() || !Frida6Class2.check()) {
super.CheckFailed();
return;
}
CheckSuccess();
startActivity(new Intent(this, FridaActivity7.class));
finishActivity(0);
}
}
这关是 import 了一些类,然后 调用类里的静态方法,所以我们枚举所有的类,然后过滤一下,并把过滤出来的结果 hook 上,改掉其返回值。
function ch6() {
Java.perform(function () {
Java.enumerateLoadedClasses({
onMatch: function (class_name, handle){
if (class_name.indexOf("com.example.androiddemo.Activity.Frida6") != -1) {
console.log("class_name:" + class_name + " handle:" + handle)
Java.use(class_name).check.implementation = function () {
return true
}
}
},
onComplete: function () {
console.log("end")
}
})
})
}
利用反射得到类里面实现的 interface 数组,并打印出来。
function more() {
Java.perform(function () {
Java.enumerateLoadedClasses({
onMatch: function (class_name){
if (class_name.indexOf("com.example.androiddemo") < 0) {
return
}
else {
var hook_cls = Java.use(class_name)
var interfaces = hook_cls.class.getInterfaces()
if (interfaces.length > 0) {
console.log(class_name + ": ")
for (var i in interfaces) {
console.log("\t", interfaces[i].toString())
}
}
}
},
onComplete: function () {
console.log("end")
}
})
})
}
import sys
import frida
'''
frida rpc 枚举类
'''
def on_message(message, data):
if message['type'] == 'send':
print("[*] {message['payload']}")
else:
print(message)
hook = """
Java.perform(function(){
Java.enumerateLoadedClasses({
"onMatch" : function(classname){
if(classname.indexOf("com.csair.mbp") < 0){
return;
}
// 实现类 implements
try{
var hookCls = Java.use(classname)
var interFaces = hookCls.class.getInterfaces();
if (interFaces.length > 0) {
console.log(classname)
for (var i in interFaces) {
// 接口类 interFaces
console.log("\t", interFaces[i].toString())
}
}
}catch(e){
console.log(e)
}
},
"onComplete" : function(){}
})
})
"""
process = frida.get_usb_device().attach('com.csair.mbp')
script = process.create_script(hook)
script.on('message', on_message)
print('[*] Running CTF')
script.load()
sys.stdin.read()
代码 2:
function searchInterface() {
Java.perform(function() {
Java.enumerateLoadedClasses({
onComplete: function () { },
onMatch: function (name, handle) {
if (name.indexOf("com.hexl.lessontest.logic") > -1) { // 使用
var targetInterface = "com.hexl.lessontest.logic.IAnima"
if (targetInterface === name) {
return;
}
console.log("find class");
var targetClass = Java.use(name);
console.log("\t", name);
var superClassName;
while (1) {
var interfaceList = targetClass.class.getInterfaces
if (interfaceList.length > 0) {
for (var i in interfaceList) {
var interString = interfaceList[i].toString
if (interString.indexOf(targetInterface) > -1)
console.log("\t\t\t", interString); //
break;
}
}
}
superClassName = targetClass.$super.$className;
targetClass = targetClass.$super;
if ("java.lang.Object" === superClassName) {
break;
}
console.log("\t\t", superClassName) // 打印类名
}
}
}
})
})
}
setImmediate(searchInterface)
题目下载地址:https://github.com/tlamb96/kgb_messenger
firda 的 -f 参数代表 span 启动:frida -U -f com.tlamb96.spetsnazmessenger -l frida_russian.js --no-pause
上面命令执行完后,会进入 frida ,然后在输入 %resume 恢复程序运行
/* access modifiers changed from: protected */
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView((int) R.layout.activity_main);
String property = System.getProperty("user.home");
String str = System.getenv("USER");
if (property == null || property.isEmpty() || !property.equals("Russia")) {
a("Integrity Error", "This app can only run on Russian devices.");
} else if (str == null || str.isEmpty() || !str.equals(getResources().getString(R.string.User))) {
a("Integrity Error", "Must be on the user whitelist.");
} else {
a.a(this);
startActivity(new Intent(this, LoginActivity.class));
}
}
}
这个题目比较简单,但是因为这个 check 是在 onCreate 里,所以 app 刚启动就自动检查,所以这里需要用 spawn 的方式去启动 frida 脚本 hook,而不是 attach。
这里有两个检查,一个是检查 property 的值,一个是检查 str 的值。分别从 System.getProperty 和 System.getenv 里获取,hook 住这两个函数就行。
这里要注意从资源文件里找到 User 的值。
frida_russian.js
function main() {
Java.perform(function () {
Java.use("java.lang.System").getProperty.overload('java.lang.String').implementation = function (str) {
return "Russia";
}
Java.use("java.lang.System").getenv.overload('java.lang.String').implementation = function(str){
return "RkxBR3s1N0VSTDFOR180UkNIM1J9Cg==";
}
})
}
setImmediate(main)
运行结果截图:
接下来进入到 login 功能
public void onLogin(View view) {
EditText editText = (EditText) findViewById(R.id.login_username);
EditText editText2 = (EditText) findViewById(R.id.login_password);
this.n = editText.getText().toString();
this.o = editText2.getText().toString();
if (this.n != null && this.o != null && !this.n.isEmpty() && !this.o.isEmpty()) {
if (!this.n.equals(getResources().getString(R.string.username))) {
Toast.makeText(this, "User not recognized.", 0).show();
editText.setText("");
editText2.setText("");
} else if (!j()) {
Toast.makeText(this, "Incorrect password.", 0).show();
editText.setText("");
editText2.setText("");
} else {
i();
startActivity(new Intent(this, MessengerActivity.class));
}
}
}
...
private boolean j() {
String str = "";
for (byte b : this.m.digest(this.o.getBytes())) {
str = str + String.format("%x", new Object[]{Byte.valueOf(b)});
}
return str.equals(getResources().getString(R.string.password));
}
...
private void i() {
char[] cArr = {'(', 'W', 'D', ')', 'T', 'P', ':', '#', '?', 'T'};
cArr[0] = (char) (cArr[0] ^ this.n.charAt(1));
cArr[1] = (char) (cArr[1] ^ this.o.charAt(0));
cArr[2] = (char) (cArr[2] ^ this.o.charAt(4));
cArr[3] = (char) (cArr[3] ^ this.n.charAt(4));
cArr[4] = (char) (cArr[4] ^ this.n.charAt(7));
cArr[5] = (char) (cArr[5] ^ this.n.charAt(0));
cArr[6] = (char) (cArr[6] ^ this.o.charAt(2));
cArr[7] = (char) (cArr[7] ^ this.o.charAt(3));
cArr[8] = (char) (cArr[8] ^ this.n.charAt(6));
cArr[9] = (char) (cArr[9] ^ this.n.charAt(8));
Toast.makeText(this, "FLAG{" + new String(cArr) + "}", 1).show();
}
从资源文件里找到 username,密码则是要算一个 j() 函数,要让它返回 true,顺便打印一下 i 函数 toast 到界面的 flag。
( github 代码中 不是 j() 函数,而是 checkPassword() 函数 )
var clazz = Java.use("com.tlamb96.kgbmessenger.LoginActivity")
clazz.j.implementation = function (){return true}
...
var clazz = Java.use("android.widget.Toast")
clazz.makeText.overload('android.content.Context', 'java.lang.CharSequence', 'int').implementation = function (x, y, z) {
var flag = Java.use("java.lang.String").$new(y)
console.log(flag)
}
...
[Google Pixel::com.tlamb96.spetsnazmessenger]-> FLAG{G&qG13 R0}
代码:
Java.perform(function () {
Java.use("com.tlamb96.kgbmessenger.LoginActivity").checkPassword.implementation = function () {
return true
};
Java.use("android.widget.Toast").makeText.overload('android.content.Context', 'java.lang.CharSequence', 'int').implementation = function (x, y, z) {
var flag = Java.use("java.lang.String").$new(y);
console.log(flag);
return this.makeText(x, y, z);
}
})
执行结果:
总结:hook 构造函数 是通过 Java.use 取得类,然后clazz.$init.implementation = callback
hook 构造函数。
我们先学习一下怎么 hook 构造函数。
add(new com.tlamb96.kgbmessenger.b.a(R.string.katya, "Archer, you up?", "2:20 am", true));
...
package com.tlamb96.kgbmessenger.b;
public class a {
...
public a(int i, String str, String str2, boolean z) {
this.f448a = i;
this.b = str;
this.c = str2;
this.d = z;
}
...
}
用 $init
来 hook 构造函数
function printstack() {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}
Java.use("com.tlamb96.kgbmessenger.b.a").$init.implementation = function (i, str1, str2, z) {
this.$init(i, str1, str2, z)
console.log(i, str1, str2, z)
printStack("com.tlamb96.kgbmessenger.b.a")
}
打印堆栈:
function printstack() {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}
打印栈回溯
function printStack(name) {
Java.perform(function () {
var Exception = Java.use("java.lang.Exception");
var instanceTemp = Exception.$new("Exception");
var straces = instanceTemp.getStackTrace();
if (straces != undefined && straces != null) {
var strace = straces.toString();
var replaceStr = strace.replace(/,/g, "\\n");
console.log("=============================" + name + " Stack strat=======================");
console.log(replaceStr);
console.log("=============================" + name + " Stack end=======================\r\n");
Exception.$dispose();
}
});
}
输出就是这样
[Google Pixel::com.tlamb96.spetsnazmessenger]-> 2131558449 111 02:27 下午 false
=============================com.tlamb96.kgbmessenger.b.a Stack strat=======================
com.tlamb96.kgbmessenger.b.a.(Native Method)
com.tlamb96.kgbmessenger.MessengerActivity.onSendMessage(Unknown Source:40)
java.lang.reflect.Method.invoke(Native Method)
android.support.v7.app.m$a.onClick(Unknown Source:25)
android.view.View.performClick(View.java:6294)
android.view.View$PerformClick.run(View.java:24770)
android.os.Handler.handleCallback(Handler.java:790)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loop(Looper.java:164)
android.app.ActivityThread.main(ActivityThread.java:6494)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
=============================com.tlamb96.kgbmessenger.b.a Stack end=======================
总结:编译出 dex 后,通过 Java.openClassFile("xxx.dex").load() 加载,就可以正常通过 Java.use 调用里面的方法了。
现在我们来继续解决这个问题。
public void onSendMessage(View view) {
EditText editText = (EditText) findViewById(R.id.edittext_chatbox);
String obj = editText.getText().toString();
if (!TextUtils.isEmpty(obj)) {
this.o.add(new com.tlamb96.kgbmessenger.b.a(R.string.user, obj, j(), false));
this.n.c();
if (a(obj.toString()).equals(this.p)) {
Log.d("MessengerActivity", "Successfully asked Boris for the password.");
this.q = obj.toString();
this.o.add(new com.tlamb96.kgbmessenger.b.a(R.string.boris, "Only if you ask nicely", j(), true));
this.n.c();
}
if (b(obj.toString()).equals(this.r)) {
Log.d("MessengerActivity", "Successfully asked Boris nicely for the password.");
this.s = obj.toString();
this.o.add(new com.tlamb96.kgbmessenger.b.a(R.string.boris, "Wow, no one has ever been so nice to me! Here you go friend: FLAG{" + i() + "}", j(), true));
this.n.c();
}
this.m.b(this.m.getAdapter().a() - 1);
editText.setText("");
}
}
新的一关是一个聊天框。分析一下代码可知,obj 是我们输入的内容,输入完了之后,加到一个 this.o 的 ArrayList 里。关键的 if 判断就是 if (a(obj.toString()).equals(this.p)) 和 if (b(obj.toString()).equals(this.r)),所以 hook 住 a函数 和 b函数,让它们的返回值等于下面的字符串即可。
private String p = "V@]EAASB\u0012WZF\u0012e,a$7(&am2(3.\u0003";
private String q;
private String r = "\u0000dslp}oQ\u0000 dks$|M\u0000h +AYQg\u0000P*!M$gQ\u0000";
private String s;
但实际上这题比我想象中的还要麻烦,这题的逻辑上是如果通过了 a 和 b 这两个函数的计算,等于对应的值之后,会把用来计算的 obj 的值赋值给 q 和 s,然后根据这个 q 和 s 来计算出最终的 flag。
所以如果不逆向算法,通过 hook 的方式通过了 a和b 的计算,obj 的值还是错误的,也计算不出正确的 flag。
这样就逆向一下算法好了,先自己写一个 apk,用 java 去实现注册机。
可以直接把 class 文件转成 dex,不复述,我比较懒,所以我直接解压 apk 找到 classes.dex
,并 push 到手机上。
然后用 frida 加载这个 dex,并调用里面的方法。
var dex = Java.openClassFile("/data/local/tmp/classes.dex").load();
console.log("decode_P:"+Java.use("myapplication.example.com.reversea.reverseA").decode_P());
console.log("r_to_hex:"+Java.use("myapplication.example.com.reversea.reverseA").r_to_hex());
...
...
decode_P:Boris, give me the password
r_to_hex:0064736c707d6f510020646b73247c4d0068202b4159516700502a214d24675100
Android 加固应用Hook方式 --- Frida:https://github.com/xiaokanghub/Android
转载:使用 frida 来 hook 加固的 Android 应用的 java 层:https://bbs.pediy.com/thread-246767.htm
使用 Frida 给 apk 脱壳并穿透加固 Hook 函数:https://xz.aliyun.com/t/7670
[推荐] 『Android安全』版2018年优秀和精华帖分类索引:https://bbs.pediy.com/thread-249602.htm
要 hook 加固的应用分为三步,
示例:使用 Frida 给 apk 脱壳并穿透加固 Hook 函数:https://xz.aliyun.com/t/7670
Java.perform(function () {
var application = Java.use('android.app.Application');
application.attach.overload('android.content.Context').implementation = function (context) {
var result = this.attach(context);
var classloader = context.getClassLoader();
Java.classFactory.loader = classloader;
var yeyoulogin = Java.classFactory.use('com.zcm.主窗口');
console.log("yeyoulogin:" + yeyoulogin);
yeyoulogin.按钮_用户登录$被单击.implementation = function (arg) {
console.log("retval:" + this.返回值);
}
}
});
列出加载的类
Java.enumerateLoadedClasses(
{
"onMatch": function (className) { console.log(className); },
"onComplete": function () { }
}
)
Java.perform(function () {
//创建一个DexClassLoader的wapper
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
//hook 它的构造函数$init,我们将它的四个参数打印出来看看。
dexclassLoader.$init.implementation = function (dexPath, optimizedDirectory, librarySearchPath, parent) {
console.log("dexPath:" + dexPath);
console.log("optimizedDirectory:" + optimizedDirectory);
console.log("librarySearchPath:" + librarySearchPath);
console.log("parent:" + parent);
//不破换它原本的逻辑,我们调用它原本的构造函数。
this.$init(dexPath, optimizedDirectory, librarySearchPath, parent);
}
console.log("down!");
});
Java.perform(function () {
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");
dexclassLoader.loadClass.overload('java.lang.String').implementation = function (name) {
//定义一个String变量,指定我们需要的类
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
//直接调用第二个重载方法,跟原本的逻辑相同。
var result = this.loadClass(name, false);
//如果loadClass的name参数和我们想要hook的类名相同
if (name === hookname) {
//则拿到它的值
hookClass = result;
//打印hookClass变量的值
console.log(hookClass);
send(hookClass);
return result;
}
return result;
}
});
( JAVA 中 Class> 表示 泛型 ),再调用动态加载方法
Java.perform(function(){
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var constructorclass = Java.use("java.lang.reflect.Constructor");
var objectclass= Java.use("java.lang.Object");
dexclassLoader.loadClass.overload('java.lang.String').implementation = function(name){
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
var result = this.loadClass(name,false);
if(name == hookname){
var hookClass = result;
console.log("-------------------CAST--------------------------")
//类型转换
var hookClassCast = Java.cast(hookClass,ClassUse);
//调用getMethods()获取类下的所有方法
var methods = hookClassCast.getMethods();
console.log(methods);
console.log("------------------NOT CAST----------------------")
//未进行类型转换,看看能否调用getMethods()方法
var methodtest = hookClass.getMethods();
console.log(methodtest);
console.log("-------------------OVER-----------------------")
return result;
}
return result;
}
});
利用 getDeclaredConstructor() 获取具有指定参数列表构造函数的 Constructor 并实例化
Java.perform(function () {
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");
var objectclass = Java.use("java.lang.Object");
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var orininclass = Java.use("cn.chaitin.geektan.crackme.MainActivity");
var Integerclass = Java.use("java.lang.Integer");
//实例化MainActivity对象
var mainAc = orininclass.$new();
dexclassLoader.loadClass.overload('java.lang.String').implementation = function (name) {
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
var result = this.loadClass(name, false);
if (name == hookname) {
var hookClass = result;
var hookClassCast = Java.cast(hookClass, ClassUse);
console.log("---------------------BEGIN------------------------------");
//获取构造器
var ConstructorParam = Java.array('Ljava.lang.Object;', [objectclass.class]);
var Constructor = hookClassCast.getDeclaredConstructor(ConstructorParam);
console.log("Constructor:" + Constructor);
console.log("orinin:" + mainAc);
//实例化,newInstance的参数也是Ljava.lang.Object;
var instance = Constructor.newInstance([mainAc]);
console.log("patchAc:" + instance);
send(instance);
console.log("--------------------------------------------------------");
return result;
}
return result;
}
});
利用 getDeclaredMethods(),获取本类中的所有方法
Java.perform(function(){
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");
var objectclass= Java.use("java.lang.Object");
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var orininclass = Java.use("cn.chaitin.geektan.crackme.MainActivity");
var Integerclass = Java.use("java.lang.Integer");
//实例化MainActivity对象
var mainAc = orininclass.$new();
dexclassLoader.loadClass.overload('java.lang.String').implementation = function(name){
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
var result = this.loadClass(name,false);
if(name == hookname){
var hookClass = result;
var hookClassCast = Java.cast(hookClass,ClassUse);
console.log("-----------------BEGIN-----------------------------");
//获取构造器
var ConstructorParam =Java.array('Ljava.lang.Object;',[objectclass.class]);
var Constructor = hookClassCast.getDeclaredConstructor(ConstructorParam);
console.log("Constructor:"+Constructor);
console.log("orinin:"+mainAc);
//实例化,newInstance的参数也是Ljava.lang.Object;
var instance = Constructor.newInstance([mainAc]);
console.log("MainActivityPatchInstance:"+instance);
send(instance);
console.log("-----------------Methods---------------------------");
var func = hookClassCast.getDeclaredMethods();
console.log(func);
console.log("-----------------Need Method-----------------------");
console.log(func[0]);
var f = func[0];
console.log("------------------ OVER----------------------------");
return result;
}
return result;
}
});
用法:f.invoke(instance,Array);
invoke 方法的参数
/*
* Note: Only compatible with libc++, though libstdc++'s std::string is a lot simpler.
*/
function readStdString(str) {
const isTiny = (str.readU8() & 1) === 0;
if (isTiny) {
return str.add(1).readUtf8String();
}
return str.add(2 * Process.pointerSize).readPointer().readUtf8String();
}
Java.perform(function () {
var SystemDef = Java.use('java.lang.System');
var RuntimeDef = Java.use('java.lang.Runtime');
var exceptionClass = Java.use('java.lang.Exception');
var SystemLoad_1 = SystemDef.load.overload('java.lang.String');
var SystemLoad_2 = SystemDef.loadLibrary.overload('java.lang.String');
var RuntimeLoad_1 = RuntimeDef.load.overload('java.lang.String');
var RuntimeLoad_2 = RuntimeDef.loadLibrary.overload('java.lang.String');
var ThreadDef = Java.use('java.lang.Thread');
var ThreadObj = ThreadDef.$new();
SystemLoad_1.implementation = function (library) {
send("Loading dynamic library => " + library);
stackTrace();
return SystemLoad_1.call(this, library);
}
SystemLoad_2.implementation = function (library) {
send("Loading dynamic library => " + library);
stackTrace();
SystemLoad_2.call(this, library);
return;
}
RuntimeLoad_1.implementation = function (library) {
send("Loading dynamic library => " + library);
stackTrace();
RuntimeLoad_1.call(this, library);
return;
}
RuntimeLoad_2.implementation = function (library) {
send("Loading dynamic library => " + library);
stackTrace();
RuntimeLoad_2.call(this, library);
return;
}
function stackTrace() {
var stack = ThreadObj.currentThread().getStackTrace();
for (var i = 0; i < stack.length; i++) {
send(i + " => " + stack[i].toString());
}
send("--------------------------------------------------------");
}
});
不会安装使用 FRIDA 的,请先自行百度学会。。。然后,默念一声 "我想脱个壳"。
或者可以将脚本封装成命令:
完整的代码已经上传github,Frida-Scripts/shell at master · smartdone/Frida-Scripts · GitHub
要hook加固的应用分为三步,第一步是拿到加载应用本事dex的classloader;第二步是通过这个classloader去找到被加固的类;第三步是通过这个类去hook需要hook的方法
得到第一步的classloader之后的hook操作和hook未加固的应用基本类似。
如何获取classloader
我们看Android的android.app.Application
的源码http://androidxref.com/7.1.2_r36/xref/frameworks/base/core/java/android/app/Application.java#188可以发现,自己定义的Application
的attachBaseContext
方法是在Application
的attach
方法里面被调用的。而基本上所有的壳都是在attachBaseContext
里面完成的代码解密并且内存加载dex,在attachBaseContext
执行完之后就可以去拿classloader,此时的classloader就已经是加载过加固dex的classloader了。
以 "i春秋 app ( https://www.wandoujia.com/apps/7456953 )" 为例,此应用使用的360加固,我们的目标是hook他的flytv.run.monitor.fragment.user.AyWelcome
的 onCreate
方法,然后弹出一个 Toast
。
直接使用 Java.use
我们直接使用Java.use来获取这个Activity,代码如下:
if(Java.available) {
Java.perform(function(){
var AyWelcome = Java.use("flytv.run.monitor.fragment.user.AyWelcome");
if(AyWelcome != undefined) {
console.log("AyWelcome: " + AyWelcome.toString());
} else {
console.log("AyWelcome: undefined");
}
});
}
使用如下命令来注入这个js:
frida -R -f com.ni.ichunqiu -l hook_java.js
运行之后会报如下的错误:
也就是找不到这个类,也就是我们现在这个默认的 classloader 找不到flytv.run.monitor.fragment.user.AyWelcome
这个类。
获取 classloader
代码如下:
if(Java.available) {
Java.perform(function(){
var application = Java.use("android.app.Application");
application.attach.overload('android.content.Context').implementation = function(context) {
var result = this.attach(context); // 先执行原来的attach方法
var classloader = context.getClassLoader();
return result;
}
});
}
现在我们在attach
方法执行之后拿到了Context,并且通过context获取了classloader,我们看现在的classloader是否加载了被加固的dex。我们使用classloader的loadClass
方法去加载flytv.run.monitor.fragment.user.AyWelcome
这个类,看是否成功:
if(Java.available) {
Java.perform(function(){
var application = Java.use("android.app.Application");
var reflectClass = Java.use("java.lang.Class");
application.attach.overload('android.content.Context').implementation = function(context) {
var result = this.attach(context); // 先执行原来的attach方法
var classloader = context.getClassLoader(); // 获取classloader
var AyWelcome = classloader.loadClass("flytv.run.monitor.fragment.user.AyWelcome"); // 使用classloader加载类
AyWelcome = Java.cast(AyWelcome, reflectClass); // 因为loadClass得到的是一个Object对象,我们需要把它强制转换成Class
console.log("AyWelcome class name: " + AyWelcome.getName());
return result;
}
});
}
注入这个 js,可以正确的打印出flytv.run.monitor.fragment.user.AyWelcome
类名,说明我们拿到这个这个classloader是加载了加固过的dex的。
转换成Java.use
获取到的js对象
在上一步我们虽然可以通过frida来获取到加固之后的class,但是你如果直接使用这个{class}.{fuction}
依然会失败,因为class没有这个成员变量,所以我们需要来实现获取到与Java.use
一样的js对象,那么如何解决呢?当然是read the fuking source code
。
我们看frida-java的use
方法的实现,代码在frida-java-bridge/class-factory.js at 9becc27091576fc198dc2a719c0fedb30a270b28 · frida/frida-java-bridge · GitHub
代码如下:
this.use = function (className) {
let C = classes[className];
if (!C) {
const env = vm.getEnv();
if (loader !== null) {
const usedLoader = loader;
if (cachedLoaderMethod === null) {
cachedLoaderInvoke = env.vaMethod('pointer', ['pointer']);
cachedLoaderMethod = loader.loadClass.overload('java.lang.String').handle;
}
const getClassHandle = function (env) {
const classNameValue = env.newStringUtf(className);
const tid = Process.getCurrentThreadId();
ignore(tid);
try {
return cachedLoaderInvoke(env.handle, usedLoader.$handle, cachedLoaderMethod, classNameValue);
} finally {
unignore(tid);
env.deleteLocalRef(classNameValue);
}
};
C = ensureClass(getClassHandle, className);
} else {
const canonicalClassName = className.replace(/\./g, '/');
const getClassHandle = function (env) {
const tid = Process.getCurrentThreadId();
ignore(tid);
try {
return env.findClass(canonicalClassName);
} finally {
unignore(tid);
}
};
C = ensureClass(getClassHandle, className);
}
}
return new C(null);
};
从代码中我们可以看出来,他会先到他存class的一个列表里面去找,如果找不到,就会判断loader是不是null,loader不为null,就会使用loader加载class,loader为null就会使用JNIEnv
的findClass方法去找类,也就是使用默认的classloader。所以现在目标明确了,我们只需要让这个loader
是我们从Applicaiton
的attach方法获取到的classloader即可,那么怎么替换呢?
很显然直接Java.loader
会说undefined,我们看最终导出的是index.js这个脚本frida-java-bridge/index.js at 022bc7d95c00d627091d4edc0ff87b67de5a9739 · frida/frida-java-bridge · GitHub,有下面几个成员变量:
let initialized = false;
let api = null;
let apiError = null;
let vm = null;
let classFactory = null;
let pending = [];
let threadsInPerform = 0;
let cachedIsAppProcess = null;
我们看到了,这个classFactory
不就是我们刚刚上面看到的那个loader
所在的地方吗,那么要引用这个loader就很简单了,直接Java.classFactory.loader
就可以引用了,你可以使用console.log("classloader: " + Java.classFactory.loader);
来获取这个loader的值,后面我们直接将这个值替换为我们获取的classloader就行了,代码如下:
if(Java.available) {
Java.perform(function(){
var application = Java.use("android.app.Application");
var reflectClass = Java.use("java.lang.Class");
console.log("application: " + application);
application.attach.overload('android.content.Context').implementation = function(context) {
var result = this.attach(context); // 先执行原来的attach方法
var classloader = context.getClassLoader(); // 获取classloader
Java.classFactory.loader = classloader;
var AyWelcome = Java.classFactory.use("flytv.run.monitor.fragment.user.AyWelcome"); //这里能直接使用Java.use,因为java.use会检查在不在perform里面,不在就会失败
console.log("AyWelcome: " + AyWelcome);
return result;
}
});
}
写 hook 加固的类的代码,弹出 toast
if(Java.available) {
Java.perform(function(){
var application = Java.use("android.app.Application");
var Toast = Java.use('android.widget.Toast');
application.attach.overload('android.content.Context').implementation = function(context) {
var result = this.attach(context); // 先执行原来的attach方法
var classloader = context.getClassLoader(); // 获取classloader
Java.classFactory.loader = classloader;
var AyWelcome = Java.classFactory.use("flytv.run.monitor.fragment.user.AyWelcome"); //这里不能直接使用Java.use,因为java.use会检查在不在perform里面,不在就会失败
console.log("AyWelcome: " + AyWelcome);
// 然后下面的代码就和写正常的hook一样啦
AyWelcome.onCreate.overload('android.os.Bundle').implementation = function(bundle) {
var ret = this.onCreate(bundle);
Toast.makeText(context, "onCreate called", 1).show(); //弹出Toast
return ret;
}
return result;
}
});
}
bytes array 是 object,String 和 bytes array 可以相互转化
String.getBytes() :字符串 转 bytes array
new String(bytes) :bytes 转成 字符串
$className 参看官方文档说明:
Gson 是谷歌官方推出的支持 JSON 和 Java Object
相互转换的 Java 序列化/反序列化
库。
gson 基本用法:https://blog.csdn.net/chenrenxiang/article/details/80291224 https://www.cnblogs.com/baiqiantao/p/7512336.html
Android Gson使用详解:https://www.jianshu.com/p/0444693c2639
Frida 打印 [object] 解决 'gson' 包重名的问题:https://www.52pojie.cn/thread-1167397-1-1.html
使用 Frida 时,想要打印 Java 对象的内容,可以使用谷歌的 gson包,可以非常优秀的将 Java 对象的内容,以 json 的格式打印出来。
Java.openClassFile("/data/local/tmp/r0gson.dex").load();
const gson = Java.use('com.r0ysue.gson.Gson');
console.log(gson.$new().toJson(xxx));
Log.d("SimpleArray", "onCreate: SImpleArray");
char arr[][] = new char[4][]; // 创建一个4行的二维数组
arr[0] = new char[] { '春', '眠', '不', '觉', '晓' }; // 为每一行赋值
arr[1] = new char[] { '处', '处', '闻', '啼', '鸟' };
arr[2] = new char[] { '夜', '来', '风', '雨', '声' };
arr[3] = new char[] { '花', '落', '知', '多', '少' };
Log.d("SimpleArray", "-----横版-----");
for (int i = 0; i < 4; i++) { // 循环4行
Log.d("SimpleArraysToString", Arrays.toString(arr[i]));
Log.d("SimpleStringBytes", Arrays.toString(Arrays.toString(arr[i]).getBytes()));
for (int j = 0; j < 5; j++) { // 循环5列
Log.d("SimpleArray", Character.toString(arr[i][j])); // 输出数组中的元素
}
if (i % 2 == 0) {
Log.d("SimpleArray", ",");// 如果是一、三句,输出逗号
} else {
Log.d("SimpleArray", "。");// 如果是二、四句,输出句号
}
}
新建一个 Android 项目,把上面代码放到 onCreate 函数中,再导入缺失的包
import android.util.Log; import java.util.Arrays;
点击菜单栏上的 build ---> 生成 apk,成功生成 apk 后,手机安装 apk,在启动 frida-server 和端口转发
执行命令:frida-ps -Ua 查看 apk 的包名
main.js
function main() {
Java.perform(function x() {
Java.openClassFile("/data/local/tmp/r0gson.dex").load();
const gson = Java.use('com.r0ysue.gson.Gson');
Java.use("java.lang.Character").toString.overload('char').implementation = function (char) {
var result = this.toString(char);
console.log("char,result", char, result);
return result;
}
Java.use("java.util.Arrays").toString.overload('[C').implementation = function (charArray) {
var result = this.toString(charArray);
console.log("charArray,result:", charArray, result)
console.log("charArray Object Object:", gson.$new().toJson(charArray));
return result;
}
})
}
setImmediate(main)
在执行命令:frida -U -f com.example.myapplication -l .\main.js --no-pause
这里的 [C
是 JNI 函数签名
方法1:使用 gson
function main() {
Java.perform(function x() {
Java.openClassFile("/data/local/tmp/r0gson.dex").load();
const gson = Java.use('com.r0ysue.gson.Gson');
Java.use("java.util.Arrays").toString.overload('[B').implementation = function (byteArray) {
var result = this.toString(byteArray);
console.log("byteArray,result):", byteArray, result)
console.log("byteArray Object Object:", gson.$new().toJson(byteArray));
return result;
}
})
}
setImmediate(main)
方法 2:通过 Array.toString() 方法
// byte[]类型 转 String
var clazzArray = Java.use("java.util.Arrays");
var JavaString = Java.use("java.lang.String");
send("参数对应数组:" + clazzArray.toString(x))
send("参数对应字符串:" + JavaString.$new(x))
方法 3:通过 String 类方法
// byte[] bArray = "123abc".getBytes() // java 代码
var strTemp = Java.use("java.lang.String").$new(bArray);
console.log(strTemp)
如果不只是想打印出结果,而是要替换原本的参数,就要先自己构造出一个charArray,使用 Java.array
这个API
/**
* Creates a Java array with elements of the specified `type`, from a
* JavaScript array `elements`. The resulting Java array behaves like
* a JS array, but can be passed by reference to Java APIs in order to
* allow them to modify its contents.
*
* @param type Type name of elements.
* @param elements Array of JavaScript values to use for constructing the
* Java array.
*/
function array(type: string, elements: any[]): any[];
Java.use("java.util.Arrays").toString.overload('[C').implementation = function(charArray){
var newCharArray = Java.array('char', [ '一','去','二','三','里' ]);
var result = this.toString(newCharArray);
console.log("newCharArray,result:",newCharArray,result)
console.log("newCharArray Object Object:",gson.$new().toJson(newCharArray));
var newResult = Java.use('java.lang.String').$new(Java.array('char', [ '烟','村','四','五','家']))
return newResult;
}
可以用来构造参数重发包,用在爬虫上。
可以通过 getClass().getName().toString()
来查看当前实例的类型。
找到一个 instance,通过 Java.cast
来强制转换对象的类型。
/**
* Creates a JavaScript wrapper given the existing instance at `handle` of
* given class `klass` as returned from `Java.use()`.
*
* @param handle An existing wrapper or a JNI handle.
* @param klass Class wrapper for type to cast to.
*/
function cast(handle: Wrapper | NativePointerValue, klass: Wrapper): Wrapper;
java 示例代码( 定义一个 water 类 和 一个 Juice 类,同时 Juice 继承 water ):
public class Water { // 水 类
public static String flow(Water W) { // 水 的方法
// SomeSentence
Log.d("2Object", "water flow: I`m flowing");
return "water flow: I`m flowing";
}
public String still(Water W) { // 水 的方法
// SomeSentence
Log.d("2Object", "water still: still water runs deep!");
return "water still: still water runs deep!";
}
}
...
public class Juice extends Water { // 果汁 类 继承了水类
public String fillEnergy(){
Log.d("2Object", "Juice: i`m fillingEnergy!");
return "Juice: i`m fillingEnergy!";
}
js 示例代码:
var JuiceHandle = null ;
Java.choose("com.r0ysue.a0526printout.Juice",{
onMatch:function(instance){
console.log("found juice instance",instance);
console.log("juice instance call fill",instance.fillEnergy());
JuiceHandle = instance;
},onComplete:function(){
console.log("juice handle search completed!")
}
})
console.log("Saved juice handle :",JuiceHandle);
var WaterHandle = Java.cast(JuiceHandle,Java.use("com.r0ysue.a0526printout.Water"))
console.log("call Waterhandle still method:",WaterHandle.still(WaterHandle));
示例:
function printHashMap(param_hm){
var HashMap = Java.use('java.util.HashMap');
var args_map = Java.cast(param_hm, HashMap)
send('args_map:' + args_map.toString());
}
public interface liquid {
public String flow();
}
frida 提供能力去创建一个新的 java class
/**
* Creates a new Java class.
*
* @param spec Object describing the class to be created.
*/
function registerClass(spec: ClassSpec): Wrapper;
首先获取要实现的 interface,然后调用 registerClass 来实现 interface。
function main() {
Java.perform(function(){
var liquid = Java.use("com.r0ysue.a0526printout.liquid");
var beer = Java.registerClass({
name: 'com.r0ysue.a0526printout.beer',
implements: [liquid],
methods: {
flow: function () {
console.log("look, beer is flowing!")
return "look, beer is flowing!";
}
}
});
console.log("beer.bubble:",beer.$new().flow())
})
}
setImmediate(main)
看 smali 或者 枚举出来的类。
function printstack() {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}
function hook_class_all_method(class_name) {
// 得到一个 类
let clazz = Java.use(class_name);
// 得到一个类的所有方法
let clazz_all_methods = clazz.class.getDeclareMethods();
// 遍历类的所有方法
clazz_all_methods.forEach(function (method) {
let methodName = method.getName();
// 得到一个方法的所有重载
let all_overloads = clazz[methodName].overloads;
// 遍历一个方法的所有重载
all_overloads.forEach(function (overload) {
let temp = "("
for (let i = 0; i < overload.argumentTypes.length; i++) {
temp += overload.argumentTypes[i].className + ","
}
temp += ")"
let print_method = class_name + '.' + methodName + '.' + temp;
// hook 重载
overload.implementation = function () {
console.log(print_method);
for (let i = 0; i < arguments.length; i++) {
// console.log("参数" + i + ":" + arguments[i]);
console.log("参数" + i + ":" + JSON.stringify(arguments[i]));
}
let ret = this[methodName].apply(this, arguments);
// console.log("返回值:" + ret);
console.log("返回值:" + JSON.stringify(ret));
printstack();
return ret;
}
})
});
}
Java.perform(() => {
hook_class_all_method("com.xxx.xxx");
})
关于 java 枚举,从这篇文章了解:https://www.cnblogs.com/jingmoxukong/p/6098351.html
enum Signal {
GREEN, YELLOW, RED
}
public class TrafficLight {
public static Signal color = Signal.RED;
public static void main() {
Log.d("4enum", "enum "+ color.getClass().getName().toString());
switch (color) {
case RED:
color = Signal.GREEN;
break;
case YELLOW:
color = Signal.RED;
break;
case GREEN:
color = Signal.YELLOW;
break;
}
}
}
Java.perform(function(){
Java.choose("com.r0ysue.a0526printout.Signal",{
onMatch:function(instance){
console.log("instance.name:",instance.name());
console.log("instance.getDeclaringClass:",instance.getDeclaringClass());
},onComplete:function(){
console.log("search completed!")
}
})
})
方法 1: ( hashmap 有个 toString 函数,可以直接打印 )
function printHashMap(param_hm){
var ClazzHashMap = Java.use('java.util.HashMap');
var args_map = Java.cast(param_hm, ClazzHashMap)
send('args_map:' + args_map.toString());
}
【深入Java基础】HashMap 的基本用法:https://blog.csdn.net/wxgxgp/article/details/79194360
Java集合之HashMap的用法:https://blog.csdn.net/weixin_43263961/article/details/86427533
方法 2:通过 Java.choose 获取堆栈中指定类名的实例,获得实例后可以调用实例的函数。
Java.perform(function(){
Java.choose("java.util.HashMap",{
onMatch:function(instance){
if(instance.toString().indexOf("ISBN")!= -1){
console.log("instance.toString:",instance.toString());
}
},onComplete:function(){
console.log("search complete!")
}
})
})
frida 复杂类型参数打印、参数转换、调用栈打印:https://blog.csdn.net/weixin_35762183/article/details/106802647
方法 3:通过 hook 住 map或者 hashmap 重载的方法
# -*- coding: UTF-8 -*-
import frida, sys
jsCode = """
Java.perform(function () {
/*
var clazz = Java.use("xxx");
clazz.b.overload('java.util.Map').implementation = function (args1) {
var result = "";
var keyset = args1.keySet();
var it = keyset.iterator();
while (it.hasNext()) {
var keystr = it.next().toString();
var valuestr = args1.get(keystr).toString();
console.log(keystr)
console.log(valuestr)
result += valuestr;
}
var args = this.b(args1)
console.log("出参--", args)
return args
}
*/
var HashMap = Java.use('java.util.HashMap');
var ShufferMap = Java.use('com.xiaojianbang.app.ShufferMap');
ShufferMap.show.implementation = function (map) {
var hm = HashMap.$new();
hm.put("user","dajianbang");
hm.put("pass","87654321");
hm.put("code","123456");
return this.show(hm);
}
});
""";
def message(message, data):
if message["type"] == 'send':
print(u"[*] {0}".format(message['payload']))
else:
print(message)
process = frida.get_remote_device().attach("com.xiaojianbang.app")
script= process.create_script(jsCode)
script.on("message", message)
script.load()
sys.stdin.read()
js_code:
js_code = '''
Java.perform(function() {
var clazz = Java.use('com.xxx.xxx');
clazz.signUrl.overload('java.lang.String', 'java.util.Map').implementation = function(arg_str, arg_map) {
console.log('arg_str:', arg_str);
console.log('arg_map:', arg_map);
var result = "";
var key_set = arg_map.keySet();
var key_set_it = key_set.iterator();
while(key_set_it.hasNext()){
var key_str = key_set_it.next().toString();
var value_str = arg_map.get(key_str).toString();
console.log(key_str)
console.log(value_str)
}
return this.signUrl(arg_str, arg_map)
}
});
'''
var currentApplication= Java.use("android.app.ActivityThread").currentApplication();
var context = currentApplication.getApplicationContext();
https://api-caller.com/2019/03/30/frida-note/#non-ascii
类名非 ASCII 字符串时,先编码打印出来, 再用编码后的字符串去 hook。
//场景 hook cls.forName寻找目标类的 classloader。
cls.forName.overload('java.lang.String', 'boolean', 'java.lang.ClassLoader').implementation = function (arg1, arg2, arg3) {
var clsName = cls.forName(arg1, arg2, arg3);
console.log('oriClassName:' + arg1)
var base64Name = encodeURIComponent(arg1)
console.log('encodeName:' + base64Name);
//通过日志确认base64后的非ascii字符串,下面对比并打印classloader
//clsName为特殊字符o.ÎÉ«
if ('o.%CE%99%C9%AB' == base64Name) {
//打印classloader
console.log(arg3);
}
return clsName;
}
如果代码进行了混淆,一些函数、方法会变成 非ASCII、甚至有一些不可见的字符,所以可以先编码打印出来,再用编码后的字符串去 hook
int ֏(int x) {
return x + 100;
}
JavaScript 代码:
Java.perform(
function x() {
var targetClass = "com.example.hooktest.MainActivity";
var hookCls = Java.use(targetClass);
var methods = hookCls.class.getDeclaredMethods();
for (var i in methods) {
console.log(methods[i].toString());
console.log(encodeURIComponent(methods[i].toString().replace(/^.*?\.([^\s\.\(\)]+)\(.*?$/, "$1")));
}
hookCls[decodeURIComponent("%D6%8F")]
.implementation = function (x) {
console.log("original call: fun(" + x + ")");
var result = this[decodeURIComponent("%D6%8F")](900);
return result;
}
}
)
类名是奇怪字符。可以找个取巧的方式,把这个包下的类都遍历出来,这样不就可以知道这个类名的UTF-8 编码的转义了吗?
Java.enumerateLoadedClasses({
onMatch: function(className) {
if(className.indexOf('com.google.android.material.tooltip') >=0 ){
console.log(className.toString());
console.log(encodeURIComponent(className.toString()));
}
},
onComplete:function(){
}
});
拿到了转义编码之后如何hook呢? 这时候就需要 decodeURIComponent函数了。这次frida就不抱怨找不到类名了。找到了类名当然不是我们的目的,我们的目的是星辰大海,哦不,是hook成员函数呀。这时候我们就需要去遍历方法名了
var hookCls = Java.use(decodeURIComponent('com.google.android.material.tooltip.%DB%A4%DB%A4%DB%9F%DB%A6'));
var methods = hookCls.class.getDeclaredMethods();for (var i in methods) {
console.log(methods[i].toString());
console.log(encodeURIComponent(methods[i].toString().replace(/^.*?.([^\s.()]+)(.*?$/, "$1")));
}
结果倒是没问题,就是分辨起来还有点麻烦。只能从成员函数的入参和返回值来分辨我们想要hook的成员函数
hookCls[decodeURIComponent("%DB%9F%DB%A3%DB%A5%DB%9F%DB%A3")]
.implementation = function () {
console.log("m1344 =============== ");
return "xxx";}
使用 objection 打印混淆的方法名,然后再 hook 打印的方法名即可 hook 对应的函数。
objection 是基于 frida 的命令行 hook 工具,可以让你不写代码, 敲几句命令就可以对 java 函数的高颗粒度 hook, 还支持 RPC 调用。
objection 目前只支持 Java层的 hook,但是 objection 有提供插件接口,可以自己写 frida 脚本去定义接口,
比如葫芦娃大佬的脱壳插件,实名推荐: https://github.com/hluwa/FRIDA-DEXDump
frida-dexdump 的命令行是基于 frida-tools,
- 快速的 dump 前台应用:frida-dexdump -FU
- 或者通过指定app且以spawn方式dump应用:frida-dexdump -U -f com.app.pkgname
-h 查看帮助
- -o OUTPUT, --output OUTPUT 输出的目录路径,默认 './
/'. - -d, --deep-search 启用深度搜索模式。(推荐加上 -d 参数,花费时间长,但是结果更完整)
- --sleep SLEEP 开始前等待时间, spawn模式默认是5s.
官方仓库: https://github.com/sensepost/objection
这里以 腾讯新闻.apk 为例:
通过 抓包分析可知,腾讯新闻 app 有三个参数需要破解:qn-rid、qn-sig、qn-newsig,通过 jadx-gui 分析源码可知,
这里分析 qn-newsig 参数:
这里分析 qn-newsig 参数:
双击,定位到 qn-newsig
找到真正的 加密方法
使用 objection 注入 com.tencent.news,命令:objection -g com.tencent.news explore
列出 类 中的所有方法:
命令:android hooking list class_methods com.tencent.news.utils.n.b
在结合反编译后的源码,根据 函数返回值、反编译后的注释、函数参数的类型和个数 找出 真正要hook的函数。
public static java.lang.String com.tencent.news.utils.n.b.ʼ(byte[])
public static java.lang.String com.tencent.news.utils.n.b.ʼ(java.lang.String)
public static java.lang.String com.tencent.news.utils.n.b.ʼ(java.lang.String,int)
public static java.lang.String com.tencent.news.utils.n.b.ʼ(java.lang.String,java.lang.String)
public static java.lang.String com.tencent.news.utils.n.b.ʼ(long)
public static java.lang.String com.tencent.news.utils.n.b.ʼ(long,int)
public static java.lang.String com.tencent.news.utils.n.b.ʼ(java.lang.String) 这个函数就是真正要 hook 的函数,直接 hook,打印参数、返回值。即可
var SQLiteDatabase = Java.use('com.tencent.wcdb.database.SQLiteDatabase');
var Set = Java.use("java.util.Set");
var ContentValues = Java.use("android.content.ContentValues");
SQLiteDatabase.insert.implementation = function (arg1, arg2, arg3) {
this.insert.call(this, arg1, arg2, arg3);
console.log("[insert] -> arg1:" + arg1 + "\t arg2:" + arg2);
var values = Java.cast(arg3, ContentValues);
var sets = Java.cast(values.keySet(), Set);
var arr = sets.toArray().toString().split(",");
for (var i = 0; i < arr.length; i++) {
console.log("[insert] -> key:" + arr[i] + "\t value:" + values.get(arr[i]));
}
};
https://www.jianshu.com/p/87ce6f565d37
extern "C" 与 名称修饰 (name mangling)
#define TAG "sakura1328"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__)
extern "C" JNIEXPORT jstring JNICALL
Java_myapplication_example_com_ndk_1demo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
LOGD("sakura1328");
return env->NewStringUTF(hello.c_str());
}
...
public class MainActivity extends AppCompatActivity {
private static final String TAG = "sakura";
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
Log.d(TAG, stringFromJNI());
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
public native String stringWithJNI(String context);
...
extern "C"
JNIEXPORT jstring JNICALL
Java_myapplication_example_com_ndk_1demo_MainActivity_stringWithJNI(
JNIEnv *env, jobject instance, jstring context_) {
const char *context = env->GetStringUTFChars(context_, 0);
int context_size = env->GetStringUTFLength(context_);
if (context_size > 0) {
LOGD("%s\n", context);
}
env->ReleaseStringUTFChars(context_, context);
return env->NewStringUTF("sakura1328");
}
12-26 22:30:00.548 15764-15764/myapplication.example.com.ndk_demo D/sakura1328: sakura
总结:多去读一下 Java 的反射 API。
Java高级特性 ----- 反射:https://www.jianshu.com/p/9be58ee20dee
这里其实有一个伏笔,就是为什么我们要 trace artmethod,hook artmethod ?????
是因为有些 so 混淆得非常厉害,然后也就很难静态分析看出 so 里面调用了哪些 java 函数,也不是通过类似 JNI 的 GetMethodID 这样来调用的。
而是通过类似 findclass 这种方法先得到类,然后再反射调用 app 里面的某个 java 函数。
所以去 hook 它执行的位置,每一个 java 函数对于 Android 源码而言都是一个 artmethod 结构体,然后 hook 拿到 artmethod 实例后,再去调用类函数,打印这个函数的名称。
public class MainActivity extends AppCompatActivity {
private static final String TAG = "sakura";
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText(stringWithJNI("sakura"));
// Log.d(TAG, stringFromJNI());
// Log.d(TAG, stringWithJNI("sakura"));
try {
testClass();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
public void testClass() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Test sakuraTest = new Test();
// 获得Class的方法(三种)
Class testClazz = MainActivity.class.getClassLoader().loadClass("myapplication.example.com.ndk_demo.Test");
Class testClazz2 = Class.forName("myapplication.example.com.ndk_demo.Test");
Class testClazz3 = Test.class;
Log.i(TAG, "Classloader.loadClass->" + testClazz);
Log.i(TAG, "Classloader.loadClass->" + testClazz2);
Log.i(TAG, "Classloader.loadClass->" + testClazz3.getName());
// 获得类中属性相关的方法
Field publicStaticField = testClazz3.getDeclaredField("publicStaticField");
Log.i(TAG, "testClazz3.getDeclaredField->" + publicStaticField);
Field publicField = testClazz3.getDeclaredField("publicField");
Log.i(TAG, "testClazz3.getDeclaredField->" + publicField);
//对于Field的get方法,如果是static,则传入null即可;如果不是,则需要传入一个类的实例
String valueStaticPublic = (String) publicStaticField.get(null);
Log.i(TAG, "publicStaticField.get->" + valueStaticPublic);
String valuePublic = (String) publicField.get(sakuraTest);
Log.i(TAG, "publicField.get->" + valuePublic);
//对于private属性,需要设置Accessible
Field privateStaticField = testClazz3.getDeclaredField("privateStaticField");
privateStaticField.setAccessible(true);
String valuePrivte = (String) privateStaticField.get(null);
Log.i(TAG, "modified before privateStaticField.get->" + valuePrivte);
privateStaticField.set(null, "modified");
valuePrivte = (String) privateStaticField.get(null);
Log.i(TAG, "modified after privateStaticField.get->" + valuePrivte);
Field[] fields = testClazz3.getDeclaredFields();
for (Field i : fields) {
Log.i(TAG, "testClazz3.getDeclaredFields->" + i);
}
// 获得类中method相关的方法
Method publicStaticMethod = testClazz3.getDeclaredMethod("publicStaticFunc");
Log.i(TAG, "testClazz3.getDeclaredMethod->" + publicStaticMethod);
publicStaticMethod.invoke(null);
Method publicMethod = testClazz3.getDeclaredMethod("publicFunc", java.lang.String.class);
Log.i(TAG, "testClazz3.getDeclaredMethod->" + publicMethod);
publicMethod.invoke(sakuraTest, " sakura");
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
public native String stringWithJNI(String context);
}
...
public class Test {
private static final String TAG = "sakura_test";
public static String publicStaticField = "i am a publicStaticField";
public String publicField = "i am a publicField";
private static String privateStaticField = "i am a privateStaticField";
private String privateField = "i am a privateField";
public static void publicStaticFunc() {
Log.d(TAG, "I`m from publicStaticFunc");
}
public void publicFunc(String str) {
Log.d(TAG, "I`m from publicFunc" + str);
}
private static void privateStaticFunc() {
Log.i(TAG, "I`m from privateFunc");
}
private void privateFunc() {
Log.i(TAG, "I`m from privateFunc");
}
}
...
...
12-26 23:57:11.784 17682-17682/myapplication.example.com.ndk_demo I/sakura: Classloader.loadClass->class myapplication.example.com.ndk_demo.Test
12-26 23:57:11.784 17682-17682/myapplication.example.com.ndk_demo I/sakura: Classloader.loadClass->class myapplication.example.com.ndk_demo.Test
12-26 23:57:11.784 17682-17682/myapplication.example.com.ndk_demo I/sakura: Classloader.loadClass->myapplication.example.com.ndk_demo.Test
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredField->public static java.lang.String myapplication.example.com.ndk_demo.Test.publicStaticField
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredField->public java.lang.String myapplication.example.com.ndk_demo.Test.publicField
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: publicStaticField.get->i am a publicStaticField
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: publicField.get->i am a publicField
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: modified before privateStaticField.get->i am a privateStaticField
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: modified after privateStaticField.get->modified
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredFields->private java.lang.String myapplication.example.com.ndk_demo.Test.privateField
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredFields->public java.lang.String myapplication.example.com.ndk_demo.Test.publicField
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredFields->private static final java.lang.String myapplication.example.com.ndk_demo.Test.TAG
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredFields->private static java.lang.String myapplication.example.com.ndk_demo.Test.privateStaticField
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredFields->public static java.lang.String myapplication.example.com.ndk_demo.Test.publicStaticField
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredMethod->public static void myapplication.example.com.ndk_demo.Test.publicStaticFunc()
12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo D/sakura_test: I`m from publicStaticFunc
12-26 23:57:11.786 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredMethod->public void myapplication.example.com.ndk_demo.Test.publicFunc(java.lang.String)
12-26 23:57:11.786 17682-17682/myapplication.example.com.ndk_demo D/sakura_test: I`m from publicFunc sakura
memory list modules
流出加载的 so 库
这一节的主要内容就是关于反调试的原理和如何破解反调试,重要内容还是看文章理解即可。
因为我并不需要做反调试相关的工作,所以部分内容略过。
Frida 反调试 与 反反调试 基本思路
(Java层API、Native层API、Syscall)
对 native 函数的 java 层 hook 和主动调用和普通 java 函数完全一致,略过。
导入 jni.h,先 search 一下这个文件在哪。
Error /Users/sakura/Library/Android/sdk/ndk-bundle/sysroot/usr/include/jni.h,27: Can't open include file 'stdarg.h'
Total 1 errors
Caching 'Exports'... ok
报错,所以拷贝一份 jni.h 出来,将这两个头文件导入删掉
导入成功
现在就能识别_JNIEnv了,如图
先查看一下导出了哪些函数。
extern "C" JNIEXPORT jstring JNICALL
Java_myapplication_example_com_ndk_1demo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
LOGD("sakura1328");
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_myapplication_example_com_ndk_1demo_MainActivity_stringWithJNI(JNIEnv *env, jobject instance,
jstring context_) {
const char *context = env->GetStringUTFChars(context_, 0);
int context_size = env->GetStringUTFLength(context_);
if (context_size > 0) {
LOGD("%s\n", context);
}
env->ReleaseStringUTFChars(context_, context);
return env->NewStringUTF("sakura1328");
}
这里有几个需要的 API。
Process.enumerateModules()
,这个API可以枚举被加载到内存的 modules。Module.findBaseAddress(module name)
来查找要hook的函数所在的so的基地址,如果找不到就返回null。findExportByName(moduleName: string, exportName: string): NativePointer
来查找导出函数的绝对地址。如果不知道moduleName是什么,可以传入一个null进入,但是会花费一些时间遍历所有的module。如果找不到就返回null。Interceptor.attach
。使用方法见下代码。Java.vm.getEnv().getStringUtfChars(args[2], null).readCString()
这里是循环调用的 string_with_jni,如果不循环调用,就要主动调用一下这个函数,或者 hook dlopen。
hook dlopen 的方法( https://github.com/lasting-yang/frida_dump/blob/master/dump_dex.js )可以参考。
function hook_native() {
// console.log(JSON.stringify(Process.enumerateModules()));
var libnative_addr = Module.findBaseAddress("libnative-lib.so")
console.log("libnative_addr is: " + libnative_addr)
if (libnative_addr) {
var string_with_jni_addr = Module.findExportByName("libnative-lib.so",
"Java_myapplication_example_com_ndk_1demo_MainActivity_stringWithJNI")
console.log("string_with_jni_addr is: " + string_with_jni_addr)
}
Interceptor.attach(string_with_jni_addr, {
onEnter: function (args) {
console.log("string_with_jni args: " + args[0], args[1], args[2])
console.log(Java.vm.getEnv().getStringUtfChars(args[2], null).readCString())
},
onLeave: function (retval) {
console.log("retval:", retval)
console.log(Java.vm.getEnv().getStringUtfChars(retval, null).readCString())
var newRetval = Java.vm.getEnv().newStringUtf("new retval from hook_native");
retval.replace(ptr(newRetval));
}
})
}
libnative_addr is: 0x7a0842f000
string_with_jni_addr is: 0x7a08436194
[Google Pixel::myapplication.example.com.ndk_demo]-> string_with_jni args: 0x7a106cc1c0 0x7ff0b71da4 0x7ff0b71da8
sakura
retval: 0x75
sakura1328
这里还写了一个 hook env 里的 GetStringUTFChars 的代码,和上面一样,不赘述了。
function hook_art(){
var addr_GetStringUTFChars = null;
//console.log( JSON.stringify(Process.enumerateModules()));
var symbols = Process.findModuleByName("libart.so").enumerateSymbols();
for(var i = 0;i=0)){
if(symbol.indexOf("GetStringUTFChars")>=0){
console.log(symbols[i].name);
console.log(symbols[i].address);
addr_GetStringUTFChars = symbols[i].address;
}
}
}
console.log("addr_GetStringUTFChars:", addr_GetStringUTFChars);
Java.perform(function (){
Interceptor.attach(addr_GetStringUTFChars, {
onEnter: function (args) {
console.log("addr_GetStringUTFChars OnEnter args[0],args[1]",args[0],args[1]);
//console.log(hexdump(args[0].readPointer()));
//console.log(Java.vm.tryGetEnv().getStringUtfChars(args[0]).readCString());
}, onLeave: function (retval) {
console.log("addr_GetStringUTFChars OnLeave",ptr(retval).readCString());
}
})
})
}
function hook_libc(){
var pthread_create_addr = null;
var symbols = Process.findModuleByName("libc.so").enumerateSymbols();
for(var i = 0;i=0){
//console.log(symbols[i].name);
//console.log(symbols[i].address);
pthread_create_addr = symbols[i].address;
}
}
console.log("pthread_create_addr,",pthread_create_addr);
Interceptor.attach(pthread_create_addr,{
onEnter:function(args){
console.log("pthread_create_addr args[0],args[1],args[2],args[3]:",args[0],args[1],args[2],args[3]);
},onLeave:function(retval){
console.log("retval is:",retval)
}
})
}
https://github.com/android/ndk-samples
JNI_Onload / 动态注册 / Frida hook RegisterNative
详细的内容参见我写的文章,这里只给出例子。
Log.d(TAG,stringFromJNI2());
public native String stringFromJNI2();
JNIEXPORT jstring JNICALL stringFromJNI2(
JNIEnv *env,
jclass clazz) {
jclass testClass = env->FindClass("myapplication/example/com/ndk_demo/Test");
jfieldID publicStaticField = env->GetStaticFieldID(testClass, "publicStaticField",
"Ljava/lang/String;");
jstring publicStaticFieldValue = (jstring) env->GetStaticObjectField(testClass,
publicStaticField);
const char *value_ptr = env->GetStringUTFChars(publicStaticFieldValue, NULL);
LOGD("now content is %s", value_ptr);
std::string hello = "Hello from C++ stringFromJNI2";
return env->NewStringUTF(hello.c_str());
}
...
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
vm->GetEnv((void **) &env, JNI_VERSION_1_6);
JNINativeMethod methods[] = {
{"stringFromJNI2", "()Ljava/lang/String;", (void *) stringFromJNI2},
};
env->RegisterNatives(env->FindClass("myapplication/example/com/ndk_demo/MainActivity"), methods,
1);
return JNI_VERSION_1_6;
}
使用下面这个脚本来打印出 RegisterNatives 的参数,这里需要注意的是使用了enumerateSymbolsSync,它是 enumerateSymbols 的同步版本。
另外和我们之前通过 Java.vm.tryGetEnv().getStringUtfChars
来调用env里的方法不同。
这里则是通过将之前找到的getStringUtfChars函数地址和参数信息封装起来,直接调用,具体的原理我没有深入分析,先记住用法。
原理其实是一样的,都是 根据符号找到地址,然后hook符号地址,然后打印参数。
declare const NativeFunction: NativeFunctionConstructor;
interface NativeFunctionConstructor {
new(address: NativePointerValue, retType: NativeType, argTypes: NativeType[], abiOrOptions?: NativeABI | NativeFunctionOptions): NativeFunction;
readonly prototype: NativeFunction;
}
...
var funcGetStringUTFChars = new NativeFunction(addrGetStringUTFChars, "pointer", ["pointer", "pointer", "pointer"]);
var ishook_libart = false;
function hook_libart() {
if (ishook_libart === true) {
return;
}
var symbols = Module.enumerateSymbolsSync("libart.so");
var addrGetStringUTFChars = null;
var addrNewStringUTF = null;
var addrFindClass = null;
var addrGetMethodID = null;
var addrGetStaticMethodID = null;
var addrGetFieldID = null;
var addrGetStaticFieldID = null;
var addrRegisterNatives = null;
var addrAllocObject = null;
var addrCallObjectMethod = null;
var addrGetObjectClass = null;
var addrReleaseStringUTFChars = null;
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
if (symbol.name == "_ZN3art3JNI17GetStringUTFCharsEP7_JNIEnvP8_jstringPh") {
addrGetStringUTFChars = symbol.address;
console.log("GetStringUTFChars is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI12NewStringUTFEP7_JNIEnvPKc") {
addrNewStringUTF = symbol.address;
console.log("NewStringUTF is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI9FindClassEP7_JNIEnvPKc") {
addrFindClass = symbol.address;
console.log("FindClass is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI11GetMethodIDEP7_JNIEnvP7_jclassPKcS6_") {
addrGetMethodID = symbol.address;
console.log("GetMethodID is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI17GetStaticMethodIDEP7_JNIEnvP7_jclassPKcS6_") {
addrGetStaticMethodID = symbol.address;
console.log("GetStaticMethodID is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI10GetFieldIDEP7_JNIEnvP7_jclassPKcS6_") {
addrGetFieldID = symbol.address;
console.log("GetFieldID is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI16GetStaticFieldIDEP7_JNIEnvP7_jclassPKcS6_") {
addrGetStaticFieldID = symbol.address;
console.log("GetStaticFieldID is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi") {
addrRegisterNatives = symbol.address;
console.log("RegisterNatives is at ", symbol.address, symbol.name);
} else if (symbol.name.indexOf("_ZN3art3JNI11AllocObjectEP7_JNIEnvP7_jclass") >= 0) {
addrAllocObject = symbol.address;
console.log("AllocObject is at ", symbol.address, symbol.name);
} else if (symbol.name.indexOf("_ZN3art3JNI16CallObjectMethodEP7_JNIEnvP8_jobjectP10_jmethodIDz") >= 0) {
addrCallObjectMethod = symbol.address;
console.log("CallObjectMethod is at ", symbol.address, symbol.name);
} else if (symbol.name.indexOf("_ZN3art3JNI14GetObjectClassEP7_JNIEnvP8_jobject") >= 0) {
addrGetObjectClass = symbol.address;
console.log("GetObjectClass is at ", symbol.address, symbol.name);
} else if (symbol.name.indexOf("_ZN3art3JNI21ReleaseStringUTFCharsEP7_JNIEnvP8_jstringPKc") >= 0) {
addrReleaseStringUTFChars = symbol.address;
console.log("ReleaseStringUTFChars is at ", symbol.address, symbol.name);
}
}
if (addrRegisterNatives != null) {
Interceptor.attach(addrRegisterNatives, {
onEnter: function (args) {
console.log("[RegisterNatives] method_count:", args[3]);
var env = args[0];
var java_class = args[1];
var funcAllocObject = new NativeFunction(addrAllocObject, "pointer", ["pointer", "pointer"]);
var funcGetMethodID = new NativeFunction(addrGetMethodID, "pointer", ["pointer", "pointer", "pointer", "pointer"]);
var funcCallObjectMethod = new NativeFunction(addrCallObjectMethod, "pointer", ["pointer", "pointer", "pointer"]);
var funcGetObjectClass = new NativeFunction(addrGetObjectClass, "pointer", ["pointer", "pointer"]);
var funcGetStringUTFChars = new NativeFunction(addrGetStringUTFChars, "pointer", ["pointer", "pointer", "pointer"]);
var funcReleaseStringUTFChars = new NativeFunction(addrReleaseStringUTFChars, "void", ["pointer", "pointer", "pointer"]);
var clz_obj = funcAllocObject(env, java_class);
var mid_getClass = funcGetMethodID(env, java_class, Memory.allocUtf8String("getClass"), Memory.allocUtf8String("()Ljava/lang/Class;"));
var clz_obj2 = funcCallObjectMethod(env, clz_obj, mid_getClass);
var cls = funcGetObjectClass(env, clz_obj2);
var mid_getName = funcGetMethodID(env, cls, Memory.allocUtf8String("getName"), Memory.allocUtf8String("()Ljava/lang/String;"));
var name_jstring = funcCallObjectMethod(env, clz_obj2, mid_getName);
var name_pchar = funcGetStringUTFChars(env, name_jstring, ptr(0));
var class_name = ptr(name_pchar).readCString();
funcReleaseStringUTFChars(env, name_jstring, name_pchar);
//console.log(class_name);
var methods_ptr = ptr(args[2]);
var method_count = parseInt(args[3]);
for (var i = 0; i < method_count; i++) {
var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
var name = Memory.readCString(name_ptr);
var sig = Memory.readCString(sig_ptr);
var find_module = Process.findModuleByAddress(fnPtr_ptr);
console.log("[RegisterNatives] java_class:", class_name, "name:", name, "sig:", sig, "fnPtr:", fnPtr_ptr, "module_name:", find_module.name, "module_base:", find_module.base, "offset:", ptr(fnPtr_ptr).sub(find_module.base));
}
},
onLeave: function (retval) { }
});
}
ishook_libart = true;
}
hook_libart();
结果很明显的打印了出来,包括动态注册的函数的名字,函数签名,加载地址和在so里的偏移量,
[RegisterNatives] java_class: myapplication.example.com.ndk_demo.MainActivity name: stringFromJNI2 sig: ()Ljava/lang/String; fnPtr: 0x79f8698484 module_name: libnative-lib.so module_base: 0x79f8691000 offset: 0x7484
最后测试一下 yang 开源的一个hook art的脚本,很有意思,trace 出了非常多的需要的信息。
frida -U --no-pause -f package_name -l hook_art.js
...
[FindClass] name:myapplication/example/com/ndk_demo/Test
[GetStaticFieldID] name:publicStaticField, sig:Ljava/lang/String;
[GetStringUTFChars] result:i am a publicStaticField
[NewStringUTF] bytes:Hello from C++ stringFromJNI2
[GetStringUTFChars] result:sakura
直接使用 frida 提供的接口打印栈回溯。
Interceptor.attach(f, {
onEnter: function (args) {
console.log('RegisterNatives called from:\n' +
Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
}
});
效果如下,我加到了 hook registerNative 的地方。
[Google Pixel::myapplication.example.com.ndk_demo]-> RegisterNatives called from:
0x7a100be03c libart.so!0xe103c
0x7a100be038 libart.so!0xe1038
0x79f85699a0 libnative-lib.so!_ZN7_JNIEnv15RegisterNativesEP7_jclassPK15JNINativeMethodi+0x44
0x79f85698e0 libnative-lib.so!JNI_OnLoad+0x90
0x7a102b9fd4 libart.so!_ZN3art9JavaVMExt17LoadNativeLibraryEP7_JNIEnvRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEP8_jobjectP8_jstringPS9_+0x638
0x7a08e3820c libopenjdkjvm.so!JVM_NativeLoad+0x110
0x70b921c4 boot.oat!oatexec+0xa81c4
使用 Interceptor.replace
,不赘述。主要目的还是为了改掉函数原本的执行行为,而不是仅仅打印一些信息。
inline hook 简单理解就是:不是 hook 函数开始执行的地方,而是 hook 函数中间执行的指令
整体来说没什么区别,就是把找函数符号地址改成从so里找到偏移,然后加到so基地址上就行,注意一下它的 attach 的 callback。
/**
* Callback to invoke when an instruction is about to be executed.
*/
type InstructionProbeCallback = (this: InvocationContext, args: InvocationArguments) => void;
type InvocationContext = PortableInvocationContext | WindowsInvocationContext | UnixInvocationContext;
interface PortableInvocationContext {
/**
* Return address.
*/
returnAddress: NativePointer;
/**
* CPU registers. You may also update register values by assigning to these keys.
*/
context: CpuContext;
/**
* OS thread ID.
*/
threadId: ThreadId;
/**
* Call depth of relative to other invocations.
*/
depth: number;
/**
* User-defined invocation data. Useful if you want to read an argument in `onEnter` and act on it in `onLeave`.
*/
[x: string]: any;
}
...
...
interface Arm64CpuContext extends PortableCpuContext {
x0: NativePointer;
x1: NativePointer;
x2: NativePointer;
x3: NativePointer;
x4: NativePointer;
x5: NativePointer;
x6: NativePointer;
x7: NativePointer;
x8: NativePointer;
x9: NativePointer;
x10: NativePointer;
x11: NativePointer;
x12: NativePointer;
x13: NativePointer;
x14: NativePointer;
x15: NativePointer;
x16: NativePointer;
x17: NativePointer;
x18: NativePointer;
x19: NativePointer;
x20: NativePointer;
x21: NativePointer;
x22: NativePointer;
x23: NativePointer;
x24: NativePointer;
x25: NativePointer;
x26: NativePointer;
x27: NativePointer;
x28: NativePointer;
fp: NativePointer;
lr: NativePointer;
}
我的 so 是自己编译的,具体的汇编代码如下,总之这里很明显在 775C 时,x0 里保存的是一个指向 "sakura" 这个字符串的指针。(其实我也不是很看得懂 arm64 了已经,就随便 hook 了一下)
所以 hook 这个指令,然后 Memory.readCString(this.context.x0);
打印出来,结果如下
.text:000000000000772C ; __unwind {
.text:000000000000772C SUB SP, SP, #0x40
.text:0000000000007730 STP X29, X30, [SP,#0x30+var_s0]
.text:0000000000007734 ADD X29, SP, #0x30
.text:0000000000007738 ; 6: v6 = a1;
.text:0000000000007738 MOV X8, XZR
.text:000000000000773C STUR X0, [X29,#var_8]
.text:0000000000007740 ; 7: v5 = a3;
.text:0000000000007740 STUR X1, [X29,#var_10]
.text:0000000000007744 STR X2, [SP,#0x30+var_18]
.text:0000000000007748 ; 8: v4 = (const char *)_JNIEnv::GetStringUTFChars(a1, a3, 0LL);
.text:0000000000007748 LDUR X0, [X29,#var_8]
.text:000000000000774C LDR X1, [SP,#0x30+var_18]
.text:0000000000007750 MOV X2, X8
.text:0000000000007754 BL ._ZN7_JNIEnv17GetStringUTFCharsEP8_jstringPh ; _JNIEnv::GetStringUTFChars(_jstring *,uchar *)
.text:0000000000007758 STR X0, [SP,#0x30+var_20]
.text:000000000000775C ; 9: if ( (signed int)_JNIEnv::GetStringUTFLength(v6, v5) > 0 )
.text:000000000000775C LDUR X0, [X29,#var_8]
.text:0000000000007760 LDR X1, [SP,#0x30+var_18]
function inline_hook() {
var libnative_lib_addr = Module.findBaseAddress("libnative-lib.so");
if (libnative_lib_addr) {
console.log("libnative_lib_addr:", libnative_lib_addr);
var addr_775C = libnative_lib_addr.add(0x775C);
console.log("addr_775C:", addr_775C);
Java.perform(function () {
Interceptor.attach(addr_775C, {
onEnter: function (args) {
var name = this.context.x0.readCString()
console.log("addr_775C OnEnter :", this.returnAddress, name);
},
onLeave: function (retval) {
console.log("retval is :", retval)
}
})
})
}
}
setImmediate(inline_hook())
Attaching...
libnative_lib_addr: 0x79fabe0000
addr_775C: 0x79fabe775c
TypeError: cannot read property 'apply' of undefined
at [anon] (../../../frida-gum/bindings/gumjs/duktape.c:56618)
at frida/runtime/core.js:55
[Google Pixel::myapplication.example.com.ndk_demo]-> addr_775C OnEnter : 0x79fabe7758 sakura
addr_775C OnEnter : 0x79fabe7758 sakura
到这里已经可以总结一下我目前的学习了,需要补充一些 frida api 的学习,比如 NativePointr 里居然有个 readCString,这些 API 是需要再看看的。
首先看下 logcat
n/u0a128 for activity com.gdufs.xman/.MainActivity
12-28 05:53:26.898 26615 26615 V com.gdufs.xman: JNI_OnLoad()
12-28 05:53:26.898 26615 26615 V com.gdufs.xman: RegisterNatives() --> nativeMethod() ok
12-28 05:53:26.898 26615 26615 D com.gdufs.xman m=: 0
12-28 05:53:26.980 26615 26615 D com.gdufs.xman m=: Xman
sakura@sakuradeMacBook-Pro:~/gitsource/frida-agent-example/agent$ frida -U --no-pause -f com.gdufs.xman -l hook_reg.js
...
[Google Pixel::com.gdufs.xman]-> [RegisterNatives] method_count: 0x3
[RegisterNatives] java_class: com.gdufs.xman.MyApp name: initSN sig: ()V fnPtr: 0xd4ddf3b1 module_name: libmyjni.so module_base: 0xd4dde000 offset: 0x13b1
[RegisterNatives] java_class: com.gdufs.xman.MyApp name: saveSN sig: (Ljava/lang/String;)V fnPtr: 0xd4ddf1f9 module_name: libmyjni.so module_base: 0xd4dde000 offset: 0x11f9
[RegisterNatives] java_class: com.gdufs.xman.MyApp name: work sig: ()V fnPtr: 0xd4ddf4cd module_name: libmyjni.so module_base: 0xd4dde000 offset: 0x14cd
initSN
感觉意思应该是从 /sdcard/reg.dat
里读一个值,然后和 EoPAoY62@ElRD
进行比较。
最后setValue,从导出函数看一下,最后推测第一个参数应该是JNIEnv *env,然后就看到了给字段m赋值。
aveSN
这个看上去就是根据str的值,去变换”W3_arE_whO_we_ARE”字符串,然后写入到/sdcard/reg.dat
里
结合一下看,只要initSN检查到/sdcard/reg.dat
里是EoPAoY62@ElRD
,应该就会给m设置成1。
只要m的值是1,就能走到work()函数的逻辑。
参考:frida 的 file api:https://frida.re/docs/javascript-api/#file
function main() {
var file = new File("/sdcard/reg.dat",'w')
file.write("EoPAoY62@ElRD")
file.flush()
file.close()
}
setImmediate(main())
这样我们继续看 work 的逻辑
v2 是从 getValue 得到的,看上去就是 m字段的值,此时应该是1,一会 hook 一下看看。
[NewStringUTF] bytes:输入即是flag,格式为xman{……}!
callWork 里又调用了 work 函数,死循环了。
那看来看去最后还是回到了initSN,那其实我们看的顺序似乎错了。
理一下逻辑,n2执行完保存到文件,然后n1 check一下,所以最后还是要逆n2的算法,pass。
github:https://github.com/chame1eon/jnitrace
一个基于frida的工具,用来追踪安卓应用的 JNI API
安装:pip install jnitrace
D:\> jnitrace --help
usage: jnitrace [options] -l libname targetpositional arguments:
target 要追踪的 "应用名"options:
- -h, --help 帮助
- -m {spawn,attach} 指定注入进程的方式,默认是 spawn
- -R [REMOTE], --remote 通过 IP:PORT 连接到远程 frida-server
- -b {fuzzy,accurate,none}, --backtrace {fuzzy,accurate,none} 打印每个JNI调用的追踪
- -i INCLUDE, --include INCLUDE 指定一个正则表达式来过滤出方法名
- -e EXCLUDE, --exclude EXCLUDE 和 -i 相反,同样通过正则表达式来过滤,
- -I INCLUDE_EXPORT, --include-export INCLUDE_EXPORT trace 导出的方法,jnitrace 认为导出的函数应该是从 Java 端能够直接调用的函数,所以可以包括使用 RegisterNatives 来注册的函数,例如
-I stringFromJNI -I nativeMethod([B)V
,就包括导出名里有 stringFromJNI,以及使用 RegisterNames 来注册,并带有 nativeMethod([B)V 签名的函数。- -E EXCLUDE_EXPORT, --exclude-export EXCLUDE_EXPORT 不跟踪的库导出符号的列表
- --hide-data 打印参数的内容。
- --ignore-env 不追踪 JNIEnv calls.
- --ignore-vm 不追踪 JavaVM calls.
- -p PREPEND, --prepend PREPEND 在jnitrace运行之前预先运行Frida脚本。这可以用于在 jnitrace 启动之前对抗反调试。
- -a APPEND, --append APPEND 在jnitrace启动后运行。附加一个Frida脚本。
- -o OUTPUT, --output OUTPUT 将跟踪数据输出到JSON格式的文件。
-v, --version 显示版本
-l LIBRARIES, --libraries LIBRARIES
指定跟踪JNI调用的so库。输入*,跟踪全部库
或多次使用该参数指定一组库。
--aux name=(string|bool|int)value
set aux option when spawning
D:\>
默认是 spawn 运行的,
sakura@sakura:~/frida_learn/lib/armeabi-v7a$ jnitrace -l libmyjni.so com.gdufs.xman
Tracing. Press any key to quit...
Traced library "libmyjni.so" loaded from path "/data/app/com.gdufs.xman-X0HkzLhbptSc0tjGZ3yQ2g==/lib/arm".
/* TID 28890 */
355 ms [+] JavaVM->GetEnv
355 ms |- JavaVM* : 0xefe99140
355 ms |- void** : 0xda13e028
355 ms |: 0xeff312a0
355 ms |- jint : 65542
355 ms |= jint : 0
355 ms ------------------------Backtrace------------------------
355 ms |-> 0xda13a51b: JNI_OnLoad+0x12 (libmyjni.so:0xda139000)
/* TID 28890 */
529 ms [+] JNIEnv->FindClass
529 ms |- JNIEnv* : 0xeff312a0
529 ms |- char* : 0xda13bdef
529 ms |: com/gdufs/xman/MyApp
529 ms |= jclass : 0x81 { com/gdufs/xman/MyApp }
529 ms ------------------------Backtrace------------------------
529 ms |-> 0xda13a539: JNI_OnLoad+0x30 (libmyjni.so:0xda139000)
/* TID 28890 */
584 ms [+] JNIEnv->RegisterNatives
584 ms |- JNIEnv* : 0xeff312a0
584 ms |- jclass : 0x81 { com/gdufs/xman/MyApp }
584 ms |- JNINativeMethod* : 0xda13e004
584 ms |: 0xda13a3b1 - initSN()V
584 ms |: 0xda13a1f9 - saveSN(Ljava/lang/String;)V
584 ms |: 0xda13a4cd - work()V
584 ms |- jint : 3
584 ms |= jint : 0
584 ms ------------------------Backtrace------------------------
584 ms |-> 0xda13a553: JNI_OnLoad+0x4a (libmyjni.so:0xda139000)
/* TID 28890 */
638 ms [+] JNIEnv->FindClass
638 ms |- JNIEnv* : 0xeff312a0
638 ms |- char* : 0xda13bdef
638 ms |: com/gdufs/xman/MyApp
638 ms |= jclass : 0x71 { com/gdufs/xman/MyApp }
638 ms -----------------------Backtrace-----------------------
638 ms |-> 0xda13a377: setValue+0x12 (libmyjni.so:0xda139000)
/* TID 28890 */
688 ms [+] JNIEnv->GetStaticFieldID
688 ms |- JNIEnv* : 0xeff312a0
688 ms |- jclass : 0x71 { com/gdufs/xman/MyApp }
688 ms |- char* : 0xda13be04
688 ms |: m
688 ms |- char* : 0xda13be06
688 ms |: I
688 ms |= jfieldID : 0xf1165004 { m:I }
688 ms -----------------------Backtrace-----------------------
688 ms |-> 0xda13a38d: setValue+0x28 (libmyjni.so:0xda139000)
官网:https://strace.io/
strace 跟踪进程中的系统调用:https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/strace.html
Linux 内核监控在 Android 攻防中的应用:https://zhuanlan.zhihu.com/p/453303913
Android上利用 strace 跟踪系统调用:https://mabin004.github.io/2019/06/27/Android%E4%B8%8A%E5%88%A9%E7%94%A8Strace%E8%B7%9F%E8%B8%AA%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8/
使用 strace:https://source.android.google.cn/docs/core/tests/debug/strace?hl=zh-cn
安卓 strace 下载:https://github.com/ipduh/strace/tree/master/binaries/arm64
strace常用来跟踪进程执行时的系统调用和所接收的信号。 在Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间。
-c 统计每一系统调用的所执行的时间,次数和出错的次数等. -d 输出strace关于标准错误的调试信息. -f 跟踪由fork调用所产生的子进程. -ff 如果提供-o filename,则所有进程的跟踪结果输出到相应的filename.pid中,pid是各进程的进程号. -F 尝试跟踪vfork调用.在-f时,vfork不被跟踪. -h 输出简要的帮助信息. -i 输出系统调用的入口指针. -q 禁止输出关于脱离的消息. -r 打印出相对时间关于,,每一个系统调用. -t 在输出中的每一行前加上时间信息. -tt 在输出中的每一行前加上时间信息,微秒级. -ttt 微秒级输出,以秒了表示时间. -T 显示每一调用所耗的时间. -v 输出所有的系统调用.一些调用关于环境变量,状态,输入输出等调用由于使用频繁,默认不输出. -V 输出strace的版本信息. -x 以十六进制形式输出非标准字符串 -xx 所有字符串以十六进制形式输出. -a column 设置返回值的输出位置.默认 为40. -e expr 指定一个表达式,用来控制如何跟踪.格式如下: [qualifier=][!]value1[,value2]... qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一.value是用来限定的符号或数字.默认的 qualifier是 trace.感叹号是否定符号.例如: -eopen等价于 -e trace=open,表示只跟踪open调用.而-etrace!=open表示跟踪除了open以外的其他调用.有两个特殊的符号 all 和 none. 注意有些shell使用!来执行历史记录里的命令,所以要使用\\. -e trace=set 只跟踪指定的系统 调用.例如:-e trace=open,close,rean,write表示只跟踪这四个系统调用.默认的为set=all. -e trace=file 只跟踪有关文件操作的系统调用. -e trace=process 只跟踪有关进程控制的系统调用. -e trace=network 跟踪与网络有关的所有系统调用. -e strace=signal 跟踪所有与系统信号有关的 系统调用 -e trace=ipc 跟踪所有与进程通讯有关的系统调用 -e abbrev=set 设定 strace输出的系统调用的结果集.-v 等与 abbrev=none.默认为abbrev=all. -e raw=set 将指 定的系统调用的参数以十六进制显示. -e signal=set 指定跟踪的系统信号.默认为all.如 signal=!SIGIO(或者signal=!io),表示不跟踪SIGIO信号. -e read=set 输出从指定文件中读出 的数据.例如: -e read=3,5 -e write=set 输出写入到指定文件中的数据. -o filename 将strace的输出写入文件filename -p pid 跟踪指定的进程pid. -s strsize 指定输出的字符串的最大长度.默认为32.文件名一直全部输出. -u username 以username 的UID和GID执行被跟踪的命令
strace -o output.txt -T -tt -e trace=all -p 28979
跟踪 28979 进程的所有系统调用(-e trace=all),并统计系统调用的花费时间,以及开始时间(并以可视化的时分秒格式显示),最后将记录结果存在output.txt文件里面。
Systrace Android 工具在内部调用一个名为 atrace 的工具,它是 ftrace 或 strace (Linux工具)的扩展。
frida-trace:https://frida.re/docs/frida-trace/
用法:frida-trace [options] target
frida-trace -U -i "strcmp" -f com.gdufs.xman
...
5634 ms strcmp(s1="fi", s2="es-US")
5635 ms strcmp(s1="da", s2="es-US")
5635 ms strcmp(s1="es", s2="es-US")
5635 ms strcmp(s1="eu-ES", s2="es-US")
5635 ms strcmp(s1="et-EE", s2="es-US")
5635 ms strcmp(s1="et-EE", s2="es-US")
:https://github.com/lasting-yang/frida_hook_libart
:https://github.com/lasting-yang/frida_hook_libart/blob/master/hook_artmethod.js
改 aosp 源码 trace 信息:https://bbs.pediy.com/thread-255653-1.htm
常见的保护都会在 init_array 里面做,关于其原理,主要阅读以下文章即可。
// 编译生成后在.init段 [名字不可更改]
extern "C" void _init(void) {
LOGD("Enter init......");
}
// 编译生成后在.init_array段 [名字可以更改]
__attribute__((__constructor__)) static void sakura_init() {
LOGD("Enter sakura_init......");
}
...
...
2016-12-29 16:51:23.017 5160-5160/com.example.ndk_demo D/sakura1328: Enter init......
2016-12-29 16:51:23.017 5160-5160/com.example.ndk_demo D/sakura1328: Enter sakura_init......
IDA快捷键shift+F7
找到segment,然后就可以找到.init_array
段,然后就可以找到里面保存的函数地址。
打开要调试的 apk,找到入口
sakura@sakura:~/.gradle/caches$ adb shell dumpsys activity top | grep TASK
TASK com.android.systemui id=29 userId=0
TASK null id=26 userId=0
TASK com.example.ndk_demo id=161 userId=0
sailfish:/data/local/tmp # ./android_server64
IDA Android 64-bit remote debug server(ST) v1.22. Hex-Rays (c) 2004-2017
Listening on 0.0.0.0:23946...
adb forward tcp:23946 tcp:23946
adb forward tcp:<本地机器的网络端口号> tcp:<模拟器或是真机的网络端口号>
例:adb [-d|-e|-s ] forward tcp:6100 tcp:7100 表示把本机的6100端口号与模拟器的7100端口建立起相关,当模拟器或真机向自己的7100端口发送了数据,那们我们可以在本机的6100端口读取其发送的内容,这是一个很关键的命令,以后我们使用jdb调试apk之前,就要用它先把目标进程和本地端口建立起关联
打开IDA,选择菜单Debugger -> Attach -> Remote ARM Linux/Android debugger
打开IDA,选择菜单Debugger -> Process options, 填好,然后选择进程去attach。
adb jdwp
sakura@sakuradeMacBook-Pro:~$ adb jdwp
10436
转发端口adb forward tcp:8700 jdwp:10436
,将该进程的调试端口和本机的8700绑定。
jdb连接调试端口,从而让程序继续运行 jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
找到断点并断下。
打开 module
找到 linker64
找到 call array 函数
下断,并按 F9 断下
最终我确实可以调试到 .init_array
的初始化,具体的代码分析见 Linker学习笔记 这里。
目标是找到动态注册的函数的地址,因为这种函数没有导出。
JNINativeMethod methods[] = {
{"stringFromJNI2", "()Ljava/lang/String;", (void *) stringFromJNI2},
};
env->RegisterNatives(env->FindClass("com/example/ndk_demo/MainActivity"), methods, 1);
首先:jnitrace -m spawn -i "RegisterNatives" -l libnative-lib.so com.example.ndk_demo
525 ms [+] JNIEnv->RegisterNatives
525 ms |- JNIEnv* : 0x7a106cc1c0
525 ms |- jclass : 0x89 { com/example/ndk_demo/MainActivity }
525 ms |- JNINativeMethod* : 0x7ff0b71120
525 ms |: 0x79f00d36b0 - stringFromJNI2()Ljava/lang/String;
然后:objection -d -g com.example.ndk_demo run memory list modules explore | grep demo
sakura@sakuradeMacBook-Pro:~$ objection -d -g com.example.ndk_demo run memory list modules explore | grep demo
[debug] Attempting to attach to process: `com.example.ndk_demo`
Warning: Output is not to a terminal (fd=1).
base.odex 0x79f0249000 106496 (104.0 KiB) /data/app/com.example.ndk_demo-HGAFhnKyKCSIpzn227pwXw==/oat/arm64/base.odex
libnative-lib.so 0x79f00c4000 221184 (216.0 KiB) /data/app/com.example.ndk_demo-HGAFhnKyKCSIpzn227pwXw==/lib/arm64/libnative...
offset = 0x79f00d36b0 - 0x79f00c4000 = 0xf6b0
这样就找到了
没有支持 arm64,可以在安装 app 的时候 adb install --abi armeabi-v7a
强制让 app 运行在32位模式
这个脚本整体来说就是 hook callfunction,然后打印出 init_array 里面的函数地址和参数等。
从源码看,关键就是 call_array 这里调用的 call_function,第一个参数代表这是注册的 init_array 里面的 function,第二个参数则是 init_array 里存储的函数的地址。
template
static void call_array(const char* array_name __unused,
F* functions,
size_t count,
bool reverse,
const char* realpath) {
if (functions == nullptr) {
return;
}
TRACE("[ Calling %s (size %zd) @ %p for '%s' ]", array_name, count, functions, realpath);
int begin = reverse ? (count - 1) : 0;
int end = reverse ? -1 : count;
int step = reverse ? -1 : 1;
for (int i = begin; i != end; i += step) {
TRACE("[ %s[%d] == %p ]", array_name, i, functions[i]);
call_function("function", functions[i], realpath);
}
TRACE("[ Done calling %s for '%s' ]", array_name, realpath);
}
function LogPrint(log) {
var theDate = new Date();
var hour = theDate.getHours();
var minute = theDate.getMinutes();
var second = theDate.getSeconds();
var mSecond = theDate.getMilliseconds()
hour < 10 ? hour = "0" + hour : hour;
minute < 10 ? minute = "0" + minute : minute;
second < 10 ? second = "0" + second : second;
mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond;
var time = hour + ":" + minute + ":" + second + ":" + mSecond;
var threadid = Process.getCurrentThreadId();
console.log("[" + time + "]" + "->threadid:" + threadid + "--" + log);
}
function hooklinker() {
var linkername = "linker";
var call_function_addr = null;
var arch = Process.arch;
LogPrint("Process run in:" + arch);
if (arch.endsWith("arm")) {
linkername = "linker";
} else {
linkername = "linker64";
LogPrint("arm64 is not supported yet!");
}
var symbols = Module.enumerateSymbolsSync(linkername);
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
//LogPrint(linkername + "->" + symbol.name + "---" + symbol.address);
if (symbol.name.indexOf("__dl__ZL13call_functionPKcPFviPPcS2_ES0_") != -1) {
call_function_addr = symbol.address;
LogPrint("linker->" + symbol.name + "---" + symbol.address)
}
}
if (call_function_addr != null) {
var func_call_function = new NativeFunction(call_function_addr, 'void', ['pointer', 'pointer', 'pointer']);
Interceptor.replace(new NativeFunction(call_function_addr,
'void', ['pointer', 'pointer', 'pointer']), new NativeCallback(function (arg0, arg1, arg2) {
var functiontype = null;
var functionaddr = null;
var sopath = null;
if (arg0 != null) {
functiontype = Memory.readCString(arg0);
}
if (arg1 != null) {
functionaddr = arg1;
}
if (arg2 != null) {
sopath = Memory.readCString(arg2);
}
var modulebaseaddr = Module.findBaseAddress(sopath);
LogPrint("after load:" + sopath + "--start call_function,type:" + functiontype + "--addr:" + functionaddr + "---baseaddr:" + modulebaseaddr);
if (sopath.indexOf('libnative-lib.so') >= 0 && functiontype == "DT_INIT") {
LogPrint("after load:" + sopath + "--ignore call_function,type:" + functiontype + "--addr:" + functionaddr + "---baseaddr:" + modulebaseaddr);
} else {
func_call_function(arg0, arg1, arg2);
LogPrint("after load:" + sopath + "--end call_function,type:" + functiontype + "--addr:" + functionaddr + "---baseaddr:" + modulebaseaddr);
}
}, 'void', ['pointer', 'pointer', 'pointer']));
}
}
setImmediate(hooklinker)
我调试了一下 linker64,因为没有导出 call_function 的地址,所以不能直接 hook 符号名,而是要根据偏移去 hook,以后再说。
其实要看 init_array
,直接 shift+F7 去 segment 里面找 .init_array
段就可以了,这里主要是为了反反调试,因为可能反调试会加在 init_array 里,hook call_function 就可以让它不加载反调试程序。
现在我想要主动调用 sakura_add 来打印值,可以 ida 打开找符号,或者根据偏移,总之最终用这个 NativePointer 指针来初始化一个 NativeFunction 来调用。
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_ndk_1demo_MainActivity_sakuraWithInt(JNIEnv *env, jobject thiz, jint a, jint b) {
// TODO: implement sakuraWithInt()
return sakura_add(a,b);
}
...
int sakura_add(int a, int b){
int sum = a+b;
LOGD("sakura add a+b:",sum);
return sum;
}
function main() {
var libnative_lib_addr = Module.findBaseAddress("libnative-lib.so");
console.log("libnative_lib_addr is :", libnative_lib_addr);
if (libnative_lib_addr) {
var sakura_add_addr1 = Module.findExportByName("libnative-lib.so", "_Z10sakura_addii");
var sakura_add_addr2 = libnative_lib_addr.add(0x0F56C) ;
console.log("sakura_add_addr1 ", sakura_add_addr1);
console.log("sakura_add_addr2 ", sakura_add_addr2)
}
var sakura_add1 = new NativeFunction(sakura_add_addr1, "int", ["int", "int"]);
var sakura_add2 = new NativeFunction(sakura_add_addr2, "int", ["int", "int"]);
console.log("sakura_add1 result is :", sakura_add1(200, 33));
console.log("sakura_add2 result is :", sakura_add2(100, 133));
}
setImmediate(main())
...
...
libnative_lib_addr is : 0x79fa1c5000
sakura_add_addr1 0x79fa1d456c
sakura_add_addr2 0x79fa1d456c
sakura_add1 result is : 233
sakura_add2 result is : 233
//todo
jni的基本类型要通过调用jni相关的api转化成c++对象,才能打印和调用。
jni主动调用的时候,参数构造有两种方式,一种是Java.vm.getenv
,另一种是hook获取env之后来调用jni相关的api构造参数。
frida-net git地址:https://github.com/frida/gumjs-net
使用frida-net简单玩转frida-rpc:https://mp.weixin.qq.com/s/zu3jEd38NxlemBpfN3TnHw
下载 frida-net,进入 gumjs-net/examples/http-server/
readme 内容
# HTTP server example
Compile with:
npm install
Load into a running process:
frida Twitter -l _agent.js
Talk to it:
curl http://127.0.0.1:1337/modules
再看下当前目录,库依赖 和 _agent.js 都有了,然后修改 agent.js,像这样
再执行:npm run build 又生成了新的 _agent.js
readme 接下来内容就是注入
在设备上启动 frida-server
然后 frida -FU -l _agent.js
再来看下设备的 ip: adb shell "ifconfig |grep inet"
访问接口
使用 frida-net 脱离 pc 在手机上直接暴漏 app 的算法供三方调用:https://mp.weixin.qq.com/s/w1XtNYEOkhlGeZ8T1JcVxQ
./frida-inject-16.0.8-android-arm64 -n com.xxx.xxx -s _agent.js
测试
环境部署完毕,接下来是找到算法接口,写入agent.js中,重编译再放入手机运行。
修改 frida-net 项目下的 agent.js,然后重新编译,推到手机中。
用 frida-inject 执行脚本,然后尝试让他输出几个加密参数。