Frida-Boot workshop
https://github.com/leonjza/frida-boot
Youtube link: https://www.youtube.com/watch?v=CLpW1tZCblo
Chapter 1 - Part 1: LD_PRELOAD
~/code$ ldd pew
linux-vdso.so.1 (0x00007ffd6abd4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd0396d2000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd0398a0000)
Identify shared libraries that we can mess with LD_PRELOAD
.
If the binary is compiled statically == no external shared libraries.
LD_PRELOAD
will not help here.
~/code$ gcc -static pew.c -o pew-static
~/code$ ldd pew-static
not a dynamic executable
Not discussed in the video but to discover shared library calls we can also
use ltrace
.
~/code$ ltrace ./pew
puts("[+] Starting up!"[+] Starting up! = 17
time(0) = 1619288817
srand(0x608462f1, 0x55dd85dfc2a0, 0, 0x7ff07518e673) = 0
rand(1, 5, 0x7ff07525c1d0, 0) = 0x1340f51f
printf("[+] Sleeping for %d seconds\n", 5[+] Sleeping for 5 seconds = 27
sleep(5) = 0
rand(1, 5, 0, 0x7ff07516ae93) = 0xaf08cb7
printf("[+] Sleeping for %d seconds\n", 4[+] Sleeping for 4 seconds) = 27
sleep(4) = 0
rand(1, 5, 0, 0x7ff07516ae93) = 0x61534c8c
printf("[+] Sleeping for %d seconds\n", 2[+] Sleeping for 2 seconds = 27
sleep(2^C <no return ...>
--- SIGINT (Interrupt) ---
+++ killed by SIGINT +++
Or use nm
:
~/code$ nm -D pew
w __cxa_finalize@@GLIBC_2.2.5
w __gmon_start__
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
U __libc_start_main@@GLIBC_2.2.5
U printf@@GLIBC_2.2.5
U puts@@GLIBC_2.2.5
U rand@@GLIBC_2.2.5
U sleep@@GLIBC_2.2.5
U srand@@GLIBC_2.2.5
U time@@GLIBC_2.2.5
We want to hook sleep
and ``.
Make our own implementation of sleep. We need to know the function signature.
#include <stdio.h>
unsigned int sleep(unsigned int seconds) {
printf(" [-] sleep goes brrr\n");
return 0;
}
Now, we need to compile it into a shared object with gcc
using -shared
.
fPIC
generates position-independent code.
~/code$ gcc -fPIC -shared ../software/fake_sleep.c -o fake_sleep.so
LD_PRELOAD
needs a full path.
~/code$ LD_PRELOAD=./fake_sleep.so ./pew
The app prints the strings but does not sleep at all because we have modified
the sleep
function.
Now let's make a proxy so. E.g., fake_sleep
calls sleep
. To get the original
sleep we need to call dlsym.
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
// man 3 sleep
// unsigned int sleep(unsigned int seconds);
unsigned int sleep(unsigned int seconds) {
// you've never slept this well in your life!
printf("[-] sleep goes brrr\n");
seconds = 1;
unsigned int (*original_sleep)(unsigned int);
// "RTLD_NEXT": Get the handle for the next symbol named "sleep"
original_sleep = dlsym(RTLD_NEXT, "sleep");
return (original_sleep)(seconds);
}
Now, we need to add the -ldl
switch in the end.
LD_PRELOAD=./fake_sleep.so ./pew
: each sleep is now 1 second.
Chapter 1 - Part 2: gdb
gdb -q ./pew
Inside we can do info func
to list all functions. Some functions have plt in
the end printf@plt
. PLT
stands for Procedure Linkage Table. Inside PLT we
have GOT
or Global Offset Table.
- First call: Check if
printf
exists in the GOT. It's not because it has not been called before. - If it does not exist, dynamic linker it looks for it in all of the shared libraries.
- Write the record in the GOT and store the address of the real
printf
. - Every new call, the program uses the record in the GOT.
Inside gdb (probably need gef
) we can do got
to see the GOT. The items that have been resolved are
in green and the rest are red (color is definitely gef
).
gef➤ got
GOT protection: Partial RelRO | GOT functions: 6
[0x5569a25ec018] puts@GLIBC_2.2.5 → 0x7f9b8a61b030 # this is green
[0x5569a25ec020] printf@GLIBC_2.2.5 → 0x5569a25e9046 # the rest is yellow
[0x5569a25ec028] srand@GLIBC_2.2.5 → 0x5569a25e9056
[0x5569a25ec030] time@GLIBC_2.2.5 → 0x5569a25e9066
[0x5569a25ec038] sleep@GLIBC_2.2.5 → 0x5569a25e9076
[0x5569a25ec040] rand@GLIBC_2.2.5 → 0x5569a25e9086
gef➤ info symbol 0x5569a25ec018
puts@got.plt in section .got.plt of /root/code/pew # puts have been resolved to its actual address
gef➤ info symbol 0x5569a25ec020
printf@got.plt in section .got.plt of /root/code/pew # printf has not been resolved, yet
Rage quit, moved on to the Frida stuff.
Chapter 2 - Part 1: Frida
Some tmux Stuff
Everything starts with ctrl+b
then you get one character to do stuff.
ctrl+b
then w
list all windows.
To destroy any window, select it from the list above, then ctrl+b
then &
. A prompt will ask for y/n
, press y
.
splits: ctrl+b
then
%
: horizontal split"
: vertical split
ctrl+b then
o swap panes
q show pane numbers
x kill pane
space - toggle between layouts
From outside to kill and list sessions.
tmux list-session
# then kill
tmux kill-session -t 2
scroll up and down
ctrl+b
[
then you can use page up/down
or arrow key
. ctrl+c
when done.
tmux cheat sheet: https://gist.github.com/MohamedAlaa/2961058
Frida REPL
frida pew --runtime=v8
will attach to the process named pew
. Will not work
if we have multiple processes named pew
. We can do frida -p pid
instead.
Using the v8 runtime allows us to use things like arrow functions or raw format
strings (e.g., value is ${val}
).
frida pew -l index.js
loads the script. We can modify the script on the fly
and after the save the script will be executed again.
Chapter 2 - Part 2: Interceptor
https://frida.re/docs/javascript-api/#interceptor
The target in the end is a memory address. In practice it can be offset or symbol. With symbol we get the address quickly.
Resolving sleep in gdb
Let's try to resolve sleep
in pew.
~/code$ nm pew | grep -i "sleep"
U sleep@@GLIBC_2.2.5
Next, we need to figure out which library has libc.
~/code$ ldd pew
linux-vdso.so.1 (0x00007fff4e3de000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f140beac000)
/lib64/ld-linux-x86-64.so.2 (0x00007f140c07a000)
To see the address of sleep
inside libc.
# -D or -dynamic
# Display the dynamic symbols rather than the normal symbols. This is only
# meaningful for dynamic objects, such as certain types of shared libraries.
~/code$ nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep -i "sleep"
000000000010a3c0 T __clock_nanosleep@@GLIBC_PRIVATE
000000000010a3c0 W clock_nanosleep@@GLIBC_2.17
00000000000cae80 T __nanosleep@@GLIBC_2.2.6
00000000000cae80 W nanosleep@@GLIBC_2.2.5
00000000000f35d0 T __nanosleep_nocancel@@GLIBC_PRIVATE
00000000000cad90 W sleep@@GLIBC_2.2.5
0000000000084d70 T thrd_sleep@@GLIBC_2.28
00000000000f5870 T usleep@@GLIBC_2.2.5
So the offset of sleep
is 0xcad90
here.
To confirm it:
- Run pew in gdb.
b *main
and then run withr
.info proc map
orvmmap
to get the libc's 0x00 offset address to get its base address.- Add
0xcad90
to it to get thesleep
's address.
gef➤ info proc map
process 411
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x55b349b21000 0x55b349b22000 0x1000 0x0 /root/code/pew
0x55b349b22000 0x55b349b23000 0x1000 0x1000 /root/code/pew
0x55b349b23000 0x55b349b24000 0x1000 0x2000 /root/code/pew
0x55b349b24000 0x55b349b25000 0x1000 0x2000 /root/code/pew
0x55b349b25000 0x55b349b26000 0x1000 0x3000 /root/code/pew
0x7fdd58148000 0x7fdd5816d000 0x25000 0x0 /lib/x86_64-linux-gnu/libc-2.30.so # this is what we want, offset 0x00
0x7fdd5816d000 0x7fdd582b7000 0x14a000 0x25000 /lib/x86_64-linux-gnu/libc-2.30.so
0x7fdd582b7000 0x7fdd58301000 0x4a000 0x16f000 /lib/x86_64-linux-gnu/libc-2.30.so
0x7fdd58301000 0x7fdd58304000 0x3000 0x1b8000 /lib/x86_64-linux-gnu/libc-2.30.so
0x7fdd58304000 0x7fdd58307000 0x3000 0x1bb000 /lib/x86_64-linux-gnu/libc-2.30.so
...
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x000055b349b21000 0x000055b349b22000 0x0000000000000000 r-- /root/code/pew
0x000055b349b22000 0x000055b349b23000 0x0000000000001000 r-x /root/code/pew
0x000055b349b23000 0x000055b349b24000 0x0000000000002000 r-- /root/code/pew
0x000055b349b24000 0x000055b349b25000 0x0000000000002000 r-- /root/code/pew
0x000055b349b25000 0x000055b349b26000 0x0000000000003000 rw- /root/code/pew
0x00007fdd58148000 0x00007fdd5816d000 0x0000000000000000 r-- /lib/x86_64-linux-gnu/libc-2.30.so # offset 0x00 (3rd column)
0x00007fdd5816d000 0x00007fdd582b7000 0x0000000000025000 r-x /lib/x86_64-linux-gnu/libc-2.30.so
0x00007fdd582b7000 0x00007fdd58301000 0x000000000016f000 r-- /lib/x86_64-linux-gnu/libc-2.30.so
0x00007fdd58301000 0x00007fdd58304000 0x00000000001b8000 r-- /lib/x86_64-linux-gnu/libc-2.30.so
0x00007fdd58304000 0x00007fdd58307000 0x00000000001bb000 rw- /lib/x86_64-linux-gnu/libc-2.30.so
The address is 0x7fdd58148000
in both commands. So sleep
will be
7fdd58148000 + cad90
= 7fdd58212d90
(we do need to do this manually).
We can check it in gdb:
gef➤ info symbol 0x7fdd58148000 + 0xcad90
sleep in section .text of /lib/x86_64-linux-gnu/libc.so.6
Resolving sleep in Frida
We can get the base address in different ways:
Process.enumerateModulesSync
Module.getBaseAddress
Process.getModuleByName.enumerateExports
Module.getExportByName
DebugSymbol.getFunctionByName
Process.enumerateModulesSync
[Local::pew]-> Process.enumerateModulesSync();
// ...
{
"base": "0x7f28c98c6000",
"name": "libc-2.30.so",
"path": "/lib/x86_64-linux-gnu/libc-2.30.so",
"size": 1830912
},
But we can also pass a filter here which is a function.
Process.enumerateModulesSync().filter(ex => ex.name.includes("libc"));
// Without the v8 runtime, we had to use:
Process.enumerateModulesSync().filter(function(m) { return m.name.includes("libc");});
Both return the same result:
[
{
"base": "0x7f7286eb2000",
"name": "libc-2.30.so",
"path": "/lib/x86_64-linux-gnu/libc-2.30.so",
"size": 1830912
}
]
We can also do
[Local::pew]-> Process.getModuleByName("libc-2.30.so");
{
"base": "0x7f7286eb2000",
"name": "libc-2.30.so",
"path": "/lib/x86_64-linux-gnu/libc-2.30.so",
"size": 1830912
}
We can just get the base
with .base
in both cases:
[Local::pew]-> Process.getModuleByName("libc-2.30.so").base;
"0x7f7286eb2000"
// enumerateModulesSync returns an array so we need to choose the first element.
[Local::pew]-> Process.enumerateModulesSync().filter(ex => ex.name.includes("libc"))[0].base;
"0x7f7286eb2000"
Module.getBaseAddress
[Local::pew]-> Module.getBaseAddress("libc-2.30.so");
"0x7f7286eb2000"
Response is a native pointer so we can add
to directly get the sleep
offset.
[Local::pew]-> Module.getBaseAddress("libc-2.30.so").add("0xcad90");
"0x7f7286f7cd90"
Process.getModuleByName.enumerateExports
sleep
is an export in libc so we can just ask Frida to resolve it for us.
[Local::pew]-> Process.getModuleByName("libc-2.30.so").enumerateExports().filter(ex => ex.name === "sleep");
[
{
"address": "0x7f7286f7cd90",
"name": "sleep",
"type": "function"
}
]
It's an array so you should get the address with [0].address
(append to the
end of the command above).
Module.getExportByName
[Local::pew]-> Module.getExportByName(null, "sleep");
"0x7f7286f7cd90"
// first argument is the name of the library but sleep is unique so we do not
// care about duplicates
[Local::pew]-> Module.getExportByName("libc-2.30.so", "sleep");
"0x7f7286f7cd90"
DebugSymbol.getFunctionByName
[Local::pew]-> DebugSymbol.getFunctionByName("sleep")
"0x7f7286f7cd90"
Interceptor
Inteceptor.attach(target, callbacks[, data]);
callbacks is an object with two functions.
Interceptor.attach(sleepPtr, {
onEnter: function(args) {}, // we can modify the arguments
onLeave: function(retval) {} // we can modify the return value
});
Note: It's important to not use the arrow function syntax for onEnter
and
onLeave
. Oh, well!
Now, we can modify the sleep
function with Frida. I am going to use ES6
because I will change my runtime to v8.
// frida pew -l myinterceptor-attach.js --runtime=v8
var sleep = Module.getExportByName(null, "sleep");
Interceptor.attach(sleep, {
// in this case using the arrow function is not that bad because we are not
// doing anything complex.
onEnter: args => { console.log("[*] Sleep from Frida!"); },
onLeave: retval => {console.log("[*] Done sleeping from Frida!"); }
});
Chapter 2 - Part 3: Hooking Arguments and Return Values
Modifying Arguments
We can modify the arguments inside onEnter
.
args
does not know how many arguments are in there.args[0]
is the first arg.- Args are of type NativePointer.
Frida does not do bounds checking here. It's possible to read the memory past
the valid arguments. E.g., args[10]
when the function only has 2 arguments.
// parse-sleep-args.js
var sleep = Module.getExportByName(null, "sleep");
Interceptor.attach(sleep, {
onEnter: args => { console.log("[*] Argument for sleep() => ", parseInt(args[0])); },
onLeave: retval => { console.log("[*] Done sleeping from Frida!"); }
});
Now, we can modify the arguments. Create a new pointer and assign it.
args[0] = ptr("0x01");
args[0] = new NativePointer("0x01");
Let's modify sleep argument to 2.
// modify-sleep-args.js
var sleep = Module.getExportByName(null, "sleep");
Interceptor.attach(sleep, {
onEnter: args => {
console.log("[*] The original argument for sleep() => ", parseInt(args[0]));
args[0] = ptr("0x02"); // change it to 2
},
onLeave: retval => { console.log("[*] Done sleeping from Frida!"); }
});
How to Allocate Strings
Allocate a new char array with Memory.allocUtf8String
like this:
onEnter: function (args) {
var buf = Memory.allocUtf8String('mystring'); // create a new string on memory
this.buf = buf; // assign it to this
args[0] = buf; // pass it to args
}
According to Best Practices - String allocation
this is bound to an object that is per-thread and per-invocation, and anything you store there will be available in onLeave, and this even works in case of recursion. This way you can read arguments in onEnter and access them later in onLeave. It is also the recommended way to keep memory allocations alive for the duration of the function-call.
Let's modify printf
arguments.
var sleep = Module.getExportByName(null, "printf");
var strBuf = Memory.allocUtf8String("pewpewpew\n");
Interceptor.attach(sleep, {
onEnter: args => {
// this did not work, I would get the pews printed after detaching Frida
// maybe because I am in an arrow function?
// var strBuf = Memory.allocUtf8String("pewpewpew\n");
console.log("[*] The original argument for printf() => ", args[0]);
args[0] = strBuf;
}
// onLeave: retval => { console.log("[*] Done sleeping from Frida!"); }
});
Get Registers/Context
this
does not work in the arrow function so, we have to use a normal one.
// get-context.js
var sleep = Module.getExportByName(null, "printf");
Interceptor.attach(sleep, {
onEnter: function(args) {
console.log(JSON.stringify(this.context, null, 4));
}
});
We can modify the registers here, too.
Modifying Return Values
Trying to modifying the return value rand_range
in pew. We need to do
retval.replace(ptr(...));
.
// modify-return-rand-range.js
var randRange = DebugSymbol.getFunctionByName("rand_range"); // we have symbols
Interceptor.attach(randRange, {
onLeave: retval => { retval.replace(ptr("0x01")); }
});
Without using arrow functions and like it was mentioned in the string allocation
section, we can pass variables to this
inside onEnter
and then use them in
onLeave
.
// modify-return-rand-range2.js
var randRange = DebugSymbol.getFunctionByName("rand_range"); // we have symbols
Interceptor.attach(randRange, {
onEnter: function(args) {
this.arg0 = args[0];
},
onLeave: function(retval) {
console.log(retval);
retval.replace(this.arg0); // now we can use this.arg0 here
}
});
Chapter 2 - Part 4: Reusing Existing Code
The crypt
utility asks for a PIN and then checks it with test_pin
. We want
to hook this function and return 1.
hexdump generates a hex dump from an ArrayBuffer or NativePointer.
var testPIN = DebugSymbol.getFunctionByName("test_pin");
Interceptor.attach(testPIN, {
onEnter: function(args) {
console.log("test_pin args:", hexdump(args[0]));
},
onLeave: function(retval) {
console.log("test_pin return value:", retval);
}
});
Let's modify the return value to always return 1
.
var testPIN = DebugSymbol.getFunctionByName("test_pin");
Interceptor.attach(testPIN, {
onEnter: function(args) {
// console.log("test_pin args:", hexdump(args[0]));
},
onLeave: function(retval) {
// console.log("test_pin return value:", retval);
retval.replace(ptr("0x01"));
}
});
Calling Functions
Try and call test_pin
manually. We use NativeFunction.
Create a JavaScript wrapper around a function. We can use it to call the target
function.
NativeFunction(address, returnType, argTypes[, abi])
var testPIN = DebugSymbol.getFunctionByName("test_pin");
// Wrapper. output is int and input is an array with one pointer.
var testPINFunction = new NativeFunction(testPIN, "int", ["pointer"]);
var pin = Memory.allocUtf8String("1234");
var res = testPINFunction(pin);
console.log("test_pin(1234):", res);
We can do this to call arbitrary functions or do things like bruteforce. Run
with --runtime=v8
to enable the raw string.
// frida crypt -l crypt-bruteforce-pin.js --runtime=v8
var testPIN = DebugSymbol.getFunctionByName("test_pin");
// Wrapper. output is int and input is an array with one pointer.
var testPINFunction = new NativeFunction(testPIN, "int", ["pointer"]);
for (var i=0; i<9999; i++) {
var pin = Memory.allocUtf8String(i);
var res = testPINFunction(pin);
console.log(`test_pin(${i}) = ${res}`);
if (res === 1) {
console.log("Found PIN:", pin);
break;
}
}
Chapter 3 - Part 1: Python Tools
We can use our own Python tools and use the message bus to pass info between the agent and the Python script. Do fancy stuff.
import frida
import sys
session = frida.attach("crypt") # attach to crypt
script = session.create_script("""
console.log(Frida.version);
""")
script.load()
If we want to load an external agent instead.
// tool2.js
console.log(Frida.version);
import frida
import sys
with open("tool2.js", "r") as f:
agent = f.read()
session = frida.attach("crypt")
script = session.create_script(agent, runtime="v8")
script.load()
Chapter 3 - Part 2: Send and Receive
script.load()
usually has a maximum time of 30 seconds. You don't want a lot
of stuff happening in the initial script that is loaded.
Send From JavaScript
In Python and JavaScript we can send message with send:
send(message[,data])
.
To receive messages in Python, we need to create handlers:
# tool4.py
import frida
import sys
# define the handler
def incoming(message, data):
print(message)
print(data)
with open("tool4.js", "r") as f:
agent = f.read()
session = frida.attach("crypt")
script = session.create_script(agent, runtime="v8")
script.on("message", incoming)
script.load()
The agent is the same, it sends out the PIN instead of printing it locally:
// tool4.js
var testPIN = DebugSymbol.getFunctionByName("test_pin");
// Wrapper. output is int and input is an array with one pointer.
var testPINFunction = new NativeFunction(testPIN, "int", ["pointer"]);
send("Starting brute forcer");
for (var i=0; i<9999; i++) {
var pin = Memory.allocUtf8String(i.toString());
var res = testPINFunction(pin);
if (res === 1) {
send(`Found PIN: ${i}`);
send("Finished brute forcing");
break;
}
}
And the result is the same:
~/code$ python3 tool4.py
{'type': 'send', 'payload': 'Starting brute forcer'}
{'type': 'send', 'payload': 'Found PIN: 3428'}
{'type': 'send', 'payload': 'Finished brute forcing'}
Send From Python
Inside Python we can do:
script.post("whatever")
sys.stdin.read() # wait for input to see the output from the script
And in JavaScript we have a recv
function:
recv(function(msg) {
console.log("message:" + msg);
});
// recv(msg => console.log("message:", msg);)
Frida RPC Interface
Exports functions in the agent and add them to rpc.exports
and in the Python
script use them with script.exports.func
.
https://frida.re/docs/javascript-api/#rpc-exports
Function names in JavaScript vs. Python? The talk says testPin
in JS becomes
test_pin
in Python. How is the conversion done? Can I find any
documentation/articles about this?
Apparently, this is to keep PEP8 in the Python side.
I should add that the intention here is to be able to follow the camelCase-convention commonly used in the JavaScript world – and used by Frida's own APIs – and still be able to follow PEP 8 on the Python side, i.e.
readByte
on the JS side becomesread_byte
on the Python side. Similarly a C#/.NET binding would map it toReadByte
. Cheers!
https://github.com/frida/frida-python/issues/104#issuecomment-281666759
Tip: Better to export everything as all lower case.
Seems like we can also name things in rpc.exports
like this:
function internalFunction(e) {
// ...
}
rpc.exports = {
// export an internal function
exportedName: internalFunction, // exported_name in Python
// we can also define and export a function right here
name2: function(e) {
// ...
}
}
Refactor Bruteforce
The agent is like this:
// tool6.js
var testPIN = DebugSymbol.getFunctionByName("test_pin");
// Wrapper. output is int and input is an array with one pointer.
var testPINFunction = new NativeFunction(testPIN, "int", ["pointer"]);
function brute() {
send("Starting brute forcer");
for (var i=0; i<9999; i++) {
var pin = Memory.allocUtf8String(i.toString());
var res = testPINFunction(pin);
if (res === 1) {
send(`Found PIN: ${i}`);
send("Finished brute forcing");
break;
}
}
};
rpc.exports = {
bruteForcer: brute // become brute_forcer in Python
};
We can call it as brute_forcer
in Python:
# tool6.py
import frida
import sys
# define the handler
def incoming(message, data):
print(message)
with open("tool6.js", "r") as f:
agent = f.read()
session = frida.attach("crypt")
script = session.create_script(agent, runtime="v8")
script.on("message", incoming)
script.load()
api = script.exports
api.brute_forcer()
sys.stdin.read()
Bruteforce in Python
Change the agent and export the test_pin
function, then loop and brute force
in Python. The agent just exports the testpin
function.
// tool7.js
var testPIN = DebugSymbol.getFunctionByName("test_pin");
// Wrapper. output is int and input is an array with one pointer.
var testPINFunction = new NativeFunction(testPIN, "int", ["pointer"]);
function testPin(e) {
var pin = Memory.allocUtf8String(e.toString());
return testPINFunction(pin);
}
rpc.exports = {
testpin: testPin // all lowercase export name
};
And the Python script calls testpin
and does the bruteforce.
# tool7.py
import frida
import sys
with open("tool7.js", "r") as f:
agent = f.read()
session = frida.attach("crypt")
script = session.create_script(agent, runtime="v8")
script.load()
api = script.exports
# we can do the loop here
for pin in range(0, 9999):
if(api.testpin(pin) == 1): # we are converting it to string in the JS
print(f"Pin found: {pin}")
break
Chapter 3 - Part 3: Typescript
We are gonna use https://github.com/oleavr/frida-agent-example.
Rewriting testPIN
in TypeScript.
// agent1.ts
// reimplementing testPIN in Typescript
import { log } from "./logger";
const testPINSymbol = DebugSymbol.getFunctionByName("test_pin");
const testPINFunction = new NativeFunction(testPINSymbol, "int", ["pointer"]);
function testPIN(pin: string) {
const pinStr = Memory.allocUtf8String(pin);
return testPINFunction(pinStr);
};
rpc.exports = {
testpin: testPIN
};
And the Python tool does not change other than loading _agent.js
.
Inject a Web Server into the Target Process
We need to modify our agent and do it.
// agent2.ts
import { log } from "./logger";
import * as http from "http";
const testPINSymbol = DebugSymbol.getFunctionByName("test_pin");
const testPINFunction = new NativeFunction(testPINSymbol, "int", ["pointer"]);
function testpin(p: string) {
const pin = Memory.allocUtf8String(p);
return testPINFunction(pin);
};
function httpServer() {
http.createServer((req, res) => {
const pin = req.url? req.url.replace('/', '') : '';
const check = testpin(pin);
log(`Request to check ${req.url} returned ${check}`);
if (check == 1) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write(`Welcome!\n`);
} else {
res.writeHead(401, {'Content-Type': 'text/plain'});
res.write(`Wrong PIN\n`);
}
res.end();
}).listen(1337);
};
rpc.exports = {
testpin: testpin,
httpServer: httpServer
}
And the Python tool is still similar:
# tool2.py
import frida
import sys
with open("_agent.js", "r") as f:
agent = f.read()
session = frida.attach("crypt")
script = session.create_script(agent)
script.load()
api = script.exports
print("starting HTTP server...")
api.httpserver()
# keep the server alive now
sys.stdin.read()
Now, we can query http://localhost:1337/1234
to test if PIN is 1234.
curl http://localhost:1337/3428
.
Chapter 3 - Part 4: frida-tools
frida-trace crypt -i "ato*"
generates handlers to log/hook (?) for these
functions.
Chapter 4 - Part 5: Operating Modes
frida-server
is the most important one. Run it on the device and connect to
it, similar to IDA server.
frida-gadget
inject into the app or embed it into the app. See
https://koz.io/using-frida-on-android-without-root/.