Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frida script improvements #91

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 63 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
# B(l)utter
Flutter Mobile Application Reverse Engineering Tool by Compiling Dart AOT Runtime

It provides data on flutter including:
- pseudo assembly code broken out by class/function with named calls
- ida script to add class/function and member field meta data
- frida script to allow live tracing function calls on unmodified binaries


Currently the application supports only Android libapp.so (arm64 only).
Also the application is currently work only against recent Dart versions.

For high priority missing features, see [TODO](#todo)

<!-- MarkdownTOC -->

- [Environment Setup](#environment-setup)
- [Debian Unstable \(gcc 13\)](#debian-unstable-gcc-13)
- [Windows](#windows)
- [macOS Ventura and Sonoma \(clang 16\)](#macos-ventura-and-sonoma-clang-16)
- [Usage](#usage)
- [Update](#update)
- [Output files](#output-files)
- [Directories](#directories)
- [Frida Trace Script](#frida-trace-script)
- [Troubleshooting](#troubleshooting)
- [Development](#development)
- [Generating Visual Studio Solution](#generating-visual-studio-solution)
- [TODO](#todo)

<!-- /MarkdownTOC -->


## Environment Setup
This application uses C++20 Formatting library. It requires very recent C++ compiler such as g++>=13, Clang>=16.
This application uses C++20 Formatting library. It requires very recent C++ compiler such as g++>=13, Clang>=16 or MSVC 17.

I recommend using Linux OS (only tested on Deiban sid/trixie) because it is easy to setup.

Expand All @@ -22,10 +46,11 @@ apt install python3-pyelftools python3-requests git cmake ninja-build \
### Windows
- Install git and python 3
- Install latest Visual Studio with "Desktop development with C++" and "C++ CMake tools"
- Install required libraries (libcapstone and libicu4c)
- Run init script to install required libraries (libcapstone and libicu4c):
```
python scripts\init_env_win.py
```
- Install python elf tools `pip install pyelftools`
- Start "x64 Native Tools Command Prompt"

### macOS Ventura and Sonoma (clang 16)
Expand Down Expand Up @@ -67,14 +92,48 @@ python3 blutter.py path/to/app/lib/arm64-v8a out_dir --rebuild
- **packages** contains the static libraries of Dart Runtime
- **scripts** contains python scripts for getting/building Dart

## Frida Trace Script
The Frida trace script (found as `output_dir/blutter_frida.js`) allows you to spy on function calls to flutter functions and gives you information about the args/vals passed to it. It tries to recursively display the entire parameter structure and vals but it makes some educated guesses that can be wrong. This can result in crashes. It will print the function it is about to try and trace when it goes to trace so if it does crash try to exclude that function from being spied.

To use this you need to have frida working on the device either the frida server or the frida gadget injected into the package. Instructions for that are beyond the scope of this project, essentially normal frida-trace / frida / objection commands should work.

Then to use this edit the BlutterOpts initialization to include the options you want. There are 3 primary functions to call to add traces (they can be called as many times as you want):

- `opts.IgnoreByClassFuncRegex(regex : RegExp)`
- `opts.SpyByClassFuncRegex(regex : RegExp, maxDepth : number = MaxDepth)`
- `opts.SpyByFunctionAddy(address : number, displayName : string = "", maxDepth : number = MaxDepth)`

An optional config would then look like:
```javascript
function GetOptions(){
opts.IgnoreByClassFuncRegex(/(anim|battery|anon_|build|widget|Dependencies|Observer|Render)/i);
opts.SpyByClassFuncRegex(/interestingclass.+::func_prefix.+/i,4);
opts.SpyByClassFuncRegex(/moreInterestingClassByCrashProne.+::myFunc.+/,0);
opts.SpyByFunctionAddy(0x2fd950, "ImportantFunc",10);
return opts;
}
```

NOTE: Any changes to the script are overwritten when you regenerate/update the output

Finally launch it like any other frida script ie:

`frida -U -l blutter_frida.py "AppToSpy"`

To find function name look at the output_dir/ida_script/addNames.py file it lists all the functions at the top.

## Troubleshooting
If you get errors during compiling / initial run make sure you pay attention to the Environment setup in detail, have the deps installed, and for example on Windows are running the script from the Developer Powershell VS instance otherwise you won't have the required items in your path.

## Development

## Generating Visual Studio Solution for Development
### Generating Visual Studio Solution
I use Visual Studio to delevlop Blutter on Windows. ```--vs-sln``` options can be used to generate a Visual Studio solution.
```
python blutter.py path\to\lib\arm64-v8a build\vs --vs-sln
```

## TODO
### TODO
- More code analysis
- Function arguments and return type
- Some psuedo code for code pattern
Expand Down
2 changes: 1 addition & 1 deletion blutter/src/DartDumper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ static std::unordered_map<std::string, std::string> OP_MAP {
{ "&", "LAnd" }, { "|", "LOr" }, { "^", "xor" }, { "~", "not" }, {">>", "shar"}, {"<<", "shal"}, {">>", "shr"}
};

static std::string getFunctionName4Ida(const DartFunction& dartFn, const std::string& cls_prefix)
std::string DartDumper::getFunctionName4Ida(const DartFunction& dartFn, const std::string& cls_prefix)
{
auto fnName = dartFn.Name();
if (dartFn.IsClosure() && fnName == "<anonymous closure>") {
Expand Down
1 change: 1 addition & 0 deletions blutter/src/DartDumper.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class DartDumper
void DumpObjects(const char* filename);

std::string ObjectToString(dart::Object& obj, bool simpleForm = false, bool nestedObj = false, int depth = 0);
static std::string getFunctionName4Ida(const DartFunction& dartFn, const std::string& cls_prefix);

private:
std::string getPoolObjectDescription(intptr_t offset, bool simpleForm = true);
Expand Down
15 changes: 15 additions & 0 deletions blutter/src/FridaWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <fstream>
#include <filesystem>
#include "Util.h"
#include "DartDumper.h"

#ifndef FRIDA_TEMPLATE_DIR
#define FRIDA_TEMPLATE_DIR "scripts"
Expand Down Expand Up @@ -149,4 +150,18 @@ void FridaWriter::Create(const char* filename)
}
}
of << "];\n";
of << "function GetFuncPtrMap(){\nreturn new Map([\n";
for (auto lib : app.libs) {
std::string lib_prefix = lib->GetName();
for (auto cls : lib->classes) {
std::string cls_prefix = cls->Name();
for (auto dartFn : cls->Functions()) {
const auto ep = dartFn->Address();
auto name = DartDumper::getFunctionName4Ida(*dartFn, cls_prefix);
of << "[" << std::format("{:#x}, \"{}_{}::{}_{:x}\"", ep, lib_prefix, cls_prefix, name.c_str(), ep) << "],\n";
}
}
}
of << "]);\n}\n";

}
95 changes: 85 additions & 10 deletions scripts/frida.template.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,95 @@
function GetOptions(){
const opts = new BlutterOpts();
opts.IgnoreByClassFuncRegex(/(anim|battery|anon_|build|widget|Dependencies|Observer|Render)/i);
//opts.SpyByClassFuncRegex(/interestingclass.+::func_prefix.+/,4);
// opts.SpyByFunctionAddy(0x2fd950, "ImportantFunc",10);
return opts;
}
const ShowNullField = false;
const MaxDepth = 5;
const WriteFuncNameBeforeTrace = true; //useful if it may crash to know what caused it
var libapp = null;

function onLibappLoaded() {
xxx("remove this line and correct the hook value");
const fn_addr = 0xdeadbeef;
Interceptor.attach(libapp.add(fn_addr), {
onEnter: function () {
init(this.context);
let objPtr = getArg(this.context, 0);
const [tptr, cls, values] = getTaggedObjectValue(objPtr);
console.log(`${cls.name}@${tptr.toString().slice(2)} =`, JSON.stringify(values, null, 2));
class BlutterOpts {

ignoreRegexes = [];
spyRegexes = [];
SpyFunctions = [];
spyCount = 0;

//similar to SpyByClassFuncRegex except any regex matches here will explicitly not be spied on. Good to be able to cast a wide net and then exclude things you don't care about or that cause crashes.
IgnoreByClassFuncRegex(regex){
this.ignoreRegexes.push(new SpyRegex(regex));
}
// Spys on a function by matching the provided js regex against the ida formatted class/function name that is in the format similar to: my_lib$bean$pathwith_bean_ClassINfoBean::get_someval_311eb0_check
SpyByClassFuncRegex(regex,maxDepth=MaxDepth){
this.spyRegexes.push(new SpyRegex(regex,maxDepth));
}
// takes an address in the form of 0x2fd950 for example, you can find a list of functions and their addresses in the generated ida_script/addNames.py file, you can optionally pass a display name to show when entered
SpyByFunctionAddy(address, name=""){
this.SpyFunctions.push(new SpyFunc(address,name));
}
SetSpys(){
if (this.spyRegexes.length > 0){
const FuncPtrToName = GetFuncPtrMap();
for( const [fnptr, fnname] of FuncPtrToName.entries()) {
let wasMatch=false;
let maxDepth=-1;
for (const spy of this.spyRegexes) {
if (spy.regex.test(fnname)){
wasMatch=true;
if (spy.maxDepth > maxDepth)
maxDepth = spy.maxDepth;
}
}
if (wasMatch){
for (const spy of this.ignoreRegexes)
if (spy.regex.test(fnname))
wasMatch=false;
}
if (wasMatch)
this.SpyByFunctionAddy(fnptr,fnname,maxDepth);

}
}
});
for (const spy of this.SpyFunctions)
this._SetSpy(spy);
}
_SetSpy(spy){
const fullName = spy.name;
const maxDepth = spy.maxDepth;
Interceptor.attach(libapp.add(spy.address), {
onEnter: function () {
if (WriteFuncNameBeforeTrace && fullName)
console.log(`${fullName}..`);
init(this.context);
let objPtr = getArg(this.context, 0);
const [tptr, cls, values] = getTaggedObjectValue(objPtr,maxDepth);
console.log(`${fullName} ${cls.name}@${tptr.toString().slice(2)} =`, JSON.stringify(values, null, 2));
}
});
console.log(`Blutter Intercept #${++this.spyCount}: ${fullName} (${spy.address})`);
}
}
class SpyRegex {
constructor(regex, maxDepth = MaxDepth){
this.maxDepth = maxDepth;
this.regex = regex;
}
}
class SpyFunc {
constructor(address,name="",maxDepth=MaxDepth){
this.address = address;
this.name = name;
this.maxDepth = maxDepth;
}
}

function onLibappLoaded() {
const opts = GetOptions();
opts.SetSpys();

}
function tryLoadLibapp() {
libapp = Module.findBaseAddress('libapp.so');
if (libapp === null)
Expand Down