各位观众,晚上好!我是今晚的讲师,江湖人称“代码老中医”。今天咱们聊聊Frida这玩意儿,看看它怎么像个孙悟空一样,钻到 JavaScript 和 Native 代码的肚子里,翻江倒海,修改内存,帮助我们做逆向工程。
准备好了吗?咱们开讲!
第一章:Frida 是个啥?凭啥这么横?
Frida,这货可不是你家厨房里的锅铲,而是一个强大的动态插桩工具包 (Dynamic Instrumentation Toolkit)。它可以让你在运行时干预应用程序的行为。想象一下,你可以在程序运行的时候,偷偷地“监听”它在干什么,甚至“篡改”它的想法,是不是很刺激?
Frida 之所以这么横,因为它有以下几个法宝:
- 跨平台: Windows、macOS、Linux、Android、iOS,想在哪儿“捣乱”就在哪儿“捣乱”。
- 多语言支持: JavaScript, Python, C,想用啥语言指挥它都行。
- 动态性: 不用重新编译应用程序,直接在运行时修改。
- 强大的 API: 提供了丰富的 API,方便你进行各种操作。
第二章:JavaScript Hook:让程序“吐真言”
JavaScript Hook,简单来说,就是拦截 JavaScript 函数的调用,在你自己的代码里“截胡”,可以查看函数的参数,修改返回值,甚至直接替换函数的实现。
- Hook 一个简单的 JavaScript 函数
假设我们有如下的网页 JavaScript 代码:
<!DOCTYPE html>
<html>
<head>
<title>JavaScript Hook Demo</title>
</head>
<body>
<script>
function calculate(a, b) {
var result = a + b;
console.log("Calculating: " + a + " + " + b + " = " + result);
return result;
}
</script>
<button onclick="calculate(5, 3)">Calculate</button>
</body>
</html>
现在我们想用 Frida Hook calculate
函数,在它被调用的时候,输出一些信息。
首先,我们需要一个 Frida 脚本 (JavaScript):
// hook_js.js
rpc.exports = {
hookCalculate: function() {
Java.perform(function() {
var WebView = Java.use("android.webkit.WebView"); //Android WebView
WebView.loadUrl.overload("java.lang.String").implementation = function(url){
console.log("WebView.loadUrl is called with: " + url);
this.loadUrl(url);
};
});
//For iOS use ObjC.choose
//ObjC.choose("UIWebView", {
// onMatch: function(instance){
// console.log("Found UIWebView: " + instance);
// instance.stringByEvaluatingJavaScriptFromString.implementation = function(jsCode){
// console.log("UIWebView execute javascript: " + jsCode);
// return this.stringByEvaluatingJavaScriptFromString(jsCode);
// };
// },
// onComplete: function(){}
//});
//Webkit Namespace in web worker
const calculate = Module.findExportByName(null, "calculate");
if(calculate){
Interceptor.attach(calculate,{
onEnter: function(args){
console.log("calculate is called");
console.log("a = " + args[0]);
console.log("b = " + args[1]);
},
onLeave: function(retval){
console.log("calculate return " + retval);
}
});
}else{
console.log("calculate is not found");
}
}
};
这个脚本做了什么?
Java.perform(function(){ ... });
: 在 Android WebView 里Hook JavaScriptWebView.loadUrl.overload("java.lang.String").implementation = function(url){ ... }
: Hook WebView 的loadUrl
方法,打印加载的 URL。ObjC.choose("UIWebView", { ... });
: 在 iOS UIWebView 里Hook JavaScriptinstance.stringByEvaluatingJavaScriptFromString.implementation = function(jsCode){ ... }
: Hook UIWebView 的stringByEvaluatingJavaScriptFromString
方法,打印执行的 JavaScript 代码。Module.findExportByName(null, "calculate")
: 查找全局作用域的calculate
函数。Interceptor.attach(calculate, { ... });
: Hookcalculate
函数。onEnter
: 在函数调用前执行,可以访问函数的参数。onLeave
: 在函数返回后执行,可以访问函数的返回值。
然后,用 Frida 连接到浏览器或 WebView,执行这个脚本:
frida -U -f com.android.chrome -l hook_js.js --no-pause
或者
frida -p <pid> -l hook_js.js
其中 -U
表示连接到 USB 设备,-f
表示启动应用程序,-l
表示加载 Frida 脚本,--no-pause
表示不暂停应用程序。<pid>
是应用程序的进程 ID。
当你点击网页上的 "Calculate" 按钮时,Frida 就会拦截 calculate
函数的调用,并在控制台输出相关信息。
- 修改 JavaScript 函数的返回值
除了“监听”,我们还可以修改 JavaScript 函数的返回值。比如,我们想让 calculate
函数永远返回 42:
// hook_js_modify.js
rpc.exports = {
hookCalculate: function() {
Java.perform(function() {
var WebView = Java.use("android.webkit.WebView");
WebView.loadUrl.overload("java.lang.String").implementation = function(url){
console.log("WebView.loadUrl is called with: " + url);
this.loadUrl(url);
};
});
//For iOS use ObjC.choose
//ObjC.choose("UIWebView", {
// onMatch: function(instance){
// console.log("Found UIWebView: " + instance);
// instance.stringByEvaluatingJavaScriptFromString.implementation = function(jsCode){
// console.log("UIWebView execute javascript: " + jsCode);
// return this.stringByEvaluatingJavaScriptFromString(jsCode);
// };
// },
// onComplete: function(){}
//});
const calculate = Module.findExportByName(null, "calculate");
if(calculate){
Interceptor.attach(calculate,{
onEnter: function(args){
console.log("calculate is called");
console.log("a = " + args[0]);
console.log("b = " + args[1]);
},
onLeave: function(retval){
console.log("calculate return " + retval);
retval.replace(42); // 修改返回值
}
});
}else{
console.log("calculate is not found");
}
}
};
注意 retval.replace(42);
这一行,它将 calculate
函数的返回值替换为 42。
- 替换 JavaScript 函数的实现
如果你觉得修改返回值还不够过瘾,可以直接替换 JavaScript 函数的实现。比如,我们想让 calculate
函数永远返回 a * b:
// hook_js_replace.js
rpc.exports = {
hookCalculate: function() {
Java.perform(function() {
var WebView = Java.use("android.webkit.WebView");
WebView.loadUrl.overload("java.lang.String").implementation = function(url){
console.log("WebView.loadUrl is called with: " + url);
this.loadUrl(url);
};
});
//For iOS use ObjC.choose
//ObjC.choose("UIWebView", {
// onMatch: function(instance){
// console.log("Found UIWebView: " + instance);
// instance.stringByEvaluatingJavaScriptFromString.implementation = function(jsCode){
// console.log("UIWebView execute javascript: " + jsCode);
// return this.stringByEvaluatingJavaScriptFromString(jsCode);
// };
// },
// onComplete: function(){}
//});
const calculate = Module.findExportByName(null, "calculate");
if(calculate){
Interceptor.replace(calculate, new NativeCallback(function (a, b) {
console.log("calculate is called");
console.log("a = " + a);
console.log("b = " + b);
var result = a * b;
console.log("calculate return " + result);
return result;
}, 'int', ['int', 'int']));
}else{
console.log("calculate is not found");
}
}
};
Interceptor.replace
函数可以将 calculate
函数的实现替换为我们自己的函数。 NativeCallback
创建一个原生回调函数,指定返回值类型和参数类型。
第三章:Native Hook:深入虎穴,直捣黄龙
Native Hook,就是 Hook Native 代码,也就是 C/C++ 代码。这部分比较硬核,需要对汇编语言和 Native 代码的结构有一定的了解。
- Hook 一个简单的 Native 函数
假设我们有一个简单的 C 函数:
// native.c
#include <stdio.h>
int add(int a, int b) {
int result = a + b;
printf("Adding: %d + %d = %dn", a, b, result);
return result;
}
我们将它编译成一个动态链接库 (so 文件) 并加载到程序中。现在我们想用 Frida Hook add
函数。
首先,我们需要找到 add
函数的地址。可以用 nm
命令查看 so 文件的符号表:
nm -D libnative.so | grep add
假设输出是 0000000000001149 T add
,那么 add
函数的地址就是 0x1149
(需要加上 so 文件的基地址)。
然后,编写 Frida 脚本:
// hook_native.js
rpc.exports = {
hookAdd: function(moduleName, offset) {
var baseAddress = Module.getBaseAddress(moduleName);
var addAddress = baseAddress.add(offset);
Interceptor.attach(addAddress, {
onEnter: function(args) {
console.log("add is called");
console.log("a = " + args[0]);
console.log("b = " + args[1]);
},
onLeave: function(retval) {
console.log("add return " + retval);
}
});
}
};
这个脚本做了什么?
Module.getBaseAddress(moduleName)
: 获取模块 (so 文件) 的基地址。baseAddress.add(offset)
: 计算add
函数的绝对地址。Interceptor.attach(addAddress, { ... });
: Hookadd
函数。
然后,用 Frida 连接到应用程序,执行这个脚本:
frida -U -f com.example.app -l hook_native.js --no-pause --eval 'rpc.exports.hookAdd("libnative.so", "0x1149")'
其中 com.example.app
是应用程序的包名,libnative.so
是 so 文件的名称,0x1149
是 add
函数在 so 文件中的偏移地址。
- 修改 Native 函数的参数和返回值
和 JavaScript Hook 类似,我们也可以修改 Native 函数的参数和返回值。比如,我们想让 add
函数的第一个参数永远是 10:
// hook_native_modify.js
rpc.exports = {
hookAdd: function(moduleName, offset) {
var baseAddress = Module.getBaseAddress(moduleName);
var addAddress = baseAddress.add(offset);
Interceptor.attach(addAddress, {
onEnter: function(args) {
console.log("add is called");
console.log("a = " + args[0]);
console.log("b = " + args[1]);
args[0].replace(10); // 修改第一个参数
},
onLeave: function(retval) {
console.log("add return " + retval);
}
});
}
};
args[0].replace(10);
将 add
函数的第一个参数替换为 10。
- 修改内存数据
Frida 不仅仅可以 Hook 函数,还可以直接修改内存中的数据。这在逆向工程中非常有用,比如修改游戏中的金币数量,修改程序的标志位等等。
假设我们想修改一个全局变量的值:
// native.c
#include <stdio.h>
int global_value = 100;
int main() {
printf("Global value: %dn", global_value);
return 0;
}
首先,我们需要找到 global_value
变量的地址。可以用 nm
命令查看:
nm -D libnative.so | grep global_value
假设输出是 0000000000004040 D global_value
,那么 global_value
变量的地址就是 0x4040
(需要加上 so 文件的基地址)。
然后,编写 Frida 脚本:
// hook_memory.js
rpc.exports = {
modifyGlobalValue: function(moduleName, offset, newValue) {
var baseAddress = Module.getBaseAddress(moduleName);
var globalValueAddress = baseAddress.add(offset);
Memory.writeS32(globalValueAddress, newValue); // 写入新的值
console.log("Global value modified to: " + newValue);
}
};
Memory.writeS32(globalValueAddress, newValue);
将 global_value
变量的值修改为 newValue
。 writeS32
表示写入一个 32 位整数,根据变量的类型选择合适的写入函数,例如 writeU8
(8 位无符号整数), writeS64
(64 位有符号整数) 等等。
第四章:一些实用技巧和注意事项
- 使用
try...catch
处理异常
在 Frida 脚本中,最好使用 try...catch
块来处理异常,防止脚本因为一个小错误而崩溃。
try {
// Some code that might throw an exception
} catch (e) {
console.error("Error: " + e);
}
- 使用
Java.perform
和ObjC.choose
在 Android 和 iOS 上 Hook
在 Android 上 Hook Java 代码,需要使用 Java.perform
函数。在 iOS 上 Hook Objective-C 代码,需要使用 ObjC.choose
函数。
-
查找函数地址的几种方法
nm
命令: 查看 so 文件的符号表。IDA Pro
等反汇编工具: 分析程序的代码,找到函数的地址。- 运行时搜索: 可以使用 Frida 的
Module.enumerateExports
和Module.findExportByName
函数来搜索函数。
-
注意代码的执行环境
Frida 脚本运行在 Frida 的 JavaScript 引擎中,和应用程序的 JavaScript 引擎可能有所不同。因此,有些 JavaScript 代码可能无法在 Frida 脚本中执行。
- 小心使用
Interceptor.replace
Interceptor.replace
函数会完全替换原始函数的实现,如果替换的代码有错误,可能会导致应用程序崩溃。
第五章:实战演练:破解一个简单的 CrackMe
咱们来个实战演练,破解一个简单的 CrackMe。假设我们有一个 Android CrackMe 程序,它会要求用户输入一个密码,如果密码正确,就会显示 "Congratulations!",否则显示 "Wrong password!"。
- 分析程序
用 APKTool
反编译 APK 文件,查看程序的代码。发现程序在 MainActivity.java
中检查密码:
// MainActivity.java
public class MainActivity extends AppCompatActivity {
private EditText passwordEditText;
private Button checkButton;
private TextView resultTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
passwordEditText = findViewById(R.id.password_edit_text);
checkButton = findViewById(R.id.check_button);
resultTextView = findViewById(R.id.result_text_view);
checkButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String password = passwordEditText.getText().toString();
if (checkPassword(password)) {
resultTextView.setText("Congratulations!");
} else {
resultTextView.setText("Wrong password!");
}
}
});
}
private native boolean checkPassword(String password);
static {
System.loadLibrary("native-lib");
}
}
checkPassword
函数是一个 Native 函数,在 native-lib.so
中实现。
- 编写 Frida 脚本
我们想 Hook checkPassword
函数,让它永远返回 true
:
// crackme.js
rpc.exports = {
crack: function() {
Java.perform(function() {
var MainActivity = Java.use("com.example.crackme.MainActivity"); // 替换为你的 MainActivity
MainActivity.checkPassword.implementation = function(password) {
console.log("checkPassword is called with: " + password);
return true; // 永远返回 true
};
});
}
};
这个脚本 Hook 了 MainActivity
的 checkPassword
函数,并将其实现替换为永远返回 true
的函数。
- 运行 Frida 脚本
frida -U -f com.example.crackme -l crackme.js --no-pause --eval 'rpc.exports.crack()'
运行程序,随便输入一个密码,点击 "Check" 按钮,就会显示 "Congratulations!"。
第六章:总结
今天我们一起学习了 Frida 的基本用法,包括 JavaScript Hook 和 Native Hook,以及如何修改内存数据。希望通过今天的学习,你能够掌握 Frida 的基本技巧,并在逆向工程的道路上越走越远。
记住,能力越大,责任越大。请合法使用 Frida,不要用它来做坏事哦!
好了,今天的讲座就到这里,谢谢大家!下课!