Огнестрел: таблица стволов с вики и границы реверса
- 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 (поиск маркера и слов дешифратора). План дополнен этими находками.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Find who reads the encrypted classes of the remake.
|
||||
|
||||
The class files inside game.jar start with DE AD C0 DE and are not readable
|
||||
after that. Something has to decrypt them at runtime, and that something
|
||||
lives in the launcher, in one of the jars or in the java directory. This
|
||||
script looks for the marker and for the words that go with it.
|
||||
|
||||
Usage: find-decryptor.py <file or folder> [...]
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MARKER = b"\xde\xad\xc0\xde"
|
||||
WORDS = [b"deadcode", b"DEADC0DE", b"decrypt", b"Decrypt", b"cipher", b"Cipher",
|
||||
b"ClassLoader", b"defineClass", b"xload", b"scramble", b"obfus"]
|
||||
|
||||
|
||||
def scan(path):
|
||||
hits = []
|
||||
try:
|
||||
data = path.read_bytes()
|
||||
except OSError:
|
||||
return hits
|
||||
pos = data.find(MARKER)
|
||||
if pos >= 0:
|
||||
hits.append(("marker", pos))
|
||||
for word in WORDS:
|
||||
pos = data.find(word)
|
||||
if pos >= 0:
|
||||
hits.append((word.decode("ascii", "replace"), pos))
|
||||
return hits
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2:
|
||||
print(__doc__)
|
||||
return 1
|
||||
for name in argv[1:]:
|
||||
path = Path(name)
|
||||
if path.is_dir():
|
||||
for child in path.rglob("*"):
|
||||
if child.is_file() and child.stat().st_size < 200_000_000:
|
||||
hits = scan(child)
|
||||
for what, where in hits:
|
||||
print("%-70s %s at %d" % (str(child)[-70:], what, where))
|
||||
elif path.is_file():
|
||||
for what, where in scan(path):
|
||||
print("%-70s %s at %d" % (str(path)[-70:], what, where))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/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))
|
||||
Reference in New Issue
Block a user