diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5d49447 --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a023993 --- /dev/null +++ b/Cargo.toml @@ -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" } diff --git a/README.md b/README.md index f03a312..06607e0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,22 @@ # GracefulDumper -all-in-one dumper for Zenless Zone Zero written in Rust \ No newline at end of file +## 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. diff --git a/dumpcs-gen/Cargo.toml b/dumpcs-gen/Cargo.toml new file mode 100644 index 0000000..19bf5e4 --- /dev/null +++ b/dumpcs-gen/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "dumpcs-gen" +version.workspace = true +edition.workspace = true + +[dependencies] +il2cpp.workspace = true diff --git a/dumpcs-gen/src/lib.rs b/dumpcs-gen/src/lib.rs new file mode 100644 index 0000000..6ef1687 --- /dev/null +++ b/dumpcs-gen/src/lib.rs @@ -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(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(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(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(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(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(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(()) +} diff --git a/idapy-gen/Cargo.toml b/idapy-gen/Cargo.toml new file mode 100644 index 0000000..7608c30 --- /dev/null +++ b/idapy-gen/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "idapy-gen" +version.workspace = true +edition.workspace = true + +[dependencies] +il2cpp.workspace = true +metadata.workspace = true diff --git a/idapy-gen/src/lib.rs b/idapy-gen/src/lib.rs new file mode 100644 index 0000000..3e3380b --- /dev/null +++ b/idapy-gen/src/lib.rs @@ -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(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(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(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(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(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, "]") +} diff --git a/idapy-gen/src/util.rs b/idapy-gen/src/util.rs new file mode 100644 index 0000000..4041166 --- /dev/null +++ b/idapy-gen/src/util.rs @@ -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: &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: &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!(), + } + } +} diff --git a/il2cpp/Cargo.toml b/il2cpp/Cargo.toml new file mode 100644 index 0000000..305a4fe --- /dev/null +++ b/il2cpp/Cargo.toml @@ -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", +] } diff --git a/il2cpp/src/ffi.rs b/il2cpp/src/ffi.rs new file mode 100644 index 0000000..83df89b --- /dev/null +++ b/il2cpp/src/ffi.rs @@ -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::() as usize; + (*class.wrapping_add(64).cast::()) as *const usize +} + +pub unsafe fn il2cpp_method_get_address(method: *const u8) -> usize { + *method.wrapping_add(0).cast::() +} + +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::() // 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::()) as *const u8 +} + +pub unsafe fn il2cpp_class_get_parent(class: *const u8) -> *const u8 { + (*class.wrapping_add(104).cast::()) as *const u8 +} + +pub unsafe fn il2cpp_generic_class_get_generic_container(generic_class: *const u8) -> *const u8 { + (*generic_class.wrapping_add(8).cast::()) 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::() 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::() 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 +} diff --git a/il2cpp/src/lib.rs b/il2cpp/src/lib.rs new file mode 100644 index 0000000..90775fc --- /dev/null +++ b/il2cpp/src/lib.rs @@ -0,0 +1,5 @@ +pub mod ffi; +mod util; +pub mod vm; + +pub use util::cstr; diff --git a/il2cpp/src/util.rs b/il2cpp/src/util.rs new file mode 100644 index 0000000..1dcd8ea --- /dev/null +++ b/il2cpp/src/util.rs @@ -0,0 +1,36 @@ +use std::{borrow::Cow, sync::OnceLock}; + +use windows::{core::s, Win32::System::LibraryLoader::LoadLibraryA}; + +static BASE: OnceLock = 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::(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; diff --git a/il2cpp/src/vm/array.rs b/il2cpp/src/vm/array.rs new file mode 100644 index 0000000..4b52e0f --- /dev/null +++ b/il2cpp/src/vm/array.rs @@ -0,0 +1,43 @@ +use super::*; + +#[repr(transparent)] +#[derive(Clone, Copy)] +pub struct Il2cppArray(pub *const u8); + +impl From for Il2cppArray { + fn from(value: usize) -> Self { + Self(value as *const u8) + } +} + +impl From 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::() as usize } + } + + pub fn data_ptr_raw(&self) -> *const u8 { + self.0.wrapping_add(32) + } + + pub fn as_slice(&self) -> &[T] { + unsafe { + std::slice::from_raw_parts(mem::transmute(self.0.wrapping_add(32)), self.length()) + } + } + + pub fn as_mut_slice(&self) -> &mut [T] { + unsafe { + std::slice::from_raw_parts_mut(mem::transmute(self.0.wrapping_add(32)), self.length()) + } + } +} diff --git a/il2cpp/src/vm/assembly.rs b/il2cpp/src/vm/assembly.rs new file mode 100644 index 0000000..a28dc1a --- /dev/null +++ b/il2cpp/src/vm/assembly.rs @@ -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)) } + } +} diff --git a/il2cpp/src/vm/attributes.rs b/il2cpp/src/vm/attributes.rs new file mode 100644 index 0000000..201e1db --- /dev/null +++ b/il2cpp/src/vm/attributes.rs @@ -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; +} diff --git a/il2cpp/src/vm/class.rs b/il2cpp/src/vm/class.rs new file mode 100644 index 0000000..6f54048 --- /dev/null +++ b/il2cpp/src/vm/class.rs @@ -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 { + 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 { + 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 { + 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) } + } +} diff --git a/il2cpp/src/vm/domain.rs b/il2cpp/src/vm/domain.rs new file mode 100644 index 0000000..bc04778 --- /dev/null +++ b/il2cpp/src/vm/domain.rs @@ -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) + } + } +} diff --git a/il2cpp/src/vm/exception.rs b/il2cpp/src/vm/exception.rs new file mode 100644 index 0000000..0b0e2fe --- /dev/null +++ b/il2cpp/src/vm/exception.rs @@ -0,0 +1,65 @@ +use super::*; + +#[repr(transparent)] +#[derive(Clone, Copy)] +pub struct Il2cppException(Il2cppObject); + +impl From for Il2cppException { + fn from(value: usize) -> Self { + Self(Il2cppObject::from(value)) + } +} + +impl From 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 {} diff --git a/il2cpp/src/vm/field.rs b/il2cpp/src/vm/field.rs new file mode 100644 index 0000000..9252620 --- /dev/null +++ b/il2cpp/src/vm/field.rs @@ -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) + } +} diff --git a/il2cpp/src/vm/image.rs b/il2cpp/src/vm/image.rs new file mode 100644 index 0000000..e3275a9 --- /dev/null +++ b/il2cpp/src/vm/image.rs @@ -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 { + let ptr = unsafe { il2cpp_class_from_name(self.0, as_cstr!(namespace), as_cstr!(name)) }; + ((ptr as usize) != 0).then_some(Il2cppClass(ptr)) + } +} diff --git a/il2cpp/src/vm/method.rs b/il2cpp/src/vm/method.rs new file mode 100644 index 0000000..50de2a1 --- /dev/null +++ b/il2cpp/src/vm/method.rs @@ -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>( + &self, + instance: &dyn Il2cppValue, + args: &[&dyn Il2cppValue], + ) -> Result { + let args = args.iter().map(|arg| arg.as_raw()).collect::>(); + + 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, + ))) + } +} diff --git a/il2cpp/src/vm/mod.rs b/il2cpp/src/vm/mod.rs new file mode 100644 index 0000000..c71d228 --- /dev/null +++ b/il2cpp/src/vm/mod.rs @@ -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> 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::().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 for Void { + fn from(_: usize) -> Self { + Self + } +} diff --git a/il2cpp/src/vm/object.rs b/il2cpp/src/vm/object.rs new file mode 100644 index 0000000..d05b3bd --- /dev/null +++ b/il2cpp/src/vm/object.rs @@ -0,0 +1,36 @@ +use super::*; + +#[repr(transparent)] +#[derive(Clone, Copy)] +pub struct Il2cppObject(pub *const u8); + +impl From 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 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) } + } +} diff --git a/il2cpp/src/vm/string.rs b/il2cpp/src/vm/string.rs new file mode 100644 index 0000000..d910b1d --- /dev/null +++ b/il2cpp/src/vm/string.rs @@ -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 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::() 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::(), + self.len(), + )) + .unwrap_or_default() + } + } +} diff --git a/launcher/Cargo.toml b/launcher/Cargo.toml new file mode 100644 index 0000000..fee1024 --- /dev/null +++ b/launcher/Cargo.toml @@ -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" +] } + diff --git a/launcher/src/main.rs b/launcher/src/main.rs new file mode 100644 index 0000000..fd7567d --- /dev/null +++ b/launcher/src/main.rs @@ -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(); + } +} diff --git a/launcher/src/reloc.rs b/launcher/src/reloc.rs new file mode 100644 index 0000000..89f27db --- /dev/null +++ b/launcher/src/reloc.rs @@ -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::()) + / std::mem::size_of::(); + + let relocation_rva = + ((relocation_table as usize) + size_of::()) 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 +} diff --git a/launcher/src/util.rs b/launcher/src/util.rs new file mode 100644 index 0000000..f0f4a10 --- /dev/null +++ b/launcher/src/util.rs @@ -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(); + } +} diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml new file mode 100644 index 0000000..0b39377 --- /dev/null +++ b/metadata/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "metadata" +version.workspace = true +edition.workspace = true + +[dependencies] +il2cpp.workspace = true diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs new file mode 100644 index 0000000..e04bda0 --- /dev/null +++ b/metadata/src/lib.rs @@ -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 { + 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 +} diff --git a/proto-gen/Cargo.toml b/proto-gen/Cargo.toml new file mode 100644 index 0000000..55a046b --- /dev/null +++ b/proto-gen/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "proto-gen" +version.workspace = true +edition.workspace = true + +[dependencies] +il2cpp.workspace = true diff --git a/proto-gen/src/cache.rs b/proto-gen/src/cache.rs new file mode 100644 index 0000000..754a855 --- /dev/null +++ b/proto-gen/src/cache.rs @@ -0,0 +1,110 @@ +use std::collections::BTreeMap; + +use il2cpp::vm::Il2cppDomain; + +pub struct TypeCache { + pub type_map: BTreeMap, +} + +#[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, + ), + ]), + } + } +} diff --git a/proto-gen/src/lib.rs b/proto-gen/src/lib.rs new file mode 100644 index 0000000..082d89d --- /dev/null +++ b/proto-gen/src/lib.rs @@ -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, +} + +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, +} + +pub struct FieldMinimalInfo { + pub tag: u32, + pub xor: u32, + pub offset: u32, + pub oneof_extra_data: Option, +} + +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::() as usize, + Single | Int32 | UInt32 => { + *field.get_field_data_ptr(object).cast::() as usize + } + Double | Int64 | UInt64 => { + *field.get_field_data_ptr(object).cast::() as usize + } + Object | Any | ByteString | String => { + *field.get_field_data_ptr(object).cast::() + } + _ => unreachable!(), + } + } + } else { + if field_class.get_generic_argument_count() != 0 { + let collection = unsafe { + Il2cppObject(*(field.get_field_data_ptr(object).cast::()) as *const u8) + }; + let get_count = collection.class().find_method("get_Count", &[]).unwrap(); + + // method returns an object-packed (boxed) value + unsafe { + *(get_count + .invoke::(&collection, &[]) + .unwrap() + .0 + .wrapping_add(16) + .cast::()) 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::() as usize } + } else { + unsafe { *field.get_field_data_ptr(object).cast::() } + } + } + } + + pub fn detect_changes_and_update(&mut self, object: &Il2cppObject) -> Vec { + 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(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::(&proto_instance, &[]) + .unwrap(); + + let cmd_id = *(get_cmd_id + .invoke::(&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 + &[5, 0x10, 0x01, 0x33, 0x10, 0x00], // map + ]; + + 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::::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, 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::(&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::(&coded_input_stream, &[&byte_array]) + .unwrap(); + + coded_input_stream +} diff --git a/proto-gen/src/output.rs b/proto-gen/src/output.rs new file mode 100644 index 0000000..66151c4 --- /dev/null +++ b/proto-gen/src/output.rs @@ -0,0 +1,108 @@ +use std::{borrow::Cow, fmt}; + +pub struct ProtoFile { + pub syntax: String, + pub imports: Vec, + pub items: Vec, +} + +pub struct Message { + pub cmd_id: u16, + pub name: String, + pub fields: Vec, + pub oneofs: Vec, +} + +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, +} + +pub struct FieldComment { + pub offset: u32, + pub xor_const: u32, +} + +pub struct Oneof { + pub name: String, + pub fields: Vec, +} + +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") + } +} diff --git a/proto-gen/src/util.rs b/proto-gen/src/util.rs new file mode 100644 index 0000000..93a090d --- /dev/null +++ b/proto-gen/src/util.rs @@ -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, 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) +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly"