121 lines
3.7 KiB
Python
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",
|
|
]
|