This commit is contained in:
xeon 2025-01-05 13:53:08 +03:00
parent 82306f3005
commit a91e74611b
37 changed files with 2699 additions and 1 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

214
Cargo.lock generated Normal file
View 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
View 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" }

View file

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
pub mod ffi;
mod util;
pub mod vm;
pub use util::cstr;

36
il2cpp/src/util.rs Normal file
View 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
View 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
View 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)) }
}
}

View 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
View 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
View 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)
}
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"