Files
RustNotMC/tools/inspect-constants.py
sasheg dev c472bd4a8c Огнестрел: таблица стволов с вики и границы реверса
- weapons.csv: 23 ствола с уроном, множителем в голову, темпом, магазином,
  перезарядкой, разбросом (конус прицела), отдачей, типом патрона, верстаком и
  ценами исследования. Источники - официальная вики и community-вики, расхождения
  помечены в колонке source. Не хватает цены (2 из 23) и дальности (18 из 23).

Границы реверса ремейка, чтобы не тратить на них время снова:
- классы в game.jar помечены DE AD C0 DE и зашифрованы своим форматом, не
  однобайтовым XOR как ассеты; единственный незашифрованный класс - точка входа
  net/minecraft/client/main/RustexMain, в ней видны два статических дешифратора
  строк (qnbt.super и vyuhke.super), а сами классы зашифрованы;
- REA не берет .class как цель (только архивы), Ghidra 12.1.4 стоит в
  D:\ghidra-tools и через analyzeHeadless импортирует распакованные классы;
- главное: урона, отдачи и спрея огнестрела в клиенте нет вообще, это серверные
  данные. Клиент знает только магазин, тип патрона, зум ADS, прочность и reach.
  REA даст формулы, а не числа, поэтому числа берём с вики.

Инструменты в tools/: inspect-constants.py (разбор constant pool класса) и
find-decryptor.py (поиск маркера и слов дешифратора).

План дополнен этими находками.
2026-10-21 20:17:55 +03:00

110 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""Look at the constant pool of a class of the remake and see what is hidden.
The remake obfuscates the string constants: they are not readable in the class
file. This script pulls the UTF-8 entries out of the constant pool and tries
to work out how they were scrambled, because the same cipher is probably what
protects the .obj and .rrs files.
Usage: inspect-constants.py <class file> [...]
"""
import struct
import sys
from pathlib import Path
TAGS = {
1: "Utf8", 3: "Integer", 4: "Float", 5: "Long", 6: "Double",
7: "Class", 8: "String", 9: "Fieldref", 10: "Methodref", 11: "InterfaceMethodref",
12: "NameAndType", 15: "MethodHandle", 16: "MethodType", 17: "Dynamic",
18: "InvokeDynamic", 19: "Module", 20: "Package",
}
def read_pool(data):
"""The constant pool of a class: a list of (tag, value) pairs."""
if data[:4] != b"\xca\xfe\xba\xbe":
raise ValueError("not a class file")
count = struct.unpack_from(">H", data, 8)[0]
pool = []
pos = 10
i = 1
while i < count:
tag = data[pos]
pos += 1
if tag == 1:
length = struct.unpack_from(">H", data, pos)[0]
pos += 2
pool.append((tag, data[pos:pos + length]))
pos += length
elif tag in (3, 4):
pool.append((tag, data[pos:pos + 4]))
pos += 4
elif tag in (5, 6):
pool.append((tag, data[pos:pos + 8]))
pos += 8
elif tag in (7, 8, 16, 19, 20):
pool.append((tag, struct.unpack_from(">H", data, pos)[0]))
pos += 2
elif tag in (9, 10, 11, 12, 17, 18):
pool.append((tag, struct.unpack_from(">HH", data, pos)))
pos += 4
elif tag == 15:
pool.append((tag, data[pos:pos + 3]))
pos += 3
else:
raise ValueError("unknown tag %d at %d" % (tag, pos))
if tag in (5, 6):
i += 2
else:
i += 1
return pool, pos
def printable(data):
good = 0
for b in data:
if 32 <= b < 127:
good += 1
return good / max(1, len(data))
def crack(text):
"""The single byte key that makes a string readable, or None."""
best = None
for key in range(1, 256):
out = bytes(b ^ key for b in text)
score = printable(out)
if score > 0.98 and all(32 <= b < 127 or b in (9, 10, 13) for b in out):
best = (key, out)
break
return best
def main(argv):
if len(argv) < 2:
print(__doc__)
return 1
for name in argv[1:]:
path = Path(name)
data = path.read_bytes()
pool, _ = read_pool(data)
strings = [v for t, v in pool if t == 1]
readable = [s for s in strings if printable(s) > 0.95]
hidden = [s for s in strings if printable(s) <= 0.95]
print("=== %s: %d strings, %d readable, %d hidden"
% (path.name, len(strings), len(readable), len(hidden)))
for s in readable[:12]:
print(" plain %r" % s[:60])
for s in hidden[:8]:
cracked = crack(s)
if cracked:
print(" xor %r key 0x%02X" % (s[:40], cracked[0]))
else:
print(" hidden %r" % s[:40])
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))