Mastering Frida: A Guide to Fixing Early Instrumentation in Gadget Mode

Introduction

One of the challenges faced in Frida’s Gadget mode is an issue where the process you’re injecting into resumes immediately after injecting the JavaScript. This behavior prevents early instrumentation, which can be crucial for certain analysis tasks. In this blog post, I will guide you through a workaround to fix this issue.

When it comes to dynamic analysis and instrumentation of applications, Frida stands out as an indispensable tool in the kit of developers, reverse engineers, and security researchers alike. Its Gadget mode, in particular, offers unparalleled flexibility, allowing users to inject custom scripts into their target applications seamlessly. However, there’s a snag in the system that’s been tripping up professionals—early instrumentation.

Imagine setting up a meticulous script, ready to unravel the inner workings from the very inception of a process only to find that Frida has already allowed the application to resume before your instrumentation could catch the critical initial functions. This challenge has been particularly pronounced with the latest release, version 16.1.11, posing a significant obstacle to those needing to hook functions at the outset of program execution.

This blog post is forged from the need for a solution—crafted for persistence, it’s a beacon of hope for those grappling with this issue. Herein, we’ll walk through the intricacies of Frida’s Gadget mode and unveil a strategic approach to modify the resume_on_attach flag, a crucial step in gaining the control you require for early instrumentation. By the end of this guide, you’ll have mastered the method to hold the execution reins, ensuring your scripts are primed to hook at the starting line.

Join me as we delve into this technical deep-dive, transforming a point of frustration into a triumph of technical prowess.

Choosing Gadget Mode over Server Mode

In gernal, Frida provides 2 modes for injection, Sever mode and Dadget mode. I am debug an ARM32 program on an embedded patlform. And I tested, the server mode is not very reliable , may because the limit RAM on the dev board, and the dev board may reboot after I start frida-server, It’s very annonying, So I select Gadget mode,and reboot issue goes away. Frida offers two distinct modes for script injection: Server Mode and Gadget Mode. When working with ARM32 programs on embedded platforms, one must consider system constraints. My experience revealed that Server Mode could be unstable, potentially due to the limited RAM available on the development board. This instability often resulted in the board rebooting after initiating frida-server, which was disruptive to the debugging process.

Given these challenges, I opted for Gadget Mode. This alternative proved to be more stable under the same conditions, eliminating the frustrating reboot issues. Gadget Mode’s lightweight footprint makes it better suited for environments with resource limitations, thereby providing a more seamless and reliable debugging experience for ARM32 embedded systems.

The Solution: Modifying resume_on_attach

The root of this issue lies within the default behavior of Frida’s Gadget mode, which is controlled by the resume_on_attach field of the ControlChannel class. This field dictates whether the process should resume immediately after Frida attaches to it.

Step by Step Fix

Here’s a step-by-step guide to addressing this issue:

1. Modifying the ControlChannel Class

Locate the ControlChannel class definition within the frida-core/lib/gadget/gadget.vala file. Search for the resume_on_attach field and change its default value to false.

1
2
3
4
class ControlChannel {
private bool resume_on_attach = false; // Change from true to false
// ...
}

2. Compile Frida with Modified Code

After making this change, recompile Frida to incorporate the modification. Ensure you have all required dependencies and follow the standard build instructions provided in the Frida documentation. This github repository provides docker images for compiling Frida.

3. Using the Modified libGadget.so

Replace the original libGadget.so with the newly compiled version. With this change, the process will no longer resume immediately, allowing your JavaScript to hook functions at the very start of the execution.

4. Utilizing Python Script for Process Resumption in Frida Gadget Mode

While working in Frida Gadget mode, a common approach to inject a JavaScript payload into the target process would involve using the frida command from the Frida-tools package, like so:

1
frida -H <Board IP address> -n Gadget -l <Javascript file>

Executing this command initiates the injection, but it doesn’t automatically resume the process once the JavaScript file is loaded. Attempts to resume the process manually with %resume in the Frida REPL might not yield success.

To overcome this hurdle, I developed a Python script—a more reliable solution. The script not only injects the JavaScript code but also effectively resumes the process afterwards. Below is the functional part of the script where the device.resume(pid); command is used to resume the process:

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
# Python script to inject JavaScript and resume process in Frida Gadget mode
import frida

# Establish a connection to the target device
device = frida.get_device_manager().add_remote_device("<Board IP address>")

