daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

frida-tutorial-2.md (11025B)


      1 ---
      2 title: "Frida Tutorial 2"
      3 section: "Mobile"
      4 sectionSlug: "mobile-pentesting"
      5 sourcePath: "src/mobile-pentesting/android-app-pentesting/frida-tutorial/frida-tutorial-2.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/mobile-pentesting/android-app-pentesting/frida-tutorial/frida-tutorial-2.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Frida Tutorial 2
     14 
     15 **This is a summary of the post**: [https://11x256.github.io/Frida-hooking-android-part-2/](https://11x256.github.io/Frida-hooking-android-part-2/) (Parts 2, 3 and 4)<sup>[[3]](#references)</sup>\
     16 **APKs and Source code**: [https://github.com/11x256/frida-android-examples](https://github.com/11x256/frida-android-examples)
     17 
     18 This part focuses on **overloaded methods**, **calling app code from your Frida script**, **keeping references to live Java objects**, and **using Python as a controller** for interactive instrumentation.
     19 
     20 ## Part 2
     21 
     22 Here you can see an example of how to **hook 2 functions with the same name** but different parameters.\
     23 Also, you are going to learn how to **call a function with your own parameters**.\
     24 And finally, there is an example of how to **find an instance of a class and make it call a function**.
     25 
     26 ```javascript
     27 // s2.js
     28 console.log("Script loaded successfully");
     29 
     30 Java.perform(function () {
     31   var MyActivity = Java.use("com.example.a11x256.frida_test.my_activity");
     32   var JString = Java.use("java.lang.String");
     33 
     34   var funInt = MyActivity.fun.overload("int", "int");
     35   funInt.implementation = function (x, y) {
     36     console.log("original call: fun(" + x + ", " + y + ")");
     37     return funInt.call(this, 2, 5);
     38   };
     39 
     40   var funString = MyActivity.fun.overload("java.lang.String");
     41   funString.implementation = function (x) {
     42     var myString = JString.$new("My TeSt String#####");
     43     console.log("Original arg: " + x);
     44     var ret = funString.call(this, myString);
     45     console.log("Return value: " + ret);
     46     return ret;
     47   };
     48 
     49   Java.choose("com.example.a11x256.frida_test.my_activity", {
     50     onMatch: function (instance) {
     51       console.log("Found instance: " + instance);
     52       console.log("Result of secret func: " + instance.secret());
     53     },
     54     onComplete: function () {},
     55   });
     56 });
     57 ```
     58 
     59 To create a `String`, this script first references `java.lang.String` and then creates a new object with `$new()`. That is the correct way to instantiate Java objects from Frida. In this specific case, passing a plain JavaScript string like `funString.call(this, "hey there!")` also works because Frida will coerce it to `java.lang.String`.
     60 
     61 ### Python
     62 
     63 ```python
     64 # loader.py
     65 import frida
     66 import time
     67 
     68 with open("s2.js", "r", encoding="utf-8") as f:
     69     jscode = f.read()
     70 
     71 device = frida.get_usb_device(timeout=5)
     72 pid = device.spawn(["com.example.a11x256.frida_test"])
     73 device.resume(pid)
     74 time.sleep(1)  # Without it Java.perform may run before ART is ready
     75 session = device.attach(pid)
     76 script = session.create_script(jscode)
     77 script.load()
     78 input()
     79 ```
     80 
     81 ```bash
     82 python3 loader.py
     83 ```
     84 
     85 ### Keeping instances after `Java.choose`
     86 
     87 `Java.choose()` gives you a live wrapper during the callback. If you want to keep using that object later, retain it explicitly:
     88 
     89 ```javascript
     90 var cached = null;
     91 
     92 Java.perform(function () {
     93   Java.choose("com.example.a11x256.frida_test.my_activity", {
     94     onMatch: function (instance) {
     95       cached = Java.retain(instance);
     96       console.log("Retained instance: " + cached);
     97       return "stop";
     98     },
     99     onComplete: function () {},
    100   });
    101 });
    102 ```
    103 
    104 This matters when you want to call the same instance later from `rpc.exports`, timers, or another hook without re-scanning the heap each time.
    105 
    106 ### Classes loaded by custom `ClassLoader`s
    107 
    108 On recent Android apps, especially apps using **dynamic feature modules**, **plugin frameworks**, or **packed/encrypted code**, `Java.use()` may fail with `ClassNotFoundException` even though the class exists. In that case enumerate the available class loaders and use the correct one through a dedicated `Java.ClassFactory`:<sup>[[1]](#references)</sup>
    109 
    110 ```javascript
    111 Java.perform(function () {
    112   var targetLoader = null;
    113 
    114   Java.enumerateClassLoaders({
    115     onMatch: function (loader) {
    116       try {
    117         if (loader.findClass("com.example.a11x256.frida_test.my_activity")) {
    118           targetLoader = loader;
    119           console.log("Found loader: " + loader);
    120         }
    121       } catch (e) {}
    122     },
    123     onComplete: function () {},
    124   });
    125 
    126   if (targetLoader !== null) {
    127     var factory = Java.ClassFactory.get(targetLoader);
    128     var MyActivity = factory.use("com.example.a11x256.frida_test.my_activity");
    129 
    130     factory.choose("com.example.a11x256.frida_test.my_activity", {
    131       onMatch: function (instance) {
    132         console.log("Instance from alternate loader: " + instance);
    133       },
    134       onComplete: function () {},
    135     });
    136   }
    137 });
    138 ```
    139 
    140 If you already know the app loads sensitive code late, this pattern is usually more reliable than retrying `Java.use()` in a loop.
    141 
    142 ## Part 3
    143 
    144 ### Python
    145 
    146 Now you are going to see how to send commands to the hooked app via Python and use **Frida RPC exports** to call JavaScript functions:
    147 
    148 ```python
    149 # loader.py
    150 import time
    151 import frida
    152 
    153 
    154 def on_message(message, payload):
    155     print(message)
    156     if payload is not None:
    157         print(payload)
    158 
    159 
    160 with open("s3.js", "r", encoding="utf-8") as f:
    161     jscode = f.read()
    162 
    163 device = frida.get_usb_device(timeout=5)
    164 pid = device.spawn(["com.example.a11x256.frida_test"])
    165 device.resume(pid)
    166 time.sleep(1)
    167 session = device.attach(pid)
    168 script = session.create_script(jscode)
    169 script.on("message", on_message)
    170 script.load()
    171 api = script.exports_sync
    172 
    173 while True:
    174     command = input(
    175         "Enter command:\n1: Exit\n2: Call secret function\n3: Hook Secret\nchoice: "
    176     ).strip()
    177     if command == "1":
    178         break
    179     if command == "2":
    180         api.callsecretfunction()
    181     elif command == "3":
    182         api.hooksecretfunction()
    183 ```
    184 
    185 The command `1` will **exit**, the command `2` will **find an instance of the class and call the private function** `secret()`, and command `3` will **hook** the function `secret()` so it **returns** a **different string**.
    186 
    187 If you call `2` first, you will get the **real secret**. If you call `3` and then `2`, you will get the **fake secret**.
    188 
    189 ### JS
    190 
    191 ```javascript
    192 console.log("Script loaded successfully");
    193 var instancesArray = [];
    194 
    195 function callSecretFun() {
    196   Java.perform(function () {
    197     if (instancesArray.length === 0) {
    198       Java.choose("com.example.a11x256.frida_test.my_activity", {
    199         onMatch: function (instance) {
    200           instancesArray.push(Java.retain(instance));
    201           console.log("Found instance: " + instance);
    202           console.log("Result of secret func: " + instance.secret());
    203           return "stop";
    204         },
    205         onComplete: function () {},
    206       });
    207     } else {
    208       console.log("Result of secret func: " + instancesArray[0].secret());
    209     }
    210   });
    211 }
    212 
    213 function hookSecret() {
    214   Java.perform(function () {
    215     var MyActivity = Java.use("com.example.a11x256.frida_test.my_activity");
    216     var JString = Java.use("java.lang.String");
    217     var secret = MyActivity.secret.overload();
    218 
    219     secret.implementation = function () {
    220       return JString.$new("TE ENGANNNNEEE");
    221     };
    222   });
    223 }
    224 
    225 rpc.exports = {
    226   callsecretfunction: callSecretFun,
    227   hooksecretfunction: hookSecret,
    228 };
    229 ```
    230 
    231 In Python, exported JavaScript methods are easier to call through `script.exports_sync`. For example, an export named `enumerateModules` becomes `script.exports_sync.enumerate_modules()`.
    232 
    233 ## Part 4
    234 
    235 Here you will see how to make **Python and JS interact** using JSON objects. JS uses the `send()` function to send data to the Python client, and Python uses `post()` to send a JSON object back to the JS script. The **JS will block the execution** until it receives a response from Python.
    236 
    237 ### Python
    238 
    239 ```python
    240 # loader.py
    241 import base64
    242 import time
    243 import frida
    244 
    245 
    246 def on_message(message, payload):
    247     print(message)
    248     if message.get("type") != "send":
    249         return
    250 
    251     encoded = message["payload"].split(":", 1)[1].strip()
    252     user, password = base64.b64decode(encoded).decode().split(":", 1)
    253     new_data = base64.b64encode(f"admin:{password}".encode()).decode()
    254     script.post({"type": "input", "payload": {"my_data": new_data}})
    255     print(f"Modified data sent for user {user}")
    256 
    257 
    258 with open("s4.js", "r", encoding="utf-8") as f:
    259     jscode = f.read()
    260 
    261 device = frida.get_usb_device(timeout=5)
    262 pid = device.spawn(["com.example.a11x256.frida_test"])
    263 device.resume(pid)
    264 time.sleep(1)
    265 session = device.attach(pid)
    266 script = session.create_script(jscode)
    267 script.on("message", on_message)
    268 script.load()
    269 input()
    270 ```
    271 
    272 ### JS
    273 
    274 ```javascript
    275 console.log("Script loaded successfully");
    276 
    277 Java.perform(function () {
    278   var TextView = Java.use("android.widget.TextView");
    279   var setText = TextView.setText.overload("java.lang.CharSequence");
    280 
    281   setText.implementation = function (x) {
    282     var outgoing = x.toString();
    283     var incoming = outgoing;
    284 
    285     send("Candidate text: " + outgoing);
    286     recv("input", function (message) {
    287       incoming = message.payload.my_data;
    288     }).wait();
    289 
    290     console.log("Final string_to_recv: " + incoming);
    291     return setText.call(this, incoming);
    292   };
    293 });
    294 ```
    295 
    296 `recv()` handlers receive **one message** and must be registered again for the next one. Using `.wait()` blocks the current hooked thread until Python replies, so keep this pattern for cases where you really need an inline decision before the original method continues.
    297 
    298 ## Modern Frida Notes
    299 
    300 - These examples still work as plain scripts loaded through `frida`, `frida-python`, or `frida-trace`.
    301 - If you migrate them to a **Frida 17+ agent project** built with `frida-create`/`frida-compile`, import the Java bridge explicitly with `import Java from "frida-java-bridge"`.<sup>[[2]](#references)</sup>
    302 - Frida 17.1.4 bumped `frida-java-bridge` to `7.0.3` in internal Android agents, adding **Android 16** support. If heap scans or Java hooks behave strangely on very recent Android versions, first verify that **frida-tools**, **frida-python**, and **frida-server/gadget** are on matching recent versions.
    303 - For **anti-Frida**, **root detection**, and **SSL pinning** bypasses, keep that content in the dedicated page:
    304 
    305 [Android Anti Instrumentation And Ssl Pinning Bypass](/hacktricks/mobile-pentesting/android-app-pentesting/android-anti-instrumentation-and-ssl-pinning-bypass)
    306 
    307 There is a part 5 that is not explained here because it doesn't add anything substantially new. If you want to read it, it is here: [https://11x256.github.io/Frida-hooking-android-part-5/](https://11x256.github.io/Frida-hooking-android-part-5/)
    308 
    309 ## References
    310 
    311 - [1] [Frida JavaScript API](https://frida.re/docs/javascript-api/)
    312 - [2] [Frida 17.0.0 Released](https://frida.re/news/2025/05/17/frida-17-0-0-released/)
    313 - [3] [11x256.github.io - Frida Hooking Android Part 2](https://11x256.github.io/Frida-hooking-android-part-2)