mirror of
https://git.ely.my.id/Tenshi/Hi3-Patcher
synced 2026-11-04 21:46:48 +01:00
Initial Commit
This commit is contained in:
commit
737780597b
51 files changed
+8675
No files matched your search
+37
@@ -0,0 +1,37 @@
|
||||
# ACE patch (Windows, clients ≤ 8.9)
|
||||
|
||||
Builds a patched `BH3.exe` from a capture of the clean client's
|
||||
initialized state. Only `BH3.exe` is written, without a backup. 9.0+
|
||||
clients use HoYoKProtect instead.
|
||||
|
||||
## Discovery (`ace/discovery.rs`)
|
||||
|
||||
Read-only; all checks must pass:
|
||||
|
||||
| File | Required layout |
|
||||
| --- | --- |
|
||||
| `BH3.exe` | 7 sections all named `.ace*`, 4 KiB section alignment, no TLS directory, entry calls the `BH3Base.dll` ordinal-1 import |
|
||||
| `BH3_Data/sharedassets0.assets` | `ChannelConfig` resolves to a known region |
|
||||
| `UnityPlayer.dll` | exports `UnityMain`; three writable decoder globals found via a `.text` code pattern |
|
||||
| `BH3_Data/Native/UserAssembly.dll` | exports `il2cpp_get_api_table`; executable `.tvm0` section with a virtual tail larger than its raw size |
|
||||
|
||||
## Capture (`ace/capture.rs`, `ace/process.rs`)
|
||||
|
||||
After confirmation, launches the client (Windows directly; Linux via
|
||||
`umu-run` + Proton) and reads its memory read-only — no injection or
|
||||
protection tampering. Once the state is stable for 2 s (120 s timeout),
|
||||
it captures the `.tvm0` section, the three decoder states and the
|
||||
committed private executable allocations.
|
||||
|
||||
## Normalize and rebuild (`ace/runtime.rs`, `ace/generate.rs`)
|
||||
|
||||
`runtime.rs` checks the captured `.tvm0` against the original after PE
|
||||
relocations, resolves `FF 25` jump slots against the captured
|
||||
allocations, and emits a position-independent blob and two thunk tables.
|
||||
`generate.rs` appends `.bh3code` (the `bootstrap.bin` stub, patched with
|
||||
the discovered RVAs/sizes per `bootstrap.json`) and `.bh3imp` (imports,
|
||||
decoder states and the runtime blob), then repoints the entry point.
|
||||
|
||||
A `HI3AUTO1` receipt with SHA-256 hashes of the patched image,
|
||||
`UnityPlayer.dll` and `UserAssembly.dll` is appended; a repeat run that
|
||||
validates it is a no-op. Written via `util::replace::verified`.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Architecture
|
||||
|
||||
`format/` holds reusable binary-format parsing; `patch/` holds the patch
|
||||
logic. Patch modules depend on `format/`, `android/` and `util/`, never on
|
||||
each other.
|
||||
|
||||
## format/
|
||||
|
||||
All reads are bounds-checked; malformed input returns an error instead of
|
||||
panicking.
|
||||
|
||||
- **bytes** — little/big-endian integer reads and writes, slicing,
|
||||
power-of-two alignment.
|
||||
- **pe** — PE32+ (x86-64) reader. Validates headers and section ranges;
|
||||
provides section lookup, RVA→offset mapping, data directories, export
|
||||
lookup, and a stricter image-bound check used by ACE and HoYoKProtect.
|
||||
Patches add their own layout checks on top.
|
||||
- **elf** — ARM64 shared-object reader. Validates headers and
|
||||
section/segment ranges; provides sections, segments, `.dynsym` symbols,
|
||||
RELA entries, and address→file-offset mapping through `PT_LOAD`.
|
||||
- **dex** — field access, checksum recompute (SHA-1 + Adler-32), string
|
||||
and `const-string` lookup, and `append_string`, which inserts a
|
||||
`string_data_item` before the annotations section and relocates every
|
||||
later offset, map entry and header field.
|
||||
- **unity** — UnityFS v6 bundles and serialized-file `TextAsset`s.
|
||||
`rebuild` re-encodes only changed blocks and re-verifies the result.
|
||||
`replace_script` and `Bundle::replace_node` resize a TextAsset or
|
||||
bundle node, rewriting object offsets, the header size and the node
|
||||
directory. Blocks may be uncompressed or LZ4.
|
||||
|
||||
## patch/
|
||||
|
||||
`region` maps the Windows client's `ChannelConfig`
|
||||
(`ChannelName`/`BundleIdentifier`) to one of six PC regions
|
||||
(CN/GLB/JP/KR/TW/SEA) with its SDK key and environment id.
|
||||
|
||||
Single-file Windows writes (RSA, ACE, HoYoKProtect) use
|
||||
`util::replace::verified`: write a sibling temporary file, flush, read it
|
||||
back, re-check the target for concurrent changes, persist, re-verify. The
|
||||
dispatch patch replaces its three files together and rolls back on
|
||||
failure.
|
||||
|
||||
## android/
|
||||
|
||||
`toolchain` finds Build-Tools and Java 17+, or downloads them (SHA-256
|
||||
verified). `sign::signed_patch` runs a patch's repack, which drops the old
|
||||
signature entries, into a temporary APK, then zip-aligns it
|
||||
(`-P 16 -f 4`), signs and verifies it with `apksigner`, and writes
|
||||
`<name>_patched.apk`.
|
||||
|
||||
## cli/
|
||||
|
||||
`interactive` is the terminal menu (no arguments). `args` parses
|
||||
`<command> --flag value` (or `--flag=value`) and validates the whole
|
||||
command line before running the patch, then reports via exit code and
|
||||
optional JSON.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Dispatch & SDK patch
|
||||
|
||||
Rewrites client URLs to a chosen HTTP or HTTPS server. The server is a
|
||||
host or IP with an optional port, without scheme or path.
|
||||
|
||||
## URL rules (`dispatch/mod.rs`)
|
||||
|
||||
- `dispatch` replaces the `"DispatchUrls": [...]` list with
|
||||
`"<origin>/query_dispatch"` entries inside its original byte range,
|
||||
space-padding the rest.
|
||||
- `sdk` rewrites the URLs of the production `bh3_*` records in
|
||||
`server_pc` (`env` `0` for `bh3_cn`, else `2`), padding each record to
|
||||
its original length.
|
||||
- `android_urls` rewrites `http(s)`/`ws(s)` URLs and bare hosts on the
|
||||
official domains (`bh3.com`, `honkaiimpact3.com`, `mihoyo.com`,
|
||||
`hoyoverse.com`, plus one known IP). Shorter replacements keep the
|
||||
original length with an `x…@` userinfo prefix; `ws`/`wss` follow the
|
||||
chosen scheme.
|
||||
|
||||
If a replacement is longer than the original, `dispatch_grow`, `sdk_grow`
|
||||
or `android_urls_grow` writes it at full length and the asset is
|
||||
enlarged: `unity::replace_script` rewrites the serialized object table and
|
||||
header size, and `Bundle::replace_node` re-chunks a UnityFS node and
|
||||
fixes its directory. The tool then prints a warning that assets were
|
||||
enlarged.
|
||||
|
||||
## Windows (`dispatch/windows.rs`)
|
||||
|
||||
Writes three files together and rolls back on failure:
|
||||
|
||||
| File | Content patched |
|
||||
| --- | --- |
|
||||
| `BH3_Data/sharedassets0.assets` | `ChannelConfig` dispatch URLs; also identifies the region |
|
||||
| `BH3_Data/resources.assets` | `server_pc` SDK URLs |
|
||||
| `BH3_Data/StreamingAssets/MiHoYoSDKRes/PC/mihoyo_sdk_res` | the `server_pc` copy inside the SDK bundle |
|
||||
|
||||
Both `server_pc` copies must be byte-equal before patching.
|
||||
|
||||
## Android (`dispatch/android.rs`)
|
||||
|
||||
Patches serialized files, UnityFS bundle nodes and plain text among the
|
||||
candidate `assets/` entries (`assets/bin/data/*` and known text
|
||||
extensions, except `package_config.json`, `share_config.json`, `.wmv`
|
||||
and `.bak`). `.splitN` parts are joined for patching and re-split at the
|
||||
original chunk size; enlarging can add parts. DEX, native libraries and
|
||||
`.wmv` are copied unchanged, except for the CN passport URL below. At
|
||||
least one `ChannelConfig` and one SDK/resource URL are required, otherwise
|
||||
no APK is written. The result is repacked and signed by `android::sign`.
|
||||
|
||||
## CN passport URL (`dispatch/cn_android.rs`)
|
||||
|
||||
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:
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,28 @@
|
||||
# HoYoKProtect patch (Windows, clients 9.0+)
|
||||
|
||||
Makes the `IsLoadMHYBase` predicate in `BH3_Data/Native/UserAssembly.dll`
|
||||
return false, turning the client-side MHYP loader off. Only that file is
|
||||
written; the region is confirmed from `ChannelConfig` first.
|
||||
|
||||
## Discovery (`hoyokprotect/discovery.rs`)
|
||||
|
||||
1. In `.text`, match the encoded `IsLoadMHYBase` string construction by
|
||||
byte pattern (not a fixed RVA); exactly one match.
|
||||
2. The exception directory gives the containing function's bounds. It
|
||||
must start with the known prologue, end with the known boolean-return
|
||||
epilogue, and contain the match.
|
||||
3. Exactly two relative `CALL`s must target the function, followed by the
|
||||
known module-check and initialization byte sequences.
|
||||
|
||||
`il2cpp_get_api_table` must be exported. Unknown or ambiguous results
|
||||
abort.
|
||||
|
||||
## Edit
|
||||
|
||||
| Location | Original | Replacement | Effect |
|
||||
| --- | --- | --- | --- |
|
||||
| First 3 bytes of the function | `55 41 57` | `31 C0 C3` | `xor eax, eax; ret` |
|
||||
|
||||
Discovery re-runs on the edited image to confirm the disabled form, then
|
||||
the file is written via `util::replace::verified`. An already-disabled
|
||||
function is left unchanged.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Android protection removal
|
||||
|
||||
One pipeline removes AnoSDK (overseas) or Mobile ACE/TSS (CN); the
|
||||
variants differ only in the constants in `protection/profile.rs`, and
|
||||
`--variant auto` picks one from the libraries present. It restores four
|
||||
protected ARM64 libraries, neutralizes the SDK startup and native API
|
||||
calls, drops the protection libraries, and signs a new APK.
|
||||
|
||||
## Variant profiles
|
||||
|
||||
| Field | AnoSDK | Mobile ACE/TSS |
|
||||
| --- | --- | --- |
|
||||
| Regions | SEA, GLB, JP, KR, TW | CN |
|
||||
| Decoder library | `libanort.so` | `libtprt.so` |
|
||||
| Guard library | `libanogs.so` | `libtersafe2.so` |
|
||||
| Key record XOR | `0xDF` | `0x49` |
|
||||
| Decoder symbol | `g_acf_array` | `g_tprt_pfn_array` |
|
||||
| `Application` class | `com.ano.gshell.AnoApplication` | `com.ace.gshell.AceApplication` |
|
||||
| SDK class | `com.gamesafe.ano.AnoSdk` | `com.tencent.tp.TssSdk` |
|
||||
| Native APIs | 6 `AnoSDK*` | 5 `tp2_*` |
|
||||
|
||||
## Identification (`protection/mod.rs`)
|
||||
|
||||
Checks `ChannelConfig` (`ChannelName`/`BundleIdentifier`) against the
|
||||
variant's regions, confirms both protection libraries are ARM64 ELFs, and
|
||||
rejects APKs that also contain the other variant's libraries. ARM64 is
|
||||
the only supported ABI.
|
||||
|
||||
## Restore (`protection/loader.rs`)
|
||||
|
||||
- `permutation` follows the decoder symbol through its routine and GOT
|
||||
reference to the eight-page permutation (a digit string permuting
|
||||
`0..8`).
|
||||
- Each 48-byte protected-section descriptor is matched to an ELF section
|
||||
by name, RVA, file offset and size. The Base64 key record at descriptor
|
||||
table `+0x1A4` is XORed with the variant byte: magic `0x20240829`, key
|
||||
id 1 drives restoration, and the mode at `+0x184` (1 or 2) selects page
|
||||
treatment. Each restored section must match its recorded CRC-32.
|
||||
- Large sections: AES-128-CBC over `0x4000` bytes in `0x800`-byte
|
||||
chunks from `0x2000 - (RVA & 0xFFF)` (key: the value's repeated
|
||||
lowercase hex; IV `0x02..0x11`, reset per chunk). Mode 1 XORs and
|
||||
shuffles `0x4000` fragments across eight `0x10000` strides; mode 2
|
||||
XORs the remainder.
|
||||
- Small sections: whole-section XOR.
|
||||
- `BBMG311DIS12IMP` gives the protected import count (`+16`), `.rela.plt`
|
||||
RVA (`+24`), entry count (`+28`) and excluded GOT slots (`+32/+36`);
|
||||
selected import `r_info` values are rotated back into order.
|
||||
`BBMG311ENC12IAI` gives the constructor array RVA (`+16`) and count
|
||||
(`+20`). The wrapper `DT_INIT`, the decoder library's `DT_NEEDED` entry
|
||||
and the APS2/RELA wrapper relocations are cleared, and the
|
||||
`fread`/`fwrite`/`fclose` GOT imports are restored. The removed
|
||||
`DT_INIT` RVA is passed to the API helper.
|
||||
|
||||
## Neutralize (`protection/native.rs`)
|
||||
|
||||
In restored `libil2cpp.so`, finds the SDK API names and the guard library
|
||||
name in `.rodata`, traces ADRP/ADD references to them, and redirects each
|
||||
resolver `BL` to a 16-byte helper written over the removed `DT_INIT`
|
||||
initializer. The helper returns a pointer to a function that returns
|
||||
zero; other P/Invoke resolutions are untouched. Aborts if the initializer
|
||||
is still active, an API is missing, or the call graph is ambiguous.
|
||||
|
||||
## DEX edits (`protection/dex.rs`)
|
||||
|
||||
Three methods are edited after each call is validated:
|
||||
|
||||
| Method | Edit |
|
||||
| --- | --- |
|
||||
| `Application.<clinit>` | NOP both native library loads (20 bytes) |
|
||||
| `Application.attachBaseContext` | Replace the invoke + move-result after the superclass call with return/NOPs (8 bytes) |
|
||||
| SDK class `<clinit>` | NOP the guard library load (`const-string` 10 bytes, or `const-string/jumbo` 12 bytes); keep the return |
|
||||
|
||||
All 8 `attachBaseContext` bytes are rewritten because leftover operand
|
||||
words after the `return` would still fail DEX verification. Checksums are
|
||||
recomputed.
|
||||
|
||||
## Output
|
||||
|
||||
The decoder and guard libraries are dropped; the restored libraries and
|
||||
edited DEX files are repacked and signed by `android::sign`. Any
|
||||
mismatched marker, CRC, method or resolver layout aborts the patch.
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# Passport RSA patch
|
||||
|
||||
Replaces the passport login RSA public key so credentials are encrypted
|
||||
for the server's `Passport:RsaPrivateKey`. Input: a single-line Base64 DER
|
||||
`SubjectPublicKeyInfo` body (no PEM markers), 2048-bit, exponent 65537;
|
||||
empty uses the built-in PemukulPaku key.
|
||||
|
||||
## Windows (`rsa/windows.rs`)
|
||||
|
||||
Target: `BH3_Data/Native/UserAssembly.dll` (x64 PE), after confirming the
|
||||
region from `ChannelConfig`.
|
||||
|
||||
1. Find the shared XML RSA routine (prefix `CRYPTO`) in the executable
|
||||
`il2cpp` section and the nearby account/password encryption `CALL`
|
||||
pair that targets it. A missing or ambiguous match aborts before any
|
||||
write.
|
||||
2. Base64-encode the key's modulus (344 characters).
|
||||
3. Append a `.hi3rsa` code section: an `HI3RSA01` header (the two
|
||||
original call RVAs and the routine RVA), the wrapper, and the modulus.
|
||||
Redirect both login `CALL`s into the wrapper; the routine and its
|
||||
other callers are untouched.
|
||||
4. Update the section count, code/image sizes and certificate directory
|
||||
offset (the certificate is kept; its signature no longer matches),
|
||||
re-verify the image, and write via `util::replace::verified`.
|
||||
|
||||
At runtime the wrapper receives the managed XML key in `RCX`. If the key
|
||||
is non-null, 505 UTF-16 chars long, and has the `<Modulus>` tags at the
|
||||
expected offsets, it writes the new modulus at byte offset `0xE2`; in
|
||||
every case it then calls the original routine.
|
||||
|
||||
A repeat run validates the existing `.hi3rsa` section and redirected
|
||||
calls and replaces only the modulus. A single known 80-byte `jk98…`
|
||||
overlay before the certificate is preserved; other overlays are rejected.
|
||||
|
||||
## Android overseas (`rsa/android.rs`)
|
||||
|
||||
`…oversea.sdk.internal.shared.crypto.RSAUtils.getRSA_PUBLIC_KEY`
|
||||
references three environment keys (398-byte, newline-wrapped 2048-bit) via
|
||||
`const-string`. All three are replaced in place with the chosen key;
|
||||
wrapping at 63/64/65 characters keeps the strings distinct and in sort
|
||||
order. String order is re-validated and checksums are recomputed.
|
||||
|
||||
## Android CN (`rsa/cn_android.rs`)
|
||||
|
||||
`…account.sdk.utils.RSAUtils` holds a 1024-bit key string. A 398-byte
|
||||
wrapped 2048-bit replacement is appended (`format::dex::append_string`),
|
||||
the key and its two neighbor string ids are reordered to keep sort order,
|
||||
and the field, both `const-string` references and both static values are
|
||||
remapped. Checksums are recomputed; unknown layouts are rejected.
|
||||
|
||||
An APK containing both CN and overseas SDKs is rejected.
|
||||
Reference in new issue
Block a user