# Specify the target process identifier (replace 'pid' with the actual process ID)
pid = 'pid'

# Path to the JavaScript file you want to inject
js_file_path = "<Javascript file>"

# Read the JavaScript code from the file
with open(js_file_path, 'r') as file:
js_code = file.read()

# Inject the JavaScript code into the process
session = device.attach(pid)
script = session.create_script(js_code)

# Load and execute the JavaScript code
script.load()

# Resume the process to continue its execution with the injected code
device.resume(pid)

print("JavaScript injected and process resumed.")

By employing this script, we ensure that the injection and process flow are controlled as expected, thus maintaining the stability of the debugging session.

Conclusion

This blog post addressed early instrumentation challenges in Frida’s Gadget mode and provided a solution by modifying the resume_on_attach flag. Gadget mode was recommended over Server mode for ARM32 programs on embedded platforms due to its stability. A Python script was introduced for injecting JavaScript and resuming the process, ensuring a controlled debugging session. Overall, this post offers a solution for fixing early instrumentation in Frida, empowering developers and researchers to gain insights into application behavior from the beginning.

An inlinehook library using Frida

This article introduces an inline hook library using Frida, and you can download all codes about this at this link. And I only implement it on ARM64 architecture.

Concepts

In my opinion, inlinehook just like put a breakpoint in debugger. Users can hook at any instructions, and executes any codes once the hook is triggered.

How to implement it

Basic steps

  • Put a jump code at the hook point, to jump to our trampoline code.
  • Trampoline code does the following:
    • Save current registers.
    • Invoke hook handler function.
    • Restore all registers.
    • Execute instructions at hook point.
    • Jump back to next instruction after the hook point.

Implementation using Frda

How to add hook

Before add inline hook, user should prepare the following things:

  • Hook point
    1
    const hook_ptr = <Address of the instruction will be hooked> ;
    We can add hooks at any instruction.
  • Hook handle function
    1
    2
    3
    const frida_fun = new NativeCallback(function(para1:NativePointer, sp:NativePointer){
    console.log('para1', para1, 'sp', sp);
    },'void',['pointer','pointer'])
    This function has two arguments, all are of type NativePointer. The first argument is named para1, and we can assign it when we add hook, the second argument is named sp, of value in the sp register, we can access values in the stack by this argument.
  • Memory for trampoline code
    1
    const trampoline_ptr = Memory.alloc(trampoline_len)
    We can allocate a new memory for trampoline code. And because allocated memory is usually far from the hook point, so we have to use a long jump instruction at the hook point. Long jump instruction usually occupies more bytes. For using near jump instruction, we can try to find a cave near the hook point, and use the found cave to store trampoline code.

    Warning: when we allocate memory, do not use a local variable for the returned pointer, use a global variable instead. Javascript’s garbage collection mechanism will free this memory automatically once the program is out of the local variable’s scope, and will crash the process.

  • Parameter1 pass to hook handle function
    1
    const para1 = <parameter1 pass to hook handle function>;
    We can pass a parameter to the hook handle function, and this parameter is of type NativePointer

Now invoke InlineHooker.inlineHookPatch to add a inline hook, just as follows

1
let sz = InlineHooker.inlineHookPatch(trampoline_ptr,hook_ptr, hook_fun_ptr, para1);

This function return a number, indicating the length of the trampoline code.

Code relocation

For inline hook, we need to move some instructions from one address to another. And we need to rewrite some instructions, just like B/BL instruction in ARM64 architecture.
I am using the code in this link for rewriting instructions. I port the C code in this file to Typescript code. This file is my ported code. ARM64 instruction set is relatively easy, and Thumb/Arm32 is too complex, we may port its code in the future.
This file exports this method to rewrite code

1
export let  sh_a64_rewrite=(buf:NativePointer, inst:number, pc:NativePointer ):number=>

This method has 3 arguments

  • buf, the target address of current instruction
  • inst, current instruction
  • pc, the source address of current instruction
    This method also return a variable of type number, to indicate how many instructions has written to target address. We actually need to rewrite several instructions for one B instruction to avoid change its behavior.

Conclusion

I wrote an inline hook library using Frida. Inline Hooking is not very easy, specially in some architecture, I only implemented on ARM64, and will do more work on other architecture.

Inline hook with Frida (Thumb version)

Interceptor.attach in Frida only can hook one function. So the first argument, target, should be a function address. In my opinion, inline hook just let users can hook arbitrary instruction at any address. In this article, I will try to introduce a method to implement inline hook using Frida. and only implement for Thumb binary so far.

