开篇汇总 frida的学习离不开如下几个网站:
frida官方文档
frida官方脚本库
看雪android论坛
开始此篇之前,需要说明一些frida入门学习需要注意的问题:
1、cmd命令行无法使用“frida”,报错“'frida' 不是内部或外部命令,也不是可运行的程序或批处理文件。”;遇到此报错,如果是因为pip没有装frida库,那回头装一个就好,但是装了frida库还是会有该报错,那么还需要装一个“frida-tools”的库,此时才可以在命令行使用frida命令;
2、“pip install frida”报“ERROR:filed building wheel for frida”,此报错主要是由于构建frida时,缺少了其egg文件导致,解决方案见本文末“踩坑排错 - unable to download it within 20 seconds”即可解决;
3、编写frida JavaScript脚本时,编译器不会提示相关的代码引用,此时只需要“npm install @types/frida-gum -g”即可
frida hook Java frida在日常渗透过程中,可以做到hook Java层代码、native层代码等诸多功能,本文依据frida 官方文档给出的API配合日常渗透经验来讲解hook Java层代码可实现的操作。
frida 主动调用动态函数 frida 主动调用函数作为frida最基础的功能,此处使用一个LoginActivity的小demo来做演示
android LoginActivity code
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 public void onCreate (Bundle bundle) { super .onCreate(bundle); this .mContext = this ; setContentView(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() { @Override 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, "用户名或密码错误" , 1 ).show(); } else if (!LoginActivity.a(obj, obj).equals(obj2)) { Toast.makeText(LoginActivity.this .mContext, "登录失败" , 1 ).show(); } else { LoginActivity.this .startActivity(new Intent(LoginActivity.this .mContext, FridaLoginDemo.class)); LoginActivity.this .finishActivity(0 ); } } }); } private static String a (byte [] bArr) { StringBuilder sb = new StringBuilder(); for (int i = 0 ; bArr != null && i < bArr.length; i++) { String hexString = Integer.toHexString(bArr[i] & 255 ); if (hexString.length() == 1 ) { sb.append('0' ); } sb.append(hexString); } return sb.toString().toLowerCase(); } public static String a (String str, String str2) { try { SecretKeySpec secretKeySpec = new SecretKeySpec(str2.getBytes(), "HmacSHA256" ); Mac mac = Mac.getInstance("HmacSHA256" ); mac.init(secretKeySpec); return a(mac.doFinal(str.getBytes())); } catch (Exception e) { e.printStackTrace(); return BuildConfig.FLAVOR; } }
上面的代码展示了app LoginActivity 的部分代码,相关代码逻辑梳理之后为:用户访问LoginActivity,调用onCreate方法后,app接受用户输入的用户名以及密码,if条件语句判断了用户输入的obj经过a方法运算后,是否等于obj2,如果不相等,则直接提示登录失败,如果相等,那么代码步入FridaLoginDemo
依据上述梳理的逻辑,不难得出结论:只要我们输入的密码等于某个特定的字符串,那么即可登录成功,下面是依据该逻辑编写的frida脚本
1 2 3 4 5 6 7 8 9 10 11 function hook_java ( ) { Java.perform(function ( ) { var LoginActivity = Java.use("com.shui.fridademo.Activity.LoginActivity" ); console .log("frida hook class:LoginActivity" ); LoginActivity.a.implementation = function (str, str2 ) { var result = this .a(str, str2); console .log("LoginActivity.a函数入参值、返回值为:" , str, str2, result); return result; }; }); }
将上述js代码通过frida注入到app后,绝对会出现如下报错
该报错的原因是由于hook脚本主动调用了LoginActivity类的a方法,但是此类下存在俩个a方法,直接调用frida也很懵,所以我们需要指定主动调用的方法,这里引出了Java的重载理念,此处就不深入讲解;
由报错可以看出,a方法有俩个重载,“.overload(‘[B’)”、 “.overload(‘java.lang.String’, ‘java.lang.String’)”,而代码块调用的a方法传参是两个,所以此处只需要在原有的hook脚本上对a方法进行重载即可,修改后的脚本如下
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 function change_return () { Java.perform(function () { var LoginActivity = Java.use("com.shui.fridademo.Activity.LoginActivity" ); console.log("hook class: LoginActivity" ); LoginActivity.a.overload('java.lang.String' , 'java.lang.String' ).implementation = function (p1, p2) { var result = this .a(p1, p2); console.log("LoginActivity.a: " , p1, p2, result); return result; }; }) } function main () { change_return(); } setImmediate(main);
frida 修改返回值 修改返回值 绝对是frida hook Java中最经典的几个操作,其依赖的api也极其简单,下面通过一段客户端的Java代码来做hook讲解
FridaLoginDemo code
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 public void onCheck () { try { if (a(b("请输入密码:" )).equals("L4+7byh/WQCFIEzjW9z6jUv/OwlhJURvGQwBq/RTe2nZHz7x0ug==" )) { CheckSuccess(); startActivity(new Intent(this , FridaLoginSuccess.class)); finishActivity(0 ); } else { 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(); }
该类中的方法很简单,只有一个if判断,在判断条件中,如果输入的密码等于字符串“L4+7byh/WQCFIEzjW9z6jUv/OwlhJURvGQwBq/RTe2nZHz7x0ug==”,那么则直接跳转到startActivity;依据该思路,我们只需要通过frida脚本将函数的返回值永久的修改为固定的字符串即可,脚本如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function change_return () { Java.perform(function () { var FridaLoginDemo = Java.use("com.shui.fridademo.Activity.FridaLoginDemo" ); console.log("hook class: FridaLoginDemo" ); FridaLoginDemo.a.implementation = function (barr) { console.log("FridaLoginDemo.a" ); return "L4+7byh/WQCFIEzjW9z6jUv/OwlhJURvGQwBq/RTe2nZHz7x0ug==" ; }; }) }
frida 主动调用静态函数 上文中讲了frida主动去调用动态函数,通过传参的方式获取对应的返回值,既然能调用动态,那么同样的也可以调用静态函数,下面通过一个demo来看一下frida静态调用的方式
FridaLoginSuccess
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class FridaLoginSuccess extends BaseFridaActivity { private static boolean static_bool_var = false ; private boolean bool_var = false ; @Override public String getNextCheckTitle () { return "frida静态调用" ; } private static void setStatic_bool_var () { static_bool_var = true ; } private void setBool_var () { this .bool_var = true ; } @Override public void onCheck () { if (static_bool_var && this .bool_var) { CheckSuccess(); startActivity(new Intent(this , FridaNext3.class)); finishActivity(0 ); return ; } super .CheckFailed(); } }
上述代码中,存在俩个boolean变量,“static_bool_var”、“bool_var”,这俩个变量的初始值均为false,而在onCheck中,if的唯一判断条件是如果这俩个变量为true,那么才可以成功执行,startActivity;使得俩个变量为true,则需要调用“setStatic_bool_var”、“setBool_var”方法,但是代码中却没有任意一处对这俩个方法做调用,所以只能通过frida对代码中的静态方法进行调用,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function call_static_methods ( ) { Java.perform(function ( ) { var FridaLoginSuccess = Java.use("com.shui.fridademo.Activity.FridaLoginSuccess" ); console .log("hook class: FridaLoginSuccess" ); FridaLoginSuccess.setStatic_bool_var(); FridaLoginSuccess.setBool_var(); }) }
将上述代码直接植入到app中运行的话,会出现如下报错
该报错的本质是由于上述的俩个方法不都是静态方法
private static void setStatic_bool_var()
private void setBool_var()
在Java中,除了static修饰的方法,其他的均为实例方法,所以frida在调用方法时,如果该方法被static修饰,那么可以调用:FridaActivity2.setStatic_bool_var(); ,但是如果是实例方法这么调用的话,就会出现上面截图的报错,这个时候用到了frida JavaScript api中的”Java.choose”,该api用于在内存中扫描 Java 堆,枚举 Java 对象(className)实例,官方文档描述如下:
那么frida代码则可以修改为如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 function call_static_methods ( ) { Java.perform(function ( ) { var FridaLoginSuccess = Java.use("com.shui.fridademo.Activity.FridaLoginSuccess" ); console .log("hook class: FridaActivity2" ); FridaLoginSuccess.setStatic_bool_var(); Java.choose("com.shui.fridademo.Activity.FridaLoginSuccess" , { onMatch : function (instance ) { instance.setBool_var(); }, onComplete : function ( ) { } }); }) }
至此,frida主动调用静态方法以及与区分的实例方法的调用全部完成
frida 修改变量值 上文描述了frida主动调用app内部方法,接下来要讲的是frida如何去修改类中的某个变量,依旧是一个demo来进行操作
FridaNext3
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 public class FridaNext3 extends BaseFridaActivity { private static boolean static_bool_var = false ; private boolean bool_var = false ; private boolean same_name_bool_var = false ; @Override public String getNextCheckTitle () { return "frida修改变量值" ; } private void same_name_bool_var () { Log.d("Frida" , static_bool_var + " " + this .bool_var + " " + this .same_name_bool_var); } @Override public void onCheck () { if (static_bool_var && this .bool_var && this .same_name_bool_var) { CheckSuccess(); startActivity(new Intent(this , FridaNext4.class)); finishActivity(0 ); return ; } super .CheckFailed(); } }
上述代码中,其实可以按照之前说过的静态方法来类比分类
静态变量:“static_bool_var”
实例变量:“bool_var”、“same_name_bool_var”
对于变量的修改,和主动调用函数类似,static修饰过的变量,frida可以直接对其进行修改,但是如果是实例化的变量,那么就只能通过Java.choose调取当前类之后,在对其进行修改
按照该思路,frida脚本编写如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function change_variable ( ) { Java.perform(function ( ) { var FridaNext3 = Java.use("com.shui.fridademo.Activity.FridaNext3" ); console .log("hook class FridaNext3" ); FridaNext3.static_bool_var.value = true ; Java.choose("com.shui.fridademo.Activity.FridaNext3" , { onMatch : function (instance ) { instance.bool_var.value = true ; instance.same_name_bool_var.value = true ; }, onComplete : function ( ) { } }); }) }
将该代码注入到app之后,发现frida好像并没有成功hook,打印log之后,我们要修改的值确实都是true
此时,就涉及到了另外一个点 —— 变量名和方法名一致时,如何修改变量的值
上文中“same_name_bool_var”这个字符串,是变量名,同时又是方法名,我们在hook代码中直接将其原始值修改为了true:instance.same_name_bool_var.value = true,但是这种方案是不可行的, 当变量名和方法名一致时,想要修改变量值,需要在字符串前加下划线,及
instance.same_name_bool_var.value = true 应该写成 instance._same_name_bool_var.value = true
这种修改后的hook脚本就是
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 function change_variable ( ) { Java.perform(function ( ) { var FridaNext3 = Java.use("com.shui.fridademo.Activity.FridaNext3" ); console .log("hook class FridaNext3" ); FridaNext3.static_bool_var.value = true ; Java.choose("com.shui.fridademo.Activity.FridaNext3" , { onMatch : function (instance ) { instance.bool_var.value = true ; instance._same_name_bool_var.value = true ; }, onComplete : function ( ) { } }); }) }
此时,即可成功hook修改FridaNext3的相关操作
frida hook 类的多个函数 在此之前的内容中,frida的操作都是针对单一的函数或者变量来进行的,但是有些场景中往往需要hook诸多的函数,之前就有大佬依据下文将要提到的frida api写的工具ZenTracer
接下来继续以demo为例来引出相关的hook操作
FridaNext4
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 public class FridaNext4 extends BaseFridaActivity { @Override public String getNextCheckTitle () { return "frida同时hook类下的多个函数" ; } 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 () { } } @Override public void onCheck () { if (InnerClasses.check1() && InnerClasses.check2() && InnerClasses.check3() && InnerClasses.check4() && InnerClasses.check5() && InnerClasses.check6()) { CheckSuccess(); startActivity(new Intent(this , FridaNext5.class)); finishActivity(0 ); return ; } super .CheckFailed(); } }
上述代码中,很直观的能看到onCheck函数的if条件只有一个,那就是代码中所出现的诸多方法都返回true;其实如果按照我们之前的思路这个以如下脚本很简单的实现
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 function hook_InnerClasses ( ) { Java.perform(function ( ) { var InnerClasses = Java.use("com.shui.fridademo.Activity.FridaNext4$InnerClasses" ); console .log(InnerClasses); 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 ; }; }); }
class.getDeclaredMethods 就像前面讲过的那样,只需要frida主动调用这些函数,然后对其返回值修改即可实现此次demo想要达成的目的,但是这种一次性调用多次,还都是同一类下的函数同时调用,写成这种排排坐吃果果的形式,未免会影响代码的美观性以及有一点小蠢,所以依赖frida api我们可以写出如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function hook_all_methods ( ) { Java.perform(function ( ) { var class_name = "com.shui.fridademo.Activity.FridaNext4$InnerClasses" ; var InnerClasses = Java.use(class_name); var all_methods = InnerClasses.class.getDeclaredMethods(); for (var i = 0 ; i < all_methods.length; i++) { var method = all_methods[i]; var methodStr = method.toString(); var substring = methodStr.substr(methodStr.indexOf(class_name) + class_name.length + 1 ); var methodname = substring.substr(0 , substring.indexOf("(" )); console .log(methodname); InnerClasses[methodname].implementation = function ( ) { console .log("hook_mul_function:" , this ); return true ; } } }); }
Java.enumerateLoadedClasses 上述代码即可完全的hook一个类下的所有方法,不过前面我有提到过大佬写的一个hook脚本ZenTracer ,大家如果去审计代码就会发现,同样是hook一个类下的所有方法,但是ZenTracer使用了frida的另一个API:Java.enumerateLoadedClasses,该方法的主要用途是“获取内存中加载的所有类”,下面是frida官方对于该api的描述信息
以此类推,我们还能将frida 的hook 脚本修改为如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hook_loaded_methods ( ) { Java.perform(function ( ) { var hook_class = "com.example.androiddemo.Activity.FridaActivity4$InnerClasses" ; Java.enumerateLoadedClasses({ onMatch : function (aClass ) { console .log("内存加载出来的所有类有:" , aClass) }, onComplete : function ( ) { } }); }) }
通过log的日志打印,可以直观的看到enumerateLoadedClasses加载出来的所有类
但是我们在本次只需要内存中的某一个类,所以需要对其做一个match匹配,然后再hook,而hook类下的其他方法时,依旧回到了之前的“getDeclareedMethods”;所以ZenTracer项目的核心原理就是:先用“enumerateLoadedClasses”捞出所以加载的类,然后再用黑白名单的机制匹配需要hook的类,最后通过“getDeclareedMethods”获取该类下的所有方法,进而hook。
frida hook 动态加载的dex 动态加载dex,作为frida的一个较为高阶用法,其核心API我们上文其实已经提到过了,在此处详细展开说一下,依旧是一个demo的引入
FridaNext5
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 public class FridaNext5 extends BaseFridaActivity { private CheckInterface DynamicDexCheck = null ; private void loaddex () { File cacheDir = getCacheDir(); if (!cacheDir.exists()) { cacheDir.mkdir(); } String str = cacheDir.getAbsolutePath() + File.separator + "DynamicPlugin.dex" ; File file = new File(str); try { if (!file.exists()) { file.createNewFile(); copyFiles(this , "DynamicPlugin.dex" , file); } } catch (IOException e) { e.printStackTrace(); } try { this .DynamicDexCheck = (CheckInterface) new DexClassLoader(str, cacheDir.getAbsolutePath(), null , getClassLoader()).loadClass("com.shui.fridademo.Dynamic.DynamicCheck" ).newInstance(); if (this .DynamicDexCheck != null ) { return ; } Toast.makeText(this , "loaddex Failed!" , 1 ).show(); } catch (Exception e2) { e2.printStackTrace(); } } public CheckInterface getDynamicDexCheck () { if (this .DynamicDexCheck == null ) { loaddex(); } return this .DynamicDexCheck; } @Override public void onCreate (Bundle bundle) { super .onCreate(bundle); loaddex(); } @Override public void onCheck () { if (getDynamicDexCheck() != null ) { if (getDynamicDexCheck().check()) { CheckSuccess(); startActivity(new Intent(this , FridaActivity6.class)); finishActivity(0 ); return ; } super .CheckFailed(); return ; } Toast.makeText(this , "onClick loaddex Failed!" , 1 ).show(); } }
上述代码中,onCheck之后,if唯一判断值是getDynamicDexCheck函数返回值不为null,而getDynamicDexCheck方法依赖于loaddex方法加载的dex文件,dex加载后调用getDynamicDexCheck接口的check方法;所以,hook脚本只需要动态hook dex,然后调用“getDynamicDexCheck().check()”即可
hook脚本如下
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 function hook_dyn_dex ( ) { Java.perform(function ( ) { var FridaNext5 = Java.use("com.shui.fridademo.Activity.FridaNext5" ); Java.choose("com.shui.fridademo.Activity.FridaNext5" , { onMatch : function (instance ) { console .log(instance.getDynamicDexCheck().$className); }, onComplete : function ( ) { } }); Java.enumerateClassLoaders({ onMatch : function (loader ) { try { if (loader.findClass("com.shui.fridademo.Dynamic.DynamicCheck" )) { console .log(loader); Java.classFactory.loader = loader; } } catch (error) { } }, onComplete : function ( ) { } }); var DynamicCheck = Java.use("com.shui.fridademo.Dynamic.DynamicCheck" ); console .log(DynamicCheck); DynamicCheck.check.implementation = function ( ) { console .log("DynamicCheck.check" ); return true ; } }); }
自此 frida 动态 hook dex 告一段落。
踩坑排错 unable to download it within 20 seconds 该问题是由于frida安装时,会依赖于egg文件,但是pip的国内源却没有下载到,就有了如下的报错截图
解决方案也很简单,下述命令敲即可
1 2 3 4 5 6 7 8 9 # frida egg文件下载地址 https://pypi.org/simple/frida/ # 依据自己系统、Python版本、想要安装的frida版本去下载合适的egg文件 easy_install frida-15.1.17-py3.8-macosx-11.0-arm64.egg # 运行easy_install命令,直至其出现reading官网包即可Ctrl c结束运行
因为会读取官网,所以结束即可,然后正常使用国内源安装frida
1 python3 -m pip install frida -i https://pypi.douban.com/simple
zsh: command not found: easy_install 第一个报错中,使用到了easy_install,但是如果基于brew安装的python之后,大概率会看到标题中这个错误“zsh: command not found: easy_install”;
其实easy_install是pip之前用于安装数据包的插件,默认会安装到python中,其依赖于setuptools ,但是如果莽撞的pip安装setuptools的话,100%还是没有easy_install(至少看到本文得你(^▽^ ));
其实easy_install依赖setuptools这个核心是不变的,但是官网介绍来看,setuptools在版本>51.3.3之后,就不会自带easy_install了,所以想要安装easy_install,只需要
1 2 3 pip3 uninstall setuptools pip3 install -v setuptools==51.3.3
setuptools移除easy_install 的说明如下,官网地址:setuptools update histody
其实,上述俩个错误,都可以通过一个方案解决:降级到3.8……
Error: VM::AttachCurrentThread failed: -1 frida hook app时,可能会出现“Error: VM::AttachCurrentThread failed: -1”这个报错,报错信息如下
该错误的解决方案也很简单
1 2 3 4 5 6 7 8 adb shell su setprop persist.device_config.runtime_native.usap_pool_enabled false getprop | grep usap
grep 结果如下
然后再 hook app 即可正常 attach
解决方案依据:frida issues 1788
参考文献 frida docs