Copy Link
Add to Bookmark
Report
Phrack Inc. Volume 16 Issue 71 File 11
==Phrack Inc.==
Volume 0x10, Issue 0x47, Phile #0x0B of 0x11
|=-----------------------------------------------------------------------=|
|=---------------=[ Reversing Dart AOT snapshots ]=----------------------=|
|=-----------------------------------------------------------------------=|
|=--------------------------=[ cryptax ]=--------------------------------=|
|=-----------------------------------------------------------------------=|
-- Table of contents
0 - Introduction
1 - First steps at disassembling an AOT snapshot
1.1 No entry point
1.2 Function prologue
1.3 Access to strings
1.4 Function arguments are pushed on the stack
1.5 Small integers are doubled
2 - Dart assembly registers
3 - The THR register
4 - The Dart Object Pool
5 - Snapshot serialization
6 - Representation of integers
7 - Function names
7.1 Stripped or non-stripped binaries
7.2 Trick for simple programs
7.3 Retrieving function names in more complex situations
8 - Conclusion and perspectives
-- 0 - Introduction
Dart is an object-oriented programming language with a C-style syntax, and
a few features such as sound null safety. Depending on the desired size vs
performance trade-off, a Dart program can be compiled in various formats:
kernel snapshots (the smallest, but the slowest), JIT snapshots, AOT
snapshots, and self-contained executables (the biggest and fastest) [1].
Dart AOT snapshots offer a particular interesting ratio and are therefore
used by Flutter release builds [2]. Flutter is an open source UI software
development kit which offers the attractive ability to develop
applications with a single code-base and compile them natively for Android
and iOS, and also non-mobile platforms.
The issue for reverse engineers is that Dart AOT snapshots are notably
difficult to reverse for the following main reasons:
1. The produced assembly code uses many unique features: specific
registers, specific calling conventions, specific encoding of
integers.
2. Information about each class used in the snapshot can only be
read sequentially. There is no random access, meaning that it is
necessary to read information about lots of potentially non-
interesting classes before we get to the one we are looking for.
3. The format is not documented and has significantly evolved since
the first versions.
In this article, we will explain how to understand Dart assembly, and get
the best out of disassemblers, even when they don't support Dart.
-- 1 - First steps at disassembling an AOT snapshot
To illustrate Dart assembly code, we'll work over a simple implementation
of the Caesar algorithm in Dart (alphabet translation by 3).
We encrypt/decrypt a string containing the sentence "Phrack Issue"
followed by a randomly selected issue number.
import 'dart:math'; // for Random
class Caesar {
int shift;
Caesar({this.shift = 3});
String encrypt(String message) {
StringBuffer ciphertext = StringBuffer();
for (int i = 0; i < message.length; i++) {
int charCode = message.codeUnitAt(i);
charCode = (charCode + shift) % 256;
ciphertext.writeCharCode(charCode);
}
return ciphertext.toString();
}
String decrypt(String ciphertext) {
this.shift = -this.shift;
String plaintext = this.encrypt(ciphertext);
this.shift = -this.shift;
return plaintext;
}
}
void main() {
print('Welcome to Caesar encryption');
List<int> issues = [ 70, 71, 72 ];
Random random = Random();
final String message = 'Phrack Issue ${issues[random.nextInt(issues.length)]}';
var caesar = Caesar();
// Encrypt
String ciphertext = caesar.encrypt(message);
print(ciphertext);
// Decrypt
String plaintext = caesar.decrypt(ciphertext);
print(plaintext);
}
This source code can be compiled to the "AOT snapshot" output format (.aot
extension) using the Dart compiler:
$ dart compile aot-snapshot phrack.dart
Generated: /tmp/caesar/phrack.aot
The resulting snapshot is quite big for very simple code: 831,352 bytes
for the non stripped version, and 541,616 bytes for the stripped version
(option -S).
Let's begin with the non-stripped AOT snapshot, and load it in a
disassembler. In this article, we'll use Radare 2 [3], but the result is
largely the same with any disassembler (IDA Pro, Binary Ninja, Ghidra...).
-- 1.1 - No entry point
First of all, the disassembler fails to identify the entry point:
ERROR: Cannot determine entrypoint, using 0x0004c000
The reason for this is that the disassembler does not understand the
format of the AOT snapshot. Actually, a "Dart AOT snapshot" contains at
least 2 snapshots: one AOT snapshot for Dart itself (Dart VM), and one
AOT snapshot per isolate.
A Dart isolate is an independent unit of execution that runs concurrently
with other isolates. Each isolate has its own memory heap, stack and event
loop. There is always at least 1 isolate, possibly more if the application
needs to handle background tasks while displaying other data, for instance.
In the example below, the file contains the minimum 2 snapshots:
$ objdump -T ./phrack.aot
./phrack.aot: file format elf64-x86-64
DYNAMIC SYMBOL TABLE:
000000000004c000 g DO .text 0000000000006860 _kDartVmSnapshotInstructions
0000000000052880 g DO .text 0000000000046910 _kDartIsolateSnapshotInstructions
0000000000000200 g DO .rodata 0000000000008a10 _kDartVmSnapshotData
0000000000008c40 g DO .rodata 000000000003f9d0 _kDartIsolateSnapshotData
00000000000001c8 g DO .note.gnu.build-id 0000000000000020 _kDartSnapshotBuildId
Radare arbitrarily sets 0x4c000 as the entry point because it is the
address of the first symbol (kDartVmSnapshotInstructions). In reality,
the main() of our Dart program is contained in a Dart isolate snapshot,
and therefore its code is expected to be found within the text segment
named kDartIsolateSnapshotInstructions.
Fortunately, if the executable is not stripped, we can search for main in
function names to locate our entry point:
[0x0004c000]> afl~main
0x00096b3c 8 351 main
0x00097268 3 33 sym.main_1
sym.main_1 is a low level main() - just like __libc_start_main in C. The
real entry point for the Dart program is "main" at 0x00096b3c. In Radare,
we go to that address with the command "s" followed by the offset, and
retrieve the name of the current symbol with "is.". You can see that
main() is indeed in kDartVmSnapshotInstructions:
[0x0004c000]> s main
[0x00096b3c]> is.
nth paddr vaddr bind type size lib name demangled
2 0x00052880 0x00052880 GLOBAL OBJ 289040 _kDartIsolateSnapshotInstructions
-- 1.2 - Function prologue
The function prologue saves the base pointer on the stack and allocates
some space. Then, there is an instruction comparing the stack pointer with
an offset from register 14. What is this doing?
push rbp
mov rbp, rsp
sub rsp, 0x30
cmp rsp, qword [r14 + 0x38]
This is a Dart specificity that we'll discuss later. Let's first ask all
the questions.
-- 1.3 - Access to strings
Our program outputs the welcome message "Welcome to Caesar encryption". We
expect to see those ASCII characters loaded in the main at some point. For
example, in the assembly produced by a similar C program, we have:
lea rax, str.Welcome_to_Caesar_encryption
mov rdi, rax
call sym.imp.puts
The bytes at the address of symbol str.Welcome_to_Caesar_encryption are
the ASCII characters of the string. Reciprocally, if we search cross
references for this string ("axt"), we get the address of the lea
instruction:
[0x000012c2]> s str.Welcome_to_Caesar_encryption
[0x00002004]> px 20
- offset - 4 5 6 7 8 9 A B C D E F 1011 1213 456789ABCDEF0123
0x00002004 5765 6c63 6f6d 6520 746f 2043 6165 7361 Welcome to Caesa
0x00002014 7220 656e r en
[0x00002004]> axt
main 0x139b [DATA:r--] lea rax, str.Welcome_to_Caesar_encryption
With the Dart assembly, we have no such thing. Those are the instructions
before the first call to print(). One way or another, the string "Welcome
to Caesar encryption" has to be provided, but we can't see it. We can only
assume it is referenced by r15 + 0x168f, but what is r15, and where does
that go?
mov r11, qword [r15 + 0x168f]
mov qword [rsp], r11
call sym.printToConsole
From another angle, we do find the string in the list of strings ("iz") at
address 0x00033680, but there is apparently no reference to it ("axt" does
not return any hit):
[0x00096b3c]> iz~Welcome
2589 0x00033680 0x00033680 28 29 .rodata ascii Welcome to Caesar encryption
[0x00096b3c]> axt @ 0x00033680
So, this is yet another mystery to solve: how are strings accessed? What
is in r15? What is at r15 + 0x168f?
-- 1.4 - Function arguments are pushed on the stack
There is something else to notice in the Dart assembly above. Normally,
at least the first few arguments of a function are copied to dedicated
registers (the exact registers depend on the platform architecture). In
Dart assembly, notice how function arguments are copied on the stack:
mov qword [rsp], r11
call sym.printToConsole
The argument for the method printToConsole() is in r11. This argument is
copied to the address pointed at by rsp, the register stack pointer. This
does not follow standard conventions [4]. We'll even allow ourselves to
digress slightly: On x86-64, rsp is the name of the register holding a
pointer to the stack. On Aarch64, there is normally no such register and
Dart creates one, X15, that it uses as a stack pointer.
-- 1.5 - Small integers are doubled
In the Dart assembly code, just after the call to printToConsole, we
notice startling instructions concerning an array:
call sym.printToConsole
mov rbx, qword [r14 + 0x68]
mov r10d, 6
call sym.stub__iso_stub_AllocateArrayStub
mov qword [var_8h], rax
mov r11d, 0x8c
mov qword [rax + 0x17], r11
mov r11d, 0x8e
mov qword [rax + 0x1f], r11
mov r11d, 0x90
mov qword [rax + 0x27], r11
Our Dart source code has a single array: the array of Phrack issues with
values 70, 71 and 72 (in hexadecimal: 0x46, 0x47 and 0x48):
List<int> issues = [ 70, 71, 72 ];
Instead, the code appears to be loading values 0x8c, 0x8e and 0x90. Why?
This is the final mystery we'll solve in this article.
-- 2 - Dart assembly registers
In our previous experiments, we have encountered r14, r15, and we also
discussed X15 on Aarch64. The source code explains what these registers
are assigned to. For example, this is an excerpt of defined constants for
the x86-64 platform:
enum Register {
RAX = 0,
RCX = 1,
RDX = 2,
RBX = 3,
RSP = 4, // SP
RBP = 5, // FP
RSI = 6,
RDI = 7,
R8 = 8,
R9 = 9,
R10 = 10,
R11 = 11, // TMP
R12 = 12, // CODE_REG
R13 = 13,
R14 = 14, // THR
R15 = 15, // PP
...
}
...
// Caches object pool pointer in generated code.
const Register PP = R15;
...
const Register THR = R14; // Caches current thread in generated code.
The comments are particularly helpful. We learn Dart features a dedicated
register pointing to the object pool (PP), and another register pointing
to the current thread. In Aarch64 the comments explicitly assign x15 as
the Stack Pointer (SP), "SP in Dart code". The other registers, like the
Frame Pointer (FP), Link Register (LR) and Program Counter (PC), use the
default values for their architecture:
+ ------------ + ----- + ----- + ----- +
| | PP | THR | SP |
+ ------------ + ----- + ----- + ----- +
| x86-64 | r15 | r14 | rsp |
| Aarch32 | r5 | r10 | r13 |
| Aarch64 | x27 | x26 | x15 |
+ ------------ + ----- + ----- + ----- +
-- 3 - The THR register
We just said Dart dedicates a register to holding a pointer to the
current running thread. This is interesting in a reverse engineering
context because the offsets to various elements are known. For example,
we know that the stack limit is at THR + 0x38 (see: Dart SDK source code;
in runtime/vm/compiler/runtime_offsets_extracted.h, search for
Thread_stack_limit_offset).
This helps us solve the mystery we mentioned in 1.2. On x86-64, the THR
register is held by r14. So, the last assembly line compares the stack
pointer with the stack limit:
push rbp ; save base pointer on the stack
mov rbp, rsp ; update base pointer
sub rsp, 0x30 ; allocate space on the stack
cmp rsp, qword [r14 + 0x38] ; compare with stack limit
In other words, the last instruction ensures that the operation we
performed on the stack do not go beyond its limit, i.e. that there is no
stack overflow.
Similarly, we find that THR + 0x68 is a null object. So, the instructions
below actually pass a null object as argument to the constructor of the
Random class:
mov r11, qword [r14 + 0x68] ; store null object in r11
mov qword [rsp], r11 ; push r11 on the stack
call sym.new_Random ; call constructor for Random()
-- 4 - The Dart Object Pool
The Object Pool is a table which stores and references frequently used
objects, immediates and constants within a Dart program.
For example, this is an excerpt of an Object Pool. See how it contains
objects (InternetAddressType), strings ("Unexpected address type"),
lists, etc:
[pp+0x170] Obj!InternetAddressType@3a7c81 : {
off_8: int(0x2)
}
[pp+0x178] String: "Unexpected address type "
[pp+0x180] String: "%"
[pp+0x188] List(5) [0, 0x2, 0x2, 0x2, Null]
[pp+0x190] List(5) [0, 0x3, 0x3, 0x3, Null]
In the assembly code, objects from the Object Pool are no longer accessed
directly, but by an offset to the beginning of the pool. This value is
held by the dedicated PP register.
Let's go back to our string mystery (1.3), when we wondered where the
input string "Welcome to Caesar encryption" was. Such a string is held
in the Object Pool. In x86-64, the register to access the pool is r15.
We spot it just before the call to the encrypt() method. The instruction
loads an object from the object pool at offset 0x168f, and passes it on
the stack as an argument to printToConsole().
mov r11, qword [r15 + 0x168f]
mov qword [rsp], r11
call sym.printToConsole
As this is our first print, and we know it prints "Welcome to Caesar
encryption", we deduce the string is referenced in the Object Pool at
this offset. The reason for this is simple. If the reverse engineering
were more complex, we'd have nothing to guide us. The real issue is that
disassemblers do not read the Object Pool and let us know what is at a
given offset.
-- 5 - Snapshot serialization
Why aren't disassemblers reading the Object Pool? What's difficult about
that? To answer this question, we need to explain the AOT snapshot format.
A Dart AOT snapshot consists of :
- A Header. It holds a magic value (0xdcdcf5f5), the snapshot size, kind
and hash. The snapshot hash identifies the Dart SDK version.
- A Cluster Information structure. A cluster is a set of objects with
the same Dart type. For example, the structure contains the number
of clusters.
- Several serialized clusters. This as a raw dump of each cluster:
+----------------------------- +
+ Dart AOT Header +
+ ---------------------------- +
+ Cluster Information +
+ ---------------------------- +
+ Serialized Cluster 1 +
+ ---------------------------- +
+ Serialized Cluster 2 +
+ ---------------------------- +
+ Serialized Cluster 3 +
+ ---------------------------- +
+ ... +
+ ---------------------------- +
For reverse engineering, we wish to parse the AOT snapshot format.
Reading the header is easy. This is the snapshot header of our Phrack
AOT snapshot, parsed with a Flutter header parser[5]:
-----------
Snapshot
offset = 35904 (0x8c40)
size = 92106
kind = SnapshotKindEnum.kFullAOT
dart sdk version = 3.3.0
features= product no-code_comments no-dwarf_stack_traces_mode
no-lazy_dispatchers dedup_instructions no-tsan no-asserts x64 linux
no-compressed-pointers null-safety
-----------
Reading the Cluster Information is slightly more difficult because it
uses a custom LEB128 format, but once we're aware of that, it poses no
more difficulty.
The complexity lies with reading serialized clusters. While we are mostly
interested in the serialized Object Pool (yes, the Object Pool is a Dart
type, therefore it is serialized in its own cluster), the Dart SDK has
over 150 clusters. Unfortunately, there is no way to reach a given cluster
(e.g. the Object Pool), we must de-serialize each cluster one by one until
we reach the one we are interested in. Said differently, there is no
random access in the snapshot, only sequential access. So, to de-serialize
the Object Pool, we must actually implement de-serialization of all
clusters, because we have no idea which cluster will be dumped before the
Object Pool.
This is lots of work, and an additional issue is that the Dart AOT format
is not officially documented and continues to evolve with new Dart SDK
versions. New versions change flags (for example, the header flag which
uses to indicate a "generic snapshot" is now used to identify an AOT
snapshot), but also many clusters have appeared. This is why tools such
as Darter [6] and Doldrum [7] unfortunately no longer work. In theory,
those tools could be ported to the current Dart SDK version, but it would
require extensive work, and we do not know how long that work would remain
operational.
To circumvent this issue, Blutter [8] uses another strategy. It implements
a Dart AOT snapshot dumper, compiled with the appropriate Dart SDK, and
uses it to parse the input snapshot. The tool reads the Object Pool and
dumps annotated assembly code. It is currently, however, limited to
Flutter applications for Android on Aarch64.
-- 6 - Representation of integers
Dart actually supports 2 types of integers: small integers (SMI) and big
integers, which are actually called "Mint" for Medium Integer. Small
integers fit in 31 bits. If they don't fit, they use the Mint type. The
least significant bit is reserved as an indicator: 0 for SMI, and 1 for
Mint:
+ -------------------------------- + - +
| 31 30 39 ..................... 1 | 0 |
+ -------------------------------- + - +
| Value | I |
+ -------------------------------- + - +
The immediate consequence to this design choice is that all small integers
appear to have their value multiplied by 2.
If we go back to the assembly of 1.5, the instructions appear to be
loading values 0x8c, 0x8e and 0x90:
mov r10d, 6
call sym.stub__iso_stub_AllocateArrayStub
mov qword [var_8h], rax
mov r11d, 0x8c
mov qword [rax + 0x17], r11
mov r11d, 0x8e
mov qword [rax + 0x1f], r11
mov r11d, 0x90
mov qword [rax + 0x27], r11
However, if we look more closely according to Dart's representation, the
least significant bit of each of those values is 0. Thus, they are SMIs,
and their value fits on bits 1-31. The represented values are consequently
0x8c / 2 = 70, 71 and 72 - which are the 3 integers we put in our integer
array.
The same applies to the first instruction: the apparent value of 6 is
provided as argument to the array stub function. This is a SMI, so we
are initializing an array of 3 cells (6 divided by 2).
For reverse engineering, knowing about this integer representation is
particularly useful when strings are represented as lists of ASCII code
values. When the ASCII code for character A is 0x41, the assembly will
actually need to load a hexadecimal literal of 0x82.
In Radare, the representation of Small Integers can be handled by a simple
r2pipe script [9]. For example, in the assembly below, the comments for
the 3 small integers were generated by the script:
mov r11d, 0x8c ; Load 0x46 (decimal=70, character="F")
mov qword [rax + 0x17], r11
mov r11d, 0x8e ; Load 0x47 (decimal=71, character="G")
mov qword [rax + 0x1f], r11
mov r11d, 0x90 ; Load 0x48 (decimal=72, character="H")
mov qword [rax + 0x27], r11
-- 7 - Function names
-- 7.1 - Stripped or non-stripped binaries
When Dart AOT snapshots are not stripped, disassemblers easily find
function names. For example, these are all methods of the Caesar class:
[0x0009ec7c]> afl~Caesar
0x00096c9c 3 80 sym.Caesar.decrypt
0x00096d28 10 245 sym.Caesar.encrypt
0x00096e20 1 11 sym.new_Caesar
But, naturally, AOT snapshots can be stripped (-S option at compilation
time), and disassemblers are unable to recover function names and generate
dummy names instead:
0x00050d34 20 490 fcn.00050d34
0x0005a0d8 3 121 fcn.0005a0d8
0x0005c440 6 129 fcn.0005c440
0x0007d210 1 30 fcn.0007d210
0x000768d0 1 90 fcn.000768d0
It is then particularly difficult to spot the main() or methods of the
Caesar class. They (probably) won't be at the same address, and there is
no easy way to locate them, as the assembly code contains no noticeable
string, no access to the Object Pool and no function name.
-- 7.2 - Trick for simple programs
In simple programs, we can search for particular instructions. For
example, our main() initializes an array of integers. Assigning the
first value is done with the instruction "mov r11d, 0x8c". We can search
for this instruction.
Note this technique is unlikely to yield good results in a real reverse
engineering situation, because (1) we don't know what to look for, (2) we
don't have access to the non stripped version, and (3) searching for an
instruction will return too many hits.
In the case of our simple Caesar program, the trick works and we are
extremely lucky to have a single hit:
[0x0007451f]> /ad mov r11d, 0x8c
0x000744b5 41bb8c000000 mov r11d, 0x8c
With several hits, we would have had to inspect the assembly lines around
the hit and check if it matches what the main() is expected to do.
We recognize the main as function fcn.00074480 (in Radare, command "afi"
tells you which function you are in, and "pi 15" disassemble 15
instructions):
[0x00034000]> s 0x000744b5
[0x000744b5]> afi~name
name: fcn.00074480
[0x000744b5]> s fcn.00074480
[0x00074480]> pi 15
push rbp
mov rbp, rsp
sub rsp, 0x28
cmp rsp, qword [r14 + 0x38]
jbe 0x745cd
mov r11, qword [r15 + 0x166f]
mov qword [rsp], r11
call fcn.00074ac4
mov rbx, qword [r14 + 0x68]
mov r10d, 6
call fcn.0007e968
mov qword [var_8h], rax
mov r11d, 0x8c
mov qword [rax + 0x17], r11
mov r11d, 0x8e
-- 7.3 - Retrieving function names in more complex situations
There are currently 3 workarounds:
1. JEB Pro Disassembler [10]. It is able to read the Object Pool and
retrieve function names in most situations. However, the tool is not
free and a license must be purchased.
2. reFlutter [11]. This open source tool patches the Flutter library to
dump function name offsets when it runs into them. The drawback with
this tool is that (1) it only works with Flutter applications, not
plain Dart snapshots, (2) the application needs to be recompiled with
the patched library, and (3) it is a dynamic analysis approach,
meaning reFlutter actually runs the application and only dumps parts
it gets into.
3. Blutter [8] is an other open source tool we have already mentioned.
It dumps assembly code with function names and their corresponding
offset. The tool currently only supports Android Flutter applications
generated for Aarch64.
For example, I have created a basic application with a basic widget
implementing the Caesar algorithm. The application has a class MyApp,
with a constructor and 2 methods: build(), which creates the widget, and
work() which performs Caesar encryption/decryption. I compiled the
application for Android Aarch64 and used Blutter on it:
// class id: 1442, size: 0xc, field offset: 0xc
// const constructor,
class MyApp extends StatelessWidget {
_ build(/* No info */) {
// ** addr: 0x221aec, size: 0x120
// 0x221aec: EnterFrame
// 0x221aec: stp fp, lr, [SP, #-0x10]!
// 0x221af0: mov fp, SP
// 0x221af4: AllocStack(0x28)
// 0x221af4: sub SP, SP, #0x28
// 0x221af8: CheckStackOverflow
// 0x221af8: ldr x16, [THR, #0x38] ; THR::stack_limit
...
_ work(/* No info */) {
// ** addr: 0x221c24, size: 0x288
// 0x221c24: EnterFrame
...
The dumped assembly shows:
- The address of build(): 0x221aec
- The address of xor_stage3(): 0x221c24
- And the instructions for both methods.
The instructions are annotated with the function name or the pool object
when the case applies, making the assembly easier to understand. For
example, see how Blutter shows the string "Welcome to Caesar encryption":
// 0x221c78: r16 = "Welcome to Caesar encryption"
// 0x221c78: ldr x16, [PP, #0x6e40] ; [pp+0x6e40] "Welcome to Caesar encryption"
// 0x221c7c: str x16, [SP]
// 0x221c80: r0 = printToConsole()
// 0x221c80: bl #0x159df4 ; [dart:_internal] ::printToConsole
Finally, remember that earlier we noticed the x86-64 assembly was passing
a null object, via THR + 0x68, as an argument to the constructor of the
Random class. In Blutter, we see the assembly for Aarch64 is different.
It doesn't use the THR register for that and explicitly passes NULL:
// 0x221cac: str NULL, [SP]
// 0x221cb0: r0 = Random()
// 0x221cb0: bl #0x206268 ; [dart:math] Random::Random
Overall, the different annotations of Blutter make assembly easier to
read, and it would be helpful to have them for other platforms and
integrate the same features in disassemblers.
-- 8 - Conclusion and perspectives
With this article, you should be able to understand the format of Dart
AOT snapshots, and grasp the complexity of parsing the Object Pool or
de-serialize any cluster.
We have explained the use of the dedicated THR and PP registers. You are
able to understand the assembly of function prologues, how strings or any
other object of the object pool is loaded, and how lists of integers are
represented.
We have also provided tricks and tools to parse the Object Pool and
recover function names, even in the case of stripped snapshots.
Major disassemblers are likely to add support for Dart in the next few
months or years. However, this is really only viable if the Dart SDK
becomes stable enough for such work to be worth it. Meanwhile, we seem
better off integrating strategies, such as Blutter, which recompile tools
from the Dart SDK.
-- References
[1] https://dart.dev/tools/dart-compile#types-of-output
[2] https://flutter.dev
[3] https://www.radare.org/
[4] https://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_calling_conventions
[5] https://github.com/cryptax/misc-code/blob/master/flutter/flutter-header.py
[6] https://github.com/mildsunrise/darter
[7] https://github.com/rscloura/Doldrums
[8] https://github.com/worawit/blutter
[9] https://github.com/cryptax/misc-code/blob/master/flutter/dart-bytes.py
[10] https://pnfsoftware.com
[11] https://github.com/Impact-I/reFlutter
|=[ EOF ]=---------------------------------------------------------------=|