mirror of
https://git.ely.my.id/Tenshi/Hi3-Patcher
synced 2026-11-04 21:46:48 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daece35d94 |
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "Hi3-Patcher"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
edition = "2024"
|
||||
description = "Honkai Impact 3rd Windows and Android patcher"
|
||||
authors = ["Tenshi"]
|
||||
|
||||
+17
-14
@@ -53,18 +53,21 @@ The CN passport login URL is a plaintext string in the DEX that declares
|
||||
`…account.sdk.utils.RSAUtils`; only that DEX is edited. The server must
|
||||
use **HTTP** — HTTPS is rejected.
|
||||
|
||||
The stock DEX has exactly one loopback string, `http://127.0.0.1` (an SDK
|
||||
default), between known neighbors in the sorted `string_ids` table. The
|
||||
patch:
|
||||
The patch inserts the origin as a new string at its sorted position in
|
||||
`string_ids` (`format::dex::insert_string`) and repoints the
|
||||
`const-string` that loaded the official login URL
|
||||
(`https://passport-api.mihoyo.com`) to it, so login goes to the server.
|
||||
Any origin is accepted, not only ones that sort next to a reusable slot.
|
||||
|
||||
1. Appends a `string_data_item` for the chosen origin
|
||||
(`format::dex::append_string`) and points the loopback string id at
|
||||
it.
|
||||
2. Points the `const-string` that loaded the official login URL
|
||||
(`https://passport-api.mihoyo.com`) at that id, so login goes to the
|
||||
server.
|
||||
3. Moves the original loopback `const-string` to the neighboring id to
|
||||
keep `string_ids` sorted.
|
||||
|
||||
The origin must sort between those neighbors; other addresses are
|
||||
rejected. Checksums are recomputed.
|
||||
Insertion shifts every string index at or after the new one, so
|
||||
`insert_string` renumbers all references — the fixed-width id tables,
|
||||
`const-string`/`const-string/jumbo` operands, and the ULEB128 string
|
||||
indices in debug info and annotations — and relocates every file offset
|
||||
(itself and the string data grow 4-aligned). A DEX with more than 65536
|
||||
strings can hold a 16-bit `const-string` whose operand would overflow
|
||||
when renumbered; `format::dex::promote` rewrites those to
|
||||
`const-string/jumbo` first, rebuilding the affected method (branch and
|
||||
switch targets fixed, the code item re-padded to 4-byte alignment). If
|
||||
the origin is already present (for example the stock `http://127.0.0.1`
|
||||
when the server runs on the device), the login is repointed to the
|
||||
existing id with no insertion. Checksums are recomputed.
|
||||
@@ -214,3 +214,863 @@ pub fn append_string(data: &mut Vec<u8>, value: &[u8]) -> Result<usize> {
|
||||
data.splice(string_end..insertion, item);
|
||||
Ok(string_end)
|
||||
}
|
||||
|
||||
// ---- String insertion (sorted) with full reference renumbering --------------
|
||||
// Ported from the validated Python prototype. Inserts a new string at its
|
||||
// sorted position, renumbering every string reference and relocating every
|
||||
// offset. `const-string` instructions whose operand would exceed 0xFFFF are
|
||||
// promoted to `const-string/jumbo` by `promote` first.
|
||||
|
||||
/// Instruction width in 16-bit code units, by opcode. Payloads are handled
|
||||
/// separately in `walk`.
|
||||
fn widths() -> [usize; 256] {
|
||||
let mut w = [1usize; 256];
|
||||
for (op, wd) in [
|
||||
(0x02, 2), (0x03, 3), (0x05, 2), (0x06, 3), (0x08, 2), (0x09, 3), (0x13, 2),
|
||||
(0x14, 3), (0x15, 2), (0x16, 2), (0x17, 3), (0x18, 5), (0x19, 2), (0x1a, 2),
|
||||
(0x1b, 3), (0x1c, 2), (0x1f, 2), (0x20, 2), (0x22, 2), (0x23, 2), (0x24, 3),
|
||||
(0x25, 3), (0x26, 3), (0x29, 2), (0x2a, 3), (0x2b, 3), (0x2c, 3),
|
||||
] {
|
||||
w[op] = wd;
|
||||
}
|
||||
for (a, b, wd) in [
|
||||
(0x2d, 0x31, 2), (0x32, 0x37, 2), (0x38, 0x3d, 2), (0x44, 0x51, 2),
|
||||
(0x52, 0x5f, 2), (0x60, 0x6d, 2), (0x6e, 0x72, 3), (0x74, 0x78, 3),
|
||||
(0x90, 0xaf, 2), (0xd0, 0xd7, 2), (0xd8, 0xe2, 2),
|
||||
] {
|
||||
for op in a..=b {
|
||||
w[op] = wd;
|
||||
}
|
||||
}
|
||||
w[0xfa] = 4;
|
||||
w[0xfb] = 4;
|
||||
w[0xfc] = 3;
|
||||
w[0xfd] = 3;
|
||||
w[0xfe] = 2;
|
||||
w[0xff] = 2;
|
||||
w
|
||||
}
|
||||
|
||||
/// Walk an `insns` array (`size` 16-bit units), returning `(unit, opcode)` for
|
||||
/// each instruction. Packed/sparse-switch and fill-array-data payloads are
|
||||
/// skipped by their encoded length.
|
||||
fn walk(insns: &[u8], size: usize, w: &[usize; 256]) -> Result<Vec<(usize, u8)>> {
|
||||
let u16at = |p: usize| -> Result<usize> {
|
||||
Ok(u16::from_le_bytes(insns.get(p..p + 2).context("insns OOB")?.try_into()?) as usize)
|
||||
};
|
||||
let mut out = vec![];
|
||||
let mut i = 0;
|
||||
while i < size {
|
||||
let op = *insns.get(i * 2).context("insns OOB")?;
|
||||
out.push((i, op));
|
||||
if op == 0x00 && insns[i * 2 + 1] != 0x00 {
|
||||
match insns[i * 2 + 1] {
|
||||
0x01 => {
|
||||
i += 4 + u16at(i * 2 + 2)? * 2;
|
||||
continue;
|
||||
}
|
||||
0x02 => {
|
||||
i += 2 + u16at(i * 2 + 2)? * 4;
|
||||
continue;
|
||||
}
|
||||
0x03 => {
|
||||
let width = u16at(i * 2 + 2)?;
|
||||
let count =
|
||||
u32::from_le_bytes(insns[i * 2 + 4..i * 2 + 8].try_into()?) as usize;
|
||||
i += 4 + (width * count + 1) / 2;
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
i += w[op as usize];
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn uleb(data: &[u8], pos: usize) -> Result<(usize, usize)> {
|
||||
let (mut result, mut shift, mut p) = (0usize, 0u32, pos);
|
||||
loop {
|
||||
let b = *data.get(p).context("truncated uleb")?;
|
||||
p += 1;
|
||||
result |= ((b & 0x7f) as usize) << shift;
|
||||
if b & 0x80 == 0 {
|
||||
return Ok((result, p));
|
||||
}
|
||||
shift += 7;
|
||||
}
|
||||
}
|
||||
|
||||
fn enc_uleb(mut v: usize) -> Vec<u8> {
|
||||
let mut out = vec![];
|
||||
loop {
|
||||
let b = (v & 0x7f) as u8;
|
||||
v >>= 7;
|
||||
out.push(if v != 0 { b | 0x80 } else { b });
|
||||
if v == 0 {
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Index where `value` belongs in the sorted string table, and whether it is
|
||||
/// already present. Byte order matches code-point order for the ASCII origins
|
||||
/// this is used with.
|
||||
pub fn sorted_position(data: &[u8], value: &[u8]) -> Result<(usize, bool)> {
|
||||
let n = word(data, 0x38)?;
|
||||
let (mut lo, mut hi) = (0usize, n);
|
||||
while lo < hi {
|
||||
let mid = (lo + hi) / 2;
|
||||
if string(data, mid)? < value {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
let exists = lo < n && string(data, lo)? == value;
|
||||
Ok((lo, exists))
|
||||
}
|
||||
|
||||
fn le4(v: usize) -> Result<[u8; 4]> {
|
||||
Ok(u32::try_from(v)?.to_le_bytes())
|
||||
}
|
||||
|
||||
/// Collected edit: write `bytes` at `relocate(pos)` in the output.
|
||||
type Edit = (usize, Vec<u8>);
|
||||
|
||||
/// Code offsets reached while walking class_data: `(code_item_off, code_off_uleb_pos)`.
|
||||
fn class_data_methods(data: &[u8], mut off: usize) -> Result<Vec<(usize, usize)>> {
|
||||
let sf;
|
||||
let ifl;
|
||||
let dm;
|
||||
let vm;
|
||||
(sf, off) = uleb(data, off)?;
|
||||
(ifl, off) = uleb(data, off)?;
|
||||
(dm, off) = uleb(data, off)?;
|
||||
(vm, off) = uleb(data, off)?;
|
||||
for _ in 0..sf + ifl {
|
||||
(_, off) = uleb(data, off)?;
|
||||
(_, off) = uleb(data, off)?;
|
||||
}
|
||||
let mut out = vec![];
|
||||
for _ in 0..dm + vm {
|
||||
(_, off) = uleb(data, off)?;
|
||||
(_, off) = uleb(data, off)?;
|
||||
let pos = off;
|
||||
let c;
|
||||
(c, off) = uleb(data, off)?;
|
||||
if c != 0 {
|
||||
out.push((c, pos));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Shared state for the renumber/relocate traversal of `insert_string`.
|
||||
struct Inserter<'a> {
|
||||
data: &'a [u8],
|
||||
p: usize, // insertion string index
|
||||
id_ins: usize, // byte position of the new string_id slot
|
||||
anno: usize, // annotation section offset (string_data insertion boundary)
|
||||
sdelta: usize, // string_data growth
|
||||
w: [usize; 256],
|
||||
edits: Vec<Edit>,
|
||||
seen_set: std::collections::BTreeSet<usize>,
|
||||
seen_item: std::collections::BTreeSet<usize>,
|
||||
seen_arr: std::collections::BTreeSet<usize>,
|
||||
}
|
||||
|
||||
impl Inserter<'_> {
|
||||
fn reloc(&self, v: usize) -> usize {
|
||||
if v == 0 {
|
||||
return 0;
|
||||
}
|
||||
v + if v >= self.id_ins { 4 } else { 0 } + if v >= self.anno { self.sdelta } else { 0 }
|
||||
}
|
||||
fn put_off(&mut self, pos: usize) -> Result<()> {
|
||||
let v = word(self.data, pos)?;
|
||||
let r = self.reloc(v);
|
||||
self.edits.push((pos, le4(r)?.to_vec()));
|
||||
Ok(())
|
||||
}
|
||||
fn bump_u32(&mut self, pos: usize) -> Result<()> {
|
||||
let v = word(self.data, pos)?;
|
||||
if v != 0xFFFF_FFFF && v >= self.p {
|
||||
self.edits.push((pos, le4(v + 1)?.to_vec()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn do_set(&mut self, off: usize) -> Result<()> {
|
||||
if off == 0 || !self.seen_set.insert(off) {
|
||||
return Ok(());
|
||||
}
|
||||
for j in 0..word(self.data, off)? {
|
||||
self.put_off(off + 4 + j * 4)?;
|
||||
self.do_item(word(self.data, off + 4 + j * 4)?)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn do_setref(&mut self, off: usize) -> Result<()> {
|
||||
if off == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
for j in 0..word(self.data, off)? {
|
||||
self.put_off(off + 4 + j * 4)?;
|
||||
self.do_set(word(self.data, off + 4 + j * 4)?)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn do_item(&mut self, off: usize) -> Result<()> {
|
||||
if off == 0 || !self.seen_item.insert(off) {
|
||||
return Ok(());
|
||||
}
|
||||
self.enc_annotation(off + 1)?; // skip visibility byte
|
||||
Ok(())
|
||||
}
|
||||
fn enc_annotation(&mut self, off: usize) -> Result<usize> {
|
||||
let (_, mut off) = uleb(self.data, off)?; // type_idx
|
||||
let size;
|
||||
(size, off) = uleb(self.data, off)?;
|
||||
for _ in 0..size {
|
||||
let (raw, end) = uleb(self.data, off)?; // name_idx (string)
|
||||
if raw >= self.p {
|
||||
let nb = enc_uleb(raw + 1);
|
||||
ensure!(nb.len() == enc_uleb(raw).len(), "annotation name_idx uleb length change");
|
||||
self.edits.push((off, nb));
|
||||
}
|
||||
off = self.enc_value(end)?;
|
||||
}
|
||||
Ok(off)
|
||||
}
|
||||
fn enc_value(&mut self, off: usize) -> Result<usize> {
|
||||
let arg = *self.data.get(off).context("encoded_value OOB")?;
|
||||
let off = off + 1;
|
||||
let vtype = arg & 0x1f;
|
||||
let varg = (arg >> 5) as usize;
|
||||
match vtype {
|
||||
0x17 => {
|
||||
// VALUE_STRING: (varg+1) LE bytes
|
||||
let n = varg + 1;
|
||||
let mut v = 0usize;
|
||||
for k in 0..n {
|
||||
v |= (self.data[off + k] as usize) << (8 * k);
|
||||
}
|
||||
if v >= self.p {
|
||||
let nv = v + 1;
|
||||
ensure!(nv >> (8 * n) == 0, "VALUE_STRING size boundary");
|
||||
self.edits.push((off, nv.to_le_bytes()[..n].to_vec()));
|
||||
}
|
||||
Ok(off + n)
|
||||
}
|
||||
0x15 | 0x16 | 0x18 | 0x19 | 0x1a | 0x1b => Ok(off + varg + 1),
|
||||
0x1c => {
|
||||
// ARRAY
|
||||
let (size, mut p) = uleb(self.data, off)?;
|
||||
for _ in 0..size {
|
||||
p = self.enc_value(p)?;
|
||||
}
|
||||
Ok(p)
|
||||
}
|
||||
0x1d => self.enc_annotation(off), // ANNOTATION
|
||||
0x1e | 0x1f => Ok(off), // NULL / BOOLEAN
|
||||
_ => Ok(off + varg + 1), // numeric
|
||||
}
|
||||
}
|
||||
fn bump_uleb_p1(&mut self, pos: usize) -> Result<usize> {
|
||||
let (raw, end) = uleb(self.data, pos)?;
|
||||
if raw != 0 && raw - 1 >= self.p {
|
||||
let nb = enc_uleb(raw + 1);
|
||||
ensure!(nb.len() == enc_uleb(raw).len(), "debug uleb length change");
|
||||
self.edits.push((pos, nb));
|
||||
}
|
||||
Ok(end)
|
||||
}
|
||||
fn renumber_debug(&mut self, off: usize) -> Result<()> {
|
||||
let (_, mut off) = uleb(self.data, off)?; // line_start
|
||||
let pc;
|
||||
(pc, off) = uleb(self.data, off)?;
|
||||
for _ in 0..pc {
|
||||
off = self.bump_uleb_p1(off)?;
|
||||
}
|
||||
loop {
|
||||
let op = *self.data.get(off).context("debug OOB")?;
|
||||
off += 1;
|
||||
match op {
|
||||
0x00 => return Ok(()),
|
||||
0x01 => (_, off) = uleb(self.data, off)?,
|
||||
0x02 => {
|
||||
while self.data[off] >= 0x80 {
|
||||
off += 1;
|
||||
}
|
||||
off += 1;
|
||||
}
|
||||
0x03 | 0x04 => {
|
||||
(_, off) = uleb(self.data, off)?; // register
|
||||
off = self.bump_uleb_p1(off)?; // name_idx
|
||||
(_, off) = uleb(self.data, off)?; // type_idx
|
||||
if op == 0x04 {
|
||||
off = self.bump_uleb_p1(off)?; // sig_idx
|
||||
}
|
||||
}
|
||||
0x05 | 0x06 => (_, off) = uleb(self.data, off)?,
|
||||
0x09 => off = self.bump_uleb_p1(off)?,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert `value` at its sorted position in the DEX string table, renumbering
|
||||
/// every string reference and relocating every offset. Returns the patched
|
||||
/// bytes and the new string index. Checksums are NOT recomputed (the caller
|
||||
/// does it after any further edits).
|
||||
pub fn insert_string(data: &[u8], value: &[u8]) -> Result<(Vec<u8>, usize)> {
|
||||
ensure!(
|
||||
data.starts_with(b"dex\n") && word(data, 0x20)? == data.len(),
|
||||
"invalid DEX"
|
||||
);
|
||||
ensure!(value.is_ascii() && !value.contains(&0), "invalid string");
|
||||
let (p, exists) = sorted_position(data, value)?;
|
||||
ensure!(!exists, "string already present");
|
||||
|
||||
let str_off = word(data, 0x3c)?;
|
||||
let id_ins = str_off + p * 4;
|
||||
let (sd_size, sd_off) = section(data, 0x2002)?;
|
||||
let (_, anno) = section(data, 0x2004)?;
|
||||
let mut pos = sd_off;
|
||||
for _ in 0..sd_size {
|
||||
loop {
|
||||
let b = *data.get(pos).context("truncated string length")?;
|
||||
pos += 1;
|
||||
if b & 0x80 == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let rel = data
|
||||
.get(pos..)
|
||||
.context("string OOB")?
|
||||
.iter()
|
||||
.position(|&b| b == 0)
|
||||
.context("unterminated string")?;
|
||||
pos += rel + 1;
|
||||
}
|
||||
let string_end = pos;
|
||||
let old_pad = anno.checked_sub(string_end).context("bad padding")?;
|
||||
ensure!(old_pad < 4, "unexpected string_data padding");
|
||||
|
||||
let mut item = enc_uleb(value.len());
|
||||
item.extend_from_slice(value);
|
||||
item.push(0);
|
||||
let sdelta = (item.len() + 3 - old_pad) & !3;
|
||||
item.resize(old_pad + sdelta, 0);
|
||||
|
||||
let mut ins = Inserter {
|
||||
data,
|
||||
p,
|
||||
id_ins,
|
||||
anno,
|
||||
sdelta,
|
||||
w: widths(),
|
||||
edits: vec![],
|
||||
seen_set: Default::default(),
|
||||
seen_item: Default::default(),
|
||||
seen_arr: Default::default(),
|
||||
};
|
||||
|
||||
for pos in [0x30, 0x34, 0x44, 0x4c, 0x54, 0x5c, 0x64, 0x6c] {
|
||||
ins.put_off(pos)?;
|
||||
}
|
||||
let type_off = word(data, 0x44)?;
|
||||
for i in 0..word(data, 0x40)? {
|
||||
ins.bump_u32(type_off + i * 4)?;
|
||||
}
|
||||
let proto_off = word(data, 0x4c)?;
|
||||
for i in 0..word(data, 0x48)? {
|
||||
ins.bump_u32(proto_off + i * 12)?;
|
||||
ins.put_off(proto_off + i * 12 + 8)?;
|
||||
}
|
||||
let field_off = word(data, 0x54)?;
|
||||
for i in 0..word(data, 0x50)? {
|
||||
ins.bump_u32(field_off + i * 8 + 4)?;
|
||||
}
|
||||
let method_off = word(data, 0x5c)?;
|
||||
for i in 0..word(data, 0x58)? {
|
||||
ins.bump_u32(method_off + i * 8 + 4)?;
|
||||
}
|
||||
let class_off = word(data, 0x64)?;
|
||||
let mut code_offs = vec![];
|
||||
let mut static_offs = vec![];
|
||||
let mut anno_dirs = vec![];
|
||||
for i in 0..word(data, 0x60)? {
|
||||
let b = class_off + i * 32;
|
||||
ins.put_off(b + 12)?;
|
||||
ins.bump_u32(b + 16)?;
|
||||
ins.put_off(b + 20)?;
|
||||
ins.put_off(b + 24)?;
|
||||
ins.put_off(b + 28)?;
|
||||
let cdo = word(data, b + 24)?;
|
||||
if cdo != 0 {
|
||||
for (co, co_pos) in class_data_methods(data, cdo)? {
|
||||
let nb = enc_uleb(ins.reloc(co));
|
||||
ensure!(nb.len() == enc_uleb(co).len(), "code_off uleb length change");
|
||||
ins.edits.push((co_pos, nb));
|
||||
code_offs.push(co);
|
||||
}
|
||||
}
|
||||
let sv = word(data, b + 28)?;
|
||||
if sv != 0 {
|
||||
static_offs.push(sv);
|
||||
}
|
||||
let ad = word(data, b + 20)?;
|
||||
if ad != 0 {
|
||||
anno_dirs.push(ad);
|
||||
}
|
||||
}
|
||||
|
||||
let mut debug_offs = std::collections::BTreeSet::new();
|
||||
let w = ins.w;
|
||||
for &coff in &code_offs {
|
||||
ins.put_off(coff + 8)?;
|
||||
let dbg = word(data, coff + 8)?;
|
||||
if dbg != 0 {
|
||||
debug_offs.insert(dbg);
|
||||
}
|
||||
let isz = word(data, coff + 12)?;
|
||||
let insns = &data[coff + 16..coff + 16 + isz * 2];
|
||||
for (u, op) in walk(insns, isz, &w)? {
|
||||
let ipos = coff + 16 + u * 2;
|
||||
if op == 0x1a {
|
||||
let v = u16::from_le_bytes([insns[u * 2 + 2], insns[u * 2 + 3]]) as usize;
|
||||
if v >= p {
|
||||
ensure!(v + 1 <= 0xFFFF, "const-string needs jumbo promotion");
|
||||
ins.edits
|
||||
.push((ipos + 2, u16::try_from(v + 1)?.to_le_bytes().to_vec()));
|
||||
}
|
||||
} else if op == 0x1b {
|
||||
let v = u32::from_le_bytes(insns[u * 2 + 2..u * 2 + 6].try_into()?) as usize;
|
||||
if v >= p {
|
||||
ins.edits.push((ipos + 2, le4(v + 1)?.to_vec()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for dbg in debug_offs {
|
||||
ins.renumber_debug(dbg)?;
|
||||
}
|
||||
|
||||
for ad in anno_dirs {
|
||||
ins.put_off(ad)?;
|
||||
ins.do_set(word(data, ad)?)?;
|
||||
let (fc, mc, pc) = (word(data, ad + 4)?, word(data, ad + 8)?, word(data, ad + 12)?);
|
||||
let mut q = ad + 16;
|
||||
for _ in 0..fc {
|
||||
ins.put_off(q + 4)?;
|
||||
ins.do_set(word(data, q + 4)?)?;
|
||||
q += 8;
|
||||
}
|
||||
for _ in 0..mc {
|
||||
ins.put_off(q + 4)?;
|
||||
ins.do_set(word(data, q + 4)?)?;
|
||||
q += 8;
|
||||
}
|
||||
for _ in 0..pc {
|
||||
ins.put_off(q + 4)?;
|
||||
ins.do_setref(word(data, q + 4)?)?;
|
||||
q += 8;
|
||||
}
|
||||
}
|
||||
for sv in static_offs {
|
||||
if ins.seen_arr.insert(sv) {
|
||||
let (size, mut q) = uleb(data, sv)?;
|
||||
for _ in 0..size {
|
||||
q = ins.enc_value(q)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let map_off = word(data, 0x34)?;
|
||||
for i in 0..word(data, map_off)? {
|
||||
let at = map_off + 4 + i * 12;
|
||||
let ty = u16::from_le_bytes(data[at..at + 2].try_into()?);
|
||||
if ty == 0x0001 || ty == 0x2002 {
|
||||
let sz = word(data, at + 4)?;
|
||||
ins.edits.push((at + 4, le4(sz + 1)?.to_vec()));
|
||||
}
|
||||
ins.put_off(at + 8)?;
|
||||
}
|
||||
|
||||
// build output
|
||||
let mut out = Vec::with_capacity(data.len() + 4 + sdelta);
|
||||
out.extend_from_slice(&data[..id_ins]);
|
||||
out.extend_from_slice(&[0, 0, 0, 0]);
|
||||
out.extend_from_slice(&data[id_ins..string_end]);
|
||||
out.extend_from_slice(&item);
|
||||
out.extend_from_slice(&data[string_end + old_pad..]);
|
||||
|
||||
out[0x20..0x24].copy_from_slice(&le4(data.len() + 4 + sdelta)?);
|
||||
out[0x38..0x3c].copy_from_slice(&le4(word(data, 0x38)? + 1)?);
|
||||
out[0x68..0x6c].copy_from_slice(&le4(word(data, 0x68)? + sdelta)?);
|
||||
|
||||
let str_size = word(data, 0x38)?;
|
||||
for j in 0..str_size + 1 {
|
||||
let val = if j == p {
|
||||
string_end + 4
|
||||
} else {
|
||||
ins.reloc(word(data, str_off + (if j < p { j } else { j - 1 }) * 4)?)
|
||||
};
|
||||
out[str_off + j * 4..str_off + j * 4 + 4].copy_from_slice(&le4(val)?);
|
||||
}
|
||||
for (pos, nb) in &ins.edits {
|
||||
let rp = ins.reloc(*pos);
|
||||
out[rp..rp + nb.len()].copy_from_slice(nb);
|
||||
}
|
||||
Ok((out, p))
|
||||
}
|
||||
|
||||
// ---- const-string -> jumbo promotion (for string tables > 65536) ------------
|
||||
|
||||
/// Branch operand layout: `(operand_byte_offset, signed_width_bytes)`.
|
||||
fn branch_fmt(op: u8) -> Option<(usize, usize)> {
|
||||
match op {
|
||||
0x28 => Some((1, 1)),
|
||||
0x29 => Some((2, 2)),
|
||||
0x2a => Some((2, 4)),
|
||||
0x32..=0x3d => Some((2, 2)),
|
||||
0x2b | 0x2c | 0x26 => Some((2, 4)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_signed(data: &[u8], pos: usize, width: usize) -> Result<isize> {
|
||||
Ok(match width {
|
||||
1 => data[pos] as i8 as isize,
|
||||
2 => i16::from_le_bytes(data[pos..pos + 2].try_into()?) as isize,
|
||||
_ => i32::from_le_bytes(data[pos..pos + 4].try_into()?) as isize,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_signed(out: &mut [u8], pos: usize, width: usize, v: isize) {
|
||||
match width {
|
||||
1 => out[pos] = v as i8 as u8,
|
||||
2 => out[pos..pos + 2].copy_from_slice(&(v as i16).to_le_bytes()),
|
||||
_ => out[pos..pos + 4].copy_from_slice(&(v as i32).to_le_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
fn newunit(u: usize, units: &[usize]) -> usize {
|
||||
u + units.iter().filter(|&&pu| u >= pu + 2).count()
|
||||
}
|
||||
|
||||
/// Rebuild a method's `insns`, promoting each unit in `units` to jumbo and
|
||||
/// fixing branch / switch targets for the resulting 1-unit shifts.
|
||||
fn rebuild_insns(insns: &[u8], isz: usize, units: &[usize], w: &[usize; 256]) -> Result<Vec<u8>> {
|
||||
let walked = walk(insns, isz, w)?;
|
||||
let mut lengths = std::collections::BTreeMap::new();
|
||||
for (k, &(s, _)) in walked.iter().enumerate() {
|
||||
let nxt = walked.get(k + 1).map(|x| x.0).unwrap_or(isz);
|
||||
lengths.insert(s, nxt - s);
|
||||
}
|
||||
let promo: std::collections::BTreeSet<usize> = units.iter().copied().collect();
|
||||
let mut out = Vec::new();
|
||||
let mut off_map = std::collections::BTreeMap::new();
|
||||
for &(s, op) in &walked {
|
||||
off_map.insert(s, out.len() / 2);
|
||||
let ln = lengths[&s];
|
||||
let mut chunk = insns[s * 2..s * 2 + ln * 2].to_vec();
|
||||
if promo.contains(&s) {
|
||||
let reg = chunk[1];
|
||||
let idx = u16::from_le_bytes([chunk[2], chunk[3]]) as u32;
|
||||
chunk = vec![0x1b, reg];
|
||||
chunk.extend_from_slice(&idx.to_le_bytes());
|
||||
} else if let Some((boff, width)) = branch_fmt(op) {
|
||||
let old = read_signed(insns, s * 2 + boff, width)?;
|
||||
let t = (s as isize + old) as usize;
|
||||
let new = newunit(t, units) as isize - newunit(s, units) as isize;
|
||||
write_signed(&mut chunk, boff, width, new);
|
||||
}
|
||||
out.extend_from_slice(&chunk);
|
||||
}
|
||||
// fix switch payload targets (relative to the switch instruction)
|
||||
for &(s, op) in &walked {
|
||||
if op == 0x2b || op == 0x2c {
|
||||
let payoff = i32::from_le_bytes(insns[s * 2 + 2..s * 2 + 6].try_into()?) as isize;
|
||||
let pay = (s as isize + payoff) as usize;
|
||||
let ident = insns[pay * 2 + 1];
|
||||
let n = u16::from_le_bytes([insns[pay * 2 + 2], insns[pay * 2 + 3]]) as usize;
|
||||
let tbl = pay * 2 + if ident == 0x01 { 8 } else { 4 + n * 4 };
|
||||
let npay = off_map[&pay];
|
||||
let ntbl = npay * 2 + if ident == 0x01 { 8 } else { 4 + n * 4 };
|
||||
for k in 0..n {
|
||||
let old = i32::from_le_bytes(insns[tbl + k * 4..tbl + k * 4 + 4].try_into()?) as isize;
|
||||
let t = (s as isize + old) as usize;
|
||||
let nv = newunit(t, units) as isize - newunit(s, units) as isize;
|
||||
out[ntbl + k * 4..ntbl + k * 4 + 4].copy_from_slice(&(nv as i32).to_le_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
struct Promoter<'a> {
|
||||
data: &'a [u8],
|
||||
growth: Vec<(usize, usize)>,
|
||||
edits: Vec<Edit>,
|
||||
seen: std::collections::BTreeSet<usize>,
|
||||
}
|
||||
|
||||
impl Promoter<'_> {
|
||||
fn reloc(&self, v: usize) -> usize {
|
||||
if v == 0 {
|
||||
return 0;
|
||||
}
|
||||
v + self
|
||||
.growth
|
||||
.iter()
|
||||
.filter(|(g, _)| v >= *g)
|
||||
.map(|(_, a)| a)
|
||||
.sum::<usize>()
|
||||
}
|
||||
fn put_off(&mut self, pos: usize) -> Result<()> {
|
||||
let v = word(self.data, pos)?;
|
||||
let r = self.reloc(v);
|
||||
self.edits.push((pos, le4(r)?.to_vec()));
|
||||
Ok(())
|
||||
}
|
||||
fn do_set(&mut self, off: usize) -> Result<()> {
|
||||
if off == 0 || !self.seen.insert(off) {
|
||||
return Ok(());
|
||||
}
|
||||
for j in 0..word(self.data, off)? {
|
||||
self.put_off(off + 4 + j * 4)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn do_setref(&mut self, off: usize) -> Result<()> {
|
||||
if off == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
for j in 0..word(self.data, off)? {
|
||||
self.put_off(off + 4 + j * 4)?;
|
||||
self.do_set(word(self.data, off + 4 + j * 4)?)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// All code items as `(code_item_off, code_off_uleb_pos)`.
|
||||
fn all_code_items(data: &[u8]) -> Result<Vec<(usize, usize)>> {
|
||||
let class_off = word(data, 0x64)?;
|
||||
let mut out = vec![];
|
||||
for i in 0..word(data, 0x60)? {
|
||||
let cdo = word(data, class_off + i * 32 + 24)?;
|
||||
if cdo != 0 {
|
||||
out.extend(class_data_methods(data, cdo)?);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Promote the 16-bit `const-string` instructions whose starts are in
|
||||
/// `positions` to `const-string/jumbo`. Returns the patched bytes (checksums
|
||||
/// not recomputed).
|
||||
pub fn promote(data: &[u8], mut positions: Vec<usize>) -> Result<Vec<u8>> {
|
||||
positions.sort_unstable();
|
||||
positions.dedup();
|
||||
if positions.is_empty() {
|
||||
return Ok(data.to_vec());
|
||||
}
|
||||
let w = widths();
|
||||
let items: Vec<(usize, usize)> = all_code_items(data)?
|
||||
.into_iter()
|
||||
.map(|(c, _)| Ok((c, word(data, c + 12)?)))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
// group promotion positions by owning code item
|
||||
let mut per_item: std::collections::BTreeMap<usize, (usize, Vec<usize>)> = Default::default();
|
||||
for &pstn in &positions {
|
||||
let &(coff, isz) = items
|
||||
.iter()
|
||||
.find(|&&(c, s)| c + 16 <= pstn && pstn < c + 16 + s * 2)
|
||||
.context("promotion position not in a code item")?;
|
||||
ensure!(data[pstn] == 0x1a, "promotion target is not const-string");
|
||||
per_item
|
||||
.entry(coff)
|
||||
.or_insert((isz, vec![]))
|
||||
.1
|
||||
.push((pstn - coff - 16) / 2);
|
||||
}
|
||||
|
||||
// per-item slots and growth
|
||||
struct Plan {
|
||||
coff: usize,
|
||||
isz: usize,
|
||||
units: Vec<usize>,
|
||||
old_slot: usize,
|
||||
new_isz: usize,
|
||||
new_slot: usize,
|
||||
}
|
||||
let mut plans = vec![];
|
||||
let mut growth = vec![];
|
||||
for (coff, (isz, units)) in &per_item {
|
||||
let tries = u16::from_le_bytes(data[coff + 6..coff + 8].try_into()?);
|
||||
ensure!(tries == 0, "const-string promotion inside a method with try blocks");
|
||||
let old_slot = (16 + isz * 2 + 3) & !3;
|
||||
let new_isz = isz + units.len();
|
||||
let new_slot = (16 + new_isz * 2 + 3) & !3;
|
||||
growth.push((coff + old_slot, new_slot - old_slot));
|
||||
plans.push(Plan {
|
||||
coff: *coff,
|
||||
isz: *isz,
|
||||
units: units.clone(),
|
||||
old_slot,
|
||||
new_isz,
|
||||
new_slot,
|
||||
});
|
||||
}
|
||||
growth.sort_unstable();
|
||||
|
||||
let mut pr = Promoter {
|
||||
data,
|
||||
growth,
|
||||
edits: vec![],
|
||||
seen: Default::default(),
|
||||
};
|
||||
let affected: std::collections::BTreeSet<usize> = plans.iter().map(|p| p.coff).collect();
|
||||
|
||||
// rebuilt code-item bodies
|
||||
let mut replacements = vec![];
|
||||
for p in &plans {
|
||||
let insns = &data[p.coff + 16..p.coff + 16 + p.isz * 2];
|
||||
let new_insns = rebuild_insns(insns, p.isz, &p.units, &w)?;
|
||||
ensure!(new_insns.len() == p.new_isz * 2, "rebuilt insns size mismatch");
|
||||
let mut body = data[p.coff..p.coff + 16].to_vec();
|
||||
body[12..16].copy_from_slice(&le4(p.new_isz)?);
|
||||
let dbg = word(data, p.coff + 8)?;
|
||||
body[8..12].copy_from_slice(&le4(pr.reloc(dbg))?);
|
||||
body.extend_from_slice(&new_insns);
|
||||
body.resize(p.new_slot, 0);
|
||||
replacements.push((p.coff, p.old_slot, body));
|
||||
}
|
||||
|
||||
// generic offset relocation (skip affected code items' debug_off)
|
||||
for pos in [0x30, 0x34, 0x44, 0x4c, 0x54, 0x5c, 0x64, 0x6c] {
|
||||
pr.put_off(pos)?;
|
||||
}
|
||||
let str_off = word(data, 0x3c)?;
|
||||
for j in 0..word(data, 0x38)? {
|
||||
pr.put_off(str_off + j * 4)?;
|
||||
}
|
||||
let proto_off = word(data, 0x4c)?;
|
||||
for i in 0..word(data, 0x48)? {
|
||||
pr.put_off(proto_off + i * 12 + 8)?;
|
||||
}
|
||||
let class_off = word(data, 0x64)?;
|
||||
let mut anno_dirs = vec![];
|
||||
for i in 0..word(data, 0x60)? {
|
||||
let b = class_off + i * 32;
|
||||
pr.put_off(b + 12)?;
|
||||
pr.put_off(b + 20)?;
|
||||
pr.put_off(b + 24)?;
|
||||
pr.put_off(b + 28)?;
|
||||
let ad = word(data, b + 20)?;
|
||||
if ad != 0 {
|
||||
anno_dirs.push(ad);
|
||||
}
|
||||
}
|
||||
for (coff, co_pos) in all_code_items(data)? {
|
||||
if !affected.contains(&coff) {
|
||||
pr.put_off(coff + 8)?;
|
||||
}
|
||||
let (co, _) = uleb(data, co_pos)?;
|
||||
let nb = enc_uleb(pr.reloc(co));
|
||||
ensure!(nb.len() == enc_uleb(co).len(), "code_off uleb length change under promotion");
|
||||
pr.edits.push((co_pos, nb));
|
||||
}
|
||||
for ad in anno_dirs {
|
||||
pr.put_off(ad)?;
|
||||
pr.do_set(word(data, ad)?)?;
|
||||
let (fc, mc, pc) = (word(data, ad + 4)?, word(data, ad + 8)?, word(data, ad + 12)?);
|
||||
let mut q = ad + 16;
|
||||
for _ in 0..fc {
|
||||
pr.put_off(q + 4)?;
|
||||
pr.do_set(word(data, q + 4)?)?;
|
||||
q += 8;
|
||||
}
|
||||
for _ in 0..mc {
|
||||
pr.put_off(q + 4)?;
|
||||
pr.do_set(word(data, q + 4)?)?;
|
||||
q += 8;
|
||||
}
|
||||
for _ in 0..pc {
|
||||
pr.put_off(q + 4)?;
|
||||
pr.do_setref(word(data, q + 4)?)?;
|
||||
q += 8;
|
||||
}
|
||||
}
|
||||
let map_off = word(data, 0x34)?;
|
||||
for i in 0..word(data, map_off)? {
|
||||
pr.put_off(map_off + 4 + i * 12 + 8)?;
|
||||
}
|
||||
let total: usize = pr.growth.iter().map(|(_, a)| a).sum();
|
||||
pr.edits.push((0x20, le4(word(data, 0x20)? + total)?.to_vec()));
|
||||
pr.edits.push((0x68, le4(word(data, 0x68)? + total)?.to_vec()));
|
||||
|
||||
// apply same-length generic edits in original coords, then splice replacements
|
||||
let mut buf = data.to_vec();
|
||||
for (pos, nb) in &pr.edits {
|
||||
buf[*pos..*pos + nb.len()].copy_from_slice(nb);
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
let mut prev = 0;
|
||||
replacements.sort_by_key(|r| r.0);
|
||||
for (coff, old_slot, body) in &replacements {
|
||||
out.extend_from_slice(&buf[prev..*coff]);
|
||||
out.extend_from_slice(body);
|
||||
prev = coff + old_slot;
|
||||
}
|
||||
out.extend_from_slice(&buf[prev..]);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Positions of 16-bit `const-string` instructions whose operand would overflow
|
||||
/// 0xFFFF when renumbered (operand == 0xFFFF and >= `p`), needing promotion.
|
||||
pub fn overflow_const_strings(data: &[u8], p: usize) -> Result<Vec<usize>> {
|
||||
let w = widths();
|
||||
let mut out = vec![];
|
||||
for (coff, _) in all_code_items(data)? {
|
||||
let isz = word(data, coff + 12)?;
|
||||
let insns = &data[coff + 16..coff + 16 + isz * 2];
|
||||
for (u, op) in walk(insns, isz, &w)? {
|
||||
if op == 0x1a {
|
||||
let v = u16::from_le_bytes([insns[u * 2 + 2], insns[u * 2 + 3]]) as usize;
|
||||
if v == 0xFFFF && v >= p {
|
||||
out.push(coff + 16 + u * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Instruction offset and opcode of the unique `const-string`/`jumbo` that
|
||||
/// references `index`, walking code items (not a raw byte scan).
|
||||
pub fn find_code_const_string(data: &[u8], index: usize) -> Result<(usize, u8)> {
|
||||
let w = widths();
|
||||
let mut found = None;
|
||||
for (coff, _) in all_code_items(data)? {
|
||||
let isz = word(data, coff + 12)?;
|
||||
let insns = &data[coff + 16..coff + 16 + isz * 2];
|
||||
for (u, op) in walk(insns, isz, &w)? {
|
||||
let v = match op {
|
||||
0x1a => Some(u16::from_le_bytes([insns[u * 2 + 2], insns[u * 2 + 3]]) as usize),
|
||||
0x1b => Some(u32::from_le_bytes(insns[u * 2 + 2..u * 2 + 6].try_into()?) as usize),
|
||||
_ => None,
|
||||
};
|
||||
if v == Some(index) {
|
||||
ensure!(found.is_none(), "ambiguous CN login const-string");
|
||||
found = Some((coff + 16 + u * 2, op));
|
||||
}
|
||||
}
|
||||
}
|
||||
found.context("missing CN login const-string")
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
//! CN passport SDK login URL redirect in DEX. Reuses the `127.0.0.1` string
|
||||
//! slot and remaps the login reference; string growth and checksum fix-up are
|
||||
//! handled by `format::dex`.
|
||||
//! CN passport SDK login URL redirect in DEX. Inserts the chosen origin as a
|
||||
//! new string at its sorted position (renumbering every reference) and repoints
|
||||
//! the passport login `const-string` to it. `const-string` instructions that
|
||||
//! would overflow 16 bits are promoted to `const-string/jumbo` first. String
|
||||
//! growth, insertion and checksum fix-up live in `format::dex`.
|
||||
use crate::format::dex;
|
||||
use anyhow::{Result, ensure};
|
||||
use anyhow::{Context, Result, ensure};
|
||||
|
||||
const CLASS: &[u8] = b"Lcom/mihoyo/platform/account/sdk/utils/RSAUtils;";
|
||||
const PASSPORT: &[u8] = b"https://passport-api.mihoyo.com";
|
||||
|
||||
pub(super) fn patch(data: &[u8], origin: &str) -> Result<Option<Vec<u8>>> {
|
||||
if !data.windows(CLASS.len()).any(|part| part == CLASS) {
|
||||
@@ -14,26 +17,47 @@ pub(super) fn patch(data: &[u8], origin: &str) -> Result<Option<Vec<u8>>> {
|
||||
origin.starts_with("http://"),
|
||||
"CN Android passport redirect requires HTTP"
|
||||
);
|
||||
let local = dex::find_string(data, |s| s == b"http://127.0.0.1")?
|
||||
.ok_or_else(|| anyhow::anyhow!("missing CN passport URL slot"))?;
|
||||
let official = dex::find_string(data, |s| s == b"https://passport-api.mihoyo.com")?
|
||||
.ok_or_else(|| anyhow::anyhow!("missing CN passport login URL"))?;
|
||||
ensure!(
|
||||
local > 0
|
||||
&& local + 1 < dex::word(data, 0x38)?
|
||||
&& dex::string(data, local - 1)? < origin.as_bytes()
|
||||
&& origin.as_bytes() < dex::string(data, local + 1)?,
|
||||
"server address does not fit CN DEX string order"
|
||||
);
|
||||
// Reuse the 127.0.0.1 slot; a URL outside its sort range needs DEX ID remapping.
|
||||
let local_ref = dex::find_const_string(data, local)?;
|
||||
let login_ref = dex::find_const_string(data, official)?;
|
||||
let mut out = data.to_vec();
|
||||
let added = dex::append_string(&mut out, origin.as_bytes())?;
|
||||
let ids = dex::word(&out, 0x3c)?;
|
||||
dex::put(&mut out, ids + local * 4, added)?;
|
||||
out[local_ref + 2..local_ref + 4].copy_from_slice(&u16::try_from(local + 1)?.to_le_bytes());
|
||||
out[login_ref + 2..login_ref + 4].copy_from_slice(&u16::try_from(local)?.to_le_bytes());
|
||||
let official = dex::find_string(data, |s| s == PASSPORT)?
|
||||
.context("missing CN passport login URL")?;
|
||||
|
||||
// If the origin string already exists (e.g. the stock loopback
|
||||
// `http://127.0.0.1`), just repoint the login to it — no insertion.
|
||||
if let Some(existing) = dex::find_string(data, |s| s == origin.as_bytes())? {
|
||||
let mut out = data.to_vec();
|
||||
repoint(&mut out, official, existing)?;
|
||||
dex::recompute_checksums(&mut out)?;
|
||||
return Ok(Some(out));
|
||||
}
|
||||
|
||||
// Promote any const-string whose index would overflow 0xFFFF after the
|
||||
// insertion shifts it, then insert the origin at its sorted position.
|
||||
let (p, _) = dex::sorted_position(data, origin.as_bytes())?;
|
||||
let promos = dex::overflow_const_strings(data, p)?;
|
||||
let promoted;
|
||||
let source: &[u8] = if promos.is_empty() {
|
||||
data
|
||||
} else {
|
||||
promoted = dex::promote(data, promos)?;
|
||||
&promoted
|
||||
};
|
||||
let (mut out, p) = dex::insert_string(source, origin.as_bytes())?;
|
||||
|
||||
// The passport string shifted up by one if it sorts after the origin.
|
||||
let official = official + if official >= p { 1 } else { 0 };
|
||||
repoint(&mut out, official, p)?;
|
||||
dex::recompute_checksums(&mut out)?;
|
||||
Ok(Some(out))
|
||||
}
|
||||
|
||||
/// Repoint the unique `const-string` that loads string `from` to load `to`.
|
||||
fn repoint(out: &mut [u8], from: usize, to: usize) -> Result<()> {
|
||||
let (pos, op) = dex::find_code_const_string(out, from)?;
|
||||
match op {
|
||||
0x1a => {
|
||||
ensure!(to <= 0xFFFF, "login index too large for a 16-bit const-string");
|
||||
out[pos + 2..pos + 4].copy_from_slice(&u16::try_from(to)?.to_le_bytes());
|
||||
}
|
||||
_ => out[pos + 2..pos + 6].copy_from_slice(&u32::try_from(to)?.to_le_bytes()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user