Method

To implement inline hook, we need to do the following thing:

  • Save original bytes at hook target
  • Write a hook handle function in .so file in C/C++.
  • Write a trampoline code in Thumb. And put original bytes into trampoline code.
  • Put a BL instruction at hook target.

I will describe every stage in detail, and for convenience, I will user the following terminology:

Term Description
hook_ptr The address of hook target, same as the target argument in Interceptor.attach
hook_fun_ptr The address of hook handle function, we define this function in .so file
tempoline_ptr The address of trampoline code

So if we set every code correctly, once CPU reaches hook_ptr, CPU will call trampoline code as a function. And the trampoline code will call hook handle function and execute the original instruction copy from hook_ptr. And we can do anything we want in hook handle function.

Save original bytes at hook_ptr

We will put a BL instruction at this address, so we need to save original bytes at first. In Thumb, a BL instruction occupies 4 bytes. So we need to save at least 4 bytes. It may include 1-2 Thumb instructions at hook_ptr, and we must be careful in selection of a hook_ptr.

  • Avoid odd hook_ptr
  • Do not select a hook_ptr at a middle of a Thumb instruction
  • Do not select a hook_ptr at a 2 bytes Thumb instruction followed by a 4 bytes Thumb instruction

I dissembled libMyGame.so with the following command:

1
arm-linux-gnueabihf-objdump -S /tmp/libMyGame.so | tee /tmp/libMyGame.s

And, there are 2 files named libMyGame.so in this game. We should use libMyGame.so file in the path lib/armeabi-v7a. Because this file is according to Thumb version. I selected hook_ptr at 0x267782:

