From e50c826d95c0aacc6051f5edc3ac27f0132b062c Mon Sep 17 00:00:00 2001 From: inliver233 Date: Sat, 19 Sep 2026 22:38:54 +0800 Subject: [PATCH] Add patch-audit reference; extend pitfalls to P30; unify script docs in English MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - references/patch-audit.md (new): two independent questions for any self-made patch — did it land, and is it legal. Length-vs-bytes comparison and the ~800-false-positive signal that makes byte diffing useless; the equal-length-replacement blind spot that hides patches from length-only audits (and why tier-2 checking must be driven by the patch set's own declared targets, not by tier-1 output); verifier-level legality (move-result must immediately follow its producer) checked as a set-difference so it runs before the device; text-matching patch traps (whitespace, line endings, instruction names, anchor uniqueness, re-emitted old fragments, one block per doc); and auditing the patch set rather than a patch. - pitfalls P28-P30: stale events-buffer records that read as this build's crash; a patch that lands and changes nothing because the edited data is not the data the UI consumes; two observations that cannot distinguish the hypotheses reported as a result. - verification.md: point the known code-item blind spot at the new reference. - Unify the four remaining script docstrings (and their inline comments) into English, and generalise a sample-specific class name out of one of them. --- README.md | 4 + SKILL.md | 1 + references/patch-audit.md | 151 +++++++++++++++++++++++++++++++++++++ references/pitfalls.md | 104 +++++++++++++++++++++++++ references/verification.md | 2 + scripts/dex_classdiff.py | 13 +++- scripts/dex_strpatch.py | 45 ++++++----- scripts/patch_smali.py | 31 ++++---- scripts/usb_net_proxy.py | 33 ++++---- 9 files changed, 335 insertions(+), 49 deletions(-) create mode 100644 references/patch-audit.md diff --git a/README.md b/README.md index 89cb827..8edf32c 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,10 @@ references/ loaded on demand, one topic each tls-and-cert.md feature-scoped network failures: expired certs, dual trust chains third-party-builds.md auditing a "cracked"/"modded" APK before trusting it dex-patching.md patch-layer table + dexlib2 technique in depth + patch-audit.md proving a patch *landed* and is *legal*: length-vs-bytes + comparison, the equal-length-replacement blind spot, verifier-level + legality (move-result adjacency) checked statically, text-matching + patch traps, and reporting a missing patch repack-and-sign.md repack rules, unpack-and-repack, signing, post-install hazards runtime-data.md DataStore / SharedPreferences / SQLite / protobuf; when the app rewrites your edit, and decoding a value that looks encrypted diff --git a/SKILL.md b/SKILL.md index 7db026b..3e8c7ba 100644 --- a/SKILL.md +++ b/SKILL.md @@ -99,6 +99,7 @@ Load only what the current step needs. | `references/membership-and-limits.md` | Task involves VIP, subscription, paid content, unlock, "fully cracked" | | `references/server-api.md` | The behavior is decided by a response, or you need to know if a patch can even matter | | `references/dex-patching.md` | Any actual editing of dex/smali, choosing a patch layer, choosing a tool | +| `references/patch-audit.md` | Proving a patch **landed**, or that it is **legal**: length-vs-bytes comparison, the equal-length blind spot, verifier-level legality (`move-result*` adjacency), text-matching patch traps, and how to report a missing patch | | `references/repack-and-sign.md` | Rebuilding, signing, installing, or a repacked app misbehaves | | `references/signature-derived-keys.md` | The app reads `signatures[0]`/`toCharsString()`, or a rebuilt APK installs and runs but every signed request fails (`sign`/`_p`/`uth` empty or `-1`) | | `references/runtime-data.md` | Local state matters: DataStore, SharedPreferences, SQLite, protobuf caches, tokens — **or your data edit keeps being reverted, or a stored value looks encrypted** | diff --git a/references/patch-audit.md b/references/patch-audit.md new file mode 100644 index 0000000..5f83984 --- /dev/null +++ b/references/patch-audit.md @@ -0,0 +1,151 @@ +# Auditing your own patch — proving it landed, and proving it is legal + +Two independent questions, and they fail independently: + +1. **Did the edit land?** Did the bytes you intended actually reach the artifact? +2. **Is the result legal?** Will the runtime's verifier accept it? + +A patch can land and be illegal (the class fails to load). It can also fail to land while every +build step reports success. Both are silent, and each needs its own check. + +## 1. Did it land? Compare *length*, not bytes + +The naive check is a byte-level diff of the target method against the original. +**It produces a flood of false positives.** A disassemble→reassemble round-trip rebuilds the +string pool — the assembler **de-duplicates identical strings** — which shifts every later +string index by a small delta. Hundreds of methods then look "changed" while their behaviour is +identical. + +Observed shape of that noise: **~800 methods reported as differing, 84% of them by only 1–4 +bytes**, all of them constant-pool index shifts, with the method byte-length unchanged. + +**Use instruction-stream length as the first-order predicate.** A real patch that changes control +flow or constant structure changes the method's `code_item.insns_size`. Pure index shifts and +debug-info reordering do not. + +### The blind spot, and how to close it + +**An equal-length replacement changes nothing measurable by length.** + +Examples that bite: `move-result vX` → `const/4 vX, 0x0` (both one code unit); swapping one +`const` for another of the same width; any edit that keeps the instruction count identical. + +Length-only auditing therefore **silently under-reports**: an audit reported 20 changed methods +on a build whose patch set actually touched 24 — the four missing ones were all equal-length +replacements. + +**Two-tier rule:** + +1. **Tier 1 (length):** scan every method. This yields candidates with essentially zero false + positives. +2. **Tier 2 (opcode-level):** compare the two versions opcode by opcode **for every method the + patch set declares as a target** — not merely the Tier-1 candidates. + +**Tier 2's input cannot come from Tier 1's output.** Equal-length replacements produce no Tier-1 +candidate at all, so the only reliable index of "what to check at Tier 2" is the patch set's own +declarations. + +⇒ **Corollary, which is really a documentation requirement: every patch must declare its target +method signature.** A patch that is both equal-length and undeclared is invisible to both tiers; +finding it requires opcode-comparing every class, which is possible but expensive. + +### A sharper Tier-2 shape for equal-length replacements + +Compare the **position set of one opcode** inside the method: + +``` +target method, `move-result` (0x0a) position sets + original : [25, 43, 66, 72, 216, ... , 899] (16 entries) + patched : [25, 43, 72, 216, ... , 899] (15 entries) <- exactly one removed, pc=66 +``` + +This proves two things at once: the intended site was hit, **and** the other same-shaped call +sites in that method were not. Asking "did anything differ" cannot distinguish those. + +## 2. Is it legal? The verifier is a layer static checks cannot see + +An assembler accepting your smali proves the file is **syntactically** valid dex. It does not run +the bytecode verifier. A build can pass assembly, pass every class-table check, and still be +rejected at class load: + +``` +java.lang.VerifyError: Verifier rejected class X: void X.() failed to verify: + [0x1C8] copyRes1 v0<- result0 type=Undefined +``` + +**Root cause in the observed case:** padding instructions were inserted **between** a producer and +its `move-result`. DEX requires `move-result*` to **immediately follow** the instruction that +produces the result (`invoke-*`, `filled-new-array*`). The padding broke adjacency, the result +type became `Undefined`, and the whole method — and therefore the class — failed to verify. +The length arithmetic ("the shorter form plus two padding units preserves the total") was +**correct** and still produced an illegal method. + +**Four layers, cheapest first:** + +| Layer | Check | Catches | +|---|---|---| +| 1 | assembler exit code | syntax, register overflow, bad labels | +| 2 | class-table diff (`scripts/dex_classdiff.py`) | class-set / access-flag drift | +| 3 | **predecessor legality of `move-result*`** | result consumers detached from their producer | +| 4 | device: first execution of that method | everything above, plus real semantics | + +**Layer 3 is a set-difference, not an absolute count.** + +- For both builds, collect every `move-result*` whose immediately preceding instruction is **not** + a producer → the "offenders" set. +- Compare the sets. **New offenders must be 0.** +- **Never use the absolute size as a signal.** A perfectly healthy build can show hundreds of + offenders under this definition — payload pseudo-instructions (`packed-switch-payload`, + `fill-array-data-payload`, …) are normally read as ordinary instructions by a naive linear + scan — so the absolute number is dominated by scanner artifacts. Only the delta means anything. + +Worked consequence: a build whose sole new offender was one `move-result` following padding +crashed at class load on device. After moving the padding to **after** the `move-result`, the +delta returned to 0 and the same build ran. **The delta predicted the device result before the +device was touched.** + +## 3. When patches are applied by text matching + +Some toolchains apply patches by literal match against a text form (smali) rather than through a +dex API. The failure modes are textual and unforgiving: + +- **Whitespace is content.** A fragment indented with 5 spaces will not match a file using 4. + There is no fuzzy matching; one misplaced space is a zero-hit. +- **Line endings.** If the reader normalises CRLF→LF and the writer does not restore it, the + patched file becomes LF-only. A later byte-level comparison against the CRLF source then shows + the *entire file* as changed. Diff with an ignore-CR-at-EOL option, or compare structure. +- **Instruction names must be complete.** `move-result vX` and `move-result-object vX` are + different instructions; an anchor missing the suffix matches the wrong thing or nothing. +- **Operand punctuation matters.** `instance-of p3, p0, LType` is not the source text when the + source reads `LType;`. +- **Uniqueness is a hard requirement.** A matcher that finds several hits must fail loudly rather + than patch the first. When a one-line anchor is not unique, extend it with a second unique line; + **when the *combination* is still not unique, add a third anchor that discriminates the + homographs** — usually the branch target label, since identical call shapes often differ in + where they jump. +- **A re-emitted old fragment is not a failed write.** A patch may legitimately re-emit the old + text inside the new one (inserting a guard *before* the lines it matched). A read-back check + asserting "the old text is gone" then reports failure on that patch forever. Assert instead + that the **new, unique** part of the replacement is present. +- **One patch document, one patch block.** A parser that treats every fenced block as a candidate + will apply an *example* diff as a real patch — silently cancelling the real one, or editing + unrelated code. Keep examples in prose, never in the same fence format. + +## 4. Audit the patch set, not a patch + +For a multi-patch build: + +``` +expected = every method declared as a target by the patch set +tier1 = methods whose insns_size changed +tier2 = methods whose opcode stream differs (run over `expected`) +missing = expected - (tier1 U tier2) # must be empty +``` + +**`missing` is the number that matters.** A build can be green on every build step and still be +missing a patch. Report `missing` explicitly; do not report the total as if the total were the +goal. + +**And re-verify the artifact, not the intent.** If the tool says "applied" but the build on the +device is unchanged, you have a stale build, a second copy, or the wrong file +(`pitfalls.md` P24). A patch tool's success message describes the tool, not the artifact. diff --git a/references/pitfalls.md b/references/pitfalls.md index a35149f..bfe9255 100644 --- a/references/pitfalls.md +++ b/references/pitfalls.md @@ -690,3 +690,107 @@ runtime value (777 bytes) yields a build in which every signing parameter is `-1 5. **Check the OAID/device-id path too** — it frequently hashes the same certificate separately. Full treatment: `references/signature-derived-keys.md`. + +--- + +## P28. `logcat -c` does not clear the events buffer, so the previous build's crash looks like this build's + +**Symptom** + +After installing a fix, a search for the crash signature returns a hit. It looks like the fix did +not work — and the "hit" is convincing, because it is the exact exception and the exact frame you +were fixing. + +**Root cause** + +`adb logcat -c` clears `main`, `system` and `crash` **by default — not `events`**. The +system's own crash record (`am_crash`, `am_proc_died`) lives in `events`. Reading `-b all` +therefore pulls in the previous build's crash, which was never cleared. + +**How to tell it apart** + +Correlate **PID and wall-clock time** against the current process: + +- The PID in the record belongs to a process that is no longer running. +- The timestamp **predates** the current process's start. + +Both together mean history, not a finding. (In the observed case the stale record's PID was the +previous build's, and its timestamp was minutes before the current process started.) + +**Do instead** + +Pick one, and say which you used: + +- Bound the query by timestamp — accept only records after the current process started, or +- Judge per buffer — `crash` is a fresh window after `-c`; `events` requires PID/time correlation. + +**The evidence window is itself a claim that needs support.** An unexamined stale window turns +"fixed" into "still broken", and this one is invisible in the log's own text. + +--- + +## P29. The patch landed and changed nothing, because the static data it edited is not the data the UI consumes + +**Symptom** + +Build is green. The patch is verifiably present in the artifact. The behaviour is unchanged. + +**Root cause** + +**The same type had two construction sites** — one for a static/default template, one inside the +runtime conversion of a server response — and only the second one feeds the UI. Editing the first +is a no-op that verifies cleanly and audits cleanly. + +**How to see it before shipping** + +From the type's constructor, count the call sites (`scripts/find_refs.py`), then ask of each: + +> **Is this one on the path the UI actually reads?** + +"It is constructed here" is not "it is consumed here". Two constructors of the same type can have +completely different fates. + +**Do instead** + +- Patch the **consumer** — the loop that converts response items into the UI model — not the + static table. +- Prefer a predicate over a **semantic discriminator** (a business code, a task type) rather than + a display string. The literal in the template is **not** necessarily the literal the server + sends: the string you want to match may not exist anywhere in the response. +- If you cannot confirm the server-side value, **predicate on both candidate fields** rather than + betting on one. + +--- + +## P30. Two observations that cannot distinguish the hypotheses, reported as a result + +**Symptom** + +A verification step "passes" or "fails" while having tested nothing. + +**Root cause** + +The observable is identical under both hypotheses, so the measurement carries no information. +Real instance: a promotional banner was absent in the logged-in state, and that run was used to +"verify" its removal — but the same banner is also absent in the logged-out state. Neither run +could distinguish an effective patch from a no-op, and the second one was skipped as +"uninformative" — correctly, since it could not have been informative either. + +**Do instead** + +Before running a verification, ask: + +> **What would this look like if the patch were absent?** + +If the answer is the same, the measurement is void. Record it as **"not applicable under this +condition"**, never as "passed". + +Two close variants of the same error: + +- Treating **"the entry point is unreachable"** as proof of removal. The honest test is whether + the **request is still issued**: a UI element that no longer renders can still fire its network + call from elsewhere. Verify at the layer the behaviour actually lives on. +- Treating **"the screenshot hash changed"** as proof that something rendered. A hash proves + *change*, never *what* changed — and it never proves *absence*. Look at the images: a status-bar + clock tick or a line of text reflowing changes the hash while a full-screen overlay would not + have been missed if the frames had actually been inspected. diff --git a/references/verification.md b/references/verification.md index 6377dc8..8152b50 100644 --- a/references/verification.md +++ b/references/verification.md @@ -48,6 +48,8 @@ access_flags diff (same interface-ness): 0 Caveat, and it is an important one: **this check cannot detect code-item damage.** A whole-tree smali round-trip can pass every table check and still crash with `IncompatibleClassChangeError` (`pitfalls.md` P3). Passing this check is necessary, not sufficient. +The checks that *can* see further — instruction-length auditing, the equal-length-replacement blind spot, and verifier-level legality (`move-result*` adjacency) — are in `references/patch-audit.md`. Use them before claiming a patch landed. + Also confirm you changed what you intended and nothing more: - Reverse the patched dex and inspect the target method. - Compare unmodified classes byte-for-byte against the original where feasible. diff --git a/scripts/dex_classdiff.py b/scripts/dex_classdiff.py index f8ddc7f..c2cac1d 100644 --- a/scripts/dex_classdiff.py +++ b/scripts/dex_classdiff.py @@ -1,11 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""对比两个 dex 的 class_defs:类名 + access_flags(重点看 ACC_INTERFACE 0x200)。 +"""Diff two dex files at the class_defs level: class names + access_flags. +The interesting flag is ACC_INTERFACE (0x200). -用途:验证 baksmali -> smali 整树往返是否损伤了 dex 的 interface/class 关系。 -ICCE "Found interface X, but class was expected" 就是这类损伤的典型症状。 +Use it to prove that a disassemble -> reassemble round-trip did not damage the dex's +interface/class relationships. The classic symptom of that damage is +IncompatibleClassChangeError ("Found interface X, but class was expected") at runtime. -用法:python dex_classdiff.py [过滤子串] +Important limitation: this check compares tables only. It cannot see code-item damage — +see references/patch-audit.md for the checks that can. + +Usage: python dex_classdiff.py [filter-substring] """ import struct import sys diff --git a/scripts/dex_strpatch.py b/scripts/dex_strpatch.py index c60fee3..48d3cd0 100644 --- a/scripts/dex_strpatch.py +++ b/scripts/dex_strpatch.py @@ -1,19 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""字节级定点 patch dex 里的字符串常量,完全不做 smali 往返。 +"""Byte-level, in-place patch of a string constant inside a dex — no smali round-trip. -为什么不用 smali 往返:实测 classes7.dex 经 baksmali->smali 整树重建后, -`io.ktor.client.engine.HttpClientEngine` 的 synthetic access bridge 被破坏, -运行时报 IncompatibleClassChangeError(Found interface, but class was expected)。 -class_def 的 access_flags 看不出问题,所以只能改用"只改字节"的方式。 +Why not a smali round-trip: a disassemble -> reassemble of a whole tree can damage +R8-optimized output in ways the class table does not show. Observed symptom: a synthetic +access bridge lost its interface/class relationship, producing +IncompatibleClassChangeError ("Found interface X, but class was expected") at runtime even +though class_defs and access_flags looked untouched. See references/pitfalls.md P3. -本脚本做法(对 dex 结构零改动): - 1. 把目标字符串替换成**等长**的另一个字符串 —— 长度相同则 string_data_item - 的 uleb128 长度前缀不变,dex 的偏移、索引、表全部不变; - 2. 重算 dex header 的 signature(SHA-1, 从 offset 32 起) 与 checksum(adler32, 从 offset 12 起)。 +What this script does instead (zero structural change to the dex): + 1. Replaces the target string with another of **exactly equal byte length**. Equal length + keeps the string_data_item uleb128 length prefix unchanged, so offsets, indices and + every table stay exactly where they were. + 2. Recomputes the dex header signature (SHA-1, from offset 32) and checksum (adler32, + from offset 12). -用法:python dex_strpatch.py -要求 len(new) == len(old),且 old 在 dex 中出现次数恰好为 1。 +Use only when the replacement can be equal-length. Otherwise use a method-level dex API +rewrite (references/dex-patching.md, technique 1). + +Usage: python dex_strpatch.py +Requires len(new) == len(old) in UTF-8 bytes, and exactly one occurrence of old in the dex. """ import hashlib import struct @@ -46,11 +52,12 @@ def _read_strings(data): def _order_ok(data, off, ob, nb): - """检查把 ob 换成 nb 后是否仍满足 string_ids 的字典序约束。 + """Check whether replacing `ob` with `nb` still satisfies the string_ids ordering rule. - 注意:string_ids 里存的是 string_data_item 的偏移,也就是**长度前缀**的位置 - (在 UTF-8 数据前若干字节),所以不能直接拿字符串数据偏移去匹配 —— - 这里改为按内容匹配目标条目。 + Note: string_ids stores the offset of each string_data_item — i.e. the position of the + **length prefix** (a few bytes ahead of the UTF-8 payload), not of the text itself — so + matching by string-data offset directly is not possible; this matches the entry by + content instead. """ entries = _read_strings(data) idx = None @@ -89,9 +96,11 @@ def main(): return 1 off = data.find(ob) - # 关键:dex 规范要求 string_ids 表按字符串内容有序。等长替换虽然不动偏移, - # 但会改变该条目在表中的字典序位置 —— 一旦越界,整个 dex 会被 ClassLoader - # 拒绝(表现为 ClassNotFoundException: )。所以这里先做区间校验。 + # The dex spec requires string_ids to be ordered by string content. An equal-length + # replacement moves no offset, but it does change where this entry sits in that ordering + # — and once it lands out of order the whole dex is rejected by the ClassLoader + # (symptom: ClassNotFoundException naming the Application class). So validate the + # interval before writing. if not _order_ok(data, off, ob, nb): print('[FAIL] replacement would break string_ids ordering. ' 'Pick a string that stays between its two neighbours.') diff --git a/scripts/patch_smali.py b/scripts/patch_smali.py index 36f9c5e..b086395 100644 --- a/scripts/patch_smali.py +++ b/scripts/patch_smali.py @@ -1,24 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -方法级 smali 补丁器。 +Method-level smali patcher. -设计要点(踩坑后确定): -- 只替换「.method 声明行的下一行」到「.end method 之前」的指令体, - 保留原 .method / .end method / .annotation 段不动,避免破坏修饰符与注解。 -- 补丁清单要求显式给出自洽的 .registers 值:smali 汇编器会校验寄存器不越界, - registers 必须 >= max(参数寄存器数) 且 >= 指令里用到的最大寄存器编号 + 1。 - 参数寄存器数:static 方法 = 参数个数;非 static = 参数个数 + 1(this)。 -- 补丁清单是 JSON 列表,每条: +Design notes (each one earned the hard way): +- Only the instruction body is replaced: from the line after the `.method` declaration up to + (but not including) `.end method`. The `.method` / `.end method` / `.annotation` sections are + left untouched, so modifiers and annotations survive. +- The patch manifest must state a self-consistent `.registers` value: the assembler rejects + out-of-range registers, so `registers` must be >= the number of parameter registers and + >= the highest register index used by any instruction + 1. Parameter registers: + static method = number of parameters; instance method = number of parameters + 1 (this). +- The manifest is a JSON list; each entry: { - "file": "com/example/Helper.smali", # path relative to smali tree root + "file": "com/example/Helper.smali", # path relative to the smali tree root "method": ".method public final show(Landroid/app/Activity;)V", # exact .method line "registers": 3, "body": ["invoke-interface {p3}, ...;", "return-void"], - "note": "干掉插屏广告" # 可选,仅用于报告 + "note": "why this patch exists" # optional, report only } -用法: +A literal-match tool: whitespace, operand punctuation and instruction names must match the +source exactly, and the anchor must be unique. See references/patch-audit.md section 3. + +Usage: python patch_smali.py [--dry-run] """ import json @@ -28,7 +33,7 @@ import sys def split_methods(text): - """把 smali 文本切成 [(method_decl, start_idx, end_idx)],按行索引。""" + """Split smali text into [(method_decl, start_idx, end_idx)] using line indices.""" lines = text.split('\n') out = [] start = None @@ -53,7 +58,7 @@ def patch_one(path, decl_wanted, registers, body): if len(hits) > 1: return False, 'ambiguous (%d matches): %s' % (len(hits), decl_wanted) _, start, end = hits[0] - # 保留 .method 行;重写内部:.registers + body + # Keep the .method line; rewrite the interior: .registers + body new_inner = [' .registers %d' % registers, ''] for b in body: new_inner.append(' ' + b) diff --git a/scripts/usb_net_proxy.py b/scripts/usb_net_proxy.py index afa33e0..398b954 100644 --- a/scripts/usb_net_proxy.py +++ b/scripts/usb_net_proxy.py @@ -1,22 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -用于给「无网络」Android 设备供网的极简 HTTP/HTTPS 代理。 +Minimal HTTP/HTTPS forward proxy for giving an offline Android device network access. -场景:手机 WiFi DHCP 失败 -> 无默认网络 -> app 所有请求失败。 -解法:手机没有可用网络接口也没关系 —— adb reverse 会在**设备本机的 loopback** -上开监听,不需要任何网络接口。于是: - 1) 电脑跑本代理 (0.0.0.0:8080) +Situation: the device has no working network interface (DHCP failure, no default route), so +every request the app makes fails. + +Why this works: it does not need a network interface on the device at all. `adb reverse` +opens a listener on the **device's own loopback**, which exists regardless: + + 1) run this proxy on the host (0.0.0.0:8080) 2) adb -s reverse tcp:8080 tcp:8080 - 3) 设备端 su -c "settings put global http_proxy 127.0.0.1:8080" - 4) app 的 HTTP/HTTPS 流量经 USB 到电脑,由电脑代发 + 3) on the device: settings put global http_proxy 127.0.0.1:8080 + 4) the app's HTTP/HTTPS traffic travels over USB and is sent by the host -支持: - - CONNECT 隧道(HTTPS 主用) - - 明文 HTTP 绝对 URI 转发 -仅做转发,不做 MITM(不解密 TLS,也不改流量),因此不干扰证书校验。 +Supports: + - CONNECT tunnelling (what HTTPS uses) + - absolute-URI plain HTTP forwarding -用法:python usb_net_proxy.py [listen_port] [logfile] +Forwards only: it does not MITM, does not decrypt TLS and does not modify traffic, so +certificate validation is unaffected. See references/environment.md. + +Usage: python usb_net_proxy.py [listen_port] [logfile] """ import socket import sys @@ -39,7 +44,7 @@ def log(msg): def pump(a, b): - """a -> b 单向搬运,直到任一端关闭。""" + """Copy a -> b in one direction until either end closes.""" try: while True: r, _, _ = select_select([a], [], [], 30) @@ -105,7 +110,7 @@ def handle(client, addr): t.start() pump(remote, client) else: - # 明文 HTTP:target 一般是绝对 URI + # Plain HTTP: target is normally an absolute URI if target.startswith('http://'): without = target[len('http://'):] hostport = without.split('/', 1)[0]