ksjsb/core/ksse_deobf.py
2026-07-30 20:25:56 +08:00

121 lines
3.7 KiB
Python

"""Pure Python port of libksse static string deobfuscation helpers.
Recovered native path:
* FUN_00150ba0 wraps the decoded bytes into a C++ string.
* FUN_001502f0 reads a little-endian uint16 length followed by encrypted bytes.
* FUN_0014fe6c seeds three LFSR-like registers from bytes 4..7 of the seed.
* FUN_00150128 emits one keystream byte and returns ``(ks + 3) ^ cipher``.
The default seed literal is visible in libksse: ``Vuz4fCHxn1CO``.
"""
from __future__ import annotations
from dataclasses import dataclass
DEFAULT_KSSE_STRING_SEED = b"Vuz4fCHxn1CO"
def _u32(value: int) -> int:
return value & 0xFFFFFFFF
@dataclass
class KsseStringState:
ac48: int
ac4c: int
ac50: int
ac54: int = 0x80000062
ac58: int = 0x40000020
ac5c: int = 0x10000002
ac60: int = 0x7FFFFFFF
ac64: int = 0x3FFFFFFF
ac68: int = 0x0FFFFFFF
ac6c: int = 0x80000000
ac70: int = 0xC0000000
ac74: int = 0xF0000000
def init_ksse_string_state(seed: bytes | str = DEFAULT_KSSE_STRING_SEED) -> KsseStringState:
if isinstance(seed, str):
seed = seed.encode("utf-8")
if len(seed) > 20 or not seed:
seed = b"quajdsfjasodfue"
if len(seed) < 12:
seed = seed + seed[: 12 - len(seed)]
# Ghidra expression is a 32-bit byte swap of *(uint32_t *)(seed + 4).
word = int.from_bytes(seed[4:8], "big")
if word == 0:
return KsseStringState(ac48=0x13579BDF, ac4c=0x2468ACE0, ac50=0xFDB97531)
return KsseStringState(ac48=word, ac4c=word, ac50=word)
def decode_ksse_string_byte(cipher_byte: int, state: KsseStringState) -> int:
"""Port sub_00150128 for a single byte."""
out = 0
bit6 = state.ac50 & 1
bit7 = state.ac4c & 1
reg50 = state.ac50
reg4c = state.ac4c
for _ in range(8):
if (state.ac48 & 1) == 0:
state.ac48 = _u32(state.ac60 & (state.ac48 >> 1))
if (reg50 & 1) == 0:
bit6 = 0
reg50 = _u32(state.ac68 & (reg50 >> 1))
state.ac50 = reg50
else:
reg50 = _u32(((state.ac5c >> 1) ^ reg50) | state.ac74)
bit6 = 1
state.ac50 = reg50
else:
state.ac48 = _u32(((state.ac54 >> 1) ^ state.ac48) | state.ac6c)
if (reg4c & 1) == 0:
reg4c = _u32(state.ac64 & (reg4c >> 1))
bit7 = 0
state.ac4c = reg4c
else:
reg4c = _u32(((state.ac58 >> 1) ^ reg4c) | state.ac70)
bit7 = 1
state.ac4c = reg4c
out = ((bit6 ^ bit7) | ((out & 0x7F) << 1)) & 0xFF
return ((out + 3) & 0xFF) ^ (cipher_byte & 0xFF)
def decode_ksse_string_payload(payload: bytes, seed: bytes | str = DEFAULT_KSSE_STRING_SEED) -> bytes:
state = init_ksse_string_state(seed)
return bytes(decode_ksse_string_byte(item, state) for item in payload)
def decode_ksse_string_blob(blob: bytes, seed: bytes | str = DEFAULT_KSSE_STRING_SEED) -> bytes:
if len(blob) < 2:
raise ValueError("encoded blob must contain a 16-bit length prefix")
length = int.from_bytes(blob[:2], "little")
payload = blob[2 : 2 + length]
if len(payload) != length:
raise ValueError(f"encoded blob length mismatch: need {length}, got {len(payload)}")
return decode_ksse_string_payload(payload, seed)
def pack_le_values(*items: tuple[int, int]) -> bytes:
"""Pack decompiler stack constants as little-endian byte fragments."""
return b"".join(value.to_bytes(size, "little") for value, size in items)
__all__ = [
"DEFAULT_KSSE_STRING_SEED",
"KsseStringState",
"decode_ksse_string_blob",
"decode_ksse_string_byte",
"decode_ksse_string_payload",
"init_ksse_string_state",
"pack_le_values",
]