146 lines
5.1 KiB
Python
146 lines
5.1 KiB
Python
"""Exact-build layouts and version classification.
|
||
|
||
The version shown by Windows is useful for choosing a compatibility family, but
|
||
it is not strong enough to select native addresses. A patched client can keep
|
||
the same product version family while moving every relevant RVA. Native
|
||
sending requires an exact executable digest or a complete structural ABI match.
|
||
Automatically relocated builds still require current-account receipt evidence.
|
||
"""
|
||
from dataclasses import dataclass, field
|
||
import hashlib
|
||
from pathlib import Path
|
||
import re
|
||
|
||
|
||
_VERSION_RE = re.compile(r"(?<![\w.])v?(\d{1,5}(?:\.\d{1,5}){3})(?![\w.])", re.IGNORECASE)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class NativeBuild:
|
||
version: str
|
||
sha256: str
|
||
service_root: int
|
||
message_vtable: int
|
||
login_vtable: int
|
||
factory: int
|
||
send: int
|
||
release: int
|
||
send_stub: str
|
||
send_argument_bytes: int
|
||
verified: bool
|
||
adapter_revision: str = "original-text-v1"
|
||
automatic: bool = False
|
||
live_checks: tuple = field(default=(), repr=False)
|
||
|
||
@property
|
||
def adapter_id(self):
|
||
return f"wxwork-{self.version}-x86"
|
||
|
||
|
||
# Addresses use the PE preferred base 0x400000; runtime rebasing happens in NativeClient.
|
||
BUILDS = (
|
||
NativeBuild("5.0.10.6023", "47b5dfafc8def0b41a648610f1ba08bbf15f710c6f81c08d36c485bd82ce2842",
|
||
0xf7fb57c, 0xb936800, 0xb92c530, 0x818e560, 0x966c1f0, 0x656340,
|
||
"send", 12, True),
|
||
NativeBuild("5.0.11.6018", "ca7280999c0ae011bf2fac296c24f796595f3558df784edc50dd3f9a6edac5d9",
|
||
0xfe3c444, 0xbe21a84, 0xbe1730c, 0x8545200, 0x9a5f400, 0x66c5a0,
|
||
"send_with_options", 16, False),
|
||
# 5.0.11.6026 x86: independently located against the signed client image
|
||
# installed on the target machine. It remains disabled until this build's
|
||
# first real server receipt is recorded by the explicit validation action.
|
||
NativeBuild("5.0.11.6026", "b597539664b69e78c7edaa7e4edce46dccc864fafb7caa00764b60ee48eccea6",
|
||
0xfe7bfbc, 0xbe5766c, 0xbe4cc1c, 0x856eec0, 0x9a8b780, 0x66d050,
|
||
"send_with_options", 16, False, "original-text-v1-6026"),
|
||
)
|
||
|
||
|
||
# A family is intentionally broader than an adapter. It lets the UI explain
|
||
# that 5.0.11.6026 was detected as a known line without treating
|
||
# 5.0.11.6018's code layout as compatible with it.
|
||
KNOWN_VERSION_FAMILIES = {
|
||
"5.0.10": "5.0.10 x86 客户端系列",
|
||
"5.0.11": "5.0.11 x86 客户端系列",
|
||
}
|
||
|
||
|
||
def normalize_version(value):
|
||
"""Return a canonical version from Windows/process metadata."""
|
||
if value is None:
|
||
return ""
|
||
text = str(value).strip()
|
||
if not text:
|
||
return ""
|
||
matches = list(_VERSION_RE.finditer(text))
|
||
if len(matches) != 1:
|
||
return ""
|
||
components = matches[0].group(1).split(".")
|
||
if any(int(component) > 65535 for component in components):
|
||
return ""
|
||
return ".".join(str(int(component)) for component in components)
|
||
|
||
|
||
def version_family(value):
|
||
"""Return the three-component family key for a client version."""
|
||
normalized = normalize_version(value)
|
||
parts = normalized.split(".")
|
||
return ".".join(parts[:3]) if len(parts) >= 3 else normalized
|
||
|
||
|
||
def classify_version(value):
|
||
"""Describe a detected version without granting native-send capability."""
|
||
normalized = normalize_version(value)
|
||
family = version_family(normalized)
|
||
build = build_for_version(normalized) if normalized else None
|
||
if build is not None:
|
||
state = "supported_exact"
|
||
elif family in KNOWN_VERSION_FAMILIES:
|
||
state = "known_family_new_patch"
|
||
elif normalized:
|
||
state = "unknown"
|
||
else:
|
||
state = "missing"
|
||
return {
|
||
"raw": "" if value is None else str(value),
|
||
"normalized": normalized,
|
||
"family": family,
|
||
"familyLabel": KNOWN_VERSION_FAMILIES.get(family, ""),
|
||
"state": state,
|
||
"adapterId": build.adapter_id if build is not None else "",
|
||
}
|
||
|
||
|
||
def build_for_hash(digest):
|
||
value = str(digest or "").strip().lower()
|
||
known = next((build for build in BUILDS if build.sha256.lower() == value), None)
|
||
if known is not None:
|
||
return known
|
||
from wecom_native_adaptation import cached_build
|
||
return cached_build(value)
|
||
|
||
|
||
def sha256_file(path):
|
||
"""Hash one client image for read-only build identification.
|
||
|
||
The caller must still validate the live process and account before a native
|
||
write. This helper only makes an exact build recognizable when the Windows
|
||
version resource is missing, localized, or formatted differently.
|
||
"""
|
||
digest = hashlib.sha256()
|
||
with Path(path).open("rb") as handle:
|
||
while chunk := handle.read(1024 * 1024):
|
||
digest.update(chunk)
|
||
return digest.hexdigest()
|
||
|
||
|
||
def build_for_version(version):
|
||
normalized = normalize_version(version)
|
||
return next((build for build in BUILDS if build.version == normalized), None)
|
||
|
||
|
||
def verified_adapter(adapter_id):
|
||
return any(build.adapter_id == adapter_id and build.verified for build in BUILDS)
|
||
|
||
|
||
def client_support_text():
|
||
return ";".join(f"{build.version} x86({'已实测' if build.verified else '首次发送验证后启用'})" for build in BUILDS)
|