Fix Insert the CN passport URL as a new DEX string so any domain works

This commit is contained in:
Tenshi committed 2026-11-01 10:06:45 +01:00
1 parent 737780597b
commit daece35d94
4 files changed
+925 -38

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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.
+860
View File
@@ -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")
}
+47 -23
View File
@@ -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 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();
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());
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(())
}