diff --git a/Cargo.toml b/Cargo.toml index c65ace4..2cbf694 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/docs/dispatch.md b/docs/dispatch.md index 1e78c2f..fe47a33 100644 --- a/docs/dispatch.md +++ b/docs/dispatch.md @@ -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. diff --git a/src/format/dex.rs b/src/format/dex.rs index 2a13aa1..3712180 100644 --- a/src/format/dex.rs +++ b/src/format/dex.rs @@ -214,3 +214,863 @@ pub fn append_string(data: &mut Vec, value: &[u8]) -> Result { 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> { + let u16at = |p: usize| -> Result { + 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 { + 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); + +/// Code offsets reached while walking class_data: `(code_item_off, code_off_uleb_pos)`. +fn class_data_methods(data: &[u8], mut off: usize) -> Result> { + 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, + seen_set: std::collections::BTreeSet, + seen_item: std::collections::BTreeSet, + seen_arr: std::collections::BTreeSet, +} + +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 { + 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 { + 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 { + 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, 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 { + 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> { + 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 = 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, + seen: std::collections::BTreeSet, +} + +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::() + } + 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> { + 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) -> Result> { + 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::>>()?; + + // group promotion positions by owning code item + let mut per_item: std::collections::BTreeMap)> = 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, + 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 = 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> { + 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") +} diff --git a/src/patch/dispatch/cn_android.rs b/src/patch/dispatch/cn_android.rs index 611cdb4..7c4e750 100644 --- a/src/patch/dispatch/cn_android.rs +++ b/src/patch/dispatch/cn_android.rs @@ -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>> { if !data.windows(CLASS.len()).any(|part| part == CLASS) { @@ -14,26 +17,47 @@ pub(super) fn patch(data: &[u8], origin: &str) -> Result>> { 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(()) +}