26777e: 9a03 ldr r2, [sp, #12]
267780: 6823 ldr r3, [r4, #0]
267782: 4630 mov r0, r6 @ here, our hook_ptr
267784: 429a cmp r2, r3
267786: d001 beq.n 26778c <_ZN7cocos2d11Application10getVersionEv@@Base+0x3c>

This hook_ptr is in the function cocos2d::Application::getVersion. So when call this getVersion function, just like we did in previous post, CPU should hit this hook_ptr. Now, we saved original bytes 30 46 9a 42.
Note: arm-linux-gnueabihf-objdump displays instruction bytes from high address to low address. We also can use hexdump to show instruction byte, it’s in a clearer way. The command is:

1
hexdump -C -n 4 -s $((0x267782)) /tmp/libMyGame.so

The output should be as follows:

00267782 30 46 9a 42 |0F.B|
00267786

Write a hook handle function.

The prototype of the hook handle function is as follows, and it’s in the file, main.cpp

1
extern "C" void hook_fun(void* baseaddress, void* sp);

This function has 2 arguments. The 1st argument, baseaddress, is the base address of the libMyGame.so, and the 2nd argument, sp, is the value int register SP. Trampoline code will pass these 2 arguments to the hook handle function. baseaddress let us can access any data or function in libMyGame.so very easily. Trampoline code will store most CPU registers in the stack, so we can access these register values using sp.

Write trampoline code

This code is in Thumb assembly. I list the code in here, and I put the offset of instructions at the beginning of every line.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0x0:	push	{r0, r1, r2, r3, r4, r5, r6, r7}
0x2: push.w {r8, sb, sl, fp, ip, lr}
0x6: mrs r0, apsr
0xa: push {r0}
0xc: nop
0xe: mov r1, sp
0x10: ldr r0, [pc, #0x14] @ --> 0x28
0x12: ldr r4, [pc, #0x18] @ --> 0x2c
0x14: blx r4
0x16: pop {r0}
0x18: msr cpsr_fc, r0
0x1c: pop.w {r8, sb, sl, fp, ip, lr}
0x20: pop {r0, r1, r2, r3, r4, r5, r6, r7}
0x22: nop
0x24: nop
0x26: bx lr
0x28: nop
0x2a: nop
0x2c: nop
0x2e: nop

0x0-0xa, save registers to stack, r0-r12, LR , and CPSR. We can learn more on ARM registers with this page.
0xe, set the 2nd argument,
0x10, set the 1nd argument, we load the 1st argument from 0x28, and we will put the base address of libMyGame.so at 0x28 using Frida.
0x12, load the address of the hook handle function to R4, and we will put hook_fun_ptr at 0x2c using Frida.
0x14, call hook handle function
0x16-0x20, load registers from stack.
0x22-0x24, run original instructions at here. We will put saved original bytes at here using Frida.
0x26, return
0x28-0x2a, We store the base address of libMyGame.so at here.
0x2c-0x2e, We store the address of hook handle function at here.
Whole trampoline code occupies 0x30 bytes.

About selection of trampoline_ptr

We can simple using Memory.alloc for trampoline code. Actually, we use near call instruction. This makes trampoline_ptr can not be too far from hook_ptr. We can find a cave to put our trampoline code in libMyGame.so. CAVE MINER is a great tool for this task. But I just do it manually for this sample case. I use the following command to list all segments in libMyGame.so:

1
readelf -l  /tmp/libMyGame.so

The following is the output snippet:

Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x00000034 0x00000034 0x00120 0x00120 R 0x4
INTERP 0x000154 0x00000154 0x00000154 0x00013 0x00013 R 0x1
[Requesting program interpreter: /system/bin/linker]
LOAD 0x000000 0x00000000 0x00000000 0x691630 0x691630 R E 0x1000
LOAD 0x691f38 0x00692f38 0x00692f38 0x36b0c 0x42fac RW 0x1000
DYNAMIC 0x6bc6c8 0x006bd6c8 0x006bd6c8 0x00150 0x00150 RW 0x4

The first load segment is end at 0x691630, and the second load segment is begin at 0x691f38. So cave at 0x691630 is perfect for our tempoline code.

Put a BL instruction at hook target.

We just use a near instruction to call trampoline code at hook_ptr, using thumbwriter in Frida for this.

Summary

I wrote a method in patchutils.js, it’s defined as follows:

1
export function putThumbHookPatch(trampoline_ptr:NativePointer, hook_ptr:NativePointer, hook_fun_ptr:NativePointer, para1:NativePointer, origin_inst?:number[]):number
  • The 1st argument, trampoline_ptr is the address of trampoline code
  • The 2nd argument, hook_ptr is the address of hook target
  • The 3rd argument, hook_fun_ptr is the address of our hook handle function
  • The 4th argument, para1 is the 1st parameter to our hook handle function, it will be assign to base address of libMyGame.so in caller
  • The 5th argument, origin_inst is the saved original bytes at hook_ptr
    And it returns the length of the trampoline code
    I also wrote a test method in inlinehooktest.ts.

Compile & run

I wrote a makefile for compilation.
We can use the following command to compile.

1
$ make build_inlinehooktest

And use the following command to run.

1
$ make run

If everything is OK, we can see the output as follows.

Hello World from so
cocos2d application version:
#################### Hook Begin ##############################
hook function from so
baseaddress 0xc8902000
CPSR 0x600e0010
R8 0x00000000
R9 0xbbdf8c18
R10 0xbbdf8b80
R11 0xbbdf8b90
R12 0xf3811ce8
LR 0xc8b69787
R0 0xbbdf8b70
R1 0x05c43127
R2 0x05c43127
R3 0x05c43127
R4 0xf3813260
R5 0xbbdf8b70
R6 0xdcc854b8
R7 0xbbdf8b88
#################### Hook end ##############################
1.8.12

The hook handle function just do the following,

  • print base address
  • dump values of all registers stored in the stack

Conclusion

This method still has many way to improve. And it only support Thumb. If you have any idea or issues, please feel free to let me know. I’m always glad to hack and discuss with others.
All code is in my github.

How to use CModule in Frida

In this article, I will try to introduce how to use CModule in Frida.
My test Android APK is a small game, named “Knife hit”, you can download it from APKpure website. This is a Cocos2d game, it has a native library named ‘libMyGame.so’. This library file has many game logic codes. It exports many functions. We will try to call one of them to get Cocos2d application version and print out it.

Why typescript?

Later version of Frida support typescript. And Frida provides a demo for starting of typescript coding. It use a NPM package, ‘frida-compile‘, for compiling of typescript code. Using typescript needs a compilation, it might take longer time to test the code, but we can avoid many issues in compilation. So I prefer typescript.

What is a CMoudule

This is its official documentation. In my option, we can write some functions in C, and Frida provides a way to invoke these functions. The following is the example in the official documentation.

1
2
3
4
5
6
7
8
9
10
11
12
const cm = new CModule(`
#include <stdio.h>

void hello(void) {
printf("Hello World from CModule\\n");
}
`);

console.log(JSON.stringify(cm));

const hello = new NativeFunction(cm.hello, 'void', []);
hello();

Line 1-7 create a CModule, it includes C code, and exports one function, ‘hello’ to print a welcome string.
Line 9 displays the information of the CModule. Frida is very smart, the C code should be compiled in here. And Frida should load the CModule in the memory space of the target process.
Line 11 convert function pointer to a NativeFunction for calling in Frida. So, cm.hello just is a NativePoiner.
Line 12 call hello function

Write our own CModule

Why not to use printf?

Typescript code and C code is actually run in the target process’ memory space. And Android process usually do not use printf. They use __android_log_print instead. But this function do not print message to Frida, we nee to run adb logcat to check its output. It’s not very convenience. We can write some functions in typescript to let C code to call.

Typescript functions for debugging

I wrote several helper functions in typescript, I put them in file fridautils.ts.

frida_log_callback

1
2
3
4
export let frida_log_callback = new NativeCallback(function(sp:NativePointer){
let s = sp.readUtf8String();
console.log(s)
},'void', ['pointer'])

This function has one argument of type NativePointer. C code passes a pointer to a string to this function.
Line 2 read a string from given address.
Line 3 outputs the string to Frida.

frida_log_cstring_callback

1
2
3
4
export let frida_log_cstring_callback = new NativeCallback(function(pString:NativePointer){
let s = pString.add(0x0).readPointer().readUtf8String(); // get string to a pointer to a std::string object
console.log(s)
},'void', ['pointer'])

Cocos2D stores the version string in a std::string. So we needs a function to extract the actual string from the std::string object and print it.
This function has only one argument, it also is of type NativePoiner, but it should be a pointer to a std::string object
Line 2 extracts the actual string from the given std::string object. Note: Android developers can specify different variable in their Application.mk file. Variables APP_ABI, APP_STL may determine different implementations of std::string class. So current method to extract a string may not work correctly when you hack other Android APKs.
Line 3 outputs extracted string to Frida.

Our own CModule

I wrote a CModule to show the Cocos2D application version string by using the functions has exist in libMyGame.so. I put code it file cmoduletest.ts.

Cocos2d functions for get version string

I read the official documentation in Cocos2d website. Class cocos2d::Application has 2 function we can use:

  • static Application* getInstance ()
    doc
    This function return a pointer to the current application instance.
  • const std::string& getVersion () const
    doc
    This function return a std::string reference to the version string. A C++ reference is a pointer in machine code actually.

Call Cocos2d functions

The following is the code

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
let test0 = function() {
let soname ="libMyGame.so"
let m = Process.getModuleByName(soname)

let loadm = new CModule(`
void* _ZN7cocos2d11Application11getInstanceEv ();
const char* _ZN7cocos2d11Application10getVersionEv(void*);
extern void frida_log_callback(const char* s);
extern void frida_log_cstring_callback(void*);
void fun(void) {
frida_log_callback("Hello World from CModule");
void* pApplication = _ZN7cocos2d11Application11getInstanceEv();
const char* version = _ZN7cocos2d11Application10getVersionEv(pApplication);
frida_log_callback("cocos2d application version: ");
frida_log_cstring_callback((void*)version);
}
`,
{
frida_log_callback : fridautils.frida_log_callback,
frida_log_cstring_callback : fridautils.frida_log_cstring_callback,
// cocos2d::Application::getInstance
// use this static function to get a pointer to the current Application instance
_ZN7cocos2d11Application11getInstanceEv : m.getExportByName("_ZN7cocos2d11Application11getInstanceEv"),
// const char* cocos2d::Application::getVersion(void*)
// use this member function of class Application to get a version string
_ZN7cocos2d11Application10getVersionEv: m.getExportByName("_ZN7cocos2d11Application10getVersionEv"),
});

console.log(JSON.stringify(loadm));
fridautils.runFunWithExceptHandling(()=>{
let fun = new NativeFunction(loadm.fun, 'void',[])
fun();
})
}

Line 6-16 is the C code.
Line 6-7 declares the Cocos2d functions we will using. Because we are in C, so we only can use mangled function names.
Line 8-9 declares the helper functions we created in Typescript.
Line 10-16, we create a function named fun to do our actual work
Line 12, we get the current application instance address using function cocos2d::Application::getInstance, this function is a static function, and it has no argument, we store the result to variable pApplication;
Line 13, we get a pointer to a std::string object using function cocos2d::Application::getVersion, this function is a member function, and it needs a this pointer as its first argument. We just pass variable pApplication to it.
Line 14-15. We print version string using helper functions.
LIne 19-26. We needs to pass all functions to the CModule using a dictionary. In the dictionary, key is the function name, and value is the function address. We call Module.getExportByName to get the functions address in the libMyGame.so

Compile & run

I wrote a makefile for compilation.
We can use the following cmd to compile.

1
$ make build_cmoduletest

And use the following cmd to run.

1
$ make run

If everything is OK, we can see the output as follows

Hello World from CModule
cocos2d application version:
1.8.12
This game’s Cocos2d application version is 1.8.12

Conclusion

Now, we know how to call the existing functions using CModule. Cocos2D exports a lot of functions in its native library file. We can do many things using these functions.
All code is in my github

Frida Android basics

This article introduces the basics about how to use Frida on Android platform.

About Frida

Frida is a great tool working on Windows, macOS and GNU/Linux allowing you to inject snippets of code into native apps. In this tutorial we’ll use Frida to inject code into Android applications.

Frida has 2 work modes, one is client-server, the other is gadget mode.

Server mode

The server runs on the Android phone and the client on your computer. Note that you need a phone or an Android emulator that rooted.

Frida installation

Installing Frida on your computer is a super easy:

1
$ pip install frida-tools

Note that the latest Python 3 is recommended. Lets check which version is installed:

1
$ frida --version

And we need to find the architecture of you phone.

1
$ adb shell getprop ro.product.cpu.abi

Now, we need to install the server on our Android phone. Visit frida release page, and find a file named like “frida-server-XX.XX.XX-android-YYYY.xz”. ‘XX.XX.XX’ is the version of your installed frida, and YYYY is the architecture of your Android device. Note frida server version should be better to match the frida version on you computer.
We uncompress the archive and rename the server to “frida-server”
And install the server on the phone:

1
2
3
$ adb root # might be required
$ adb push frida-server /data/local/tmp/
$ adb shell "chmod 777 /data/local/tmp/frida-server"

Now try to start the server.

1
adb shell "/data/local/tmp/frida-server &"

Frida test

Run the following command to list all applications on your Android device:

1
frida-ps -Uai

Gadget mode

This mode needs not a rooted android device, but need to repackage APKs you wnat to hack.

Download Gadget file

Also visit frida release page, and find a file named like “frida-gadget-XX.XX.XX-android-YYYY.so.xz”. ‘XX.XX.XX’ is the version of your installed frida, and YYYY is the architecture of your Android device.
We uncompress the archive and rename the so to “libgadget.so”

Inject libgadget.so to your APK

Try to find any native library file in you APK, and inject libgadget.so to it.
The following is a Python script to inject libgadget.so, just add libgadget.so as a dependency of a native libraries embedded in the APK.

1
2
3
4
import lief # you can install lief package with `pip install lief`
libnative = lief.parse("libnative.so") # you should replace libnative.so to you actual so file name
libnative.add_library("libgadget.so") # Injection!
libnative.write("libnative.so")

Write a gadget config file

Create a file name with ‘libgadget.config.so’. and put the following content in it:

1
2
3
4
5
6
{
"interaction": {
"type": "listen",
"on_load": "wait"
}
}

And copy this file into same directory with your patched libnative.so in.

Repackage APK file

There are many APK repackage utils. I prefer APK easy Tool

Frida test

If you install patched APK to your Android device and open it.
The app seems it sucks, don’t be worry, it’s waiting for you to run frida client to connect it.
Run the following command, you will find a process with a name ‘Gadget’.

1
frida-ps -Uai

This process just is your patched APK, and we can not see other applications, because we’re not root now

Getting started with Frida

Write a javascript file you want to inject

The content of the test javascript file (named tt.js)

1
console.log('hello world')

Inject javascript code to an Android process

Frida provides serval utils to let us life easy. One of them is ‘frida’.
Run the following command to connect to your wait process in gadget mode

1
frida -U -n Gadget -l tt.js --no-pause 

You will see a frida shell, and it print ‘hello world’
For client/server mode, you should run frida with other options:

1
frida -U -f <package name> -l tt.js --no-pause  # this command will reopen your app

or

1
frida -U -n <app title> -l tt.js --no-pause  # this command do not reopen your app

Conclusion

Now we can inject a basic javascript code into an Android app, you can refer frida Javascript documentation for more learning.