本文为看雪论坛优秀文章
看雪论坛作者ID:DMemory
TypeScript 开发环境
TypeScript + npm 的方式搭建 frida 开发环境,使得在用 IDE 编写 frida 时可以有代码补全提示同时 TypeScript 又能使开发更容易达到模块化,代码复用更方便。
优点
-
JavaScript的一个超集,扩展了JavaScript的语法。
-
加强代码可读性。明确参数类型,代码语义更清晰易懂。
-
更友好、更精准的代码补全提示。
-
更贴近面向对象编程的编写习惯,利于模块化和复用。
-
以前用 js 写的脚本也可以被 ts 直接引用,不会浪费。
>> 环境搭建
clone git://github.com/oleavr/frida-agent-example.git git
cd frida-agent-example/
npm install
实际上就是利用 frida-compile 编译脚本
npm run watch
或者
frida-compile agent/index.ts -o _agent.js -w
>> 使用
frida -U -f com.example.android --no-pause -l _agent.js
JS单步调试
frida -l </Users/name/path/test.js> --debug --runtime=v8 <port/name>
session = dev.attach(app.pid)
script = session.create_script(jscode, runtime="v8")
session.enable_debugger()
Chrome Inspector server listening on port 9229
>> chrome
>> pycharm
>> 优缺点
-
用 Chrome 调试支持的更为顺滑,调试脚本自动重加载,断点也能正确响应。
-
用 PyCharm 调试断点有时需要手动激活有点麻烦,纯粹是个人偏爱PyCharm 的Debug 窗口和快捷键。
-
PyCharm 使用 ts 环境,调试时可以直接在 ts 文件上下断,也不需要手动激活断点,比较顺畅。
FridaContainer 脚本集分享
>> 反反调试 anti-anti-debug
/proc/<pid>/status 检测 TracerPid: 0 、 State: S (sleeping) 、 SigBlk: 0000000000001204 ,
/proc/<pid>/stat 检测 t (tracing stop)
/proc/<pid>/wchan 检测 SyS_epoll_wait 等
static anti_fgets() {
const tag = 'anti_fgets';
const fgetsPtr = Module.findExportByName(null, 'fgets');
DMLog.i(Anti.tag, 'fgets addr: ' + fgetsPtr);
if (null == fgetsPtr) {
return;
}
var fgets = new NativeFunction(fgetsPtr, 'pointer', ['pointer', 'int', 'pointer']);
Interceptor.replace(fgetsPtr, new NativeCallback(function (buffer, size, fp) {
if (null == this) {
return 0;
}
var logTag = null;
// 进入时先记录现场
const lr = FCCommon.getLR(this.context);
// 读取原 buffer
var retval = fgets(buffer, size, fp);
var bufstr = (buffer as NativePointer).readCString();
...完整版请点击左下角阅读原文
FCAnd.anti.anti_debug();
>> anti-ssl-pinning
-
TrustManager (Android < 7)
-
TrustManagerImpl (Android > 7)
-
OkHTTPv3 (quadruple bypass)
-
Trustkit (triple bypass)
-
Appcelerator Titanium
-
OpenSSLSocketImpl Conscrypt
-
OpenSSLEngineSocketImpl Conscrypt
-
OpenSSLSocketImpl Apache Harmony
-
PhoneGap sslCertificateChecker
-
IBM MobileFirst pinTrustedCertificatePublicKey (double bypass)
-
IBM WorkLight (ancestor of MobileFirst) HostNameVerifierWithCertificatePinning (quadruple bypass)
-
Conscrypt CertPinManager
-
CWAC-Netsecurity (unofficial back-port pinner for Android<4.2) CertPinManager
-
Worklight Androidgap WLCertificatePinningPlugin
-
Netty FingerprintTrustManagerFactory
-
Squareup CertificatePinner [OkHTTP<v3] (double bypass)
-
Squareup OkHostnameVerifier [OkHTTP v3] (double bypass)
-
Android WebViewClient (double bypass)
-
Apache Cordova WebViewClient
-
Boye AbstractVerifier
FCAnd.anti.anti_ssl_unpinning();
>> dump dex
FCAnd.dump_dex_common();
>> multi-dex
比如遇到利用 InMemoryDexClassLoader 来加载内存中的 dex 时,可以 Hook 它,当其触发时先走原流程,让其动态加载 dex ,然后利用此时的 loader object 修改 Java.classFactory.loader,再使用 Java.use 来获取类就可以了,使用后记得恢复现场,否则会崩溃。
function anti_InMemoryDexClassLoader(callbackfunc) {
// dalvik.system.InMemoryDexClassLoader
const InMemoryDexClassLoader = Java.use('dalvik.system.InMemoryDexClassLoader');
InMemoryDexClassLoader.$init.overload('java.nio.ByteBuffer', 'java.lang.ClassLoader')
.implementation = function (buff, loader) {
this.$init(buff, loader);
var oldcl = Java.classFactory.loader;
Java.classFactory.loader = this;
callbackfunc();
Java.classFactory.loader = oldcl; // 恢复现场
}
}
FCAnd.anti.anti_InMemoryDexClassLoader(function(){
const cls = Java.use('find/same/multi/dex/class');
...
});
>> frida 的 java 反射调用
// 获取 so 基址
var base = Module.findBaseAddress('libxxxx.so');
// 根据偏移获取 jni 函数地址
var jnifunc_ptr = libsgmainso.add(0xE729);
// 声明 jni 函数
var jnifunc = new NativeFunction(jnifunc_ptr, 'pointer', ['pointer', 'pointer', 'int', 'pointer']);
// ********* 拼装 obj *********
// JNIEnv
var env = Java.vm.getEnv();
// 调用
var retval = jnifunc(env.handle, ptr(0), 10401, obj);
// staticVaMethod 实现 Integer.valueOf(7)
const Integer_jcls = env.findClass('java/lang/Integer');
const Integer_valueOf = env.getStaticMethodId(Integer_jcls, 'valueOf', '(I)Ljava/lang/Integer;');
const invorkeStaticOjbectMethod = env.staticVaMethod('pointer', ['int']);
var pIn2 = invorkeStaticOjbectMethod(env.handle, Integer_jcls, Integer_valueOf, 7);
// 利用 constructor | vaMethod 组装 HashMap obj
// new HashMap().put('INPUT', 'xxxxxxxxxx')
const HashMap_jcls = env.findClass('java/util/HashMap');
const invokeHashmap_constructor = env.constructor([]);
const HashMap_init = env.getMethodId(HashMap_jcls, '<init>', '()V');
var HashMap_obj = invokeHashmap_constructor(env.handle, HashMap_jcls, HashMap_init);
const HashMap_put = env.getMethodId(HashMap_jcls, 'put', '(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;');
const invokeOjbectMethod = env.vaMethod('pointer', ['pointer', 'pointer']);
invokeOjbectMethod(env.handle, HashMap_obj, HashMap_put, env.newStringUtf('INPUT'), env.newStringUtf('xxxxxxxxxx'));
>> trace java methods
因为基于 Frida 框架,如果直接 trace 所有的类效率太慢,也容易崩溃。所以这里是以白名单的方式实现的。核心方法就是枚举所有类,按过滤名单,匹配需要 trace 的类,Hook 目标类的所有方法(可指定),在方法被调用时,将其入参和返回值记录下来。
遍历所有类,Hook 白名单中的类及方法。
// staticVaMethod 实现 Integer.valueOf(7)
const Integer_jcls = env.findClass('java/lang/Integer');
const Integer_valueOf = env.getStaticMethodId(Integer_jcls, 'valueOf', '(I)Ljava/lang/Integer;');
const invorkeStaticOjbectMethod = env.staticVaMethod('pointer', ['int']);
var pIn2 = invorkeStaticOjbectMethod(env.handle, Integer_jcls, Integer_valueOf, 7);
// 利用 constructor | vaMethod 组装 HashMap obj
// new HashMap().put('INPUT', 'xxxxxxxxxx')
const HashMap_jcls = env.findClass('java/util/HashMap');
const invokeHashmap_constructor = env.constructor([]);
const HashMap_init = env.getMethodId(HashMap_jcls, '<init>', '()V');
var HashMap_obj = invokeHashmap_constructor(env.handle, HashMap_jcls, HashMap_init);
const HashMap_put = env.getMethodId(HashMap_jcls, 'put', '(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;');
const invokeOjbectMethod = env.vaMethod('pointer', ['pointer', 'pointer']);
invokeOjbectMethod(env.handle, HashMap_obj, HashMap_put, env.newStringUtf('INPUT'), env.newStringUtf('xxxxxxxxxx'));
-
支持精确/模糊匹配类名
-
支持某类按白名单方式 trace 方法
-
支持匹配到指定值时收集栈信息
Java.enumerateLoadedClassesSync().forEach((curClsName, index, array) => {
dest_cls.forEach((destCls) => {
// 按规则匹配是否需要 trace
if (match(destCls, curClsName)) {
// trace 核心方法
traceArtMethodsCore(curClsName);
return false; // end forEach
}
});
});
// Hook 核心逻辑
function traceArtMethodsCore(clsname: string) {
let cls = Java.use(clsname);
// 枚举方法
let methods = cls.class.getDeclaredMethods();
methods.forEach(function (method: any) {
...
// 枚举重载
let methodOverloads = cls[methodName].overloads;
methodOverloads.forEach(function (overload: any) {
...
// Hook
overload.implementation = function () {
// ... send entry msg
// 利用 js 参数特性 arguments ,调用原函数以适配所有 Hook 方法的传参
const retval = this[methodName].apply(this, arguments);
// ... send exit msg
return retval;
}
}
}
}
推荐用于轻量级的 Java 方法 trace 可以有效的定位核心算法。
>> jni hook & trace
例如可以将其封装成更便捷的获取各 jni 函数地址的功能,方便 Hook。
// 列出 JNI 函数数组
const jni_struct_array = [
"reserved0",
"reserved1",
"reserved2",
"reserved3",
"GetVersion",
"DefineClass",
"FindClass",
"FromReflectedMethod",
...
];
// 获取 JNIEnv 地址
var env = Java.vm.getEnv();
var env_ptr = env.handle.readPointer();
// 根据函数名计算索引偏移
var offset = jni_struct_array.indexOf(func_name) * Process.pointerSize;
// 读取函数地址
jnienv_addr.add(offset).readPointer();
// Hook
Interceptor.attach(addr, callbacksOrProbe);
FCAnd.jni.hookJNI('NewStringUTF', {
onEnter: function (args) {
...
}
});
export function hook_registNatives() {
const tag = 'fridaRegstNtv';
Jni.hookJNI("RegisterNatives", {
onEnter: function (args) {
var env = Java.vm.getEnv();
var p_size = Process.pointerSize;
var methods = args[2];
var methodcount = args[3].toInt32();
// 获取类名
var name = env.getClassName(args[1]);
DMLog.i(tag, "==== class: " + name + " ====");
DMLog.i(tag, "==== methods: " + methods + " nMethods: " + methodcount + " ====");
/** 根据函数结构原型遍历动态注册信息
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
jint RegisterNatives(JNIEnv* env, jclass clazz, const JNINativeMethod* methods, jint nMethods)
*/
for (var i = 0; i < methodcount; i++) {
var idx = i * p_size * 3;
var fnPtr = methods.add(idx + p_size * 2).readPointer();
const module = Process.getModuleByAddress(fnPtr);
if (module) {
const modulename = module.name;
const modulebase = module.base;
var logstr = "name: " + methods.add(idx).readPointer().readCString()
+ ", signature: " + methods.add(idx + p_size).readPointer().readCString()
+ ", fnPtr: " + fnPtr
+ ", modulename: " + modulename + " -> base: " + modulebase;
if (null != modulebase) {
logstr += ", offset: " + fnPtr.sub(modulebase);
}
DMLog.i(tag, logstr);
}
else {
DMLog.e(tag, 'module is null');
}
}
}
});
}
==== class: com.xxxx.class.name ====
==== methods: 0xcd52d428 nMethods: 41 ====
[INFO][fridaRegstNtv]: name: initialize, signature: ()V, fnPtr: 0xcd50b6bd, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x66bd
[INFO][fridaRegstNtv]: name: onExit, signature: ()V, fnPtr: 0xcd50b6c7, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x66c7
[INFO][fridaRegstNtv]: name: getMMKVWithID, signature: (Ljava/lang/String;ILjava/lang/String;)J, fnPtr: 0xcd50b6d1, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x66d1
[INFO][fridaRegstNtv]: name: encodeBool, signature: (JLjava/lang/String;Z)Z, fnPtr: 0xcd50b76d, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x676d
[INFO][fridaRegstNtv]: name: decodeBool, signature: (JLjava/lang/String;Z)Z, fnPtr: 0xcd50b7bf, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x67bf
[INFO][fridaRegstNtv]: name: encodeInt, signature: (JLjava/lang/String;I)Z, fnPtr: 0xcd50b80f, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x680f
[INFO][fridaRegstNtv]: name: decodeInt, signature: (JLjava/lang/String;I)I, fnPtr: 0xcd50b85b, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x685b
[INFO][fridaRegstNtv]: name: encodeLong, signature: (JLjava/lang/String;J)Z, fnPtr: 0xcd50b8a5, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x68a5
[INFO][fridaRegstNtv]: name: decodeLong, signature: (JLjava/lang/String;J)J, fnPtr: 0xcd50b8f7, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x68f7
[INFO][fridaRegstNtv]: name: encodeFloat, signature: (JLjava/lang/String;F)Z, fnPtr: 0xcd50b953, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x6953
......
FCAnd.jni.hook_registNatives();
export function traceAllJNISimply() {
// 遍历 Hook Jni 函数
jni_struct_array.forEach(function (func_name, idx) {
if (!func_name.includes("reserved")) {
Jni.hookJNI(func_name, {
onEnter(args) {
// 触发时将信息保存到对象中
let md = new MethodData(this.context, func_name, JNI_ENV_METHODS[idx], args);
this.md = md;
},
onLeave(retval) {
// 退出时将返回值追加到对象中
this.md.setRetval(retval);
// 发送日志
send(JSON.stringify({tid: this.threadId, status: "jnitrace", data: this.md}));
}
});
}
})
}
FCAnd.jni.traceAllJNISimply();
Stalker 的应用
地址:https://bbs.pediy.com/thread-264680.htm
Frida 的检测方式:
-
文件名 frida-agent**
-
默认端口 27042
-
特征字符 frida:rpc、LIBFRIDA
-
端口应答特征
if (connect(sock , (struct sockaddr*)&sa , sizeof sa) != -1) {
memset(res, 0 , 7);
send(sock, "x00", 1, NULL);
send(sock, "AUTHrn", 6, NULL);
usleep(100); // Give it some time to answer
if ((ret = recv(sock, res, 6, MSG_DONTWAIT)) != -1) {
if (strcmp(res, "REJECT") == 0) {
__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "FRIDA DETECTED [1] - frida server running on port %d!", i);
}
}
}
总结
fridaRegstNtv:https://github.com/deathmemory/fridaRegstNtv
以上两个仓库还请大佬们多多拍砖,提建议。
抛砖引玉,希望能和业界大佬多多交流。感谢。
看雪ID:DMemory
https://bbs.pediy.com/user-home-264470.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!
本文始发于微信公众号(看雪学院):FRIDA 使用经验交流分享
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论