Hi
This commit is contained in:
parent
82306f3005
commit
a91e74611b
37 changed files with 2699 additions and 1 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
target/
|
214
Cargo.lock
generated
Normal file
214
Cargo.lock
generated
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dumpcs-gen"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"il2cpp",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idapy-gen"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"il2cpp",
|
||||||
|
"metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "il2cpp"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "launcher"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"dumpcs-gen",
|
||||||
|
"idapy-gen",
|
||||||
|
"il2cpp",
|
||||||
|
"metadata",
|
||||||
|
"proto-gen",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "metadata"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"il2cpp",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proto-gen"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"il2cpp",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.91"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.58.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||||
|
dependencies = [
|
||||||
|
"windows-result",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[workspace]
|
||||||
|
members = ["dumpcs-gen", "idapy-gen", "il2cpp", "launcher", "metadata", "proto-gen"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
windows = "0.58.0"
|
||||||
|
|
||||||
|
il2cpp = { path = "il2cpp" }
|
||||||
|
metadata = { path = "metadata" }
|
||||||
|
dumpcs-gen = { path = "dumpcs-gen" }
|
||||||
|
idapy-gen = { path = "idapy-gen" }
|
||||||
|
proto-gen = { path = "proto-gen" }
|
21
README.md
21
README.md
|
@ -1,3 +1,22 @@
|
||||||
# GracefulDumper
|
# GracefulDumper
|
||||||
|
|
||||||
all-in-one dumper for Zenless Zone Zero written in Rust
|
## What
|
||||||
|
A proof-of-concept dumper for Zenless Zone Zero 0.2.0. Made for educational and recreational purposes.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
This software can help you with dumping:
|
||||||
|
- C# definitions
|
||||||
|
- Protocol Buffers definitions
|
||||||
|
##### Bonus: `script.json` for applying dumped names in IDA
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
This dumper is written in Rust, without use of any 3rd party dependencies (except `windows` crate for WINAPI). It also implements idiomatic rust bindings for internal il2cpp functions.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Just compile it, put the launcher.exe in client folder and run it. Launcher will inject itself and dump all the definitions after il2cpp initializes. Supported game client: `OSCBWin0.2.0`.
|
||||||
|
|
||||||
|
## Need help?
|
||||||
|
Consider joining our [discord server](https://discord.gg/reversedrooms)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
Contributions are welcome. You can submit them in form of a `.patch` file in our discord server. After review, it may be merged to upstream.
|
||||||
|
|
7
dumpcs-gen/Cargo.toml
Normal file
7
dumpcs-gen/Cargo.toml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[package]
|
||||||
|
name = "dumpcs-gen"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
il2cpp.workspace = true
|
190
dumpcs-gen/src/lib.rs
Normal file
190
dumpcs-gen/src/lib.rs
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
use il2cpp::vm::{attributes::*, Il2cppField, Il2cppMethod};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
use il2cpp::vm::{Il2cppDomain, Il2cppString};
|
||||||
|
|
||||||
|
unsafe fn write_assemblies_list<W: Write>(out: &mut W, domain: &Il2cppDomain) -> io::Result<()> {
|
||||||
|
for assembly in domain.assemblies() {
|
||||||
|
let image = assembly.image();
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
out,
|
||||||
|
"// Assembly {}, class count: {}",
|
||||||
|
image.name(),
|
||||||
|
image.get_class_count()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dump<W: Write>(out: &mut W) -> io::Result<()> {
|
||||||
|
il2cpp::ffi::il2cpp_gc_disable();
|
||||||
|
let domain = Il2cppDomain::get();
|
||||||
|
|
||||||
|
// First write assemblies list on top of dump.cs
|
||||||
|
write_assemblies_list(out, &domain)?;
|
||||||
|
|
||||||
|
for assembly in domain.assemblies() {
|
||||||
|
let image = assembly.image();
|
||||||
|
let image_name = image.name();
|
||||||
|
|
||||||
|
for i in 0..image.get_class_count() {
|
||||||
|
let class = image.get_class(i);
|
||||||
|
class.init();
|
||||||
|
class.init_methods();
|
||||||
|
|
||||||
|
let namespace = class.namespace();
|
||||||
|
|
||||||
|
writeln!(out, "// Assembly: {image_name}, Namespace: {namespace}")?;
|
||||||
|
write!(out, "class {}", class.il2cpp_type().name())?;
|
||||||
|
|
||||||
|
if let Some(parent) = class.parent_class() {
|
||||||
|
write!(out, " : {}", parent.il2cpp_type().name())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, interface) in class.interfaces().into_iter().enumerate() {
|
||||||
|
if i == 0 && class.parent_class().is_none() {
|
||||||
|
write!(out, " : {}", interface.il2cpp_type().name())?;
|
||||||
|
} else {
|
||||||
|
write!(out, ", {}", interface.il2cpp_type().name())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(out, " {{ // Token: 0x{:X}", class.token())?;
|
||||||
|
|
||||||
|
class
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.try_for_each(|field| write_class_field(out, field))?;
|
||||||
|
|
||||||
|
writeln!(out)?;
|
||||||
|
|
||||||
|
class
|
||||||
|
.methods()
|
||||||
|
.iter()
|
||||||
|
.try_for_each(|method| write_class_method(out, method))?;
|
||||||
|
|
||||||
|
writeln!(out, "}}\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_class_field<W: Write>(out: &mut W, field: &Il2cppField) -> io::Result<()> {
|
||||||
|
let field_type = field.il2cpp_type();
|
||||||
|
let attrs = field_type.attrs();
|
||||||
|
|
||||||
|
write!(out, " ")?;
|
||||||
|
prepend_field_modifiers(out, attrs)?;
|
||||||
|
write!(out, "{} {}", field_type.name(), field.name())?;
|
||||||
|
|
||||||
|
if attrs & FIELD_ATTRIBUTE_LITERAL != 0 {
|
||||||
|
let value = field.static_get_value();
|
||||||
|
|
||||||
|
match field_type.type_enum() {
|
||||||
|
0x3 => write!(out, " = '{}'", char::from(value as u8))?,
|
||||||
|
0xC => write!(out, " = {}", f32::from_bits(value as u32))?,
|
||||||
|
0xD => write!(out, " = {}", f64::from_bits(value as u64))?,
|
||||||
|
0xE => write!(
|
||||||
|
out,
|
||||||
|
" = \"{}\"",
|
||||||
|
Il2cppString(value as *const u8).to_string()
|
||||||
|
)?,
|
||||||
|
_ => write!(out, " = {value}")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
out,
|
||||||
|
"; // Offset: 0x{:X}, Token: 0x{:X}",
|
||||||
|
field.offset(),
|
||||||
|
field.token()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_class_method<W: Write>(out: &mut W, method: &Il2cppMethod) -> io::Result<()> {
|
||||||
|
write!(
|
||||||
|
out,
|
||||||
|
" // RVA: 0x{:X}\n ",
|
||||||
|
(method.address() != 0)
|
||||||
|
.then_some(method.address() - il2cpp::ffi::base())
|
||||||
|
.unwrap_or(0)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
prepend_method_modifiers(out, method.attrs())?;
|
||||||
|
write!(out, "{} {}(", method.return_type().name(), method.name())?;
|
||||||
|
|
||||||
|
for i in 0..method.arg_count() {
|
||||||
|
if i != 0 {
|
||||||
|
write!(out, ", ")?;
|
||||||
|
}
|
||||||
|
write!(out, "{} {}", method.arg_type(i).name(), method.arg_name(i))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(out, ") {{}}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepend_field_modifiers<W: Write>(out: &mut W, attrs: u32) -> io::Result<()> {
|
||||||
|
match attrs & FIELD_ATTRIBUTE_FIELD_ACCESS_MASK {
|
||||||
|
FIELD_ATTRIBUTE_PUBLIC => write!(out, "public "),
|
||||||
|
FIELD_ATTRIBUTE_PRIVATE => write!(out, "private "),
|
||||||
|
FIELD_ATTRIBUTE_FAMILY => write!(out, "protected "),
|
||||||
|
FIELD_ATTRIBUTE_ASSEMBLY | FIELD_ATTRIBUTE_FAM_AND_ASSEM => {
|
||||||
|
write!(out, "internal ")
|
||||||
|
}
|
||||||
|
FIELD_ATTRIBUTE_FAM_OR_ASSEM => write!(out, "protected internal "),
|
||||||
|
_ => Ok(()),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
if attrs & FIELD_ATTRIBUTE_LITERAL != 0 {
|
||||||
|
write!(out, "const ")?;
|
||||||
|
} else if attrs & FIELD_ATTRIBUTE_STATIC != 0 {
|
||||||
|
write!(out, "static ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if attrs & FIELD_ATTRIBUTE_INIT_ONLY != 0 {
|
||||||
|
write!(out, "readonly ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepend_method_modifiers<W: Write>(out: &mut W, attrs: u32) -> io::Result<()> {
|
||||||
|
match attrs & METHOD_ATTRIBUTE_MEMBER_ACCESS_MASK {
|
||||||
|
METHOD_ATTRIBUTE_PUBLIC => write!(out, "public "),
|
||||||
|
METHOD_ATTRIBUTE_PRIVATE => write!(out, "private "),
|
||||||
|
METHOD_ATTRIBUTE_FAMILY => write!(out, "protected "),
|
||||||
|
METHOD_ATTRIBUTE_ASSEM | METHOD_ATTRIBUTE_FAM_AND_ASSEM => {
|
||||||
|
write!(out, "internal ")
|
||||||
|
}
|
||||||
|
METHOD_ATTRIBUTE_FAM_OR_ASSEM => write!(out, "protected internal "),
|
||||||
|
_ => Ok(()),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
if attrs & METHOD_ATTRIBUTE_STATIC != 0 {
|
||||||
|
write!(out, "static ")?;
|
||||||
|
}
|
||||||
|
if attrs & METHOD_ATTRIBUTE_ABSTRACT != 0 {
|
||||||
|
write!(out, "abstract ")?;
|
||||||
|
if attrs & METHOD_ATTRIBUTE_VTABLE_LAYOUT_MASK == METHOD_ATTRIBUTE_REUSE_SLOT {
|
||||||
|
write!(out, "override ")?;
|
||||||
|
}
|
||||||
|
} else if attrs & METHOD_ATTRIBUTE_FINAL != 0 {
|
||||||
|
if attrs & METHOD_ATTRIBUTE_VTABLE_LAYOUT_MASK == METHOD_ATTRIBUTE_REUSE_SLOT {
|
||||||
|
write!(out, "sealed override ")?;
|
||||||
|
}
|
||||||
|
} else if attrs & METHOD_ATTRIBUTE_VIRTUAL != 0 {
|
||||||
|
if attrs & METHOD_ATTRIBUTE_VTABLE_LAYOUT_MASK == METHOD_ATTRIBUTE_NEW_SLOT {
|
||||||
|
write!(out, "virtual ")?;
|
||||||
|
} else {
|
||||||
|
write!(out, "override ")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if attrs & METHOD_ATTRIBUTE_PINVOKE_IMPL != 0 {
|
||||||
|
write!(out, "extern ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
8
idapy-gen/Cargo.toml
Normal file
8
idapy-gen/Cargo.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "idapy-gen"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
il2cpp.workspace = true
|
||||||
|
metadata.workspace = true
|
133
idapy-gen/src/lib.rs
Normal file
133
idapy-gen/src/lib.rs
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
use il2cpp::vm::Il2cppDomain;
|
||||||
|
use metadata::MetadataUsage;
|
||||||
|
use util::write_escaped_str;
|
||||||
|
|
||||||
|
// We don't want to overcomplicate this by using a full-fledged json library, so we're just building json by hand
|
||||||
|
pub unsafe fn write_to_file<W: Write>(out: &mut W) -> io::Result<()> {
|
||||||
|
write!(out, "{{")?;
|
||||||
|
|
||||||
|
write!(out, "\"ScriptString\": ")?;
|
||||||
|
write_string_literals(out)?;
|
||||||
|
write!(out, ", \"ScriptMetadata\": ")?;
|
||||||
|
write_type_info(out)?;
|
||||||
|
write!(out, ", \"ScriptMethod\": ")?;
|
||||||
|
write_methods(out)?;
|
||||||
|
write!(out, ", \"ScriptMetadataMethod\": ")?;
|
||||||
|
write_method_refs(out)?;
|
||||||
|
|
||||||
|
write!(out, "}}")
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn write_methods<W: Write>(out: &mut W) -> io::Result<()> {
|
||||||
|
write!(out, "[")?;
|
||||||
|
|
||||||
|
let mut first_write = true;
|
||||||
|
for assembly in Il2cppDomain::get().assemblies() {
|
||||||
|
let img = assembly.image();
|
||||||
|
for i in 0..img.get_class_count() {
|
||||||
|
let class = img.get_class(i);
|
||||||
|
class.init();
|
||||||
|
class.init_methods();
|
||||||
|
|
||||||
|
for method in class.methods() {
|
||||||
|
if !first_write {
|
||||||
|
write!(out, ", ")?;
|
||||||
|
}
|
||||||
|
first_write = false;
|
||||||
|
|
||||||
|
write!(
|
||||||
|
out,
|
||||||
|
"{{\"Address\":{}, \"Name\": \"{}$${}\", \"Signature\": \"\", \"TypeSignature\": \"\"}}",
|
||||||
|
method.address() - il2cpp::ffi::base(),
|
||||||
|
class.il2cpp_type().name(),
|
||||||
|
method.name(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(out, "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn write_method_refs<W: Write>(out: &mut W) -> io::Result<()> {
|
||||||
|
write!(out, "[")?;
|
||||||
|
|
||||||
|
let mut first_write = true;
|
||||||
|
for i in 0..metadata::USAGES_COUNT {
|
||||||
|
if let Some(entry) = metadata::get_usage_by_index(i) {
|
||||||
|
if let MetadataUsage::MethodRef(method) = entry.usage {
|
||||||
|
if !first_write {
|
||||||
|
write!(out, ", ")?;
|
||||||
|
}
|
||||||
|
first_write = false;
|
||||||
|
let type_name = method.class().il2cpp_type().name();
|
||||||
|
write!(
|
||||||
|
out,
|
||||||
|
"{{\"Address\":{}, \"Name\": \"{}_{}\", \"MethodAddress\": {}}}",
|
||||||
|
entry.address - il2cpp::ffi::base(),
|
||||||
|
&type_name,
|
||||||
|
&method.name(),
|
||||||
|
method.address() - il2cpp::ffi::base(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(out, "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn write_type_info<W: Write>(out: &mut W) -> io::Result<()> {
|
||||||
|
write!(out, "[")?;
|
||||||
|
|
||||||
|
let mut first_write = true;
|
||||||
|
for i in 0..metadata::USAGES_COUNT {
|
||||||
|
if let Some(entry) = metadata::get_usage_by_index(i) {
|
||||||
|
if let MetadataUsage::TypeInfo(class) = entry.usage {
|
||||||
|
if !first_write {
|
||||||
|
write!(out, ", ")?;
|
||||||
|
}
|
||||||
|
first_write = false;
|
||||||
|
let type_name = class.il2cpp_type().name();
|
||||||
|
write!(
|
||||||
|
out,
|
||||||
|
"{{\"Address\":{}, \"Name\": \"{}_TypeInfo\", \"Signature\": \"{}_c*\"}}",
|
||||||
|
entry.address - il2cpp::ffi::base(),
|
||||||
|
&type_name,
|
||||||
|
&type_name
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(out, "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn write_string_literals<W: Write>(out: &mut W) -> io::Result<()> {
|
||||||
|
write!(out, "[")?;
|
||||||
|
|
||||||
|
let mut first_write = true;
|
||||||
|
for i in 0..metadata::USAGES_COUNT {
|
||||||
|
if let Some(entry) = metadata::get_usage_by_index(i) {
|
||||||
|
if let MetadataUsage::StringLiteral(s) = entry.usage {
|
||||||
|
if !first_write {
|
||||||
|
write!(out, ", ")?;
|
||||||
|
}
|
||||||
|
first_write = false;
|
||||||
|
|
||||||
|
write!(
|
||||||
|
out,
|
||||||
|
"{{\"Address\":{},\"Value\":\"",
|
||||||
|
entry.address - il2cpp::ffi::base()
|
||||||
|
)?;
|
||||||
|
write_escaped_str(out, &s.to_string())?;
|
||||||
|
write!(out, "\"}}")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(out, "]")
|
||||||
|
}
|
136
idapy-gen/src/util.rs
Normal file
136
idapy-gen/src/util.rs
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
// Proper implementation of string escape
|
||||||
|
// https://github.com/serde-rs/json/blob/3f4e30a584cc4b81f288f83d79f5066cdba3af3b/src/ser.rs#L1515
|
||||||
|
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
pub fn write_escaped_str<W: Write>(w: &mut W, value: &str) -> io::Result<()> {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
let mut start = 0;
|
||||||
|
|
||||||
|
for (i, &byte) in bytes.iter().enumerate() {
|
||||||
|
let escape = ESCAPE[byte as usize];
|
||||||
|
if escape == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if start < i {
|
||||||
|
w.write_all(&value[start..i].as_bytes())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let char_escape = CharEscape::from_escape_table(escape, byte);
|
||||||
|
write_char_escape(w, char_escape)?;
|
||||||
|
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if start == bytes.len() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
w.write_all(&value[start..].as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_char_escape<W: Write>(w: &mut W, char_escape: CharEscape) -> io::Result<()> {
|
||||||
|
use self::CharEscape::*;
|
||||||
|
|
||||||
|
let s = match char_escape {
|
||||||
|
Quote => b"\\\"",
|
||||||
|
ReverseSolidus => b"\\\\",
|
||||||
|
Solidus => b"\\/",
|
||||||
|
Backspace => b"\\b",
|
||||||
|
FormFeed => b"\\f",
|
||||||
|
LineFeed => b"\\n",
|
||||||
|
CarriageReturn => b"\\r",
|
||||||
|
Tab => b"\\t",
|
||||||
|
|
||||||
|
// This breaks idapython, skipping
|
||||||
|
AsciiControl(_byte) => {
|
||||||
|
return Ok(());
|
||||||
|
// static HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";
|
||||||
|
// let bytes = &[
|
||||||
|
// b'\\',
|
||||||
|
// b'u',
|
||||||
|
// b'0',
|
||||||
|
// b'0',
|
||||||
|
// HEX_DIGITS[(byte >> 4) as usize],
|
||||||
|
// HEX_DIGITS[(byte & 0xF) as usize],
|
||||||
|
// ];
|
||||||
|
// return w.write_all(bytes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
w.write_all(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
const BB: u8 = b'b'; // \x08
|
||||||
|
const TT: u8 = b't'; // \x09
|
||||||
|
const NN: u8 = b'n'; // \x0A
|
||||||
|
const FF: u8 = b'f'; // \x0C
|
||||||
|
const RR: u8 = b'r'; // \x0D
|
||||||
|
const QU: u8 = b'"'; // \x22
|
||||||
|
const BS: u8 = b'\\'; // \x5C
|
||||||
|
const UU: u8 = b'u'; // \x00...\x1F except the ones above
|
||||||
|
const __: u8 = 0;
|
||||||
|
|
||||||
|
// Lookup table of escape sequences. A value of b'x' at index i means that byte
|
||||||
|
// i is escaped as "\x" in JSON. A value of 0 means that byte i is not escaped.
|
||||||
|
const ESCAPE: [u8; 256] = [
|
||||||
|
// 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||||
|
UU, UU, UU, UU, UU, UU, UU, UU, BB, TT, NN, UU, FF, RR, UU, UU, // 0
|
||||||
|
UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, // 1
|
||||||
|
__, __, QU, __, __, __, __, __, __, __, __, __, __, __, __, __, // 2
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 3
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 4
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, BS, __, __, __, // 5
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 6
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 7
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 8
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 9
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // A
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // B
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // C
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // D
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // E
|
||||||
|
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // F
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Represents a character escape code in a type-safe manner.
|
||||||
|
pub enum CharEscape {
|
||||||
|
/// An escaped quote `"`
|
||||||
|
Quote,
|
||||||
|
/// An escaped reverse solidus `\`
|
||||||
|
ReverseSolidus,
|
||||||
|
/// An escaped solidus `/`
|
||||||
|
#[expect(unused)]
|
||||||
|
Solidus,
|
||||||
|
/// An escaped backspace character (usually escaped as `\b`)
|
||||||
|
Backspace,
|
||||||
|
/// An escaped form feed character (usually escaped as `\f`)
|
||||||
|
FormFeed,
|
||||||
|
/// An escaped line feed character (usually escaped as `\n`)
|
||||||
|
LineFeed,
|
||||||
|
/// An escaped carriage return character (usually escaped as `\r`)
|
||||||
|
CarriageReturn,
|
||||||
|
/// An escaped tab character (usually escaped as `\t`)
|
||||||
|
Tab,
|
||||||
|
/// An escaped ASCII plane control character (usually escaped as
|
||||||
|
/// `\u00XX` where `XX` are two hex characters)
|
||||||
|
AsciiControl(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CharEscape {
|
||||||
|
#[inline]
|
||||||
|
fn from_escape_table(escape: u8, byte: u8) -> CharEscape {
|
||||||
|
match escape {
|
||||||
|
self::BB => CharEscape::Backspace,
|
||||||
|
self::TT => CharEscape::Tab,
|
||||||
|
self::NN => CharEscape::LineFeed,
|
||||||
|
self::FF => CharEscape::FormFeed,
|
||||||
|
self::RR => CharEscape::CarriageReturn,
|
||||||
|
self::QU => CharEscape::Quote,
|
||||||
|
self::BS => CharEscape::ReverseSolidus,
|
||||||
|
self::UU => CharEscape::AsciiControl(byte),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
il2cpp/Cargo.toml
Normal file
14
il2cpp/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "il2cpp"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
windows = { workspace = true, features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_System_SystemServices",
|
||||||
|
"Win32_System_LibraryLoader",
|
||||||
|
"Win32_System_Console",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_System_Memory",
|
||||||
|
] }
|
135
il2cpp/src/ffi.rs
Normal file
135
il2cpp/src/ffi.rs
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
pub use crate::util::base;
|
||||||
|
use crate::util::{self, import};
|
||||||
|
|
||||||
|
import!(il2cpp_gc_disable() -> () = 0xB89A70);
|
||||||
|
import!(il2cpp_domain_get() -> usize = 0xAA7F30);
|
||||||
|
import!(il2cpp_get_corlib() -> *const u8 = 0xAA82B0);
|
||||||
|
import!(il2cpp_domain_assembly_open(domain: usize, name: *const u8) -> usize = 0xAA7F20);
|
||||||
|
import!(il2cpp_domain_get_assemblies(domain: usize, size: &mut usize) -> *const usize = 0xAA7F40);
|
||||||
|
import!(il2cpp_assembly_get_image(assembly: *const u8) -> *const u8 = 0xAA7C10);
|
||||||
|
import!(il2cpp_class_from_name(image: *const u8, namespace: *const u8, name: *const u8) -> *const u8 = 0xAA7C70);
|
||||||
|
import!(il2cpp_image_get_class_count(image: *const u8) -> usize = 0xAA82E0);
|
||||||
|
import!(il2cpp_image_get_class(image: *const u8, index: usize) -> *const u8 = 0xAA82D0);
|
||||||
|
import!(il2cpp_image_get_name(image: *const u8) -> *const i8 = 0xAA82F0);
|
||||||
|
import!(il2cpp_class_from_il2cpp_type(il2cpp_type: *const u8) -> *const u8 = 0xAA7C60);
|
||||||
|
import!(il2cpp_class_get_name(class: *const u8) -> *const i8 = 0xAA7D70);
|
||||||
|
import!(il2cpp_class_get_namespace(class: *const u8) -> *const i8 = 0xAA7D80);
|
||||||
|
import!(il2cpp_field_get_name(field: *const u8) -> *const i8 = 0xAA7F90);
|
||||||
|
import!(il2cpp_field_get_type(field: *const u8) -> *const u128 = 0xAA7FC0);
|
||||||
|
import!(il2cpp_field_get_offset(field: *const u8) -> u32 = 0xAA7FA0);
|
||||||
|
import!(il2cpp_field_get_token(field: *const u8) -> u32 = 0xAEF8C0);
|
||||||
|
import!(il2cpp_vm_class_init(class: *const u8) -> () = 0xAF2220);
|
||||||
|
import!(il2cpp_vm_class_init_methods(class: *const u8) -> () = 0xB020F0);
|
||||||
|
import!(il2cpp_type_get_assembly_qualified_name(il2cpp_type: *const u8) -> *const i8 = 0xAA86F0);
|
||||||
|
import!(il2cpp_type_get_name(il2cpp_type: *const u128) -> *const i8 = 0xAA87B0);
|
||||||
|
import!(il2cpp_field_static_get_value(field: *const u8, out: *const usize) -> () = 0xB03400);
|
||||||
|
import!(il2cpp_method_get_name(method: *const u8) -> *const i8 = 0xAEB5F0);
|
||||||
|
import!(il2cpp_class_get_method_from_name(class: *const u8, name: *const i8, args_count: i32) -> *const u8 = 0xAEB1F0);
|
||||||
|
import!(il2cpp_method_get_return_type(method: *const u8) -> *const u8 = 0xAEBC40);
|
||||||
|
import!(il2cpp_array_new(ty: *const u8, size: u32) -> *const u8 = 0xBA99E0);
|
||||||
|
import!(il2cpp_object_new(class: *const u8) -> *const u8 = 0xBA9D80);
|
||||||
|
import!(il2cpp_runtime_invoke(method: *const u8, obj: *const u8, params: *const usize, exception: &mut usize) -> usize = 0xAA8510);
|
||||||
|
import!(il2cpp_thread_attach(domain: *const u8) -> () = 0xAA86B0);
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_is_fully_initialized() -> bool {
|
||||||
|
const G_IL2CPP_IS_FULLY_INITIALIZED: usize = 0xB225FC0;
|
||||||
|
|
||||||
|
*(util::base().wrapping_add(G_IL2CPP_IS_FULLY_INITIALIZED) as *const u8) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_class_get_token(class: *const u8) -> u32 {
|
||||||
|
*(class.wrapping_add(280) as *const u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_method_get_arg_count(method: *const u8) -> usize {
|
||||||
|
*method.wrapping_add(42) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_method_get_attrs(method: *const u8) -> u32 {
|
||||||
|
*method.wrapping_add(38) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_method_get_class(method: *const u8) -> *const u8 {
|
||||||
|
*(method.wrapping_add(8) as *const usize) as *const u8
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_method_get_arg_name(method: *const u8, index: usize) -> *const i8 {
|
||||||
|
import!(il2cpp_vm_method_get_arguments(method: *const u8) -> *const u8 = 0xAEB900);
|
||||||
|
let args = il2cpp_vm_method_get_arguments(method);
|
||||||
|
*(args.wrapping_add(0 + 24 * index) as *const usize) as *const i8
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_method_get_arg_type(method: *const u8, index: usize) -> *const u128 {
|
||||||
|
import!(il2cpp_vm_method_get_arguments(method: *const u8) -> *const u8 = 0xAEB900);
|
||||||
|
let args = il2cpp_vm_method_get_arguments(method);
|
||||||
|
*(args.wrapping_add(8 + 24 * index) as *const usize) as *const u128
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_class_get_methods(class: *const u8, count: &mut usize) -> *const usize {
|
||||||
|
*count = *class.wrapping_add(308).cast::<u16>() as usize;
|
||||||
|
(*class.wrapping_add(64).cast::<usize>()) as *const usize
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_method_get_address(method: *const u8) -> usize {
|
||||||
|
*method.wrapping_add(0).cast::<usize>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_type_get_attrs(ty: *const u128) -> u32 {
|
||||||
|
*(ty.wrapping_byte_add(8) as *const u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_class_get_interfaces(class: *const u8, count: &mut usize) -> *const usize {
|
||||||
|
*count = *(class.wrapping_add(300) as *const u16) as usize; // implementedInterfacesCount
|
||||||
|
*(class.wrapping_add(224) as *const usize) as *const usize
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_class_get_type(class: *const u8) -> *const u128 {
|
||||||
|
class.wrapping_add(160).cast::<u128>() // in-place struct
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_class_get_image(class: *const u8) -> *const u8 {
|
||||||
|
*(class.wrapping_add(200) as *const usize) as *const u8
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_class_get_fields(class: *const u8, count: &mut usize) -> *const u8 {
|
||||||
|
*count = *(class.wrapping_add(296) as *const u16) as usize;
|
||||||
|
*(class.wrapping_add(216) as *const usize) as *const u8
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_class_get_generic_class(class: *const u8) -> *const u8 {
|
||||||
|
(*class.wrapping_add(40).cast::<usize>()) as *const u8
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_class_get_parent(class: *const u8) -> *const u8 {
|
||||||
|
(*class.wrapping_add(104).cast::<usize>()) as *const u8
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_generic_class_get_generic_container(generic_class: *const u8) -> *const u8 {
|
||||||
|
(*generic_class.wrapping_add(8).cast::<usize>()) as *const u8
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_class_get_generic_arg_count(class: *const u8) -> usize {
|
||||||
|
let generic_class = il2cpp_class_get_generic_class(class);
|
||||||
|
if (generic_class as usize) != 0 {
|
||||||
|
let generic_container = il2cpp_generic_class_get_generic_container(generic_class);
|
||||||
|
if (generic_container as usize) != 0 {
|
||||||
|
return *generic_container.cast::<u32>() as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_class_get_generic_arg_type(class: *const u8, index: usize) -> *const u8 {
|
||||||
|
let generic_class = il2cpp_class_get_generic_class(class);
|
||||||
|
let generic_container = il2cpp_generic_class_get_generic_container(generic_class);
|
||||||
|
|
||||||
|
let argv = *generic_container.wrapping_add(8).cast::<usize>() as *const usize;
|
||||||
|
*(argv.wrapping_byte_add(index * 8)) as *const u8
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn il2cpp_class_is_generic(class: *const u8) -> bool {
|
||||||
|
let generic_class = il2cpp_class_get_generic_class(class);
|
||||||
|
(generic_class as usize) != 0
|
||||||
|
&& (il2cpp_generic_class_get_generic_container(generic_class) as usize) != 0
|
||||||
|
}
|
5
il2cpp/src/lib.rs
Normal file
5
il2cpp/src/lib.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod ffi;
|
||||||
|
mod util;
|
||||||
|
pub mod vm;
|
||||||
|
|
||||||
|
pub use util::cstr;
|
36
il2cpp/src/util.rs
Normal file
36
il2cpp/src/util.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use std::{borrow::Cow, sync::OnceLock};
|
||||||
|
|
||||||
|
use windows::{core::s, Win32::System::LibraryLoader::LoadLibraryA};
|
||||||
|
|
||||||
|
static BASE: OnceLock<usize> = OnceLock::new();
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn base() -> usize {
|
||||||
|
*BASE.get_or_init(|| unsafe { LoadLibraryA(s!("GameAssembly.dll")).unwrap().0 as usize })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn cstr(s: *const i8) -> Cow<'static, str> {
|
||||||
|
std::ffi::CStr::from_ptr(s).to_string_lossy()
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! import {
|
||||||
|
($name:ident($($arg_name:ident: $arg_type:ty),*) -> $ret_type:ty = $rva:expr) => {
|
||||||
|
pub unsafe fn $name($($arg_name: $arg_type,)*) -> $ret_type {
|
||||||
|
type FuncType = unsafe extern "fastcall" fn($($arg_type,)*) -> $ret_type;
|
||||||
|
::std::mem::transmute::<usize, FuncType>(crate::util::base() + $rva)($($arg_name,)*)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! as_cstr {
|
||||||
|
($s:expr) => {
|
||||||
|
::std::ffi::CString::new($s)
|
||||||
|
.unwrap()
|
||||||
|
.to_bytes_with_nul()
|
||||||
|
.as_ptr()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use as_cstr;
|
||||||
|
pub(crate) use import;
|
43
il2cpp/src/vm/array.rs
Normal file
43
il2cpp/src/vm/array.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Il2cppArray(pub *const u8);
|
||||||
|
|
||||||
|
impl From<usize> for Il2cppArray {
|
||||||
|
fn from(value: usize) -> Self {
|
||||||
|
Self(value as *const u8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Il2cppArray> for usize {
|
||||||
|
fn from(value: Il2cppArray) -> Self {
|
||||||
|
value.0 as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Il2cppArray {
|
||||||
|
pub fn new(array_type: &Il2cppClass, size: usize) -> Self {
|
||||||
|
unsafe { Self(il2cpp_array_new(array_type.0, size as u32)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn length(&self) -> usize {
|
||||||
|
unsafe { *self.0.wrapping_add(24).cast::<u32>() as usize }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data_ptr_raw(&self) -> *const u8 {
|
||||||
|
self.0.wrapping_add(32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_slice<T>(&self) -> &[T] {
|
||||||
|
unsafe {
|
||||||
|
std::slice::from_raw_parts(mem::transmute(self.0.wrapping_add(32)), self.length())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_mut_slice<T>(&self) -> &mut [T] {
|
||||||
|
unsafe {
|
||||||
|
std::slice::from_raw_parts_mut(mem::transmute(self.0.wrapping_add(32)), self.length())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
il2cpp/src/vm/assembly.rs
Normal file
18
il2cpp/src/vm/assembly.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Assembly(*const u8);
|
||||||
|
|
||||||
|
impl From<*const u8> for Assembly {
|
||||||
|
fn from(value: *const u8) -> Self {
|
||||||
|
(value as usize != 0)
|
||||||
|
.then_some(Self(value))
|
||||||
|
.expect("Assembly::from(null)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Assembly {
|
||||||
|
pub fn image(&self) -> Il2cppImage {
|
||||||
|
unsafe { Il2cppImage::from(il2cpp_assembly_get_image(self.0)) }
|
||||||
|
}
|
||||||
|
}
|
54
il2cpp/src/vm/attributes.rs
Normal file
54
il2cpp/src/vm/attributes.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
macro_rules! define_block {
|
||||||
|
($($name:ident $value:expr;)*) => {
|
||||||
|
$(
|
||||||
|
pub const $name: u32 = $value;
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field attribues
|
||||||
|
define_block! {
|
||||||
|
FIELD_ATTRIBUTE_FIELD_ACCESS_MASK 0x0007;
|
||||||
|
FIELD_ATTRIBUTE_COMPILER_CONTROLLED 0x0000;
|
||||||
|
FIELD_ATTRIBUTE_PRIVATE 0x0001;
|
||||||
|
FIELD_ATTRIBUTE_FAM_AND_ASSEM 0x0002;
|
||||||
|
FIELD_ATTRIBUTE_ASSEMBLY 0x0003;
|
||||||
|
FIELD_ATTRIBUTE_FAMILY 0x0004;
|
||||||
|
FIELD_ATTRIBUTE_FAM_OR_ASSEM 0x0005;
|
||||||
|
FIELD_ATTRIBUTE_PUBLIC 0x0006;
|
||||||
|
|
||||||
|
FIELD_ATTRIBUTE_STATIC 0x0010;
|
||||||
|
FIELD_ATTRIBUTE_INIT_ONLY 0x0020;
|
||||||
|
FIELD_ATTRIBUTE_LITERAL 0x0040;
|
||||||
|
FIELD_ATTRIBUTE_NOT_SERIALIZED 0x0080;
|
||||||
|
FIELD_ATTRIBUTE_SPECIAL_NAME 0x0200;
|
||||||
|
FIELD_ATTRIBUTE_PINVOKE_IMPL 0x2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method attributes
|
||||||
|
define_block! {
|
||||||
|
METHOD_ATTRIBUTE_MEMBER_ACCESS_MASK 0x0007;
|
||||||
|
METHOD_ATTRIBUTE_COMPILER_CONTROLLED 0x0000;
|
||||||
|
METHOD_ATTRIBUTE_PRIVATE 0x0001;
|
||||||
|
METHOD_ATTRIBUTE_FAM_AND_ASSEM 0x0002;
|
||||||
|
METHOD_ATTRIBUTE_ASSEM 0x0003;
|
||||||
|
METHOD_ATTRIBUTE_FAMILY 0x0004;
|
||||||
|
METHOD_ATTRIBUTE_FAM_OR_ASSEM 0x0005;
|
||||||
|
METHOD_ATTRIBUTE_PUBLIC 0x0006;
|
||||||
|
|
||||||
|
METHOD_ATTRIBUTE_STATIC 0x0010;
|
||||||
|
METHOD_ATTRIBUTE_FINAL 0x0020;
|
||||||
|
METHOD_ATTRIBUTE_VIRTUAL 0x0040;
|
||||||
|
METHOD_ATTRIBUTE_HIDE_BY_SIG 0x0080;
|
||||||
|
|
||||||
|
METHOD_ATTRIBUTE_VTABLE_LAYOUT_MASK 0x0100;
|
||||||
|
METHOD_ATTRIBUTE_REUSE_SLOT 0x0000;
|
||||||
|
METHOD_ATTRIBUTE_NEW_SLOT 0x0100;
|
||||||
|
|
||||||
|
METHOD_ATTRIBUTE_STRICT 0x0200;
|
||||||
|
METHOD_ATTRIBUTE_ABSTRACT 0x0400;
|
||||||
|
METHOD_ATTRIBUTE_SPECIAL_NAME 0x0800;
|
||||||
|
|
||||||
|
METHOD_ATTRIBUTE_PINVOKE_IMPL 0x2000;
|
||||||
|
METHOD_ATTRIBUTE_UNMANAGED_EXPORT 0x0008;
|
||||||
|
}
|
122
il2cpp/src/vm/class.rs
Normal file
122
il2cpp/src/vm/class.rs
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Il2cppClass(pub *const u8);
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Il2cppClass {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.il2cpp_type().name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Il2cppClass {
|
||||||
|
pub fn init(&self) {
|
||||||
|
unsafe { il2cpp_vm_class_init(self.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_methods(&self) {
|
||||||
|
unsafe { il2cpp_vm_class_init_methods(self.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> Cow<'static, str> {
|
||||||
|
unsafe { cstr(il2cpp_class_get_name(self.0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn namespace(&self) -> Cow<'static, str> {
|
||||||
|
unsafe { cstr(il2cpp_class_get_namespace(self.0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn image(&self) -> Il2cppImage {
|
||||||
|
unsafe { Il2cppImage::from(il2cpp_class_get_image(self.0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent_class(&self) -> Option<Self> {
|
||||||
|
let ptr = unsafe { il2cpp_class_get_parent(self.0) };
|
||||||
|
((ptr as usize) != 0).then_some(Self(ptr))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_generic_argument(&self, index: usize) -> Il2cppType {
|
||||||
|
unsafe { Il2cppType(il2cpp_class_get_generic_arg_type(self.0, index).cast()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_generic_argument_count(&self) -> usize {
|
||||||
|
unsafe { il2cpp_class_get_generic_arg_count(self.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interfaces(&self) -> &[Il2cppClass] {
|
||||||
|
unsafe {
|
||||||
|
let mut count = 0;
|
||||||
|
let interfaces = il2cpp_class_get_interfaces(self.0, &mut count);
|
||||||
|
(count != 0)
|
||||||
|
.then_some(std::slice::from_raw_parts(
|
||||||
|
mem::transmute(interfaces),
|
||||||
|
count,
|
||||||
|
))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fields(&self) -> &[Il2cppField] {
|
||||||
|
self.init();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let mut count = 0;
|
||||||
|
let fields = il2cpp_class_get_fields(self.0, &mut count);
|
||||||
|
std::slice::from_raw_parts(mem::transmute(fields), count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn methods(&self) -> &[Il2cppMethod] {
|
||||||
|
self.init_methods();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let mut count = 0;
|
||||||
|
let methods = il2cpp_class_get_methods(self.0, &mut count);
|
||||||
|
std::slice::from_raw_parts(mem::transmute(methods), count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_method(&self, name: &str, arg_types: &[&str]) -> Option<Il2cppMethod> {
|
||||||
|
self.init();
|
||||||
|
self.init_methods();
|
||||||
|
|
||||||
|
for method in self.methods() {
|
||||||
|
if method.name() == name {
|
||||||
|
if method.arg_count() == arg_types.len() {
|
||||||
|
let mut fail = false;
|
||||||
|
for i in 0..method.arg_count() {
|
||||||
|
if arg_types[i] != method.arg_type(i).name() {
|
||||||
|
fail = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fail {
|
||||||
|
return Some(Il2cppMethod(method.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_method(&self, name: &str, arg_count: i32) -> Option<Il2cppMethod> {
|
||||||
|
self.init();
|
||||||
|
self.init_methods();
|
||||||
|
|
||||||
|
let ptr = unsafe {
|
||||||
|
il2cpp_class_get_method_from_name(self.0, as_cstr!(name) as *const i8, arg_count)
|
||||||
|
};
|
||||||
|
|
||||||
|
(ptr as usize != 0).then_some(Il2cppMethod(ptr))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn il2cpp_type(&self) -> Il2cppType {
|
||||||
|
unsafe { Il2cppType(il2cpp_class_get_type(self.0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token(&self) -> u32 {
|
||||||
|
unsafe { il2cpp_class_get_token(self.0) }
|
||||||
|
}
|
||||||
|
}
|
29
il2cpp/src/vm/domain.rs
Normal file
29
il2cpp/src/vm/domain.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Il2cppDomain(usize);
|
||||||
|
|
||||||
|
impl Il2cppDomain {
|
||||||
|
pub fn get() -> Self {
|
||||||
|
unsafe { Il2cppDomain(il2cpp_domain_get()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attach_thread(&self) {
|
||||||
|
unsafe {
|
||||||
|
il2cpp_thread_attach(self.0 as *const u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assembly_open(&self, name: &str) -> Assembly {
|
||||||
|
unsafe { Assembly::from(il2cpp_domain_assembly_open(self.0, as_cstr!(name)) as *const u8) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assemblies(&self) -> &[Assembly] {
|
||||||
|
unsafe {
|
||||||
|
let mut count = 0;
|
||||||
|
let assemblies = il2cpp_domain_get_assemblies(self.0, &mut count);
|
||||||
|
|
||||||
|
std::slice::from_raw_parts(mem::transmute(assemblies), count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
il2cpp/src/vm/exception.rs
Normal file
65
il2cpp/src/vm/exception.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Il2cppException(Il2cppObject);
|
||||||
|
|
||||||
|
impl From<usize> for Il2cppException {
|
||||||
|
fn from(value: usize) -> Self {
|
||||||
|
Self(Il2cppObject::from(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Il2cppObject> for Il2cppException {
|
||||||
|
fn from(value: Il2cppObject) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Il2cppException {
|
||||||
|
pub fn object(&self) -> Il2cppObject {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message(&self) -> Il2cppString {
|
||||||
|
self.0
|
||||||
|
.class()
|
||||||
|
.get_method("get_Message", 0)
|
||||||
|
.unwrap()
|
||||||
|
.invoke(&self.0, &[])
|
||||||
|
.expect("failed to get exception message")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stack_trace(&self) -> Il2cppString {
|
||||||
|
self.0
|
||||||
|
.class()
|
||||||
|
.get_method("get_StackTrace", 0)
|
||||||
|
.unwrap()
|
||||||
|
.invoke(&self.0, &[])
|
||||||
|
.expect("failed to get exception stack trace")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Il2cppException {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Il2cppException")
|
||||||
|
.field("type", &self.0.class().il2cpp_type().name())
|
||||||
|
.field("message", &self.message().to_string())
|
||||||
|
.field("stack_trace", &self.stack_trace().to_string())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Il2cppException {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Exception: type: {}\nMessage: {}\nStackTrace:\n{}",
|
||||||
|
self.0.class().il2cpp_type().name(),
|
||||||
|
self.message(),
|
||||||
|
self.stack_trace()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Il2cppException {}
|
38
il2cpp/src/vm/field.rs
Normal file
38
il2cpp/src/vm/field.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Il2cppField([u8; 32]);
|
||||||
|
|
||||||
|
impl Il2cppField {
|
||||||
|
pub fn name(&self) -> Cow<'static, str> {
|
||||||
|
unsafe { cstr(il2cpp_field_get_name(self.0.as_ptr())) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn il2cpp_type(&self) -> Il2cppType {
|
||||||
|
unsafe { Il2cppType(il2cpp_field_get_type(self.0.as_ptr())) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn offset(&self) -> u32 {
|
||||||
|
unsafe { il2cpp_field_get_offset(self.0.as_ptr()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token(&self) -> u32 {
|
||||||
|
unsafe { il2cpp_field_get_token(self.0.as_ptr()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_instance(&self) -> bool {
|
||||||
|
self.il2cpp_type().attrs() & attributes::FIELD_ATTRIBUTE_STATIC == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn static_get_value(&self) -> usize {
|
||||||
|
unsafe {
|
||||||
|
let mut out = 0;
|
||||||
|
il2cpp_field_static_get_value(self.0.as_ptr(), &mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_field_data_ptr(&self, obj: &Il2cppObject) -> *const u8 {
|
||||||
|
obj.0.wrapping_add(self.offset() as usize)
|
||||||
|
}
|
||||||
|
}
|
31
il2cpp/src/vm/image.rs
Normal file
31
il2cpp/src/vm/image.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Il2cppImage(*const u8);
|
||||||
|
|
||||||
|
impl From<*const u8> for Il2cppImage {
|
||||||
|
fn from(value: *const u8) -> Self {
|
||||||
|
(value as usize != 0)
|
||||||
|
.then_some(Self(value))
|
||||||
|
.expect("Il2cppImage::from(null)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Il2cppImage {
|
||||||
|
pub fn name(&self) -> Cow<'static, str> {
|
||||||
|
unsafe { cstr(il2cpp_image_get_name(self.0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_class_count(&self) -> usize {
|
||||||
|
unsafe { il2cpp_image_get_class_count(self.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_class(&self, index: usize) -> Il2cppClass {
|
||||||
|
unsafe { Il2cppClass(il2cpp_image_get_class(self.0, index)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_class_by_name(&self, namespace: &str, name: &str) -> Option<Il2cppClass> {
|
||||||
|
let ptr = unsafe { il2cpp_class_from_name(self.0, as_cstr!(namespace), as_cstr!(name)) };
|
||||||
|
((ptr as usize) != 0).then_some(Il2cppClass(ptr))
|
||||||
|
}
|
||||||
|
}
|
62
il2cpp/src/vm/method.rs
Normal file
62
il2cpp/src/vm/method.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Il2cppMethod(pub *const u8);
|
||||||
|
|
||||||
|
impl Il2cppMethod {
|
||||||
|
pub fn name(&self) -> Cow<'static, str> {
|
||||||
|
unsafe { cstr(il2cpp_method_get_name(self.0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn address(&self) -> usize {
|
||||||
|
unsafe { il2cpp_method_get_address(self.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arg_count(&self) -> usize {
|
||||||
|
unsafe { il2cpp_method_get_arg_count(self.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arg_name(&self, index: usize) -> Cow<'static, str> {
|
||||||
|
unsafe { cstr(il2cpp_method_get_arg_name(self.0, index)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arg_type(&self, index: usize) -> Il2cppType {
|
||||||
|
unsafe { Il2cppType(il2cpp_method_get_arg_type(self.0, index)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn return_type(&self) -> Il2cppType {
|
||||||
|
unsafe { Il2cppType(il2cpp_method_get_return_type(self.0).cast()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attrs(&self) -> u32 {
|
||||||
|
unsafe { il2cpp_method_get_attrs(self.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn class(&self) -> Il2cppClass {
|
||||||
|
unsafe { Il2cppClass(il2cpp_method_get_class(self.0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invoke<T: From<usize>>(
|
||||||
|
&self,
|
||||||
|
instance: &dyn Il2cppValue,
|
||||||
|
args: &[&dyn Il2cppValue],
|
||||||
|
) -> Result<T, Il2cppException> {
|
||||||
|
let args = args.iter().map(|arg| arg.as_raw()).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut exception = 0;
|
||||||
|
let ret = unsafe {
|
||||||
|
il2cpp_runtime_invoke(
|
||||||
|
self.0,
|
||||||
|
instance.as_raw() as *const u8,
|
||||||
|
args.as_ptr() as *const usize,
|
||||||
|
&mut exception,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
(exception == 0)
|
||||||
|
.then_some(T::from(ret))
|
||||||
|
.ok_or(Il2cppException::from(Il2cppObject::from(
|
||||||
|
exception as *const u8,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
66
il2cpp/src/vm/mod.rs
Normal file
66
il2cpp/src/vm/mod.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
mod array;
|
||||||
|
mod assembly;
|
||||||
|
pub mod attributes;
|
||||||
|
mod class;
|
||||||
|
mod domain;
|
||||||
|
mod exception;
|
||||||
|
mod field;
|
||||||
|
mod image;
|
||||||
|
mod method;
|
||||||
|
mod object;
|
||||||
|
mod string;
|
||||||
|
|
||||||
|
use crate::ffi::*;
|
||||||
|
use crate::util::{as_cstr, cstr};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
pub use array::Il2cppArray;
|
||||||
|
pub use assembly::Assembly;
|
||||||
|
pub use class::Il2cppClass;
|
||||||
|
pub use domain::Il2cppDomain;
|
||||||
|
pub use exception::Il2cppException;
|
||||||
|
pub use field::Il2cppField;
|
||||||
|
pub use image::Il2cppImage;
|
||||||
|
pub use method::Il2cppMethod;
|
||||||
|
pub use object::Il2cppObject;
|
||||||
|
pub use string::Il2cppString;
|
||||||
|
|
||||||
|
pub trait Il2cppValue {
|
||||||
|
fn as_raw(&self) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Copy + Into<usize>> Il2cppValue for T {
|
||||||
|
fn as_raw(&self) -> usize {
|
||||||
|
(*self).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Il2cppType(*const u128);
|
||||||
|
|
||||||
|
impl Il2cppType {
|
||||||
|
pub fn name(&self) -> Cow<'static, str> {
|
||||||
|
unsafe { cstr(il2cpp_type_get_name(self.0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attrs(&self) -> u32 {
|
||||||
|
unsafe { il2cpp_type_get_attrs(self.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_enum(&self) -> u8 {
|
||||||
|
unsafe { *self.0.cast::<u8>().wrapping_add(10) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_class(&self) -> Il2cppClass {
|
||||||
|
unsafe { Il2cppClass(il2cpp_class_from_il2cpp_type(self.0 as *const u8)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Void;
|
||||||
|
|
||||||
|
impl From<usize> for Void {
|
||||||
|
fn from(_: usize) -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
36
il2cpp/src/vm/object.rs
Normal file
36
il2cpp/src/vm/object.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Il2cppObject(pub *const u8);
|
||||||
|
|
||||||
|
impl From<usize> for Il2cppObject {
|
||||||
|
fn from(value: usize) -> Self {
|
||||||
|
Self(value as *const u8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<*const u8> for Il2cppObject {
|
||||||
|
fn from(value: *const u8) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Il2cppObject> for usize {
|
||||||
|
fn from(value: Il2cppObject) -> Self {
|
||||||
|
value.0 as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Il2cppObject {
|
||||||
|
pub fn new(class: &Il2cppClass) -> Self {
|
||||||
|
class.init();
|
||||||
|
class.init_methods();
|
||||||
|
|
||||||
|
unsafe { Self(il2cpp_object_new(class.0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn class(&self) -> Il2cppClass {
|
||||||
|
unsafe { Il2cppClass(*(self.0 as *const usize) as *const u8) }
|
||||||
|
}
|
||||||
|
}
|
39
il2cpp/src/vm/string.rs
Normal file
39
il2cpp/src/vm/string.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Il2cppString(pub *const u8);
|
||||||
|
|
||||||
|
impl Il2cppValue for Il2cppString {
|
||||||
|
fn as_raw(&self) -> usize {
|
||||||
|
self.0 as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Il2cppString {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<usize> for Il2cppString {
|
||||||
|
fn from(value: usize) -> Self {
|
||||||
|
Self(value as *const u8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Il2cppString {
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
unsafe { *self.0.wrapping_add(16).cast::<u32>() as usize }
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't implement a cheap, copy-less conversion because of utf-16, sigh
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
unsafe {
|
||||||
|
String::from_utf16(std::slice::from_raw_parts(
|
||||||
|
self.0.wrapping_add(20).cast::<u16>(),
|
||||||
|
self.len(),
|
||||||
|
))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
launcher/Cargo.toml
Normal file
24
launcher/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "launcher"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
il2cpp.workspace = true
|
||||||
|
metadata.workspace = true
|
||||||
|
dumpcs-gen.workspace = true
|
||||||
|
idapy-gen.workspace = true
|
||||||
|
proto-gen.workspace = true
|
||||||
|
|
||||||
|
windows = { workspace = true, features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_System_Diagnostics_Debug",
|
||||||
|
"Win32_System_LibraryLoader",
|
||||||
|
"Win32_System_Memory",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_System_SystemServices",
|
||||||
|
"Win32_System_SystemInformation",
|
||||||
|
"Win32_System_Console"
|
||||||
|
] }
|
||||||
|
|
115
launcher/src/main.rs
Normal file
115
launcher/src/main.rs
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufWriter, Write};
|
||||||
|
|
||||||
|
use std::ptr::null_mut;
|
||||||
|
use windows::core::{s, PCSTR, PSTR};
|
||||||
|
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||||
|
use windows::Win32::System::Console::AllocConsole;
|
||||||
|
use windows::Win32::System::LibraryLoader::GetModuleHandleA;
|
||||||
|
use windows::Win32::System::Threading::{
|
||||||
|
CreateProcessA, CreateRemoteThread, ResumeThread, WaitForSingleObject, CREATE_SUSPENDED,
|
||||||
|
PROCESS_INFORMATION, STARTUPINFOA,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod reloc;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
// 0.1.x - Win.exe
|
||||||
|
// 0.2.x - ZZZ.exe
|
||||||
|
// 0.3.0+ - ZenlessZoneZero.exe
|
||||||
|
// 0.3.0+ (NDA) - ZenlessZoneZeroBeta.exe
|
||||||
|
|
||||||
|
const GAME_EXECUTABLE: PCSTR = s!("ZZZ.exe");
|
||||||
|
|
||||||
|
unsafe fn inject_self(target: HANDLE) {
|
||||||
|
let self_base = GetModuleHandleA(PCSTR::null()).unwrap();
|
||||||
|
let reloc_delta = reloc::relocate_image(self_base, target);
|
||||||
|
|
||||||
|
let h_thread = CreateRemoteThread(
|
||||||
|
target,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
Some(std::mem::transmute(entry_point as usize + reloc_delta)),
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
WaitForSingleObject(h_thread, 0xFFFFFFFF);
|
||||||
|
CloseHandle(h_thread).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn dump_thread() {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
println!("ReversedRooms™ GracefulDumper for ZZZ 0.2.0");
|
||||||
|
println!("Copyright xeondev, 2025. All bytes reversed.");
|
||||||
|
|
||||||
|
while !il2cpp::ffi::il2cpp_is_fully_initialized() {
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("il2cpp is fully initialized now, time to dump!");
|
||||||
|
|
||||||
|
print!("Generating dump.cs...");
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
|
let mut dump_cs = File::create("dump.cs").unwrap();
|
||||||
|
dumpcs_gen::dump(&mut BufWriter::new(&mut dump_cs)).unwrap();
|
||||||
|
println!("done!");
|
||||||
|
|
||||||
|
print!("Generating script.json...");
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
|
let mut script_json = File::create("script.json").unwrap();
|
||||||
|
idapy_gen::write_to_file(&mut BufWriter::new(&mut script_json)).unwrap();
|
||||||
|
println!("done!");
|
||||||
|
|
||||||
|
print!("Generating nap.proto...");
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
|
let mut nap_proto = File::create("nap.proto").unwrap();
|
||||||
|
proto_gen::dump(&mut BufWriter::new(&mut nap_proto)).unwrap();
|
||||||
|
println!("done!");
|
||||||
|
|
||||||
|
println!("dump finished!");
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "system" fn entry_point(_: *mut c_void) -> u32 {
|
||||||
|
if util::is_wine() {
|
||||||
|
util::patch_wintrust();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = AllocConsole();
|
||||||
|
std::thread::spawn(|| unsafe {
|
||||||
|
dump_thread();
|
||||||
|
});
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut proc_info = PROCESS_INFORMATION::default();
|
||||||
|
let mut startup_info = STARTUPINFOA::default();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
CreateProcessA(
|
||||||
|
GAME_EXECUTABLE,
|
||||||
|
PSTR(null_mut()),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
CREATE_SUSPENDED,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
&mut startup_info,
|
||||||
|
&mut proc_info,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
inject_self(proc_info.hProcess);
|
||||||
|
ResumeThread(proc_info.hThread);
|
||||||
|
|
||||||
|
CloseHandle(proc_info.hThread).unwrap();
|
||||||
|
CloseHandle(proc_info.hProcess).unwrap();
|
||||||
|
}
|
||||||
|
}
|
69
launcher/src/reloc.rs
Normal file
69
launcher/src/reloc.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use windows::Win32::{
|
||||||
|
Foundation::{HANDLE, HMODULE},
|
||||||
|
System::{
|
||||||
|
Diagnostics::Debug::{
|
||||||
|
WriteProcessMemory, IMAGE_DIRECTORY_ENTRY_BASERELOC, IMAGE_NT_HEADERS64,
|
||||||
|
},
|
||||||
|
Memory::{VirtualAlloc, VirtualAllocEx, MEM_COMMIT, PAGE_EXECUTE_READWRITE},
|
||||||
|
SystemServices::{IMAGE_BASE_RELOCATION, IMAGE_DOS_HEADER},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub unsafe fn relocate_image(source: HMODULE, target_process: HANDLE) -> usize {
|
||||||
|
let dos_header = source.0 as *const IMAGE_DOS_HEADER;
|
||||||
|
let nt_header =
|
||||||
|
source.0.wrapping_add((*dos_header).e_lfanew as usize) as *const IMAGE_NT_HEADERS64;
|
||||||
|
|
||||||
|
let size_of_image = (*nt_header).OptionalHeader.SizeOfImage as usize;
|
||||||
|
|
||||||
|
let local_image = VirtualAlloc(None, size_of_image, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
|
||||||
|
std::ptr::copy_nonoverlapping(source.0 as *const u8, local_image as *mut u8, size_of_image);
|
||||||
|
|
||||||
|
let target_image = VirtualAllocEx(
|
||||||
|
target_process,
|
||||||
|
None,
|
||||||
|
size_of_image,
|
||||||
|
MEM_COMMIT,
|
||||||
|
PAGE_EXECUTE_READWRITE,
|
||||||
|
);
|
||||||
|
|
||||||
|
let delta_image_base = target_image.wrapping_sub(source.0 as usize);
|
||||||
|
let mut relocation_table = local_image.wrapping_add(
|
||||||
|
(*nt_header).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC.0 as usize]
|
||||||
|
.VirtualAddress as usize,
|
||||||
|
) as *const IMAGE_BASE_RELOCATION;
|
||||||
|
|
||||||
|
while (*relocation_table).SizeOfBlock > 0 {
|
||||||
|
let relocation_entries_count = ((*relocation_table).SizeOfBlock as usize
|
||||||
|
- std::mem::size_of::<IMAGE_BASE_RELOCATION>())
|
||||||
|
/ std::mem::size_of::<u16>();
|
||||||
|
|
||||||
|
let relocation_rva =
|
||||||
|
((relocation_table as usize) + size_of::<IMAGE_BASE_RELOCATION>()) as *const u16;
|
||||||
|
|
||||||
|
for i in 0..relocation_entries_count {
|
||||||
|
if (*(relocation_rva.wrapping_add(i)) & 0xFFF) != 0 {
|
||||||
|
let patched_address = local_image
|
||||||
|
.wrapping_add((*relocation_table).VirtualAddress as usize)
|
||||||
|
.wrapping_add((*(relocation_rva.wrapping_add(i)) & 0xFFF) as usize)
|
||||||
|
as *mut usize;
|
||||||
|
|
||||||
|
*patched_address += delta_image_base as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
relocation_table = ((relocation_table as usize) + (*relocation_table).SizeOfBlock as usize)
|
||||||
|
as *const IMAGE_BASE_RELOCATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteProcessMemory(
|
||||||
|
target_process,
|
||||||
|
target_image,
|
||||||
|
local_image,
|
||||||
|
(*nt_header).OptionalHeader.SizeOfImage as usize,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
delta_image_base as usize
|
||||||
|
}
|
39
launcher/src/util.rs
Normal file
39
launcher/src/util.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
use windows::{
|
||||||
|
core::{s, PCSTR},
|
||||||
|
Win32::System::{
|
||||||
|
LibraryLoader::{GetModuleHandleA, GetProcAddress, LoadLibraryA},
|
||||||
|
Memory,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn is_wine() -> bool {
|
||||||
|
const NTDLL: PCSTR = s!("ntdll.dll");
|
||||||
|
const WINE_GET_VERSION: PCSTR = s!("wine_get_version");
|
||||||
|
|
||||||
|
let ntdll = GetModuleHandleA(NTDLL).unwrap();
|
||||||
|
GetProcAddress(ntdll, WINE_GET_VERSION).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn patch_wintrust() {
|
||||||
|
const STUB: [u8; 6] = [0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3];
|
||||||
|
const WINTRUST: PCSTR = s!("wintrust.dll");
|
||||||
|
const PATCH_FUNCTIONS: [PCSTR; 3] = [
|
||||||
|
s!("CryptCATAdminEnumCatalogFromHash"),
|
||||||
|
s!("CryptCATCatalogInfoFromContext"),
|
||||||
|
s!("CryptCATAdminReleaseCatalogContext"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let dll = LoadLibraryA(WINTRUST).unwrap();
|
||||||
|
|
||||||
|
for name in PATCH_FUNCTIONS {
|
||||||
|
let addr = GetProcAddress(dll, name).unwrap();
|
||||||
|
let mut prot = Memory::PAGE_EXECUTE_READWRITE;
|
||||||
|
|
||||||
|
Memory::VirtualProtect(addr as *const c_void, STUB.len(), prot, &mut prot).unwrap();
|
||||||
|
std::ptr::copy_nonoverlapping(STUB.as_ptr(), addr as *mut u8, STUB.len());
|
||||||
|
Memory::VirtualProtect(addr as *const c_void, STUB.len(), prot, &mut prot).unwrap();
|
||||||
|
}
|
||||||
|
}
|
7
metadata/Cargo.toml
Normal file
7
metadata/Cargo.toml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[package]
|
||||||
|
name = "metadata"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
il2cpp.workspace = true
|
72
metadata/src/lib.rs
Normal file
72
metadata/src/lib.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
use il2cpp::vm::{Il2cppClass, Il2cppMethod, Il2cppString};
|
||||||
|
|
||||||
|
pub struct MetadataEntry {
|
||||||
|
pub address: usize,
|
||||||
|
pub usage: MetadataUsage,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum MetadataUsage {
|
||||||
|
TypeInfo(Il2cppClass),
|
||||||
|
MethodRef(Il2cppMethod),
|
||||||
|
StringLiteral(Il2cppString),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const USAGES_COUNT: usize = 0x38845;
|
||||||
|
|
||||||
|
const USAGE_TYPE_INFO: u32 = 1;
|
||||||
|
const USAGE_IL2CPP_TYPE: u32 = 2;
|
||||||
|
const USAGE_METHOD_DEF: u32 = 3;
|
||||||
|
const USAGE_FIELD_INFO: u32 = 4;
|
||||||
|
const USAGE_STRING_LITERAL: u32 = 5;
|
||||||
|
const USAGE_METHOD_REF: u32 = 6;
|
||||||
|
|
||||||
|
pub fn get_usage_by_index(index: usize) -> Option<MetadataEntry> {
|
||||||
|
const GLOBAL_METADATA: usize = 0xB225F30;
|
||||||
|
const TYPE_INFO_TABLE: usize = 0xB2277F0;
|
||||||
|
const METHOD_REF_TABLE: usize = 0xB2A9C90;
|
||||||
|
const STRING_LITERAL_TABLE: usize = 0xB3A1890;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
index < USAGES_COUNT,
|
||||||
|
"usage index out of range: {index}/{USAGES_COUNT}"
|
||||||
|
);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let global_metadata = *((il2cpp::ffi::base() + GLOBAL_METADATA) as *const usize);
|
||||||
|
let v10 = (*((global_metadata + 41952208 + 8 * index + 4) as *const i32) ^ 0x56089010)
|
||||||
|
- 1344935350 * ((-849621129 * (index as i32) - 214430094) ^ 0x3939D59A);
|
||||||
|
let v11 = (*((global_metadata + 41952208 + 8 * index) as *const i32) ^ 0x6EFCC6A2)
|
||||||
|
- 1344935350 * ((-849621129 * (index as i32) - 214430094) ^ 0x3939D59A);
|
||||||
|
|
||||||
|
Some(match v10 as u32 >> 29 {
|
||||||
|
USAGE_TYPE_INFO => {
|
||||||
|
let address = il2cpp::ffi::base() + TYPE_INFO_TABLE + ((v11 as usize) * 8);
|
||||||
|
MetadataEntry {
|
||||||
|
address,
|
||||||
|
usage: MetadataUsage::TypeInfo(Il2cppClass(dereference(address))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
USAGE_IL2CPP_TYPE => panic!("USAGE_IL2CPP_TYPE is not supported in this build"),
|
||||||
|
USAGE_METHOD_REF | USAGE_METHOD_DEF => {
|
||||||
|
let address = il2cpp::ffi::base() + METHOD_REF_TABLE + ((v11 as usize) * 8);
|
||||||
|
MetadataEntry {
|
||||||
|
address,
|
||||||
|
usage: MetadataUsage::MethodRef(Il2cppMethod(dereference(address))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
USAGE_FIELD_INFO => return None,
|
||||||
|
USAGE_STRING_LITERAL => {
|
||||||
|
let address = il2cpp::ffi::base() + STRING_LITERAL_TABLE + ((v11 as usize) * 8);
|
||||||
|
MetadataEntry {
|
||||||
|
address,
|
||||||
|
usage: MetadataUsage::StringLiteral(Il2cppString(dereference(address))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => todo!("metadata usage type {other} is not implemented"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn dereference(address: usize) -> *const u8 {
|
||||||
|
*(address as *const usize) as *const u8
|
||||||
|
}
|
7
proto-gen/Cargo.toml
Normal file
7
proto-gen/Cargo.toml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[package]
|
||||||
|
name = "proto-gen"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
il2cpp.workspace = true
|
110
proto-gen/src/cache.rs
Normal file
110
proto-gen/src/cache.rs
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use il2cpp::vm::Il2cppDomain;
|
||||||
|
|
||||||
|
pub struct TypeCache {
|
||||||
|
pub type_map: BTreeMap<usize, CachedType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum CachedType {
|
||||||
|
Object,
|
||||||
|
Boolean,
|
||||||
|
Byte,
|
||||||
|
SByte,
|
||||||
|
UInt16,
|
||||||
|
Int16,
|
||||||
|
UInt32,
|
||||||
|
Int32,
|
||||||
|
UInt64,
|
||||||
|
Int64,
|
||||||
|
Single,
|
||||||
|
Double,
|
||||||
|
String,
|
||||||
|
ByteString,
|
||||||
|
Any,
|
||||||
|
Enum,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeCache {
|
||||||
|
pub fn init(domain: &Il2cppDomain) -> Self {
|
||||||
|
use CachedType::*;
|
||||||
|
let corlib = domain.assembly_open("mscorlib.dll").image();
|
||||||
|
let protobuf_lib = domain.assembly_open("Protobuf.dll").image();
|
||||||
|
|
||||||
|
TypeCache {
|
||||||
|
type_map: BTreeMap::from([
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "Object").unwrap().0 as usize,
|
||||||
|
Object,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "Boolean").unwrap().0 as usize,
|
||||||
|
Boolean,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "Byte").unwrap().0 as usize,
|
||||||
|
Byte,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "SByte").unwrap().0 as usize,
|
||||||
|
SByte,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "UInt16").unwrap().0 as usize,
|
||||||
|
UInt16,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "Int16").unwrap().0 as usize,
|
||||||
|
Int16,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "UInt32").unwrap().0 as usize,
|
||||||
|
UInt32,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "Int32").unwrap().0 as usize,
|
||||||
|
Int32,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "UInt64").unwrap().0 as usize,
|
||||||
|
UInt64,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "Int64").unwrap().0 as usize,
|
||||||
|
Int64,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "Single").unwrap().0 as usize,
|
||||||
|
Single,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "Double").unwrap().0 as usize,
|
||||||
|
Double,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "String").unwrap().0 as usize,
|
||||||
|
String,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
corlib.get_class_by_name("System", "Enum").unwrap().0 as usize,
|
||||||
|
Enum,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
protobuf_lib
|
||||||
|
.get_class_by_name("", super::BYTE_STRING)
|
||||||
|
.unwrap()
|
||||||
|
.0 as usize,
|
||||||
|
ByteString,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
protobuf_lib
|
||||||
|
.get_class_by_name("", super::PROTOBUF_ANY)
|
||||||
|
.unwrap()
|
||||||
|
.0 as usize,
|
||||||
|
Any,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
588
proto-gen/src/lib.rs
Normal file
588
proto-gen/src/lib.rs
Normal file
|
@ -0,0 +1,588 @@
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
collections::BTreeMap,
|
||||||
|
io::{self, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use cache::{CachedType, TypeCache};
|
||||||
|
use il2cpp::vm::*;
|
||||||
|
use output::{Enum, Field, FieldComment, Message, Oneof, ProtoFile};
|
||||||
|
use util::{
|
||||||
|
pack_wire_tag, WIRE_TYPE_I32, WIRE_TYPE_I64, WIRE_TYPE_LENGTH_PREFIXED, WIRE_TYPE_VAR_INT,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod cache;
|
||||||
|
mod output;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
// Names
|
||||||
|
const CODED_INPUT_STREAM: &str = "MBJDEDCJACC";
|
||||||
|
const MERGE_FROM: &str = "BLCPKKJCPDM";
|
||||||
|
const GET_CMD_ID: &str = "FKEKBMJGFOM";
|
||||||
|
const UNKNOWN_FIELD_SET: &str = "IHANLCCDMBG";
|
||||||
|
const BYTE_STRING: &str = "HNGPONOFMLO";
|
||||||
|
const PROTOBUF_ANY: &str = "DBIEFFIJOMC";
|
||||||
|
|
||||||
|
struct TrackedValues<'tc> {
|
||||||
|
type_cache: &'tc TypeCache,
|
||||||
|
pub values: Vec<(u32, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MessageMinimalInfo {
|
||||||
|
pub cmd_id: u16,
|
||||||
|
pub fields: Vec<FieldMinimalInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageMinimalInfo {
|
||||||
|
pub fn new(cmd_id: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
cmd_id,
|
||||||
|
fields: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FieldDetectionInfo {
|
||||||
|
pub value: u32,
|
||||||
|
pub offset: u32,
|
||||||
|
pub oneof_extra_data: Option<OneofVariantInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FieldMinimalInfo {
|
||||||
|
pub tag: u32,
|
||||||
|
pub xor: u32,
|
||||||
|
pub offset: u32,
|
||||||
|
pub oneof_extra_data: Option<OneofVariantInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OneofVariantInfo {
|
||||||
|
pub oneof_enum_offset: u32,
|
||||||
|
pub variant_type: Il2cppType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'tc> TrackedValues<'tc> {
|
||||||
|
pub fn new(tc: &'tc TypeCache, object: &Il2cppObject) -> Self {
|
||||||
|
// dump initial values
|
||||||
|
let mut values = Vec::with_capacity(object.class().fields().len());
|
||||||
|
for field in object.class().fields() {
|
||||||
|
if field.is_instance() && field.il2cpp_type().name() != UNKNOWN_FIELD_SET {
|
||||||
|
values.push((field.offset(), Self::fetch_value(tc, field, object)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
type_cache: tc,
|
||||||
|
values,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need this because RepeatedField and Map need special handling
|
||||||
|
fn fetch_value(cache: &TypeCache, field: &Il2cppField, object: &Il2cppObject) -> usize {
|
||||||
|
let field_class = field.il2cpp_type().to_class();
|
||||||
|
if let Some(ty) = cache.type_map.get(&(field_class.0 as usize)) {
|
||||||
|
use CachedType::*;
|
||||||
|
unsafe {
|
||||||
|
match ty {
|
||||||
|
Boolean | Byte | SByte => *field.get_field_data_ptr(object) as usize,
|
||||||
|
Int16 | UInt16 => *field.get_field_data_ptr(object).cast::<u16>() as usize,
|
||||||
|
Single | Int32 | UInt32 => {
|
||||||
|
*field.get_field_data_ptr(object).cast::<u32>() as usize
|
||||||
|
}
|
||||||
|
Double | Int64 | UInt64 => {
|
||||||
|
*field.get_field_data_ptr(object).cast::<u64>() as usize
|
||||||
|
}
|
||||||
|
Object | Any | ByteString | String => {
|
||||||
|
*field.get_field_data_ptr(object).cast::<usize>()
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if field_class.get_generic_argument_count() != 0 {
|
||||||
|
let collection = unsafe {
|
||||||
|
Il2cppObject(*(field.get_field_data_ptr(object).cast::<usize>()) as *const u8)
|
||||||
|
};
|
||||||
|
let get_count = collection.class().find_method("get_Count", &[]).unwrap();
|
||||||
|
|
||||||
|
// method returns an object-packed (boxed) value
|
||||||
|
unsafe {
|
||||||
|
*(get_count
|
||||||
|
.invoke::<Il2cppObject>(&collection, &[])
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.wrapping_add(16)
|
||||||
|
.cast::<i32>()) as usize
|
||||||
|
}
|
||||||
|
} else if field_class
|
||||||
|
.parent_class()
|
||||||
|
.map(|p| matches!(cache.type_map.get(&(p.0 as usize)), Some(&CachedType::Enum)))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
unsafe { *field.get_field_data_ptr(object).cast::<u32>() as usize }
|
||||||
|
} else {
|
||||||
|
unsafe { *field.get_field_data_ptr(object).cast::<usize>() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_changes_and_update(&mut self, object: &Il2cppObject) -> Vec<u32> {
|
||||||
|
let mut changed_offsets = Vec::new();
|
||||||
|
for field in object.class().fields() {
|
||||||
|
if field.is_instance() && field.il2cpp_type().name() != UNKNOWN_FIELD_SET {
|
||||||
|
let value = Self::fetch_value(&self.type_cache, field, object);
|
||||||
|
if value != self.get_saved_value(field.offset()) {
|
||||||
|
changed_offsets.push(field.offset());
|
||||||
|
self.replace_value(field.offset(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changed_offsets
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_saved_value(&self, offset: u32) -> usize {
|
||||||
|
self.values
|
||||||
|
.iter()
|
||||||
|
.find(|(off, _)| offset == *off)
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replace_value(&mut self, offset: u32, value: usize) {
|
||||||
|
self.values
|
||||||
|
.iter_mut()
|
||||||
|
.find(|(off, _)| offset == *off)
|
||||||
|
.unwrap()
|
||||||
|
.1 = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dump<W: Write>(out: &mut W) -> io::Result<()> {
|
||||||
|
il2cpp::ffi::il2cpp_gc_disable();
|
||||||
|
let domain = Il2cppDomain::get();
|
||||||
|
domain.attach_thread();
|
||||||
|
|
||||||
|
let type_cache = TypeCache::init(&domain);
|
||||||
|
let nap_proto_gen = domain.assembly_open("NapProtoGen.dll").image();
|
||||||
|
|
||||||
|
let mut minimal_info_map = BTreeMap::new();
|
||||||
|
for i in 0..nap_proto_gen.get_class_count() {
|
||||||
|
let proto_class = nap_proto_gen.get_class(i);
|
||||||
|
let Some(get_cmd_id) = proto_class.get_method(GET_CMD_ID, 0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let proto_instance = Il2cppObject::new(&proto_class);
|
||||||
|
proto_class
|
||||||
|
.get_method(".ctor", 0)
|
||||||
|
.unwrap()
|
||||||
|
.invoke::<usize>(&proto_instance, &[])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let cmd_id = *(get_cmd_id
|
||||||
|
.invoke::<Il2cppObject>(&proto_instance, &[])
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.wrapping_add(16) as *const u16);
|
||||||
|
|
||||||
|
let mut message_info = MessageMinimalInfo::new(cmd_id);
|
||||||
|
|
||||||
|
let mut bruteforcer = Bruteforcer::new(&type_cache, proto_instance);
|
||||||
|
|
||||||
|
// Pre-estimate number of fields for messages without oneof
|
||||||
|
let estimated_field_count = (!proto_class
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.any(|f| f.il2cpp_type().name() == "System.Object"))
|
||||||
|
.then_some(bruteforcer.tracked_values.values.len());
|
||||||
|
|
||||||
|
for field_id in 1..4096 {
|
||||||
|
if let Some(count) = estimated_field_count {
|
||||||
|
if message_info.fields.len() >= count {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First: try as varint
|
||||||
|
let tag = pack_wire_tag(field_id, WIRE_TYPE_VAR_INT);
|
||||||
|
if let Ok(Some(info)) = bruteforcer.input(tag, &[1]) {
|
||||||
|
// varint fields are likely to get xored, so we're using varint of value 1 as input
|
||||||
|
// and xoring set value with it, to get the xor constant
|
||||||
|
message_info.fields.push(FieldMinimalInfo {
|
||||||
|
xor: info.value ^ 1,
|
||||||
|
offset: info.offset,
|
||||||
|
tag,
|
||||||
|
oneof_extra_data: info.oneof_extra_data,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An empty length-prefixed data
|
||||||
|
// This will work for nested messages. Non-null but all fields are default.
|
||||||
|
// For collections this won't change anything
|
||||||
|
|
||||||
|
let tag = pack_wire_tag(field_id, WIRE_TYPE_LENGTH_PREFIXED);
|
||||||
|
if let Ok(Some(info)) = bruteforcer.input(tag, &[0]) {
|
||||||
|
message_info.fields.push(FieldMinimalInfo {
|
||||||
|
xor: 0,
|
||||||
|
offset: info.offset,
|
||||||
|
tag,
|
||||||
|
oneof_extra_data: info.oneof_extra_data,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now let's try to bruteforce collections
|
||||||
|
match bruteforcer.input(tag, &[1, 0]) {
|
||||||
|
Ok(Some(info)) => {
|
||||||
|
// Length: 1, value: 0
|
||||||
|
// OR RepeatedField Count: 1 and empty message!
|
||||||
|
// OR ByteString with 1 byte (0)
|
||||||
|
// OR System.String '\0'
|
||||||
|
|
||||||
|
message_info.fields.push(FieldMinimalInfo {
|
||||||
|
xor: 0,
|
||||||
|
offset: info.offset,
|
||||||
|
tag,
|
||||||
|
oneof_extra_data: info.oneof_extra_data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(_exception) => {
|
||||||
|
// Got an exception!
|
||||||
|
// This means that field with this tag definitely exists
|
||||||
|
// but we don't know which one yet, because nothing is set yet!
|
||||||
|
// reason of exception: data format is incorrect (maybe expected a map)
|
||||||
|
// or length was not enough (for fixed32/float/etc for example...)
|
||||||
|
|
||||||
|
const LENGTH_PREFIXED_SAMPLES: &[&[u8]] = &[
|
||||||
|
&[1, 0, 0, 0, 0], // repeated fixed32/float
|
||||||
|
&[1, 0, 0, 0, 0, 0, 0, 0, 0], // repeated fixed64/double
|
||||||
|
&[5, 0x08, 0x01, 0x33, 0x01, 0x00], // map<varint, varint/string>
|
||||||
|
&[5, 0x10, 0x01, 0x33, 0x10, 0x00], // map<string, varint/string>
|
||||||
|
];
|
||||||
|
|
||||||
|
for sample in LENGTH_PREFIXED_SAMPLES.iter() {
|
||||||
|
match bruteforcer.input(tag, sample) {
|
||||||
|
Ok(Some(info)) => {
|
||||||
|
message_info.fields.push(FieldMinimalInfo {
|
||||||
|
xor: 0,
|
||||||
|
offset: info.offset,
|
||||||
|
tag,
|
||||||
|
oneof_extra_data: info.oneof_extra_data,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(_exc) => (), // try another sample!
|
||||||
|
Ok(None) => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => (), // the specified tag doesn't exist in this message
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixed32/float
|
||||||
|
|
||||||
|
let tag = pack_wire_tag(field_id, WIRE_TYPE_I32);
|
||||||
|
if let Ok(Some(info)) = bruteforcer.input(tag, &1u32.to_be_bytes()) {
|
||||||
|
message_info.fields.push(FieldMinimalInfo {
|
||||||
|
xor: 0,
|
||||||
|
offset: info.offset,
|
||||||
|
tag,
|
||||||
|
oneof_extra_data: info.oneof_extra_data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixed64/double
|
||||||
|
|
||||||
|
let tag = pack_wire_tag(field_id, WIRE_TYPE_I64);
|
||||||
|
if let Ok(Some(info)) = bruteforcer.input(tag, &1u64.to_be_bytes()) {
|
||||||
|
message_info.fields.push(FieldMinimalInfo {
|
||||||
|
xor: 0,
|
||||||
|
offset: info.offset,
|
||||||
|
tag,
|
||||||
|
oneof_extra_data: info.oneof_extra_data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minimal_info_map.insert(proto_class.token(), message_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now build proto file from obtained MessageMinimalInfos
|
||||||
|
|
||||||
|
let mut proto_file = ProtoFile {
|
||||||
|
syntax: String::from("proto3"),
|
||||||
|
imports: vec![String::from("google/protobuf/any.proto")],
|
||||||
|
items: Vec::with_capacity(nap_proto_gen.get_class_count()),
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in 0..nap_proto_gen.get_class_count() {
|
||||||
|
let class = nap_proto_gen.get_class(i);
|
||||||
|
if class.name() == "__HOLLOW__1_0" {
|
||||||
|
// Beebyte class is always the last one in assembly
|
||||||
|
// after it only there are only ILFix classes
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(message_info) = minimal_info_map.get(&class.token()) {
|
||||||
|
let mut fields = Vec::with_capacity(message_info.fields.len());
|
||||||
|
|
||||||
|
for field_info in message_info
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.filter(|f| f.oneof_extra_data.is_none())
|
||||||
|
{
|
||||||
|
let field = class
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.is_instance() && f.offset() == field_info.offset)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
fields.push((
|
||||||
|
field.token(),
|
||||||
|
Field {
|
||||||
|
kind: csharp_type_to_protobuf_type(
|
||||||
|
&type_cache,
|
||||||
|
&field.il2cpp_type().to_class(),
|
||||||
|
),
|
||||||
|
name: field.name().to_string(),
|
||||||
|
number: field_info.tag >> 3,
|
||||||
|
comment: Some(FieldComment {
|
||||||
|
offset: field.offset(),
|
||||||
|
xor_const: field_info.xor,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're sorting fields by their token
|
||||||
|
// even though proto field ordering is shuffled before translation to C#
|
||||||
|
// for the ones that aren't shuffled at all, correct field order is preserved
|
||||||
|
fields.sort_by_key(|(token, _)| *token);
|
||||||
|
let mut oneofs = Vec::<Oneof>::new();
|
||||||
|
|
||||||
|
for field_info in message_info
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.filter(|f| f.oneof_extra_data.is_some())
|
||||||
|
{
|
||||||
|
let oneof_data_field = class
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.is_instance() && f.offset() == field_info.offset)
|
||||||
|
.unwrap();
|
||||||
|
let oneof_variant = field_info.oneof_extra_data.as_ref().unwrap();
|
||||||
|
let oneof_enum_field = class
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.is_instance() && f.offset() == oneof_variant.oneof_enum_offset)
|
||||||
|
.unwrap();
|
||||||
|
let oneof_enum = oneof_enum_field.il2cpp_type().to_class();
|
||||||
|
let oneof_case_enum_field = oneof_enum
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.find(|f| {
|
||||||
|
!f.is_instance() && f.static_get_value() as u32 == (field_info.tag >> 3)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let oneof = if let Some(oneof) = oneofs
|
||||||
|
.iter_mut()
|
||||||
|
.find(|o| o.name == oneof_data_field.name())
|
||||||
|
{
|
||||||
|
oneof
|
||||||
|
} else {
|
||||||
|
oneofs.push(Oneof {
|
||||||
|
name: oneof_data_field.name().to_string(),
|
||||||
|
fields: Vec::new(),
|
||||||
|
});
|
||||||
|
oneofs.last_mut().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
oneof.fields.push(Field {
|
||||||
|
kind: csharp_type_to_protobuf_type(
|
||||||
|
&type_cache,
|
||||||
|
&oneof_variant.variant_type.to_class(),
|
||||||
|
),
|
||||||
|
name: oneof_case_enum_field.name().to_string(),
|
||||||
|
number: field_info.tag >> 3,
|
||||||
|
comment: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
proto_file.items.push(output::ProtoItem::Message(Message {
|
||||||
|
name: class.name().to_string(),
|
||||||
|
cmd_id: message_info.cmd_id,
|
||||||
|
fields: fields.into_iter().map(|f| f.1).collect(),
|
||||||
|
oneofs,
|
||||||
|
}));
|
||||||
|
} else if class
|
||||||
|
.parent_class()
|
||||||
|
.map(|p| {
|
||||||
|
matches!(
|
||||||
|
type_cache.type_map.get(&(p.0 as usize)),
|
||||||
|
Some(&CachedType::Enum)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
let enum_name = class.name().to_string();
|
||||||
|
proto_file.items.push(output::ProtoItem::Enum(Enum {
|
||||||
|
variants: class
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.filter(|f| !f.is_instance() && f.static_get_value() == 0)
|
||||||
|
.chain(
|
||||||
|
class
|
||||||
|
.fields()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|f| !f.is_instance() && f.static_get_value() != 0),
|
||||||
|
)
|
||||||
|
.map(|f| {
|
||||||
|
(
|
||||||
|
// Google with their C++ism (enum scoping rules)
|
||||||
|
format!("{}_{}", &enum_name, f.name().to_string()),
|
||||||
|
f.static_get_value() as i32,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
name: enum_name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(out, "{proto_file}")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn csharp_type_to_protobuf_type(cache: &TypeCache, ty: &Il2cppClass) -> Cow<'static, str> {
|
||||||
|
if let Some(ty) = cache.type_map.get(&(ty.0 as usize)) {
|
||||||
|
use CachedType::*;
|
||||||
|
match ty {
|
||||||
|
Boolean => Cow::Borrowed("bool"),
|
||||||
|
Int32 => Cow::Borrowed("int32"),
|
||||||
|
UInt32 => Cow::Borrowed("uint32"),
|
||||||
|
Int64 => Cow::Borrowed("int64"),
|
||||||
|
UInt64 => Cow::Borrowed("uint64"),
|
||||||
|
Single => Cow::Borrowed("float"),
|
||||||
|
Double => Cow::Borrowed("double"),
|
||||||
|
String => Cow::Borrowed("string"),
|
||||||
|
ByteString => Cow::Borrowed("bytes"),
|
||||||
|
Any => Cow::Borrowed("google.protobuf.Any"),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match ty.get_generic_argument_count() {
|
||||||
|
1 => Cow::Owned(format!(
|
||||||
|
"repeated {}",
|
||||||
|
csharp_type_to_protobuf_type(cache, &ty.get_generic_argument(0).to_class())
|
||||||
|
)),
|
||||||
|
2 => Cow::Owned(format!(
|
||||||
|
"map<{}, {}>",
|
||||||
|
csharp_type_to_protobuf_type(cache, &ty.get_generic_argument(0).to_class()),
|
||||||
|
csharp_type_to_protobuf_type(cache, &ty.get_generic_argument(1).to_class())
|
||||||
|
)),
|
||||||
|
_ => ty.name(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Bruteforcer<'tc> {
|
||||||
|
object: Il2cppObject,
|
||||||
|
tracked_values: TrackedValues<'tc>,
|
||||||
|
merge_from_method: Il2cppMethod,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'tc> Bruteforcer<'tc> {
|
||||||
|
pub fn new(type_cache: &'tc TypeCache, object: Il2cppObject) -> Self {
|
||||||
|
let tracked_values = TrackedValues::new(type_cache, &object);
|
||||||
|
let merge_from_method = object
|
||||||
|
.class()
|
||||||
|
.find_method(MERGE_FROM, &[CODED_INPUT_STREAM])
|
||||||
|
.expect("failed to find MergeFrom(CodedInputStream)");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
object,
|
||||||
|
tracked_values,
|
||||||
|
merge_from_method,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn input(
|
||||||
|
&mut self,
|
||||||
|
wire_tag: u32,
|
||||||
|
data: &[u8],
|
||||||
|
) -> Result<Option<FieldDetectionInfo>, Il2cppException> {
|
||||||
|
let mut buf = Vec::with_capacity(data.len() + util::varint_length(wire_tag));
|
||||||
|
util::encode_varint(&mut buf, wire_tag);
|
||||||
|
buf.extend(data);
|
||||||
|
|
||||||
|
self.merge_from_method
|
||||||
|
.invoke::<Void>(&self.object, &[&create_input_stream(&buf)])?;
|
||||||
|
|
||||||
|
let changed_offsets = self.tracked_values.detect_changes_and_update(&self.object);
|
||||||
|
match changed_offsets.as_slice() {
|
||||||
|
&[offset] => Ok(Some(FieldDetectionInfo {
|
||||||
|
value: self.tracked_values.get_saved_value(offset) as u32,
|
||||||
|
offset,
|
||||||
|
oneof_extra_data: None,
|
||||||
|
})),
|
||||||
|
&[first, second] => {
|
||||||
|
// oneof (storage field + enum case)
|
||||||
|
|
||||||
|
let class = self.object.class();
|
||||||
|
let first_field = class.fields().iter().find(|f| f.offset() == first).unwrap();
|
||||||
|
|
||||||
|
let (data_offset, oneof_enum_offset) =
|
||||||
|
if first_field.il2cpp_type().to_class().name() == "Object" {
|
||||||
|
(first, second)
|
||||||
|
} else {
|
||||||
|
(second, first)
|
||||||
|
};
|
||||||
|
|
||||||
|
let data_field_type = Il2cppClass(
|
||||||
|
*(*(self.object.0.wrapping_add(data_offset as usize) as *const usize)
|
||||||
|
as *const usize) as *const u8,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Some(FieldDetectionInfo {
|
||||||
|
value: 0,
|
||||||
|
offset: data_offset,
|
||||||
|
oneof_extra_data: Some(OneofVariantInfo {
|
||||||
|
oneof_enum_offset,
|
||||||
|
variant_type: data_field_type.il2cpp_type(),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
&[] => Ok(None),
|
||||||
|
_ => panic!(
|
||||||
|
"abnormal number of fields changed: {}",
|
||||||
|
changed_offsets.len()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn create_input_stream(buf: &[u8]) -> Il2cppObject {
|
||||||
|
let protobuf_lib = Il2cppDomain::get().assembly_open("Protobuf.dll").image();
|
||||||
|
|
||||||
|
let coded_input_stream = Il2cppObject::new(
|
||||||
|
&protobuf_lib
|
||||||
|
.get_class_by_name("", CODED_INPUT_STREAM)
|
||||||
|
.expect("failed to find CodedInputStream in Protobuf.dll"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let constructor = coded_input_stream
|
||||||
|
.class()
|
||||||
|
.find_method(".ctor", &["System.Byte[]"])
|
||||||
|
.expect("failed to find CodedInputStream.ctor(byte[])");
|
||||||
|
|
||||||
|
let byte_array_class = constructor.arg_type(0).to_class();
|
||||||
|
let byte_array = Il2cppArray::new(&byte_array_class, buf.len());
|
||||||
|
byte_array.as_mut_slice().copy_from_slice(buf);
|
||||||
|
|
||||||
|
constructor
|
||||||
|
.invoke::<Il2cppObject>(&coded_input_stream, &[&byte_array])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
coded_input_stream
|
||||||
|
}
|
108
proto-gen/src/output.rs
Normal file
108
proto-gen/src/output.rs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
use std::{borrow::Cow, fmt};
|
||||||
|
|
||||||
|
pub struct ProtoFile {
|
||||||
|
pub syntax: String,
|
||||||
|
pub imports: Vec<String>,
|
||||||
|
pub items: Vec<ProtoItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Message {
|
||||||
|
pub cmd_id: u16,
|
||||||
|
pub name: String,
|
||||||
|
pub fields: Vec<Field>,
|
||||||
|
pub oneofs: Vec<Oneof>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Enum {
|
||||||
|
pub name: String,
|
||||||
|
pub variants: Vec<(String, i32)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Field {
|
||||||
|
pub kind: Cow<'static, str>,
|
||||||
|
pub name: String,
|
||||||
|
pub number: u32,
|
||||||
|
pub comment: Option<FieldComment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FieldComment {
|
||||||
|
pub offset: u32,
|
||||||
|
pub xor_const: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Oneof {
|
||||||
|
pub name: String,
|
||||||
|
pub fields: Vec<Field>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ProtoItem {
|
||||||
|
Message(Message),
|
||||||
|
Enum(Enum),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ProtoFile {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
writeln!(f, "syntax = \"{}\";", self.syntax)?;
|
||||||
|
for import in self.imports.iter() {
|
||||||
|
writeln!(f, "import \"{}\";", import)?;
|
||||||
|
}
|
||||||
|
writeln!(f)?;
|
||||||
|
|
||||||
|
for item in self.items.iter() {
|
||||||
|
match item {
|
||||||
|
ProtoItem::Message(message) => write!(f, "{message}")?,
|
||||||
|
ProtoItem::Enum(enumeration) => write!(f, "{enumeration}")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Message {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "message {} {{", self.name)?;
|
||||||
|
if self.cmd_id != 0 {
|
||||||
|
write!(f, " // CmdID: {}", self.cmd_id)?;
|
||||||
|
}
|
||||||
|
writeln!(f)?;
|
||||||
|
|
||||||
|
for field in self.fields.iter() {
|
||||||
|
write!(f, " {} {} = {};", field.kind, field.name, field.number)?;
|
||||||
|
if let Some(comment) = field.comment.as_ref() {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
" // offset: {}, xor const: {}",
|
||||||
|
comment.offset, comment.xor_const
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for oneof in self.oneofs.iter() {
|
||||||
|
write!(f, "{oneof}")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(f, "}}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Oneof {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
writeln!(f, " oneof {} {{", self.name)?;
|
||||||
|
for field in self.fields.iter() {
|
||||||
|
writeln!(f, " {} {} = {};", field.kind, field.name, field.number)?;
|
||||||
|
}
|
||||||
|
writeln!(f, " }}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Enum {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
writeln!(f, "enum {} {{", self.name)?;
|
||||||
|
for (name, discriminant) in self.variants.iter() {
|
||||||
|
writeln!(f, " {name} = {discriminant};")?;
|
||||||
|
}
|
||||||
|
writeln!(f, "}}\n")
|
||||||
|
}
|
||||||
|
}
|
40
proto-gen/src/util.rs
Normal file
40
proto-gen/src/util.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// VarInt encoding helpers.
|
||||||
|
pub fn varint_length(mut v: u32) -> usize {
|
||||||
|
if v == 0 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut logcounter = 0;
|
||||||
|
while v > 0 {
|
||||||
|
logcounter += 1;
|
||||||
|
v >>= 7;
|
||||||
|
}
|
||||||
|
logcounter
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_varint(dst: &mut Vec<u8>, value: u32) -> usize {
|
||||||
|
const MSB: u8 = 0b1000_0000;
|
||||||
|
|
||||||
|
let mut n = value;
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while n >= 0x80 {
|
||||||
|
dst.push(MSB | (n as u8));
|
||||||
|
i += 1;
|
||||||
|
n >>= 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
dst.push(n as u8);
|
||||||
|
i + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire types. 3 and 4 are deprecated and hoyo don't use them (SGROUP and EGROUP)
|
||||||
|
pub const WIRE_TYPE_VAR_INT: u8 = 0;
|
||||||
|
pub const WIRE_TYPE_I64: u8 = 1;
|
||||||
|
pub const WIRE_TYPE_LENGTH_PREFIXED: u8 = 2;
|
||||||
|
pub const WIRE_TYPE_I32: u8 = 5;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn pack_wire_tag(field_id: u32, wire_type: u8) -> u32 {
|
||||||
|
(field_id << 3) | (wire_type as u32)
|
||||||
|
}
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
Loading…
Reference in a new issue