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
1. Frida Hook Android 加固应用 方法
1、Android 加固应用 Hook 方式
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 () { }}
)
2、Hook 动态加载类
获取构造函数的参数
Java.perform(function () {//创建一个DexClassLoader的wappervar 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.cast 处理泛型方法(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;}
});
调用 Method.invoke() 去执行方法
invoke 方法的参数
- 第一个参数:是执行这个方法的对象实例,
- 第二个参数:是带入的实际值数组,
- 返回值:是 Object,也既是该方法执行后的返回值
f.invoke(instance,Array);
read-std-string
/** 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();
}
where_is_native
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("--------------------------------------------------------------------------");}
});
Non --- ASCII ( 不可见字符 )
如果代码进行了混淆,一些函数、方法会变成 非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;}}
)
使用 objection 打印混淆的方法名
使用 objection 打印混淆的方法名,然后再 hook 打印的方法名即可 hook 对应的函数
objection 是基于 frida 的命令行 hook 工具,可以让你不写代码, 敲几句命令就可以对 java 函数的高颗粒度 hook, 还支持 RPC 调用。
objection 目前只支持 Java层的 hook,但是 objection 有提供插件接口,可以自己写 frida 脚本去定义接口,
比如葫芦娃大佬的脱壳插件,实名推荐: https://github.com/hluwa/FRIDA-DEXDump
官方仓库: https://github.com/sensepost/objection
这里以 腾讯新闻.apk 为例:
通过 抓包分析可知,腾讯新闻 app 有三个参数需要破解:qn-rid、qn-sig、qn-newsig,通过 jadx-gui 分析源码可知,
- qn-rid :是一个 uuid。
- qn-sig :通过 qn-rid 加上一些别的参数,然后 md5 得到。
- qn-newsig :通过请求中的一些参数组合,然后 sha-256 得到
这里分析 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,打印参数、返回值。即可
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]));}
};
2. 使用 Frida - DEXDump 进行 apk 脱壳
From:https://mp.weixin.qq.com/s/x8_aa762wpsvA4nhSLoppQ
github 地址:https://github.com/hluwa/FRIDA-DEXDump
示例:使用 Frida 给 apk 脱壳并穿透加固 Hook 函数:https://xz.aliyun.com/t/7670
脱壳的需求
APP 加固发展到现在已经好几代了,从整体加固
到代码抽取
到虚拟机保护
,加固和脱壳的方案也逐渐趋于稳定。随着保护越来越强,脱壳机们也变得越来越费劲,繁琐。
不过对于我来说,很多时候其实并不需要那些被强保护起来的代码,我只是想单纯的看看这个地方的业务逻辑。所以,我追求的是更加快速、简便的脱壳方法。
实现
得益于FRIDA
, 在 PC 上面进行内存搜索、转储都变得十分方便,再也不需要考虑什么Xposed
、什么Android开发
、什么代码注入
,只需要关注如何去搜索想要的东西,于是依赖一个几十行代码的小脚本,就可以将大部分内存中的 dex 脱下来。在过去的一年,我几乎所有脱壳的工作都是由此脚本来完成,目前已经随手开源:https://github.com/hluwa/FRIDA-DEXDump
对于完整的 dex,采用暴力搜索 dex035 即可找到。而对于抹头的 dex,通过匹配一些特征来找到。
使用
不会安装使用 FRIDA 的,请先自行百度学会。。。然后,默念一声 "我想脱个壳"。
- 启动 APP。
- 启动 frida-server。
- python main.py。默数三秒,脱好了。
或者可以将脚本封装成命令:
3. 示 例
获取完整的demo
完整的代码已经上传github,https://github.com/smartdone/Frida-Scripts/tree/master/shell
需求
在对一些加固的Android应用做测试的时候,脱壳二次打包是一个相当相当复杂的工作。所以一般是脱壳分析代码,然后用hook的方式来动态劫持代码。使用xposed来hook加固的应用大家可能已经很熟悉了,但是使用frida大概没有多少人尝试,今天就给大家分享下如何使用xposed来hook加固之后的Android应用。
基本原理
要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了。
开始 hook 加固应用
以 "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(); // 获取classloadervar AyWelcome = classloader.loadClass("flytv.run.monitor.fragment.user.AyWelcome"); // 使用classloader加载类AyWelcome = Java.cast(AyWelcome, reflectClass); // 因为loadClass得到的是一个Object对象,我们需要把它强制转换成Classconsole.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(); // 获取classloaderJava.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(); // 获取classloaderJava.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(); //弹出Toastreturn ret;}return result;}});
}
最后效果如下: