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 (поиск маркера и слов дешифратора). План дополнен этими находками.
110 lines
3.3 KiB
Python
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))
|