Frida Labs全解
Frida labs 参数解释 1 2 3 4 5 -U 指定USB 设备 -f 指定app包名,通过spawn方式启动(就是重新启动的意思) -P 指定APP 的pid --no-pause 不暂停 -l 加载hook脚本
0x1 Hook修改被调用的方法的逻辑,返回值,传入参数 使用随机数,对输入进行判断,如果输入数的满足一个表达式,就会打印出flag
于是这里就有两种思路了。第一种是 hook get_random 函数,让该函数的返回值变成一个固定的值,从而计算表达式的值
第二种思路是 hook check 函数,直接将我们自己定义的值传入进去
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 ( ) { var MainActivity = Java .use ("com.ad2001.frida0x1.MainActivity" ); MainActivity .get_random .implementation = function ( ) { return 0 ; } }function hook2 ( ) { var MainActivity = Java .use ("com.ad2001.frida0x1.MainActivity" ); MainActivity .check .overload ('int' , 'int' ).implementation = function (a, b ) { console .log ("Origin i and i2 = " , a, b); return this .check (3 , b); } }function main ( ) { Java .perform (function ( ) { hook2 (); }) }setImmediate (main);
注入
1 frida -U -f com.ad2001 .frida0x1 -l hook.js
-U -f 缺一不可
为什么
缺少 -f
1 2 3 4 5 6 7 8 9 10 11 12 13 C:\Users\29660\Desktop\Frida\Frida-Labs-main\Frida-Labs-main\Frida 0x1>frida -U com.ad2001.frida0x1 -l hook.js ____ / _ | Frida 16.4.10 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit /quit -> Exit . . . . . . . . More info at https://frida.re/docs/home/ . . . . . . . . Connected to Android Emulator 5554 (id =emulator-5554) Failed to spawn: unable to find process with name 'com.ad2001.frida0x1'
Failed to spawn: unable to find process with name ‘com.ad2001.frida0x1’表示没有Frida 找不到名为 com.ad2001.frida0x1 的进程,详见lab02
缺少-U
1 2 3 4 5 6 7 8 9 10 11 12 13 C:\Users\29660\Desktop\Frida\Frida-Labs-main\Frida-Labs-main\Frida 0x1>frida -f com.ad2001.frida0x1 -l hook.js ____ / _ | Frida 16.4.10 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit /quit -> Exit . . . . . . . . More info at https://frida.re/docs/home/ . . . . . . . . Connected to Local System (id =local ) Failed to spawn: unable to find executable at 'com.ad2001.frida0x1'
说明 Frida 没有连接到 Android 模拟器 ,而是连接到了你本地电脑(Windows),所以它以为是要在本地运行一个叫 com.ad2001.frida0x1 的可执行文件。
0x2 Hook调用静态的未被调用的方法 可以看到这里定义了一个 get_flag 方法,但是并没有在主函数中被调用
使用 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 setImmediate (function ( ) { Java .perform (function ( ) { var a = Java .use ("com.ad2001.frida0x2.MainActivity" ) a.get_flag (4919 ); }) })function hook ( ) { var MainActivity = Java .use ("com.ad2001.frida0x2.MainActivity" ); MainActivity .get_flag (4919 ); }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main);
hook的时候使用如下命令有可能会hook不上
1 frida -U -f com.ad2001 .frida0x2 -l hook.js
原因是setImmediate(main);是程序启动后立即注入,可能会导致程序还没完全启动就注入了,所以hook不上
解决办法是使用
1 frida -U 'frida 0x2' -l hook.js
那么,为什么将frida 0x2写成包名com.ad2001.frida0x2就不行呢
1 frida -U com.ad2001 .frida0x2 -l hook.js
这个命令尝试的是:根据包名注入,默认是 spawn(启动)模式 。
实际上这条命令是尝试 spawn(启动)包名对应的 app ,但没写 -f,它又不是 attach 模式,就会失败。
或者将hook代码中的
setImmediate(main);改成
setTimeout(main,1000);延迟启动
1 2 3 4 5 6 7 8 9 10 11 12 13 function hook ( ) { var MainActivity = Java .use ("com.ad2001.frida0x2.MainActivity" ); MainActivity .get_flag (4919 ); }function main ( ) { Java .perform (function ( ) { hook (); }) }setTimeout (main, 1000 );
0x3 更改类中的静态变量 hook code的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hook ( ) { var a = Java .use ("com.ad2001.frida0x3.Checker" ) a.code .value = 512 ; }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main);
思考:
a是什么
a.code.value具体是什么意思
以下内容来自GPT
在这段 Frida 脚本中:
1 var a = Java .use ("com.ad2001.frida0x3.Checker" )
a 是什么?
👉 a 是一个 Frida 创建的 Java 类代理对象(Java class wrapper) ,你可以把它当成是一个“控制器”或者“代理”,用来访问和操作 Java 中的类 com.ad2001.frida0x3.Checker 的静态字段、静态方法、实例方法,甚至可以 hook 它的方法。
类比一下:
在 Java 代码中你可能会这样用一个类:
在 Frida 中,为了达到类似的效果,我们写:
1 2 var a = Java .use ("com.ad2001.frida0x3.Checker" ) a.code .value = 512 ;
更具体地说:
所以总结一下:
名称
解释
a
Frida 中的类包装对象,表示Checker类
a.code.value
表示访问并修改类中code这个静态字段的值
a.$new()
(可选)表示创建这个类的一个新实例
0x4 调用非MainActivity,非静态方法 MainActivity里没有任何东西
定义了一个Check类,里面有get_flag方法
想调用这个get_flag方法,必须实例化一个check类对象,在frida中可以用以下脚本实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function hook ( ) { var c = Java .use ("com.ad2001.frida0x4.Check" ); var c_check = c.$new(); var flag = c_check.get_flag (1337 ); console .log (flag); }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main);
运行结果如下
0x5 调用MainActivity中的非静态方法 在 0x2 中,我们使用frida调用了静态方法
但在本次,这是一个非静态方法
Java中静态方法可以直接调用,而非静态方法必须实例化一个对象之后才能调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Hello { public static void sayStatic ( ) { System .out .println ("我是静态方法" ); } public void sayInstance ( ) { System .out .println ("我是非静态方法" ); } public static void main (String [] args ) { Hello .sayStatic (); Hello h = new Hello (); h.sayInstance (); } }
我们直接用 0x2 的脚本hook一下
1 2 3 4 5 6 7 8 9 10 11 12 function hook ( ) { var MainActivity = Java .use ("com.ad2001.frida0x5.MainActivity" ); MainActivity .flag (1337 ); }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main);
哦豁,报错了
以下解释来自SWDD师傅
直接使用Frida创建MainActivity或任何Android组件可能会很棘手,因为Android的生命周期和线程规则。Android组件,如Activity子类,依赖于应用程序上下文进行正确运行。在Frida中,您可能缺少必要的上下文。Android UI组件通常需要具有关联Looper的特定线程。如果涉及UI任务,请确保在具有活动Looper的主线程上执行。活动是较大的Android应用程序生命周期的一部分。创建MainActivity的实例可能需要应用处于特定状态,并且通过Frida管理整个生命周期可能并不直接。总之,为MainActivity创建实例并不是一个好主意。
通俗点来说就是,
用Frida直接创建 MainActivity 或其他 Android 组件(比如 Activity)是很麻烦的,甚至是不可行的。
原因是这样的:
Activity 需要上下文(Context)才能正常工作 :就像你造房子要有地基一样,Activity 这种组件运行的时候,需要依赖应用程序提供的运行环境(就是 Context)。在 Frida 里,你是“旁观者”,而不是“房子的主人”,所以没法轻松提供这个环境。
UI 相关的代码必须在主线程(带 Looper 的线程)里跑 :Android 要求所有跟界面有关的操作都得在“主线程”里运行,这个线程有个叫 Looper 的消息循环系统。在 Frida 里,如果你随便在哪个线程里跑 UI 代码,程序很可能直接崩掉。
Activity 的生命周期很复杂 :系统会自动帮 Activity 调用一系列方法,比如 onCreate、onStart、onResume 等。这一整套流程是系统控制的。如果你自己用 Frida 去“强行”创建一个 Activity 实例,系统其实并不知道它存在,所以它无法正常工作。
那么应该怎么办呢
SWDD说
当Android应用程序启动时,系统会创建MainActivity的一个实例(或AndroidManifest.xml文件中指定的启动器活动)。创建MainActivity实例是Android应用程序生命周期的一部分。因此,我们可以使用frida获取MainActivity的实例,然后调用flag()方法来获取我们的标志。
让我们看看GPT怎么说
📱 正常来说,Android 应用是怎么启动的?
当你点击某个 App 图标时,
Android 系统就会去看这个 App 的 AndroidManifest.xml 文件,
找到标注为「启动入口」的 Activity(比如 MainActivity),
然后系统会自动创建这个 Activity 的实例 ,并调用它的一系列生命周期方法(比如 onCreate()、onResume() 等)。
🧠 那这段话的核心意思就是:
你自己不用 new 一个 MainActivity,系统已经帮你创建好了!
所以与其自己创建一个可能不完整的实例(会有很多问题),不如直接去“拿到”系统已经创建的那个实例,然后调用你想要的函数,比如 flag() 。
✅ 总结一下,通俗点说就是:
“Android 系统会自动帮你创建 MainActivity 的实例,所以你不需要自己造一个 。用 Frida 可以直接找到这个已经存在的实例,然后调用你想要的方法(比如 flag() )来拿 flag。”
有这样一个模板
1 2 3 4 5 6 7 8 Java .performNow (function ( ) { Java .choose ('<包名>.<类名>' , { onMatch : function (instance ) { }, onComplete : function ( ) {} }); });
onMatch
onMatch回调函数在Java.choose操作期间找到指定类的每个实例时执行。
这个回调函数接收当前实例作为它的参数。
您可以在onMatch回调中定义自定义操作,以在每个实例上执行。
function(instance) {},instance参数表示目标类的每个匹配实例。您可以使用任何其他名称。参数 instance 就是这个 MainActivity 的对象,你可以在这里调用它的函数,比如 instance.flag()
onComplete
onComplete回调在Java.choose操作完成后执行操作或清理任务。此块是可选的,如果您在搜索完成后不需要执行任何特定操作,则可以选择将其留空。
尝试使用该模板寻找MainActivity实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function hook ( ) { Java .choose ('com.ad2001.frida0x5.MainActivity' , { onMatch : function (instance ) { console .log ("Sucess found!!!" ); }, onComplete : function ( ) { console .log ("end" ); } }); }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main);
在这里还发现个问题,当使用如下命令启动frida时,只会打印一个 end ,hook不上,需要重新修改一下脚本(例如注释掉某一行再加上)
1 frida -U -f com.ad2001 .frida0x5 -l lab05.js
原因其实跟 0x2 一样,由于脚本是立即注入,app还来不及创建MainActivity实例,自然就没找到
解决办法也跟 0x2 一样,这里就不再赘述
0x6 MainActivity中的非静态方法且参数通过非MainActivity非静态方法传递 MainActivity中的非静态方法 get_flag ,并且参数是通过 Checker 类对象 A 来传递的
Checker类如下
所以这题就是0x5 0x4 0x3的结合,但是在 0x3 中,变量为静态变量,所以需要这里需要创建一个类的对象来对变量进行更改
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 function hook ( ) { Java .choose ("com.ad2001.frida0x6.MainActivity" , { onMatch : function (MainActivity ) { var c = Java .use ("com.ad2001.frida0x6.Checker" ) var A = c.$new(); A.num1 .value = 1234 ; A.num2 .value = 4321 ; console .log ("success" ); MainActivity .get_flag (A); }, onComplete : function ( ) { console .log ("end" ); } }); }function main ( ) { Java .perform (function ( ) { hook (); }) }setTimeout (main, 1000 );
0x7 Hook构造函数 构造函数是 Java 类中的一种特殊方法,它的作用是:在创建对象的时候用来初始化对象的属性 。
先举个例子来了解一下森莫是Java中的构造函数
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 public class Dog { String name; int age; public Dog (String n, int a) { name = n; age = a; } public void bark () { System.out.println(name + " 汪汪叫!" ); } public void showInfo () { System.out.println("这只狗叫 " + name + ",它 " + age + " 岁了。" ); } public static void main (String[] args) { Dog d = new Dog ("小黑" , 3 ); d.bark(); d.showInfo(); } }
来看本题,调用了flag方法,并且通过Checker构造函数对 num1 和 num2 赋值为 123 和 321
在Frida中使用 $init 关键字来 hook 构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function hook ( ) { var Checker = Java .use ("com.ad2001.frida0x7.Checker" ); Checker .$init .implementation = function (a, b ) { console .log ("Origin num" , a, b); this .$init(666 , 666 ); console .log ("success" ); } }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main);
写到这我有点好奇如果使用 0x1 的方法直接去 hook 构造函数会怎样
来吧,实践出真知
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hook ( ) { var Ch = Java .use ("com.ad2001.frida0x7.Checker" ); Ch .Checker .overload ('int' , 'int' ).implementation = function (a, b ) { console .log ("Origin num" , a, b); return this .Checker (666 , 666 ); } }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main);
哈哈,报错了,cannot read property ‘overload’ of undefined 说明试图 hook 的 Java 方法并没有找到
然后我又好奇,如果使用 0x3 的方法直接改变 num1 和 num2 会怎样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function hook ( ) { var Checker = Java .use ("com.ad2001.frida0x7.Checker" ); Checker .$init .implementation = function (a, b ) { console .log ("Origin num" , a, b); Checker .num1 .value = 666 ; Checker .num2 .value = 666 ; console .log ("success" ); } }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main);
哈哈,果然又报错了 Error: Cannot access an instance field without an instance 表示试图访问 Checker.num1,但这是 实例字段(成员变量) ,你必须通过一个具体对象(也就是 this)来访问它
改成这样或许会比初代脚本好理解一些,跟 0x1 也更像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function hook ( ) { var Checker = Java .use ("com.ad2001.frida0x7.Checker" ); Checker .$init .overload ('int' , 'int' ).implementation = function (a, b ) { console .log ("Origin num:" , a, b); this .$init(a, b); this .num1 .value = 666 ; this .num2 .value = 666 ; console .log ("Success: num1 and num2 changed to 666" ); }; }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main);
0x8 Hook Native层中调用的函数并且读取传入的参数 从本章开始进入 Native层
模板如下
1 2 3 4 5 6 7 8 9 10 Interceptor .attach (targetAddress, { onEnter : function (args ) { console .log ('Entering ' + functionName); }, onLeave : function (retval ) { console .log ('Leaving ' + functionName); } });
它的作用是:
当程序运行到某个特定函数(地址为 targetAddress )时:
函数刚进入时(onEnter) :打印一句话,说“我进来了”;
函数执行完返回时(onLeave) :再打印一句“我出去了”。
每行的意思
1 Interceptor .attach (targetAddress, {
用 Frida 的 Interceptor.attach 来拦截(hook)某个函数地址 。这个地址是你想观察或修改的函数位置(targetAddress)。
1 2 3 4 onEnter : function (args ) { console .log ('Entering ' + functionName); },
当程序刚进入这个函数时,会执行 onEnter 这个回调。 你可以:
打印日志(像现在这样打印“正在进入函数”);
查看和修改传入参数(args 是参数列表)。
1 2 3 4 onLeave : function (retval ) { console .log ('Leaving ' + functionName); }
当函数执行完准备返回时,会执行 onLeave。 你可以:
打印返回值;
修改返回值(retval 是返回值)。
需要获取targetAddress我们可以使用如下API
Module.enumerateExports() 通过调用 Module.enumerateExports(),我们可以获取到导出函数的名称、地址以及其他相关信息。这些信息对于进行函数挂钩、函数跟踪或者调用其他函数都非常有用。
Module.getExportByName() 当我们知道要查找的导出项的名称但不知道其地址时,可以使用 Module.getExportByName()。通过提供导出项的名称作为参数,这个函数会返回与该名称对应的导出项的地址。
Module.findExportByName() 这与 Module.getExportByName() 是一样的。唯一的区别在于,如果未找到导出项,Module.getExportByName() 会引发异常,而 Module.findExportByName() 如果未找到导出项则返回 null
Module.getBaseAddress() 通过调用 Module.getBaseAddress() 函数,我们可以获取指定模块的基址地址,然后可以基于这个基址地址进行偏移计算,以定位模块内部的特定函数、变量或者数据结构
Module.enumerateImports() 通过调用 Module.enumerateImports() 函数,我们可以获取到指定模块导入的外部函数或变量的名称、地址以及其他相关信息。
使用 Module.enumerateImports(“libfrida0x8.so”) 查看导入表
使用Module.findExportByName(“libc.so”,”strcmp”);来获取strcmp的地址
来看这题
加载了一个 frida0x8
so文件里有一个 strcmp 函数,第二个参数 s2 就是经过处理后的正确的 flag ,所以我们需要把strcmp函数的第二个参数hook出来
我先按照我自己的想法写了一个脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function hook ( ) { var target = Module .findExportByName ("libc.so" , "strcmp" ); console .log ("strcmp addr is :" , target.toString (16 )); Interceptor .attach (target, { onEnter : function (args ) { console .log (Memory .readUtf8String (args[1 ])); }, onLeave : function (retval ) { } }) }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main);
输出如下
这里其实是因为,我以为我hook的是特定的某一个strcmp,实际上这个 hook 是生效于所有调用 libc.so!strcmp 的地方,无论是 native 调用还是通过 JNI 被 Java 调用的 native 方法,都会被拦截
所以导致了这种情况
所以要加一个判断,当strcmp的第一个参数包含某个数(例如666)时,就打印第二个参数的结果,使我们的目标更加“精确”
修改脚本如下
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 hook ( ) { var target = Module .findExportByName ("libc.so" , "strcmp" ); console .log ("strcmp addr is :" , target.toString (16 )); Interceptor .attach (target, { onEnter : function (args ) { var input = Memory .readUtf8String (args[0 ]); if (input.includes ("666" )) { console .log (Memory .readUtf8String (args[1 ])); } }, onLeave : function (retval ) { } }) }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main);
在 Frida 中:
你用的 Memory.readUtf8String() 是它最常用的函数之一,专门处理 C 字符串(以 null 结尾的 UTF-8 字符串)。
0x9 Hook Native层函数的返回值 check_flag的值如果是1337就打印flag
native层中的该函数的返回值是1
修改该函数的返回值即可
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 ( ) { var target = Module .enumerateExports ("liba0x9.so" )[0 ]["address" ]; console .log ("addr is :" , target); Interceptor .attach (target, { onEnter : function (args ) { }, onLeave : function (retval ) { console .log ("Origin retval is :" , retval); retval.replace (1337 ); } }) }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main);
思考:
这一句
1 var target = Module .enumerateExports ("liba0x9.so" )[0 ]["address" ];
在 0x8 中使用的是
1 var target = Module .findExportByName ("libc.so" , "strcmp" );
我的猜想是,在 0x8 中,我们使用
Module.enumerateImports(“libfrida0x8.so”)查看导入表,而在该题目中,由于hook的是自己的函数,所以需要使用Module.enumerateExports(“liba0x9.so”)查看导出表
导入表(Imports) : 是这个库 调用别人的函数 ,通常是系统函数,比如 libc.so 里的 malloc、printf、strlen 等。
导出表(Exports) : 是这个库 暴露给别人用的函数 ,很多时候是你自己写的,比如 Java_com_example_myapp_NativeMethod 这种 native 接口,或者一些插件调用的入口函数。
查看导出表
尝试改成跟 0x8 中差不多的写法
1 var target = Module .findExportByName ("liba0x9.so" , "Java_com_ad2001_a0x9_MainActivity_check_1flag" );
好吧这样也可以,实践出真知了
还是解释一下这一句吧
1 var target = Module .enumerateExports ("liba0x9.so" )[0 ]["address" ];
Module.enumerateExports("liba0x9.so") 这会列出 liba0x9.so 这个 native 库里导出的所有函数(也就是那些对外开放可以调用的函数),返回一个包含信息的数组,比如:
1 2 3 4 5 [ {name : "check_flag" , address : ptr ("0x12345678" ), type : "function" }, {name : "init" , address : ptr ("0x12345690" ), type : "function" }, ... ]
[0]["address"] 表示取这个导出函数列表里的第一个函数的地址 ,也就是 check_flag 的地址(假设第一个刚好是它)。这一行把这个地址赋值给 check_flag 变量。[0]表示索引
0xA Hook Native层未被调用的方法 从本章开始使用模拟器会闪退,这里使用的环境是Redmi K60 安卓14 已ROOT
可以使用如下模板
1 2 3 var native_adr = new NativePointer (<address_of_the_native_function>);const native_function = new NativeFunction (native_adr, '<return type>' , ['argument_data_type' ]);native_function (<arguments >);
让我们来逐行解释一下
1 var native_adr = new NativePointer (<address_of_the_native_function>);
这一句的意思是,我们知道一个原生函数在内存中的地址(比如 0x12345678),把它封装成一个 NativePointer 类型的对象,好让 Frida 能识别这个地址。<address_of_the_native_function> 是手动填进去的地址,通常是通过逆向分析(IDA、Frida hook 等)找到的函数地址。
1 const native_function = new NativeFunction (native_adr, '<return type>' , ['argument_data_type' ]);
这一行的意思是,把这个地址(native_adr)对应的函数转换成 JavaScript 能调用的函数。
<return type> 表示这个函数的返回值类型,比如 'int'、'void'、'pointer' 等。
['argument_data_type'] 表示这个函数的参数类型列表,比如 ['int', 'pointer']。
所以这一行的作用是:用 Frida 的 NativeFunction 包装原生函数,让你可以像普通 JS 函数那样去调用它。
1 native_function (<arguments >);
最后直接调用这个原生函数,传入你需要的参数(这些参数要和上面声明的参数类型匹配)
现在来看看例题
Java层并不能看到什么有用的,只是加载了stringFromJNI()
我们来看看native层,stringFromJNI也没什么有用的信息,但是有一个get_flag函数可以打印flag
于是思路就是,利用上面的模板 hook 出get_flag函数
模仿 0x9 中使用 Module.enumerateExports(“libfrida0xa.so”) 查看导出表,发现导出表非常之长,直接使用var target = Module.enumerateExports(“liba0x9.so”)[0][“address”];进行索引会有点麻烦,所以我们换一种方法
由于ALSR(地址随机化)的问题,所以我们先使用Module.findBaseAddress("libfrida0xa.so");查看一下基地址
可以构造如下循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var base_addr = Module .findBaseAddress ("libfrida0xa.so" ); var exports = Module .enumerateExports ("libfrida0xa.so" ); var target = null ;for (var i = 0 ; exports [i] != null ; i++){ if (exports [i]["name" ] == "_Z8get_flagii" ) { console .log ("function get_flag : " , exports [i]["address" ]); console .log ((exports [i]["address" ] - base_addr).toString (16 )); target = exports [i]["address" ]; } }
这里我们找到基地址后,遍历导出表,如果在导出表中找到了 get_flag 函数,就打印出 get_flag 函数的绝对地址
同时由于我们知道了绝对地址和基地址,于是我们就可以顺便把偏移地址也打印出来
偏移 = 函数地址 - 模块基地址
这样我们就找到了我们需要 hook 的目标函数的地址
这里还有个问题,为什么 get_flag 要写成 “_Z8get_flagii” 呢
这其实是C++ 的 name mangling(名字改编 / 名字重整)问题
什么是 Name Mangling?
在 C++ 中 ,函数名会被编译器自动改写 成一串带有额外信息的名字,称为 mangled name(重整名字) 。
这是因为 C++ 支持函数重载(同名函数,不同参数) ,而汇编语言、链接器、操作系统等底层东西并不支持这一特性。
举个例子:
1 2 int add (int a, int b) ;float add (float a, float b) ;
这两个函数在 C++ 中是合法的,因为它们参数不同。但如果你不做 name mangling,它们在汇编/符号表里都叫 add,系统就不知道你到底想调用哪个。
所以编译器会把它们改名:
1 2 int add (int , int ) float add (float , float )
这就是 name mangling。
由于我们主要研究frida 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 ( ) { var base_addr = Module .findBaseAddress ("libfrida0xa.so" ); var exports = Module .enumerateExports ("libfrida0xa.so" ); var target = null ; for (var i = 0 ; exports [i] != null ; i++){ if (exports [i]["name" ] == "_Z8get_flagii" ) { console .log ("function get_flag : " , exports [i]["address" ]); console .log ("base addr is:" , (exports [i]["address" ] - base_addr).toString (16 )); target = exports [i]["address" ]; } } var get_flag_ptr = new NativePointer (target); const My _get_flag = new NativeFunction (get_flag_ptr, 'char' , ['int' , 'int' ]); var flag = My _get_flag(1 , 2 ); console .log (flag); }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main)
由于打印出的是函数的返回值,而且so文件中使用的函数是__android_log_print,所以flag在logcat中可以看到
思考:能否直接在frida面板中打印出flag?
有的兄弟,有的
还记得lab0x8的内容吗,我们可以Hook __android_log_print这个函数,直接截获 flag
flag的内容是__android_log_print的第四个参数,所以我们直接读取它的第四个参数然后打印出来就行
完整hook并打印flag的脚本如下
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 function hook ( ) { var base_addr = Module .findBaseAddress ("libfrida0xa.so" ); var exports = Module .enumerateExports ("libfrida0xa.so" ); var target = null ; for (var i = 0 ; exports [i] != null ; i++){ if (exports [i]["name" ] == "_Z8get_flagii" ) { console .log ("function get_flag : " , exports [i]["address" ]); console .log ("base addr is:" , (exports [i]["address" ] - base_addr).toString (16 )); target = exports [i]["address" ]; } } var get_flag_ptr = new NativePointer (target); const My _get_flag = new NativeFunction (get_flag_ptr, 'char' , ['int' , 'int' ]); var print_addr = Module .findExportByName (null , "__android_log_print" ); Interceptor .attach (print_addr, { onEnter : function (args ) { var flagPtr = args[3 ]; var flag = flagPtr.readCString (); console .log (flag); }, onLeave : function (retval ) { } }); var flag = My _get_flag(1 , 2 ); console .log (flag); }function main ( ) { Java .perform (function ( ) { hook (); }) }setImmediate (main)
0xB 更改Native层的汇编指令 先来看x86下的模板
1 2 3 4 5 6 7 8 9 10 var writer = new X86Writer (opcodeaddr);Memory .protect (opcodeaddr, 0x1000 , "rwx" );try { writer.flush (); } finally { writer.dispose (); }
我们来解释一下
1 var writer = new X86Writer (opcodeaddr);
创建了一个写指令的“写手”对象,叫 writer。
X86Writer 是 Frida 提供的一个类,用来向指定内存地址写入 x86 汇编指令。
opcodeaddr 是你想修改的地址,比如某个函数开头或者中间的指令地址。
1 emory.protect (opcodeaddr, 0x1000 , "rwx" );
把从 opcodeaddr 开始的一段内存(大小为 0x1000 字节,也就是一页)设置为可读(r)、可写(w)、可执行(x)。
默认情况下,程序的代码区域一般是只读的;要想修改它,就得先把它“解锁”。
try块中我们可以插入要修改/添加的x86指令。X86Writer实例提供了各种方法来插入各种x86指令。
writer.flush();
插入指令后,调用flush方法将更改应用到内存中。这确保修改后的指令被写入内存位置。
1 finally { writer.dispose (); }
最后无论是否出错,都调用 dispose() 来释放资源。否则可能会造成内存泄漏
对于x86而言,我们可以查阅如下文档
JavaScript API | Frida • A world-class dynamic instrumentation toolkit
对于arm64而言,我们可以查阅如下文档
JavaScript API | Frida • A world-class dynamic instrumentation toolkit
接下来让我们看看例题,使用的架构为arm64架构
Java层依旧是什么都没有
native层中的MainActivity伪代码界面什么都没有,很明显这不正常
看看控制流,发现这里构成了一个永假跳转指令,导致我们的IDA反编译出错
那么,我们直接nop掉这个B.NE即可
IDA查看偏移地址是在0x15248处
开始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 function hook ( ) { var base_addr = Module .getBaseAddress ("libfrida0xb.so" ); console .log ("Base address : " , base_addr); var BNE_addr = base_addr.add (0x15248 ); Memory .protect (base_addr, 0x1000 , "rwx" ); var writer = new Arm64Writer (BNE_addr); try { writer.putNop (); writer.flush (); console .log ("Success!!" ); } finally { writer.dispose (); } }function main ( ) { Java .perform (function ( ) { hook (); }) }setTimeout (main, 1000 );
同样是需要到logcat中查看flag