├── README.md ├── app ├── java │ └── com │ │ └── example │ │ └── testfrida │ │ ├── Animal.java │ │ └── MainActivity.java └── res │ ├── layout │ └── activity_main.xml │ └── values │ └── strings.xml ├── hook_sript ├── .idea │ ├── encodings.xml │ ├── misc.xml │ ├── modules.xml │ ├── testhook.iml │ └── workspace.xml ├── anony_inner_class.js ├── common_function.js ├── constructor.js ├── inner_class.js ├── main.py └── property.js └── pictures ├── 1-普通函数-hook.gif ├── 1-普通函数.gif ├── 2-构造函数-hook.gif ├── 2-构造函数.gif ├── 3-内部类-hook.gif ├── 3-内部类.gif ├── 4-匿名内部类-hook.gif ├── 4-匿名内部类.gif ├── 5-私有属性-hook.gif ├── 5-私有属性.gif ├── BLE.png ├── Screenshot_20201230_140155_cn.wch.bledemo.jpg └── innerclass.png /README.md: -------------------------------------------------------------------------------- 1 | # FridaHookAndroid 2 | 3 | Frida-Android 进阶 4 | 5 | frida 版本:12.11.18 6 | 7 | 系统:Ubuntu 20.04 LTS 8 | 9 | 博客地址:[Frida Hook Android App 进阶用法之 Java 运行时](https://blog.csdn.net/song_lee/article/details/111999565) 10 | 11 | ## 0x10 官方 API 12 | 13 | ### Java 运行时 14 | 15 | 官方API地址: [https://www.frida.re/docs/javascript-api/#java](https://www.frida.re/docs/javascript-api/) ,这里给出几个常用的 API 16 | 17 | **Java.perform(fn)** 18 | 19 | 确保当前线程被附加到 VM 上,并且调用`fn`函数。此函数在内部调用 VM::AttachCurrentThread,然后执行 fn 回调函数中的 Javascript 脚本操作 Java 运行时,最后使用 VM::DetachCurrentThread 释放资源。 20 | 21 | **Java.use(className)** 22 | 23 | 通过类名获得 Java 类,返回一个包裹好的 Javascript 对象。通过该对象,可访问类成员。通过调用 `$new()`调用构造函数,实例化对象。 24 | 25 | ```javascript 26 | Java.perform(function(){ 27 | var Activity = Java.use("android.app.Activity"); // 反射获取类 28 | var Exception = Java.use("java.lang.Exception"); 29 | Activity.onResume.implementation = function(){ // 调用构造器抛出异常 30 | throw Exception.$new("Oh noes!"); 31 | }; 32 | }); 33 | ``` 34 | 35 | **Java.choose(className, callback)** 36 | 37 | 在内存中扫描 Java 堆,枚举 Java 对象(className)实例。比如可以使用 `java.lang.String` 扫描内存中的字符串。callbacks 提供两个参数:`onMatch(instance)` 和 `onComplete`,分别是找到匹配对象和扫描完成调用。 38 | 39 | **Java.scheduleOnMainThread(fn)** 40 | 41 | 在 VM 主线程(UI 线程)执行回调函数。Android 中操作 UI 元素需要在主线程中执行代码,`scheduleOnMainThread` 的作用就是用来在主线程中执行函数。此函数需要使用 `Java.perform` 包裹。 42 | 43 | ```js 44 | Java.perform(function(){ 45 | var Toast = Java.use("android.widget.Toast"); 46 | // 获取 context 47 | var currentApplication = Java.use("android.app.ActivityThread").currentApplication(); 48 | var context = currentApplication.getApplicationContext(); 49 | // 在主线程中运行回调 50 | Java.scheduleOnMainThread(function(){ 51 | Toast.makeText(context, "Hello frida!", Toast.LENGTH_LONG.value).show(); 52 | }); 53 | }); 54 | ``` 55 | 56 | **enumerateLoadedClasses(callbacks)** 57 | 58 | 枚举当前已加载的类。`callbacks` 参数是一个对象,需要提供两个回调函数—— `onMatch(className)` 和 `onComplete`。每次找到一个类就会调用一次 `onMatch`,全部找完之后,调用 `onComplete`。 59 | 60 | ### 如何拉起 Frida 脚本 61 | 62 | #### 方法一:运行时 hook 63 | 64 | ```python 65 | import frida 66 | import sys 67 | 68 | def read_js(file): 69 | with open(file) as fp: 70 | return fp.read() 71 | 72 | def on_message(message, data): 73 | if message["type"] == "send": 74 | print("[+] {}".format(message["payload"])) 75 | else: 76 | print("[-] {}".format(message)) 77 | 78 | remote_device = frida.get_usb_device() 79 | session = remote_device.attach("com.example.testfrida") 80 | 81 | src = read_js("./test.js") 82 | script = session.create_script(src) 83 | script.on("message", on_message) 84 | script.load() 85 | sys.stdin.read() 86 | ``` 87 | 88 | #### 方法二:spawn 拉起进程 89 | 90 | 如果需要 hook app 执行 onCreate() 方法中的一些功能,就需要使用 spawn 模式 91 | 92 | ```shell 93 | frida -U -l test.js -f com.example.testfrida 94 | ``` 95 | 96 | 参数 97 | 98 | - -U connect to USB device 99 | - -l SCRIPT, --load=SCRIPT 100 | - -f FILE, --file=FILE spawn FILE 101 | 102 | 后文中的案例,由于 hook 的显示结果通过 Toast 展示,Toast 会在 app 刚启动时加载。 103 | 104 | - 使用方法一,运行时 hook,需要在 app 刚启动时,就运行 python 程序; 105 | - 使用方法二,用 spawn 拉起 app 106 | 107 | ## 0x20 hook 案例 108 | 109 | > **最新版的 Jadx 已经可以根据反编译的函数自动生成 Frida 脚本,大大简化了我们逆向的工作。** 110 | 111 | ### 0x21 通用案例 112 | 113 | #### 普通方法 114 | 115 | **方法一:Java.use** 116 | 117 | 这种方式比较通用,但是在某些动态加载的类中,可能无法hook 118 | 119 | ```js 120 | Java.perform(function (){ 121 | send("start hook..."); 122 | var Animal = Java.use("com.example.testfrida.Animal"); 123 | 124 | Animal.getAnimalInfo.implementation = function (){ 125 | send("hijack getAnimalInfo"); 126 | return "hello, frida!"; 127 | }; 128 | }); 129 | ``` 130 | 131 | 脚本输出结果 132 | 133 | ```shell 134 | /usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py 135 | [+] start hook... 136 | [+] hijack getAnimalInfo 137 | ``` 138 | 139 | **方法二:Java.choose** 140 | 141 | 这种方式适用于一些动态方法,静态方法不适用此方式 142 | 143 | ```js 144 | Java.choose("com.xxx.class", { 145 | onMatch: function(instance) { 146 | // hook 类 147 | }, 148 | 149 | onComplete: function() { 150 | 151 | } 152 | }); 153 | ``` 154 | 155 | 156 | 157 | #### 构造函数 158 | 159 | 形参类型通过 `overload` 传递 160 | 161 | - 可以通过 `arguments` 列表获取待 hook 函数的形参 162 | 163 | - 也可以通过`implementation = function (a, b, c...)` 获取 164 | 165 | ```js 166 | Java.perform(function (){ 167 | send("start hook..."); 168 | var Animal = Java.use("com.example.testfrida.Animal"); 169 | 170 | Animal.$init.overload("java.lang.String", "int").implementation = function (){ 171 | send("hijack Animal()"); 172 | send("参数1:" + arguments[0]); 173 | send("参数2:" + arguments[1]); 174 | return this.$init("frida", 999); // 修改 175 | }; 176 | }); 177 | ``` 178 | 179 | 脚本输出结果 180 | 181 | ```shell 182 | /usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py 183 | [+] start hook... 184 | [+] hijack Animal() 185 | [+] 参数1:duck 186 | [+] 参数2:2 187 | ``` 188 | 189 | #### 内部类 190 | 191 | 内部类的一般写法是 Class$InnerClass,对于混淆的内部类,查看 smali 源码就能看出 $1、$2 这样的类就是内部类。 192 | 193 | ```js 194 | Java.perform(function (){ 195 | send("start hook..."); 196 | var InnerTest = Java.use("com.example.testfrida.Animal$InnerlTest"); 197 | 198 | InnerTest.getClassInfo.implementation = function (){ 199 | send("hijack inner Class"); 200 | return "hello, frida!"; 201 | } 202 | }); 203 | ``` 204 | 205 | 脚本输出结果 206 | 207 | ```shell 208 | /usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py 209 | [+] start hook... 210 | [+] hijack inner Class 211 | ``` 212 | 213 | #### 匿名内部类 214 | 215 | 查看反编译得到的 smali 源码,smali 文件会将每个类作为一个单独文件保存,如下图所示的 `$1` 就是匿名内部类。 216 | 217 | ![innerclass](./pictures/innerclass.png) 218 | 219 | 其余用法跟普通类是一样的。 220 | 221 | ```js 222 | Java.perform(function (){ 223 | send("start hook..."); 224 | var AnoyClass = Java.use("com.example.testfrida.Animal$1"); 225 | 226 | AnoyClass.getAnimalInfo.implementation = function (){ 227 | return "hello,frida"; // 修改函数返回值 228 | }; 229 | }); 230 | ``` 231 | 232 | 匿名内部类 hook 还有一些独特技巧,比如说,如果你不想通过反编译的方式得到匿名内部类的符号,可以直接通过 hook 构造函数的方式获取类名,如下所示 233 | 234 | ```js 235 | Java.perform(function (){ 236 | send("start hook..."); 237 | var Animal = Java.use("com.example.testfrida.Animal"); 238 | 239 | Animal.$init.overload("java.lang.String", "int").implementation = function (){ 240 | send("constructor called from " + this.$className); 241 | const NewAnimal = Java.use(this.$className); 242 | NewAnimal.getAnimalInfo.implementation = function (){ 243 | return "hello, frida"; 244 | }; 245 | }; 246 | }); 247 | ``` 248 | 249 | 250 | 251 | #### 私有属性 252 | 253 | 如果 app 没有调用 get 等函数,怎么直接获取类的私有属性呢?早期版本的 frida 支持使用 java 提供的反射,js 同样也提供了该功能。 254 | 255 | ```js 256 | var objectionInstance = Animal.$new("test", 0); 257 | var ob = Java.cast(objectionInstance.getClass(), clazz); 258 | var name = ob.getDeclaredField("name"); // 获得某个属性对象 259 | var value = ob.get(name); // 获得obj中对应的属性值 260 | send(value); 261 | ``` 262 | 263 | 经过多次实践,发现该脚本并不能在笔者的环境中正常运行,后来才发现,新版 frida (笔者使用的版本:12.11.18)支持直接获取类中的私有属性,而不需要使用 Java 的高级特性——反射。 264 | 265 | ```js 266 | Java.perform(function (){ 267 | send("start hook..."); 268 | var Animal = Java.use("com.example.testfrida.Animal"); 269 | 270 | Animal.getAge.implementation = function (){ 271 | send("obtain key"); 272 | 273 | // 直接调用类中的函数 274 | send("call public function >> getName(): " + this.getName()); 275 | send("call private function >> getKey(): " + this.getKey()); 276 | // 直接调用类中的私有属性 277 | send("call private property >> name: " + this.name.value); 278 | send("call private property >> age: " + this.age.value); 279 | send("call private property >> key: " + this.key.value); 280 | 281 | return this.getAge(); 282 | }; 283 | }); 284 | ``` 285 | 286 | 脚本输出结果 287 | 288 | ```shell 289 | /usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py 290 | [+] start hook... 291 | [+] obtain key 292 | [+] call public function >> getName(): penguin 293 | [+] call private function >> getKey(): AEKL3KJK23KLASLDKOCVL 294 | [+] call private property >> name: penguin 295 | [+] call private property >> age: 5 296 | [+] call private property >> key: AEKL3KJK23KLASLDKOCVL 297 | ``` 298 | 299 | 修改私有属性也一样简单 300 | 301 | ```js 302 | // 修改私有属性 303 | this.age.value = 9999; 304 | ``` 305 | 306 | ### 0x22 进阶用法 307 | 308 | #### 获取内存中加载的所有类 309 | 310 | ```js 311 | Java.perform(function(){ 312 | send("enumerating classes..."); 313 | Java.enumerateLoadedClasses({ 314 | onMatch: function(className){ 315 | send("found class >> " + className); // 类名 316 | }, 317 | onComplete: function(){ 318 | send("class enumration complete"); 319 | } 320 | }); 321 | }); 322 | ``` 323 | 324 | 脚本输出结果 325 | 326 | ```shell 327 | /usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py 328 | [+] enumerating classes... 329 | [+] found class >> com.huawei.iaware.IAwareFunctionAL 330 | [+] found class >> com.huawei.iaware.IAwareConstantAL$StringConstants 331 | [+] found class >> com.huawei.iaware.IAwareConstantAL 332 | [+] found class >> com.huawei.iaware.IAwareCallbackAL 333 | [+] found class >> com.huawei.iaware.IAwareCallbackAL$MultiWinCallBackHandlerIAL 334 | [+] found class >> vendor.huawei.hardware.hwsched.V1_0.ISched 335 | ... 336 | ``` 337 | 338 | #### 获取类中的所有方法 339 | 340 | ```js 341 | Java.perform(function(){ 342 | send("obtain methods..."); 343 | 344 | var Animal = Java.use("com.example.testfrida.Animal"); 345 | var methods = Animal.class.getDeclaredMethods(); // 方法名 346 | for(var i = 0; i < methods.length; i++){ 347 | console.log(methods[i].getName()); 348 | } 349 | }); 350 | ``` 351 | 352 | 输出结果 353 | 354 | ```shell 355 | /usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py 356 | [+] obtain methods... 357 | static java.lang.String com.example.testfrida.Animal.access$000(com.example.testfrida.Animal) 358 | static int com.example.testfrida.Animal.access$100(com.example.testfrida.Animal) 359 | private java.lang.String com.example.testfrida.Animal.getKey() 360 | public int com.example.testfrida.Animal.getAge() 361 | public java.lang.String com.example.testfrida.Animal.getAnimalInfo() 362 | public java.lang.String com.example.testfrida.Animal.getAnoymousClass() 363 | public com.example.testfrida.Animal$InnerlTest com.example.testfrida.Animal.getInnerTestInstance() 364 | public java.lang.String com.example.testfrida.Animal.getName() 365 | public void com.example.testfrida.Animal.setAge(int) 366 | public void com.example.testfrida.Animal.setName(java.lang.String) 367 | ``` 368 | 369 | #### 打印调用栈 370 | 371 | 有两种方法可以跟踪函数的调用栈,推荐使用第一种。 372 | 373 | 1. 直接在脚本中直接输出错误日志 374 | 2. 触发异常,在 Android log 中输出 375 | 376 | ```js 377 | Java.perform(function(){ 378 | send("print stack..."); 379 | var Animal = Java.use("com.example.testfrida.Animal"); 380 | 381 | Animal.getAge.implementation = function (){ 382 | console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); 383 | //throw Java.use("java.lang.Exception").$new(); 384 | return this.getAge(); 385 | }; 386 | }); 387 | ``` 388 | 389 | 1.Python 输出 390 | 391 | ```shell 392 | /usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py 393 | [+] print stack... 394 | java.lang.Exception 395 | at com.example.testfrida.Animal.getAge(Native Method) 396 | at com.example.testfrida.MainActivity.getPrivte(MainActivity.java:92) 397 | at com.example.testfrida.MainActivity.onClick(MainActivity.java:60) 398 | at android.view.View.performClick(View.java:7216) 399 | at android.view.View.performClickInternal(View.java:7190) 400 | at android.view.View.access$3500(View.java:827) 401 | at android.view.View$PerformClick.run(View.java:27663) 402 | at android.os.Handler.handleCallback(Handler.java:900) 403 | at android.os.Handler.dispatchMessage(Handler.java:103) 404 | at android.os.Looper.loop(Looper.java:219) 405 | at android.app.ActivityThread.main(ActivityThread.java:8291) 406 | at java.lang.reflect.Method.invoke(Native Method) 407 | at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) 408 | at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055) 409 | ``` 410 | 411 | 2. Android 日志输出 412 | 413 | ```shell 414 | lys@lys-VirtualBox:~$ adb logcat -s AndroidRuntime 415 | --------- beginning of system 416 | --------- beginning of main 417 | 12-19 17:50:22.313 31328 31328 D AndroidRuntime: Shutting down VM 418 | --------- beginning of crash 419 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: FATAL EXCEPTION: main 420 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: Process: com.example.testfrida, PID: 31328 421 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: java.lang.RuntimeException: java.lang.reflect.InvocationTargetException 422 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:523) 423 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055) 424 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: Caused by: java.lang.reflect.InvocationTargetException 425 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 426 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) 427 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: ... 1 more 428 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: Caused by: java.lang.Exception 429 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at com.example.testfrida.Animal.getAge(Native Method) 430 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at com.example.testfrida.MainActivity.getPrivte(MainActivity.java:92) 431 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at com.example.testfrida.MainActivity.onClick(MainActivity.java:60) 432 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at android.view.View.performClick(View.java:7216) 433 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at android.view.View.performClickInternal(View.java:7190) 434 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at android.view.View.access$3500(View.java:827) 435 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at android.view.View$PerformClick.run(View.java:27663) 436 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:900) 437 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:103) 438 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at android.os.Looper.loop(Looper.java:219) 439 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8291) 440 | 12-19 17:50:22.314 31328 31328 E AndroidRuntime: ... 3 more 441 | ``` 442 | 443 | #### Frida Hook 插件类 444 | 445 | App 插件往往会改变系统默认的 classloader,这时候如果直接 hook 插件中的类,就会发现 frida 提示找不到该类。`Java.enumerateClassLoaders ` 用来枚举当前所有的 classloader,以 360 的 replugin 插件为例,相应的 hook 代码如下所示 446 | 447 | ```js 448 | Java.perform(function(){ 449 | var HostApi; 450 | 451 | Java.enumerateClassLoaders({ 452 | "onMatch": function(loader) { 453 | //console.log(loader); 454 | if (loader.toString().startsWith("com.qihoo360.replugin.PluginDexClassLoader")) { 455 | Java.classFactory.loader = loader; // 将当前class factory中的loader指定为我们需要的 456 | } 457 | }, 458 | "onComplete": function() { 459 | console.log("success"); 460 | } 461 | }); 462 | 463 | // 方法一 464 | Java.choose("com.xxx.class", { 465 | onMatch: function(instance) { 466 | HostApi = instance; 467 | }, 468 | 469 | onComplete: function() { 470 | 471 | } 472 | }); 473 | 474 | // 方法二 475 | HostApi = Java.use("com.xxx.class"); 476 | 477 | HostApi.funcname.implementation = function (arg1, arg2, arg3) { 478 | //console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); 479 | return this.funcname(arg1, arg2, arg3); 480 | }; 481 | }); 482 | ``` 483 | 484 | #### Frida Hook 按钮onclick监控事件 485 | 486 | ```js 487 | var jclazz = null; 488 | var jobj = null; 489 | 490 | function getObjClassName(obj) { 491 | if (!jclazz) { 492 | var jclazz = Java.use("java.lang.Class"); 493 | } 494 | if (!jobj) { 495 | var jobj = Java.use("java.lang.Object"); 496 | } 497 | return jclazz.getName.call(jobj.getClass.call(obj)); 498 | } 499 | 500 | function watch(obj, mtdName) { 501 | var listener_name = getObjClassName(obj); 502 | var target = Java.use(listener_name); 503 | if (!target || !mtdName in target) { 504 | return; 505 | } 506 | // send("[WatchEvent] hooking " + mtdName + ": " + listener_name); 507 | target[mtdName].overloads.forEach(function (overload) { 508 | overload.implementation = function () { 509 | //send("[WatchEvent] " + mtdName + ": " + getObjClassName(this)); 510 | console.log("[WatchEvent] " + mtdName + ": " + getObjClassName(this)) 511 | return this[mtdName].apply(this, arguments); 512 | }; 513 | }) 514 | } 515 | 516 | function OnClickListener() { 517 | Java.perform(function () { 518 | 519 | //以spawn启动进程的模式来attach的话 520 | Java.use("android.view.View").setOnClickListener.implementation = function (listener) { 521 | if (listener != null) { 522 | watch(listener, 'onClick'); 523 | } 524 | return this.setOnClickListener(listener); 525 | }; 526 | 527 | //如果frida以attach的模式进行attch的话 528 | Java.choose("android.view.View$ListenerInfo", { 529 | onMatch: function (instance) { 530 | instance = instance.mOnClickListener.value; 531 | if (instance) { 532 | console.log("mOnClickListener name is :" + getObjClassName(instance)); 533 | watch(instance, 'onClick'); 534 | } 535 | }, 536 | onComplete: function () { 537 | } 538 | }) 539 | }) 540 | } 541 | setImmediate(OnClickListener); 542 | ``` 543 | 544 | 545 | 546 | ### 0x23 助理函数 547 | 548 | #### ArrayBuffer 转换 549 | 550 | ```js 551 | function ab2Hex(buffer) { 552 | var arr = Array.prototype.map.call(new Uint8Array(buffer), function (x) {return ('00' + x.toString(16)).slice(-2)}).join(" ").toUpperCase(); 553 | return "[" + arr + "]"; 554 | } 555 | 556 | function ab2Str(buffer) { 557 | return String.fromCharCode.apply(null, new Uint8Array(buffer)); 558 | } 559 | ``` 560 | 561 | #### 获取 JS 对象类型 562 | 563 | ```js 564 | function getParamType(obj) { 565 | return obj == null ? String(obj) : Object.prototype.toString.call(obj).replace(/\[object\s+(\w+)\]/i, "$1") || "object"; 566 | } 567 | ``` 568 | 569 | 例如 570 | 571 | ```js 572 | Java.perform(function(){ 573 | send("obtain methods..."); 574 | 575 | var Animal = Java.use("com.example.testfrida.Animal"); 576 | var methods = Animal.class.getDeclaredMethods(); 577 | 578 | console.log(getParamType(Animal)); 579 | console.log(getParamType(methods)); 580 | console.log(Animal.class); 581 | }); 582 | ``` 583 | 584 | 输出 585 | 586 | ```js 587 | /usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py 588 | [+] obtain methods... 589 | Object 590 | Array 591 | class com.example.testfrida.Animal 592 | ``` 593 | 594 | #### 字节数组转十六进制 595 | 596 | ```js 597 | // thanks: https://awakened1712.github.io/hacking/hacking-frida/ 598 | function bytes2hex(array) { 599 | var result = ''; 600 | for (var i = 0; i < array.length; ++i) 601 | result += ('0' + (array[i] & 0xFF).toString(16)).slice(-2); 602 | return result; 603 | } 604 | ``` 605 | 606 | ## 0x30 Frida 数据类型 607 | 608 | Frida hook 某个方法时,如果该方法没有重载,则相当简单,我们不需要声明参数类型,直接使用 `类.方法名.implentation = function(arg1, arg2){}`。如果该方法重载,则需要添加参数类型,写法如下 `类.方法名.overload(类型1, 类型2) = function(arg1, arg2){}` 609 | 610 | > 如果你无法确定想要 Hook 的参数类型,可以在 Frida 脚本里,只写函数,不写参数,这样运行该脚本时,会自动打印错误信息,该信息包含你 Hook 的方法的所有参数类型。 611 | 612 | ### 0x31 基本数据类型 613 | 614 | | Frida中的基本类型全名 | Frida中的基本类型缩写(定义数组时使用) | 615 | | --------------------- | ------------------------------------- | 616 | | boolean | Z | 617 | | byte | B | 618 | | char | C | 619 | | double | D | 620 | | float | F | 621 | | int | I | 622 | | long | J | 623 | | short | S | 624 | 625 | Frida 基本数据类型与 Java 保持一致。 626 | 627 | #### 定义一个整型变量 628 | 629 | ```js 630 | // int jStringVar = 1; 631 | var jInt = Java.use("java.lang.Integer"); 632 | var jStringVar = jStringClass.$new(1); 633 | ``` 634 | 635 | #### 定义一个字符串变量 636 | 637 | ```js 638 | // String jStringVar = "我是字符串" 639 | var jString = Java.use("java.lang.String"); 640 | var jStringVar = jStringClass.$new("我是字符串"); 641 | ``` 642 | 643 | #### 定义一个字节数组 644 | 645 | ```js 646 | var buffer = Java.array('byte', [ 13, 37, 42 ]); 647 | ``` 648 | 649 | #### 打印变量值 650 | 651 | ```js 652 | console.log(jStringVar.value) 653 | ``` 654 | 655 | ### 0x32 数组 656 | 657 | 数组类型其实与 Smali 语法或者说是 Java 字节码保持一致,例如 658 | 659 | - int 类型的数组,写法为:`[I` 660 | - String 类型的数组,写法为:`[java.lang.String;` 注意结尾的分号 661 | 662 | #### 定义一个空数组 663 | 664 | Java/C 代码 665 | 666 | ```c 667 | int a[]; 668 | ``` 669 | 670 | Frida Js 代码 671 | 672 | ```js 673 | var emptyArray = Java.array("Ljava.lang.Object;",[]); 674 | //---简写如下 675 | var emptyArray = []; 676 | ``` 677 | 678 | #### 定义一个数组并初始化 679 | 680 | Java/C 代码 681 | 682 | ```c 683 | int a[] = {1, 2}; 684 | ``` 685 | 686 | Frida Js 代码 687 | 688 | ```js 689 | var intClass = Java.use("java.lang.Integer"); 690 | var num1 = intClass.$new(1); 691 | var num2 = intClass.$new(2); 692 | var intArray = Java.array("Ljava.lang.Object;",[num1,num2]); 693 | //---简写如下 694 | var num1 = intClass.$new(1); 695 | var num2 = intClass.$new(2); 696 | var intArray = [num1,num2]; 697 | ``` 698 | 699 | ### 0x33 引用数据类型 700 | 701 | 如果是 hook 某个**重载**函数,其中的参数为引用数据类型,那么直接写入全称即可。例如我们想 hook 这个函数 702 | 703 | ```java 704 | // Anmial 类 705 | public void onTest(boolean z, Bundle bundle); 706 | ``` 707 | 708 | 直接使用如下 js 709 | 710 | ```js 711 | var Anmial = Java.use("xxxx"); 712 | Anmial.onTest.overload("boolean", "android.os.Bundle").implementation = function(){ 713 | // 714 | }; 715 | ``` 716 | 717 | ### 0x34 强制类型转换 718 | 719 | Frida 提供了 `Java.cast()` 方法,用于强制类型转换。如下所示 720 | 721 | ```js 722 | var clazz = Java.use("java.lang.Class"); 723 | var cls = Java.cast(obj.getClass(),clazz); //先获取obje的Class,然后再强转成Class类型。 724 | ``` 725 | 726 | ## 0x40 实际案例 727 | 728 | ### 0x41 hook BLE 729 | 730 | #### 分析 731 | 732 | 简要说明一下,在 BLE 协议栈中 ,有几个关键概念 733 | 734 | - **GATT**:通过 BLE 连接,读写属性类数据的 Profile 通用规范 735 | - **Characteristic**:特征,可以理解为一个数据类型,包括 value 和 descriptor(特征值和描述) 736 | - **descriptor**:是对 Characteristic 的描述,如范围、计量单位等 737 | - **Service**:一组 Characteristic 的集合 738 | 739 | **一个蓝牙设备有多个 profile,每个profile 有多个 service,每个 service 有多个 characteristic** 740 | 741 | 例如,一个名为“Device Information”的 Service,可能包含多个 Characteristics,比如 “Manufacture Name String”,每个名字在编程中,都有一个统一的表示方法,唯一识别,这就是 UUID。 742 | 743 | ![BLE](./pictures/BLE.png) 744 | 745 | 在 Android SDK 中,低功耗蓝牙使用`BluetoothGattCallback` 提供的回调,调用 `onCharacteristicWrite` /Read 方法进行读写,具体用法可参见 https://developer.android.com/reference/android/bluetooth/BluetoothGattCallback。 746 | 747 | Android SDK 的 BLE 编程,通过 `BluetoothDevice` 类中的 `connectGatt` 注册 GATT 回调函数,在回调中完成读写操作。 748 | 749 | ```java 750 | final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); // 得到设备 751 | mBluetoothGatt = device.connectGatt(this, false, mGattCallback); //注册回调函数 752 | // 回调函数的实现 753 | private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback(){ 754 | // 重写Servcie/Characteristic的各个操作,读写数据的方法 755 | } 756 | ``` 757 | 758 | #### hook 759 | 760 | 所以,想要 hook 住与设备传输的蓝牙数据,必然是拦截 `BluetoothGattCallback` 这个构造方法。 761 | 762 | 开源项目:https://github.com/optiv/blemon/blob/master/frida/blemon.js 给了我们一个很好的案例,掌握这些回调函数以及 Frida hook 蓝牙的一些方法。 763 | 764 | ```js 765 | if (Java.available) { 766 | Java.perform(function () { 767 | var BTGattCB = Java.use("android.bluetooth.BluetoothGattCallback"); 768 | // 想 hook 匿名内部类的函数,从父类的构造方法入手 769 | BTGattCB.$init.overload().implementation = function () { 770 | console.log("[+] BluetoothGattCallback constructor called from " + this.$className); 771 | // 当前hook的类匿名类 772 | const NewCB = Java.use(this.$className); 773 | NewCB.onCharacteristicRead.implementation = function (g, c, s) { 774 | const retVal = NewCB.onCharacteristicRead.call(this, g, c, s); 775 | var uuid = c.getUuid(); 776 | console.log(Color.Blue + "[BLE Read <=]" + Color.Light.Black + " UUID: " + uuid.toString() + Color.Reset + " data: 0x" + bytes2hex(c.getValue())); 777 | return retVal; 778 | }; 779 | NewCB.onCharacteristicWrite.implementation = function (g, c, s) { 780 | // ... 781 | }; 782 | NewCB.onCharacteristicChanged.implementation = function (g, c) { 783 | // ... 784 | }; 785 | return this.$init(); 786 | }; 787 | 788 | }); // end perform 789 | } 790 | ``` 791 | -------------------------------------------------------------------------------- /app/java/com/example/testfrida/Animal.java: -------------------------------------------------------------------------------- 1 | package com.example.testfrida; 2 | 3 | public class Animal { 4 | // 私有属性 5 | private String name; 6 | private int age; 7 | private static final String key = "AEKL3KJK23KLASLDKOCVL"; 8 | 9 | // 私有方法 10 | private String getKey(){ 11 | return this.key; 12 | } 13 | 14 | // 普通方法 15 | public String getName(){ 16 | return this.name; 17 | } 18 | 19 | public int getAge(){ 20 | return this.age; 21 | } 22 | 23 | public void setName(String name){ 24 | this.name = name; 25 | } 26 | 27 | public void setAge(int age){ 28 | this.age = age; 29 | } 30 | 31 | // 普通方法 32 | public String getAnimalInfo(){ 33 | return "名字:" + this.name + "\n" + "年龄:" + Integer.toString(this.age); 34 | } 35 | 36 | // 构造方法1 37 | public Animal(String name, int age){ 38 | this.name = name; 39 | this.age = age; 40 | } 41 | 42 | // 构造方法2 43 | public Animal(){ 44 | 45 | } 46 | 47 | // 内部类 48 | class InnerlTest{ 49 | public String getClassInfo(){ 50 | return "内部类的getInfo方法"; 51 | } 52 | } 53 | 54 | public InnerlTest getInnerTestInstance(){ 55 | return new InnerlTest(); 56 | } 57 | 58 | // 匿名内部类 59 | public String getAnoymousClass(){ 60 | Animal dog = new Animal("dog", 4){ 61 | // 重写getName方法 62 | public String getAnimalInfo(){ 63 | return "匿名类重写getAnimalInfo()\n" + "名字:" + super.name + "\n" + "年龄:" + Integer.toString(super.age); 64 | } 65 | }; 66 | return dog.getAnimalInfo(); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /app/java/com/example/testfrida/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.testfrida; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.widget.Button; 8 | import android.widget.Toast; 9 | 10 | 11 | public class MainActivity extends AppCompatActivity implements View.OnClickListener{ 12 | Button toastButton; 13 | Button commonButton; 14 | Button constructorButton; 15 | Button innerButton; 16 | Button anonymousButton; 17 | Button privateButton; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_main); 23 | 24 | toastButton = findViewById(R.id.toast_button); 25 | commonButton = findViewById(R.id.common_button); 26 | constructorButton = findViewById(R.id.constructor_button); 27 | innerButton = findViewById(R.id.inner_button); 28 | anonymousButton = findViewById(R.id.anonymous_button); 29 | privateButton = findViewById(R.id.private_button); 30 | 31 | toastButton.setOnClickListener(this); 32 | commonButton.setOnClickListener(this); 33 | constructorButton.setOnClickListener(this); 34 | innerButton.setOnClickListener(this); 35 | anonymousButton.setOnClickListener(this); 36 | privateButton.setOnClickListener(this); 37 | 38 | } 39 | 40 | @Override 41 | public void onClick(View v) { 42 | switch (v.getId()){ 43 | case R.id.toast_button: 44 | Toast.makeText(MainActivity.this, "我是主线程的Toast", Toast.LENGTH_LONG).show(); 45 | break; 46 | case R.id.common_button: 47 | Toast.makeText(MainActivity.this, "普通方法\n" + commonFunction(), Toast.LENGTH_LONG).show(); 48 | break; 49 | case R.id.constructor_button: 50 | Toast.makeText(MainActivity.this, "构造方法\n" + constructorFunction(), Toast.LENGTH_LONG).show(); 51 | break; 52 | case R.id.inner_button: 53 | Toast.makeText(MainActivity.this, "内部类\n" + innerClassFunction(), Toast.LENGTH_LONG).show(); 54 | break; 55 | case R.id.anonymous_button: 56 | Toast.makeText(MainActivity.this, "匿名类内部类\n" + anonymousClassFunction(), Toast.LENGTH_LONG).show(); 57 | break; 58 | case R.id.private_button: 59 | Toast.makeText(MainActivity.this, "私有属性\n" + getPrivte(), Toast.LENGTH_LONG).show(); 60 | break; 61 | default: 62 | break; 63 | } 64 | } 65 | 66 | public String commonFunction(){ 67 | Animal animal = new Animal("cat", 1); 68 | // 调用类的普通方法 69 | return "动物信息\n" + animal.getAnimalInfo(); 70 | } 71 | 72 | public String constructorFunction(){ 73 | // 调用构造器 74 | Animal animal = new Animal("duck", 2); 75 | return "动物信息\n" + animal.getAnimalInfo(); 76 | } 77 | 78 | public String innerClassFunction(){ 79 | Animal animal = new Animal(); 80 | Animal.InnerlTest innerlTest = animal.getInnerTestInstance(); 81 | return innerlTest.getClassInfo(); 82 | } 83 | 84 | public String anonymousClassFunction(){ 85 | Animal animal = new Animal(); 86 | return animal.getAnoymousClass(); 87 | } 88 | 89 | public String getPrivte(){ 90 | Animal animal = new Animal("penguin", 5); 91 | int age = animal.getAge(); // 专为 hook 使用 92 | return "私有属性无法查看哟!\n" + "getAge() = " + Integer.toString(age); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |