解释 `Frida` (`Dynamic Instrumentation Toolkit`) 如何 Hook `JavaScript` `Native Functions` 和修改内存以进行逆向工程。

各位观众,晚上好!我是今晚的讲师,江湖人称“代码老中医”。今天咱们聊聊Frida这玩意儿,看看它怎么像个孙悟空一样,钻到 JavaScript 和 Native 代码的肚子里,翻江倒海,修改内存,帮助我们做逆向工程。

准备好了吗?咱们开讲!

第一章:Frida 是个啥?凭啥这么横?

Frida,这货可不是你家厨房里的锅铲,而是一个强大的动态插桩工具包 (Dynamic Instrumentation Toolkit)。它可以让你在运行时干预应用程序的行为。想象一下,你可以在程序运行的时候,偷偷地“监听”它在干什么,甚至“篡改”它的想法,是不是很刺激?

Frida 之所以这么横,因为它有以下几个法宝:

  • 跨平台: Windows、macOS、Linux、Android、iOS,想在哪儿“捣乱”就在哪儿“捣乱”。
  • 多语言支持: JavaScript, Python, C,想用啥语言指挥它都行。
  • 动态性: 不用重新编译应用程序,直接在运行时修改。
  • 强大的 API: 提供了丰富的 API,方便你进行各种操作。

第二章:JavaScript Hook:让程序“吐真言”

JavaScript Hook,简单来说,就是拦截 JavaScript 函数的调用,在你自己的代码里“截胡”,可以查看函数的参数,修改返回值,甚至直接替换函数的实现。

  1. 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 JavaScript
  • WebView.loadUrl.overload("java.lang.String").implementation = function(url){ ... }: Hook WebView 的 loadUrl 方法,打印加载的 URL。
  • ObjC.choose("UIWebView", { ... });: 在 iOS UIWebView 里Hook JavaScript
  • instance.stringByEvaluatingJavaScriptFromString.implementation = function(jsCode){ ... }: Hook UIWebView 的 stringByEvaluatingJavaScriptFromString 方法,打印执行的 JavaScript 代码。
  • Module.findExportByName(null, "calculate"): 查找全局作用域的 calculate 函数。
  • Interceptor.attach(calculate, { ... });: Hook calculate 函数。
    • 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 函数的调用,并在控制台输出相关信息。

  1. 修改 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。

  1. 替换 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 代码的结构有一定的了解。

  1. 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, { ... });: Hook add 函数。

然后,用 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 文件的名称,0x1149add 函数在 so 文件中的偏移地址。

  1. 修改 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。

  1. 修改内存数据

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 变量的值修改为 newValuewriteS32 表示写入一个 32 位整数,根据变量的类型选择合适的写入函数,例如 writeU8 (8 位无符号整数), writeS64 (64 位有符号整数) 等等。

第四章:一些实用技巧和注意事项

  1. 使用 try...catch 处理异常

在 Frida 脚本中,最好使用 try...catch 块来处理异常,防止脚本因为一个小错误而崩溃。

try {
  // Some code that might throw an exception
} catch (e) {
  console.error("Error: " + e);
}
  1. 使用 Java.performObjC.choose 在 Android 和 iOS 上 Hook

在 Android 上 Hook Java 代码,需要使用 Java.perform 函数。在 iOS 上 Hook Objective-C 代码,需要使用 ObjC.choose 函数。

  1. 查找函数地址的几种方法

    • nm 命令: 查看 so 文件的符号表。
    • IDA Pro 等反汇编工具: 分析程序的代码,找到函数的地址。
    • 运行时搜索: 可以使用 Frida 的 Module.enumerateExportsModule.findExportByName 函数来搜索函数。
  2. 注意代码的执行环境

Frida 脚本运行在 Frida 的 JavaScript 引擎中,和应用程序的 JavaScript 引擎可能有所不同。因此,有些 JavaScript 代码可能无法在 Frida 脚本中执行。

  1. 小心使用 Interceptor.replace

Interceptor.replace 函数会完全替换原始函数的实现,如果替换的代码有错误,可能会导致应用程序崩溃。

第五章:实战演练:破解一个简单的 CrackMe

咱们来个实战演练,破解一个简单的 CrackMe。假设我们有一个 Android CrackMe 程序,它会要求用户输入一个密码,如果密码正确,就会显示 "Congratulations!",否则显示 "Wrong password!"。

  1. 分析程序

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 中实现。

  1. 编写 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 了 MainActivitycheckPassword 函数,并将其实现替换为永远返回 true 的函数。

  1. 运行 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,不要用它来做坏事哦!

好了,今天的讲座就到这里,谢谢大家!下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注