├── 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 | 
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 | 
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 |
14 |
19 |
24 |
29 |
34 |
39 |
40 |
--------------------------------------------------------------------------------
/app/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | TestFrida
3 | Toast
4 | 普通函数
5 | 构造函数
6 | 内部类
7 | 匿名内部类
8 | 私有属性
9 |
10 |
--------------------------------------------------------------------------------
/hook_sript/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/hook_sript/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/hook_sript/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/hook_sript/.idea/testhook.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/hook_sript/.idea/workspace.xml:
--------------------------------------------------------------------------------
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 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | 1609380083400
166 |
167 |
168 | 1609380083400
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
--------------------------------------------------------------------------------
/hook_sript/anony_inner_class.js:
--------------------------------------------------------------------------------
1 | Java.perform(function (){
2 | send("start hook...");
3 | var AnoyClass = Java.use("com.example.testfrida.Animal$1");
4 |
5 | AnoyClass.getAnimalInfo.implementation = function (){
6 | return "hello,frida"; // 修改函数返回值
7 | };
8 |
9 | /*
10 | var Animal = Java.use("com.example.testfrida.Animal");
11 | Animal.$init.overload("java.lang.String", "int").implementation = function (){
12 | send("constructor called from " + this.$className);
13 | const NewAnimal = Java.use(this.$className);
14 | NewAnimal.getAnimalInfo.implementation = function (){
15 | return "hello, frida";
16 | };
17 | };
18 | */
19 | });
--------------------------------------------------------------------------------
/hook_sript/common_function.js:
--------------------------------------------------------------------------------
1 | Java.perform(function (){
2 | send("start hook...");
3 | var Animal = Java.use("com.example.testfrida.Animal");
4 |
5 | Animal.getAnimalInfo.implementation = function (){
6 | send("hijack getAnimalInfo");
7 | return "hello, frida!";
8 | };
9 | });
10 |
--------------------------------------------------------------------------------
/hook_sript/constructor.js:
--------------------------------------------------------------------------------
1 | Java.perform(function (){
2 | send("start hook...");
3 | var Animal = Java.use("com.example.testfrida.Animal");
4 |
5 | Animal.$init.overload("java.lang.String", "int").implementation = function (){
6 | send("hijack Animal()");
7 | send("参数1:" + arguments[0]);
8 | send("参数2:" + arguments[1]);
9 | return this.$init("frida", 999); // 修改
10 | };
11 | });
--------------------------------------------------------------------------------
/hook_sript/inner_class.js:
--------------------------------------------------------------------------------
1 | Java.perform(function (){
2 | send("start hook...");
3 | var InnerTest = Java.use("com.example.testfrida.Animal$InnerlTest");
4 |
5 | InnerTest.getClassInfo.implementation = function (){
6 | send("hijack inner Class");
7 | return "hello, frida!";
8 | }
9 | });
--------------------------------------------------------------------------------
/hook_sript/main.py:
--------------------------------------------------------------------------------
1 | import frida
2 | import sys
3 | import time
4 |
5 | def read_js(file):
6 | with open(file, encoding='UTF-8') as fp:
7 | return fp.read()
8 |
9 | def on_message(message, data):
10 | if message["type"] == "send":
11 | print("[+] {}".format(message["payload"]))
12 | else:
13 | print("[-] {}".format(message))
14 |
15 | '''
16 | # 运行时hook
17 | remote_device = frida.get_usb_device()
18 | print(remote_device)
19 | session = remote_device.attach("com.example.testfrida")
20 |
21 | '''
22 | # spawn hook
23 | device = frida.get_usb_device()
24 | pid = device.spawn(["com.example.testfrida"])
25 | device.resume(pid)
26 | time.sleep(1) #Without it Java.perform silently fails
27 | session = device.attach(pid)
28 |
29 | src = read_js("./common_function.js")
30 | script = session.create_script(src)
31 | script.on("message", on_message)
32 | script.load()
33 | sys.stdin.read()
--------------------------------------------------------------------------------
/hook_sript/property.js:
--------------------------------------------------------------------------------
1 | Java.perform(function (){
2 | send("start hook...");
3 | var Animal = Java.use("com.example.testfrida.Animal");
4 |
5 | Animal.getAge.implementation = function (){
6 | send("obtain key");
7 |
8 | // 直接调用类中的函数
9 | send("call public function >> getName(): " + this.getName());
10 | send("call private function >> getKey(): " + this.getKey());
11 | // 直接调用类中的私有属性
12 | send("call private property >> name: " + this.name.value);
13 | send("call private property >> age: " + this.age.value);
14 | send("call private property >> key: " + this.key.value);
15 |
16 | return 9999;
17 | };
18 | });
--------------------------------------------------------------------------------
/pictures/1-普通函数-hook.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/1-普通函数-hook.gif
--------------------------------------------------------------------------------
/pictures/1-普通函数.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/1-普通函数.gif
--------------------------------------------------------------------------------
/pictures/2-构造函数-hook.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/2-构造函数-hook.gif
--------------------------------------------------------------------------------
/pictures/2-构造函数.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/2-构造函数.gif
--------------------------------------------------------------------------------
/pictures/3-内部类-hook.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/3-内部类-hook.gif
--------------------------------------------------------------------------------
/pictures/3-内部类.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/3-内部类.gif
--------------------------------------------------------------------------------
/pictures/4-匿名内部类-hook.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/4-匿名内部类-hook.gif
--------------------------------------------------------------------------------
/pictures/4-匿名内部类.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/4-匿名内部类.gif
--------------------------------------------------------------------------------
/pictures/5-私有属性-hook.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/5-私有属性-hook.gif
--------------------------------------------------------------------------------
/pictures/5-私有属性.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/5-私有属性.gif
--------------------------------------------------------------------------------
/pictures/BLE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/BLE.png
--------------------------------------------------------------------------------
/pictures/Screenshot_20201230_140155_cn.wch.bledemo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/Screenshot_20201230_140155_cn.wch.bledemo.jpg
--------------------------------------------------------------------------------
/pictures/innerclass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secnotes/FridaHookAndroid/fb2dfdc7054a6580867b3f2af3ca0ff30223b148/pictures/innerclass.png
--------------------------------------------------------------------------------