Lots of hacking. Got passed the network boot

This commit is contained in:
573dev 2020-10-13 22:38:04 -05:00
parent e95d5d2816
commit 7b23ef0e58
9 changed files with 358 additions and 5 deletions

3
.gitignore vendored
View File

@ -101,3 +101,6 @@ ENV/
# mypy
.mypy_cache/
# request data
requests/

View File

@ -7,7 +7,15 @@ from setuptools import find_packages, setup
TEST_DEPS = ["coverage[toml]", "pytest", "pytest-cov"]
DOCS_DEPS = ["sphinx", "sphinx-rtd-theme", "sphinx-autoapi", "recommonmark"]
CHECK_DEPS = ["isort", "flake8", "flake8-quotes", "pep8-naming", "mypy", "black"]
REQUIREMENTS = ["flask", "watchdog", "pyopenssl"]
REQUIREMENTS = [
"flask",
"watchdog",
"pyopenssl",
"lxml",
"pycryptodome",
"lzss",
"kbinxml",
]
EXTRAS = {
"test": TEST_DEPS,

View File

@ -5,7 +5,7 @@ from typing import Optional, Union
from flask import Flask
from v8_server.config import Development, Production
from v8_server.utils import generate_secret_key
from v8_server.utils.flask import generate_secret_key
from .version import __version__

View File

23
v8_server/utils/arc4.py Normal file
View File

@ -0,0 +1,23 @@
from Crypto.Cipher import ARC4
from Crypto.Hash import MD5
class EamuseARC4(object):
def __init__(self, eamuse_key):
# fmt: off
self.internal_key = bytearray([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, 0xd7,
0x46, 0x27, 0xd9, 0x85, 0xee, 0x21, 0x87, 0x16,
0x15, 0x70, 0xd0, 0x8d, 0x93, 0xb1, 0x24, 0x55,
0x03, 0x5b, 0x6d, 0xf0, 0xd8, 0x20, 0x5d, 0xf5,
])
# fmt: on
self.key = MD5.new(eamuse_key + self.internal_key[6:]).digest()
self.arc = ARC4.new(self.key)
def decrypt(self, data):
return self.arc.decrypt(bytes(data))
def encrypt(self, data):
return self.arc.encrypt(bytes(data))

View File

@ -0,0 +1,5 @@
from time import time
def get_timestamp() -> str:
return str(int(round(time() * 1000)))

116
v8_server/utils/xml.py Normal file
View File

@ -0,0 +1,116 @@
from binascii import unhexlify
from pathlib import Path
from random import randint
from time import time
import lxml
import lzss
from kbinxml import KBinXML
from lxml import etree as ET
from v8_server.utils.arc4 import EamuseARC4
from v8_server.utils.eamuse import get_timestamp
EAMUSE_CONFIG = {"encrypted": False, "compressed": False}
REQUESTS_PATH = Path("./requests")
def eamuse_read_xml(request):
# Get encrypted/compressed data from client
headers = request.headers
data = request.data
if "x-eamuse-info" in headers:
# Decrypt/Compress data
x_eamuse_info = headers["x-eamuse-info"]
key = unhexlify(x_eamuse_info[2:].replace("-", ""))
arc4 = EamuseARC4(key)
xml_dec = arc4.decrypt(data)
EAMUSE_CONFIG["encrypted"] = True
else:
xml_dec = data
EAMUSE_CONFIG["encrypted"] = False
compress = headers["x-compress"] if "x-compress" in headers else None
if compress == "lz77":
xml_dec = lzss.decompress(xml_dec)
EAMUSE_CONFIG["compress"] = True
else:
EAMUSE_CONFIG["compress"] = False
xml_text = KBinXML(xml_dec).to_text().encode("UTF-8")
root = ET.fromstring(xml_text)
REQUESTS_PATH.mkdir(exist_ok=True)
output_filename = REQUESTS_PATH / f"eamuse_{get_timestamp()}_req.xml"
with output_filename.open("w") as f:
f.write(xml_text.decode("UTF-8"))
model = root.attrib["model"]
module = root[0].tag
method = root[0].attrib["method"] if "method" in root[0].attrib else None
command = root[0].attrib["command"] if "command" in root[0].attrib else None
print(
"---- Read Request ----\n"
f"Headers: [{dict(headers)}]\n"
f" Model: [{model}]\n"
f" Module: [{module}]\n"
f" Method: [{method}]\n"
f"Command: [{command}]\n"
" Data:\n"
f"{xml_text.decode('UTF-8')[:-1]}\n"
)
# Return raw XML
return xml_text, model, module, method, command
def eamuse_prepare_xml(xml):
x_eamuse_info = f"1-{int(time()):08x}-{randint(0x0000, 0xffff):04x}"
key = unhexlify(x_eamuse_info[2:].replace("-", ""))
xml_root = xml
if type(xml) == lxml.etree._Element:
xml = ET.tostring(xml, encoding="UTF-8").decode("UTF-8")
REQUESTS_PATH.mkdir(exist_ok=True)
timestamp = get_timestamp()
output_filename = REQUESTS_PATH / f"eamuse_{timestamp}_resp.xml"
with output_filename.open("wb") as f:
f.write(xml.encode("UTF-8"))
headers = {
"Content-Type": "applicaton/octet_stream",
"Server": "Microsoft-HTTPAPI/2.0",
}
# Convert XML to binary
xml_bin = KBinXML(xml.encode("UTF-8")).to_binary()
output_filename = REQUESTS_PATH / f"eamuse_{timestamp}_resp.bin"
with output_filename.open("wb") as f:
f.write(xml_bin)
if EAMUSE_CONFIG["compress"]:
headers["X-Compress"] = "lz77"
# Actually do some compression here if we have to
headers["X-Eamuse-Info"] = x_eamuse_info
if EAMUSE_CONFIG["encrypted"]:
arc4 = EamuseARC4(key)
data = arc4.encrypt(xml_bin)
else:
data = xml_bin
print(
"---- Write Response ----\n"
" Data: [\n"
f"{ET.tostring(xml_root, pretty_print=True).decode('UTF-8')[:-1]}\n"
f"Headers: [{headers}]\n"
)
return data, headers

View File

@ -1,6 +1,204 @@
from collections import OrderedDict
from flask import request
from lxml import etree as ET
from lxml.builder import E
from v8_server import app
from v8_server.utils.xml import eamuse_prepare_xml, eamuse_read_xml
@app.route("/")
def hello_world():
return "Hello, World!"
@app.route("/", defaults={"path": ""}, methods=["GET", "POST"])
@app.route("/<path:path>", methods=["GET", "POST"])
def catch_all(path):
d = f"""
{request.args}
{request.form}
{request.files}
{request.values}
{request.json}
{request.data}
{request.headers}
"""
print(d)
return "You want path: %s" % path
@app.route("/pcbtracker/service", methods=["POST"])
def pcbtracker():
"""
Handle a PCBTracker.alive request. The only method of note is the "alive" method
which returns whether PASELI should be active or not for this session.
For V8 it should not be active.
"""
request_xml, model, module, method, command = eamuse_read_xml(request)
if method == "alive":
response = E.response(E.pcbtracker(OrderedDict(ecenable="0", expire="600")))
response_body, headers = eamuse_prepare_xml(response)
else:
# There shoulnd't really even be any other methods
raise Exception("Not sure how to handle this PCBTracker Request")
return response_body, headers
@app.route("/message/service", methods=["POST"])
def message():
"""
Unknown what this does. Possibly for operator messages?
"""
request_xml, model, module, method, command = eamuse_read_xml(request)
response = E.response(E.message(OrderedDict(expire="600")))
response_body, headers = eamuse_prepare_xml(response)
return response_body, headers
@app.route("/pcbevent/service", methods=["POST"])
def pcbevent():
"""
Handle a PCBEvent request. We do nothing for this aside from logging the event.
"""
request_xml, model, module, method, command = eamuse_read_xml(request)
response = E.response(E.pcbevent(OrderedDict(expire="600")))
response_body, headers = eamuse_prepare_xml(response)
return response_body, headers
@app.route("/facility/service", methods=["POST"])
def facility():
"""
Handle a facility request. The only method of note is the "get" request,
which expects to return a bunch of information about the arcade this cabinet is in,
as well as some settings for URLs and the name of the cab.
"""
request_xml, model, module, method, command = eamuse_read_xml(request)
response = E.response(
E.facility(
E.location(
E.id("US-123"),
E.country("US"),
E.region("."),
E.name("H"),
E.type("0", {"__type": "u8"}),
),
E.line(E.id("."), E("class", "0", {"__type": "u8"}),),
E.portfw(
E.globalip("192.168.1.139", {"__type": "ip4", "__count": "1"}),
E.globalport("80", {"__type": "u16"}),
E.privateport("80", {"__type": "u16"}),
),
E.public(
E.flag("1", {"__type": "u8"}),
E.name("."),
E.latitude("0"),
E.longitude("0"),
),
E.share(
E.eacoin(
E.notchamount("3000", {"__type": "s32"}),
E.notchcount("3", {"__type": "s32"}),
E.supplylimit("10000", {"__type": "s32"}),
),
E.eapass(E.valid("365", {"__type": "u16"})),
E.url(
E.eapass("www.ea-pass.konami.net"),
E.arcadefan("www.konami.jp/am"),
E.konaminetdx("http://am.573.jp"),
E.konamiid("http://id.konami.net"),
E.eagate("http://eagate.573.jp"),
),
),
{"expire": "600"},
)
)
response_body, headers = eamuse_prepare_xml(response)
return response_body, headers
@app.route("/package/service", methods=["POST"])
def package():
"""
This is for supporting downloading of updates. We don't support this.
"""
request_xml, model, module, method, command = eamuse_read_xml(request)
response = E.response(E.package())
response_body, headers = eamuse_prepare_xml(response)
return response_body, headers
@app.route("/service/services/services/", methods=["POST"])
def services():
# We don't need to actually read the data here, but let's do it anyway as it saves a
# copy
_ = eamuse_read_xml(request)
service_names = [
"pcbtracker",
"pcbevent",
"facility",
"message",
"package",
"userdata",
"userid",
"dlstatus",
"traceroute",
"eacoin",
"netlog",
"sidmgr",
]
"""
"binary",
"cardmng",
"dlstatus",
"eacoin",
"eemall",
"facility",
"info",
"lobby",
"local",
"netlog",
"numbering",
"pcbevent",
"pkglist",
"posevent",
"reference",
"shopinf",
"sidmgr",
"userdata",
"userid",
"""
services = {
"ntp": "ntp://pool.ntp.org",
"keepalive": (
"http://eamuse.konami.fun/"
"keepalive?pa=127.0.0.1&ia=127.0.0.1&ga=127.0.0.1&ma=127.0.0.1&t1=2&t2=10"
),
**{k: f"http://eamuse.konami.fun/{k}/service" for k in service_names},
}
response = E.response(
E.services(
expire="600",
method="get",
mode="operation",
status="0",
*[E.item(OrderedDict(name=k, url=services[k])) for k in services],
)
)
response_body, headers = eamuse_prepare_xml(response)
return response_body, 200, headers