Skip to content

Commit d624db6

Browse files
committed
Add dump command to cli
1 parent ac8b1e3 commit d624db6

File tree

11 files changed

+898
-3
lines changed

11 files changed

+898
-3
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,53 @@ or
3939
npm exec frida-il2cpp-bridge -- --help
4040
```
4141

42+
### Dumping
43+
44+
Use the `dump` subcommand to dump an application:
45+
46+
```
47+
$ npm exec frida-il2cpp-bridge -- dump --help
48+
usage: frida-il2cpp-bridge [options] dump [-h] [--out-dir OUT_DIR] [--cs-output {none,stdout,flat,tree}] [--no-namespaces] [--flatten-nested-classes] [--keep-implicit-base-classes]
49+
[--enums-as-structs] [--no-type-keywords] [--actual-constructor-names] [--indentation-size INDENTATION_SIZE]
50+
51+
options:
52+
-h, --help show this help message and exit
53+
--out-dir OUT_DIR where to save the dump (defaults to current working dir)
54+
--cs-output {none,stdout,flat,tree}
55+
style of C# output (defaults to tree)
56+
- none: do nothing;
57+
- stdout: print to console;
58+
- flat: one single file (dump.cs);
59+
- tree: directory structure having one file per assembly.
60+
--no-namespaces do not emit namespace blocks, and prepend namespace name in class declarations
61+
--flatten-nested-classes
62+
write nested classes at the same level of their inclosing classes, and prepend enclosing class name in their declarations
63+
--keep-implicit-base-classes
64+
write implicit base classes (class -> System.Object, struct -> System.ValueType, enum -> System.Enum) in class declarations
65+
--enums-as-structs write enum class declarations as structs
66+
--no-type-keywords use fully qualified names for builtin types instead of their keywords (e.g. use 'System.Int32' instead of 'int', or 'System.Object' instead of 'object')
67+
--actual-constructor-names
68+
write actual constructors names (e.g. '.ctor' and '.cctor')
69+
--indentation-size INDENTATION_SIZE
70+
indentation size (defaults to 4)
71+
```
72+
73+
Example:
74+
75+
```sh
76+
npm exec frida-il2cpp-bridge -- -f com.example.application dump --out-dir dumps
77+
```
78+
79+
Output:
80+
81+
```
82+
Spawning `com.example.application`...
83+
IL2CPP module loaded in 1.13s (id=com.example.application, version=1.12.8, unity version=2019.3.0f1)
84+
Dumping mscorlib: 2872 of 2872 classes
85+
Dumping GameAssembly: 32 of 32 classes
86+
Collected 2904 classes in 4.76s
87+
Dump saved to dumps/com.example.application/1.12.8
88+
```
4289

4390
## Testing
4491

cli/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
#!/usr/bin/env python3
22

33
from src.app import FridaIl2CppBridgeApplication
4+
from src.dump.command import DumpCommand
45

56

67
if __name__ == "__main__":
78
try:
8-
FridaIl2CppBridgeApplication(commands=[]).run()
9+
FridaIl2CppBridgeApplication(commands=[DumpCommand]).run()
910
except KeyboardInterrupt:
1011
pass

cli/src/dump/__init__.py

Whitespace-only changes.

cli/src/dump/agent.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/// <reference path="../../../lib/index.ts">/>
2+
const t0 = new Date();
3+
setTimeout(() => {
4+
Il2Cpp.perform(() => {
5+
send({ action: "init", elapsed_ms: new Date() - t0, application: Il2Cpp.application, unityVersion: Il2Cpp.unityVersion });
6+
const t1 = new Date();
7+
Il2Cpp.domain.assemblies.forEach(assembly => {
8+
send({
9+
type: "assembly",
10+
handle: assembly.handle,
11+
name: assembly.name,
12+
class_count: assembly.image.classCount
13+
});
14+
assembly.image.classes.forEach((klass, i) =>
15+
send({
16+
type: "class",
17+
nth: i + 1,
18+
assembly_handle: assembly.handle,
19+
handle: klass.handle,
20+
namespace: klass.namespace,
21+
name: klass.name,
22+
declaring_class_handle: klass.declaringClass?.handle,
23+
kind: klass.isEnum ? `enum` : klass.isStruct ? `struct` : klass.isInterface ? `interface` : `class`,
24+
generics_type_names: klass.generics.map(_ => _.type.name),
25+
parent_type_name: klass.parent?.type.name,
26+
interfaces_type_names: klass.interfaces.map(_ => _.type.name),
27+
fields: klass.fields.map(field => ({
28+
name: field.name,
29+
type_name: field.type.name,
30+
is_thread_static: field.isThreadStatic,
31+
is_static: field.isStatic,
32+
is_literal: field.isLiteral,
33+
value: field.isLiteral ? (field.type.class.isEnum ? field.value.field("value__").value : field.value)?.toString() : undefined,
34+
offset: field.isThreadStatic || field.isLiteral ? undefined : field.offset
35+
})),
36+
methods: klass.methods.map(method => ({
37+
is_static: method.isStatic,
38+
name: method.name,
39+
return_type_name: method.returnType.name,
40+
generics_type_names: method.generics.map(_ => _.type.name),
41+
parameters: method.parameters.map(parameter => ({
42+
position: parameter.position,
43+
name: parameter.name,
44+
type_name: parameter.type.name
45+
})),
46+
offset: method.virtualAddress.isNull() ? undefined : method.relativeVirtualAddress
47+
}))
48+
})
49+
);
50+
});
51+
return new Date() - t1;
52+
}).then(_ => send({ action: "exit", elapsed_ms: _ }));
53+
});

cli/src/dump/command.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
from typing import override
2+
3+
import argparse
4+
from pathlib import Path
5+
6+
import colorama
7+
8+
from ..app import FridaIl2CppBridgeCommand
9+
from .dumper import Dumper
10+
from .models import AssemblyHandle, ClassHandle, AssemblyDump, ClassDump
11+
12+
13+
class DumpCommand(FridaIl2CppBridgeCommand[AssemblyDump | ClassDump, dict]):
14+
NAME = "dump"
15+
16+
def __init__(self, *args, **kwargs):
17+
self._assemblies_dump: dict[AssemblyHandle, AssemblyDump] = {}
18+
self._classes_dump: dict[ClassHandle, ClassDump] = {}
19+
super().__init__(*args, **kwargs)
20+
21+
@property
22+
def agent_src(self) -> str:
23+
with open(
24+
Path(__file__).parent / "agent.js", mode="r", encoding="utf-8"
25+
) as file:
26+
return file.read()
27+
28+
@property
29+
def parser(self) -> dict:
30+
return dict(
31+
help="performs a dump of the target application",
32+
formatter_class=argparse.RawTextHelpFormatter,
33+
)
34+
35+
@override
36+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
37+
parser.add_argument(
38+
"--out-dir",
39+
type=Path,
40+
default=Path.cwd(),
41+
help="where to save the dump (defaults to current working dir)",
42+
)
43+
parser.add_argument(
44+
"--cs-output",
45+
choices=["none", "stdout", "flat", "tree"],
46+
default="tree",
47+
help=(
48+
"style of C# output (defaults to tree)\n"
49+
"- none: do nothing;\n"
50+
"- stdout: print to console;\n"
51+
"- flat: one single file (dump.cs);\n"
52+
"- tree: directory structure having one file per assembly."
53+
),
54+
)
55+
parser.add_argument(
56+
"--no-namespaces",
57+
action="store_true",
58+
default=False,
59+
help="do not emit namespace blocks, and prepend namespace name in class declarations",
60+
)
61+
parser.add_argument(
62+
"--flatten-nested-classes",
63+
action="store_true",
64+
default=False,
65+
help="write nested classes at the same level of their inclosing classes, and prepend enclosing class name in their declarations",
66+
)
67+
parser.add_argument(
68+
"--keep-implicit-base-classes",
69+
action="store_true",
70+
default=False,
71+
help="write implicit base classes (class -> System.Object, struct -> System.ValueType, enum -> System.Enum) in class declarations",
72+
)
73+
parser.add_argument(
74+
"--enums-as-structs",
75+
action="store_true",
76+
default=False,
77+
help="write enum class declarations as structs",
78+
)
79+
parser.add_argument(
80+
"--no-type-keywords",
81+
action="store_true",
82+
default=False,
83+
help="use fully qualified names for builtin types instead of their keywords (e.g. use 'System.Int32' instead of 'int', or 'System.Object' instead of 'object')",
84+
)
85+
parser.add_argument(
86+
"--actual-constructor-names",
87+
action="store_true",
88+
default=False,
89+
help="write actual constructors names (e.g. '.ctor' and '.cctor')",
90+
)
91+
parser.add_argument(
92+
"--indentation-size",
93+
type=int,
94+
default=4,
95+
help="indentation size (defaults to 4)",
96+
)
97+
98+
@override
99+
def on_send(self, payload: AssemblyDump | ClassDump):
100+
if payload["type"] == "assembly":
101+
self._assemblies_dump[payload["handle"]] = payload
102+
assembly = self._assemblies_dump[payload["handle"]]
103+
self.app.next_status()
104+
elif payload["type"] == "class":
105+
self._classes_dump[payload["handle"]] = payload
106+
assembly = self._assemblies_dump[payload["assembly_handle"]]
107+
else:
108+
raise ValueError(f"Unknow dump type {payload}")
109+
110+
self.app.update_status(
111+
f"Dumping {colorama.Fore.BLUE}{assembly['name']}{colorama.Fore.RESET}: {payload.get('nth', 1)} of {assembly['class_count']} classes"
112+
)
113+
114+
@override
115+
def on_exit(self, payload: dict):
116+
self.app.print(
117+
f"Collected {colorama.Style.BRIGHT}{colorama.Fore.GREEN}{len(self._classes_dump)}{colorama.Style.RESET_ALL} classes in {payload['elapsed_ms'] / 1000:.2f}s"
118+
)
119+
120+
if self.app.options.cs_output != "none":
121+
if self.app.options.cs_output != "stdout":
122+
self.app.update_status("Saving dump...")
123+
124+
dumper = Dumper(
125+
assemblies_dump=self._assemblies_dump,
126+
classes_dump=self._classes_dump,
127+
output_base_path=self._output_base_path,
128+
config=Dumper.Config(
129+
one_file_per_assembly=self.app.options.cs_output == "tree",
130+
emit_namespaces=not self.app.options.no_namespaces,
131+
flatten_nested_classes=self.app.options.flatten_nested_classes,
132+
keep_implicit_base_classes=self.app.options.keep_implicit_base_classes,
133+
enums_as_structs=self.app.options.enums_as_structs,
134+
use_type_keywords=not self.app.options.no_type_keywords,
135+
use_actual_constructor_names=self.app.options.actual_constructor_names,
136+
indentation_size=self.app.options.indentation_size,
137+
),
138+
)
139+
dumper.dump()
140+
141+
if self.app.options.cs_output != "stdout":
142+
self.app.update_status(f"Dump saved to {dumper.output_base_path}")
143+
144+
@property
145+
def _output_base_path(self) -> Path | None:
146+
if self.app.options.cs_output != "stdout":
147+
return (
148+
self.app.options.out_dir.resolve().absolute()
149+
/ self.app.target["identifier"]
150+
/ self.app.target["version"]
151+
)
152+
else:
153+
return None

0 commit comments

Comments
 (0)