mirror of
https://github.com/itsPLK/ps5-webkit-autoloader.git
synced 2026-10-19 02:09:45 -05:00
feat: integrate relapse exploit and loader refinements
This commit is contained in:
1 parent
137f065d2b
commit
132be85383
26 files changed
+837
-1606
No files matched your search
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prepare the relapse copy used by the frontend and apply our autoloader patch.
|
||||
#
|
||||
# relapse lives as a pristine git submodule in third_party/relapse (never
|
||||
# modified). The frontend needs it under frontend/autoloader/relapse, so this
|
||||
# script:
|
||||
# 1. copies third_party/relapse -> frontend/autoloader/relapse (fresh copy)
|
||||
# 2. prunes what the autoloader never loads: the README, the upstream dev
|
||||
# server, and every bundled payload except the kexp shellcode
|
||||
# (elfldr comes from the shared ../../shared/ dir, and the kstuff /
|
||||
# shadowmountplus / etaHEN menu payloads are not used by the autoloader)
|
||||
# 3. applies patches/relapse-autoload.patch to the copy
|
||||
#
|
||||
# The copy is gitignored (frontend/autoloader/relapse/), so the submodule is
|
||||
# never dirtied. Run after every submodule update:
|
||||
#
|
||||
# git submodule update --init --recursive
|
||||
# tools/apply_relapse_patch.sh
|
||||
#
|
||||
# The Makefile runs this automatically before staging/serving (relapse-prepare).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SOURCE="$ROOT/third_party/relapse"
|
||||
DEST="$ROOT/frontend/autoloader/relapse"
|
||||
PATCH="$ROOT/patches/relapse-autoload.patch"
|
||||
|
||||
if [ ! -e "$SOURCE/.git" ]; then
|
||||
echo "Error: relapse submodule is not initialised."
|
||||
echo "Run: git submodule update --init --recursive"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$PATCH" ]; then
|
||||
echo "Error: patch file not found: $PATCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. Fresh copy of the exploit root. LICENSE is kept for attribution; the
|
||||
# README and the upstream dev server (serve.py) are not needed at runtime.
|
||||
rm -rf "$DEST"
|
||||
mkdir -p "$DEST"
|
||||
cp -R "$SOURCE"/. "$DEST"/
|
||||
rm -rf "$DEST/.git" "$DEST/.github" "$DEST/.gitignore" "$DEST/README.md" "$DEST/serve.py"
|
||||
|
||||
# 2. Prune payloads/ down to the kexp shellcode. elfldr is the shared one from
|
||||
# frontend/autoloader/shared/ (localhost-only), and the optional jailbreak
|
||||
# menu is never loaded by the autoloader.
|
||||
find "$DEST/payloads" -maxdepth 1 -type f ! -name 'kexp*.bin' -delete
|
||||
|
||||
# 3. Turn the copy into a throwaway git repo so `git apply` can handle the
|
||||
# patch. Two commits: pristine relapse, then our autoloader patch.
|
||||
SRC_HASH=$(git -C "$SOURCE" rev-parse --short HEAD)
|
||||
git -C "$DEST" init -q
|
||||
git -C "$DEST" config user.name "wkal"
|
||||
git -C "$DEST" config user.email "wkal@localhost"
|
||||
git -C "$DEST" add -A
|
||||
git -C "$DEST" commit -q -m "relapse pristine (submodule $SRC_HASH)"
|
||||
|
||||
# 4. Apply the patch
|
||||
cd "$DEST"
|
||||
if git apply --check "$PATCH" 2>/dev/null; then
|
||||
git apply "$PATCH"
|
||||
git add -A
|
||||
git commit -q -m "Apply WKAL autoloader patch"
|
||||
echo "relapse: copied to $DEST and autoloader patch applied."
|
||||
elif git apply --reverse --check "$PATCH" 2>/dev/null; then
|
||||
echo "relapse: autoloader patch is already applied."
|
||||
else
|
||||
echo "Error: patch does not apply cleanly to $DEST."
|
||||
echo "relapse has likely changed upstream — regenerate patches/relapse-autoload.patch:"
|
||||
echo " git -C $DEST diff HEAD~1 > $PATCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 5. Sanity check: the patched sources must carry our integration markers, the
|
||||
# bundled elfldr must be gone from the sources, the runtime offsets URL must
|
||||
# be the pinned one, and payloads/ must hold only the kexp shellcode. These
|
||||
# markers only exist once the patch applied (they are not in pristine
|
||||
# relapse), so this catches a silently truncated or empty patch.
|
||||
if ! grep -q 'const AUTOLOAD = new URLSearchParams' src/main.js \
|
||||
|| ! grep -q 'const AUTOLOAD_BASE = "../payloads/";' src/main.js \
|
||||
|| ! grep -q 'async function startAutoload' src/main.js \
|
||||
|| ! grep -q 'await startAutoload(p, chain);' src/main.js \
|
||||
|| ! grep -q 'reportAutoload(false, { why: "elfldr did not start" });' src/main.js \
|
||||
|| ! grep -q 'isElfldrListening(p, chain)' src/main.js \
|
||||
|| ! grep -q 'const why = "Already jailbroken.";' src/main.js \
|
||||
|| ! grep -q 'window.fw_str + ".js");' src/main.js \
|
||||
|| grep -qF 'fw_str}.js?v=' src/main.js \
|
||||
|| ! grep -q 'const SHARED_BASE = "../shared/";' src/kexp.js \
|
||||
|| ! grep -q 'const DEFAULT_ELFLDR = "elfldr-ps5.elf";' src/kexp.js \
|
||||
|| ! grep -q 'mapElf(DEFAULT_ELFLDR, p, chain, SHARED_BASE)' src/kexp.js \
|
||||
|| ! grep -q 'export async function loadAutoloadPayload' src/kexp.js \
|
||||
|| ! grep -q 'export async function isElfldrListening' src/kexp.js \
|
||||
|| grep -qF 'DEFAULT_ELFLDR = "elfldr-ps5-1360.elf"' src/kexp.js \
|
||||
|| grep -qF '../../shared/' src/kexp.js \
|
||||
|| grep -qF '../../payloads/' src/main.js \
|
||||
|| [ ! -f LICENSE ] \
|
||||
|| [ -e serve.py ] \
|
||||
|| [ "$(find payloads -maxdepth 1 -type f | wc -l)" -ne 1 ] \
|
||||
|| ! ls payloads/kexp*.bin >/dev/null 2>&1; then
|
||||
echo "Error: relapse patch verification FAILED — integration markers missing."
|
||||
echo "patches/relapse-autoload.patch is incomplete or out of date."
|
||||
echo "Regenerate it from a patched copy and re-run."
|
||||
exit 1
|
||||
fi
|
||||
echo "relapse: patch verification OK (early elfldr guard, ?autoload sender,"
|
||||
echo " shared elfldr, query-less offsets URL, payloads pruned to kexp)."
|
||||
|
||||
# 6. Parse-check the patched JS in the mode the browser will use it in. A
|
||||
# `node --check foo.js` parses in CommonJS (sloppy) mode, but these are ES
|
||||
# modules (strict) — a scope collision that sloppy mode accepts still kills
|
||||
# the chain on the console. See tools/check_exploit_js.py.
|
||||
"$ROOT/tools/check_exploit_js.py" "$DEST"
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prepare the slopkit copy used by the frontend and apply our autoloader patch.
|
||||
#
|
||||
# slopkit lives as a pristine git submodule in third_party/slopkit (never
|
||||
# modified). The frontend needs it under frontend/autoloader/slopkit, so this
|
||||
# script:
|
||||
# 1. copies third_party/slopkit -> frontend/autoloader/slopkit (fresh copy)
|
||||
# 2. applies patches/slopkit-autoload.patch to the copy
|
||||
#
|
||||
# The copy is gitignored (frontend/autoloader/slopkit/), so the submodule is
|
||||
# never dirtied. Run after every submodule update:
|
||||
#
|
||||
# git submodule update --init --recursive
|
||||
# tools/apply_slopkit_patch.sh
|
||||
#
|
||||
# The Makefile runs this automatically before staging/serving (slopkit-prepare).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SOURCE="$ROOT/third_party/slopkit"
|
||||
DEST="$ROOT/frontend/autoloader/slopkit"
|
||||
PATCH="$ROOT/patches/slopkit-autoload.patch"
|
||||
|
||||
if [ ! -e "$SOURCE/.git" ]; then
|
||||
echo "Error: slopkit submodule is not initialised."
|
||||
echo "Run: git submodule update --init --recursive"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$PATCH" ]; then
|
||||
echo "Error: patch file not found: $PATCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. Fresh copy (drop .git, .github, anything not needed at runtime)
|
||||
rm -rf "$DEST"
|
||||
mkdir -p "$DEST"
|
||||
cp -R "$SOURCE"/. "$DEST"/
|
||||
rm -rf "$DEST/.git" "$DEST/.github" "$DEST/.gitignore" "$DEST/.gitmodules"
|
||||
|
||||
# 2. Turn the copy into a throwaway git repo so `git apply` can handle binary
|
||||
# diffs / patch chunks — plain git apply on a non-repo dir cannot.
|
||||
# Two commits: pristine slopkit, then our autoloader patch.
|
||||
SRC_HASH=$(git -C "$SOURCE" rev-parse --short HEAD)
|
||||
git -C "$DEST" init -q
|
||||
git -C "$DEST" config user.name "wkal"
|
||||
git -C "$DEST" config user.email "wkal@localhost"
|
||||
git -C "$DEST" add -A
|
||||
git -C "$DEST" commit -q -m "slopkit pristine (submodule $SRC_HASH)"
|
||||
|
||||
# 3. Apply the patch
|
||||
cd "$DEST"
|
||||
if git apply --check "$PATCH" 2>/dev/null; then
|
||||
git apply "$PATCH"
|
||||
git add -A
|
||||
git commit -q -m "Apply WKAL autoloader patch"
|
||||
echo "slopkit: copied to $DEST and autoloader patch applied."
|
||||
elif git apply --reverse --check "$PATCH" 2>/dev/null; then
|
||||
echo "slopkit: autoloader patch is already applied."
|
||||
else
|
||||
echo "Error: patch does not apply cleanly to $DEST."
|
||||
echo "slopkit has likely changed upstream — regenerate patches/slopkit-autoload.patch:"
|
||||
echo " git -C $SOURCE diff > $PATCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. Sanity check: the patched pages must carry our integration markers.
|
||||
# Catches a silently truncated/empty patch. These markers only exist when
|
||||
# the patch applied (they are not in pristine slopkit).
|
||||
if ! grep -q 'sendPayloadToElfldr(cfg.autoload, "../../payloads/"' slopkit/poops.html \
|
||||
|| ! grep -q 'if (key === "autoload") return;' slopkit/poops.html \
|
||||
|| ! grep -q 'name: "payload.elf"' slopkit/poops.html \
|
||||
|| ! grep -q '"url=../../shared/" + name' slopkit/poops.js \
|
||||
|| ! grep -q 'const AUTOLOAD = Q.get("autoload")' slopkit/p2jb.html \
|
||||
|| ! grep -q 'sendPayloadToElfldr(AUTOLOAD, "../../payloads/")' slopkit/p2jb.html \
|
||||
|| ! grep -q '"../../shared/elfldr-ps5.elf"' slopkit/p2jb.html \
|
||||
|| ! grep -q 'function startAutoload' slopkit/poops.html \
|
||||
|| ! grep -q 'startAutoload();' slopkit/poops.html \
|
||||
|| ! grep -q 'function elfldrAccepting' slopkit/poops.html \
|
||||
|| ! grep -q 'AUTOLOAD-WAIT' slopkit/poops.html \
|
||||
|| ! grep -q 'function startAutoload' slopkit/p2jb.html \
|
||||
|| ! grep -q 'startAutoload();' slopkit/p2jb.html \
|
||||
|| ! grep -q 'function elfldrAccepting' slopkit/p2jb.html \
|
||||
|| ! grep -q 'AUTOLOAD-WAIT' slopkit/p2jb.html; then
|
||||
echo "Error: slopkit patch verification FAILED — integration markers missing."
|
||||
echo "patches/slopkit-autoload.patch is incomplete or out of date."
|
||||
echo "Regenerate it from the pristine submodule:"
|
||||
echo " git -C $SOURCE diff > $PATCH"
|
||||
exit 1
|
||||
fi
|
||||
echo "slopkit: patch verification OK (autoload block + probe-path autoload,"
|
||||
echo " exactQuery relaxation, hidden payload.elf tile, shared elfldr"
|
||||
echo " on poops + p2jb)."
|
||||
@@ -90,3 +90,9 @@ if ! grep -q 'wkalAutoloadName = new URLSearchParams' main.js \
|
||||
exit 1
|
||||
fi
|
||||
echo "umtx2: patch verification OK (mainloop autoload, own elfldr, confirm removed, payloads pruned)."
|
||||
|
||||
# Parse-check the patched JS in the mode the browser will use it in. A
|
||||
# `node --check foo.js` parses in CommonJS (sloppy) mode, but these are ES
|
||||
# modules (strict) — a scope collision that sloppy mode accepts still kills the
|
||||
# chain on the console. See tools/check_exploit_js.py.
|
||||
"$ROOT/tools/check_exploit_js.py" "$DEST"
|
||||
+8
-14
@@ -42,22 +42,16 @@ def repo_root():
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
|
||||
# slopkit's bundled payload menu servers (ftpsrv, gdbsrv, kstuff, ...) are
|
||||
# never used by the autoloader — only the kexp it boots is needed (slopkit
|
||||
# boots the shared elfldr from /app/shared/, see tools/download_deps.sh).
|
||||
# umtx2 keeps its OWN bundled elfldr (umtx2/payloads/elfldr-ps5.elf, like stock
|
||||
# umtx2) and its other bundled payloads are pruned by tools/apply_umtx2_patch.sh.
|
||||
# The copied slopkit/umtx2 are throwaway git repos (tools/apply_*_patch.sh), so
|
||||
# .git must never be embedded. The payload digest sidecars (payloads/*.sha256)
|
||||
# are build-time bookkeeping and must never be served.
|
||||
# The exploits' payload dirs are already pruned to exactly what the chains load
|
||||
# (relapse keeps only its kexp shellcode and boots the shared elfldr from
|
||||
# shared/, see tools/download_deps.sh; umtx2 keeps its OWN elfldr-ps5.elf at
|
||||
# umtx2/payloads/, like stock umtx2), so nothing here needs per-exploit payload
|
||||
# filtering. The copied relapse/umtx2 are throwaway git repos
|
||||
# (tools/apply_*_patch.sh), so .git must never be embedded. The payload digest
|
||||
# sidecars (*.sha256) are build-time bookkeeping and must never be served.
|
||||
def include_in_zip(rel):
|
||||
if "/.git/" in rel or rel.endswith("/.git"):
|
||||
return False
|
||||
if rel.startswith("slopkit/payloads/"):
|
||||
name = os.path.basename(rel)
|
||||
return name.startswith("kexp") and name.endswith(".bin")
|
||||
if rel == "slopkit/readme.png":
|
||||
return False
|
||||
if rel.endswith(".sha256"):
|
||||
return False
|
||||
return True
|
||||
@@ -116,7 +110,7 @@ def build_zip(frontend_dir, overrides_dir, version, build_time, payload_path=Non
|
||||
data = data.replace(BUILD_TIME_TOKEN, build_time.encode("utf-8"))
|
||||
zf.writestr(rel, data)
|
||||
elif rel == "app.js":
|
||||
# Build-time exploit override (auto | umtx2 | poops | p2jb),
|
||||
# Build-time exploit override (auto | umtx2 | relapse),
|
||||
# from the FORCE_EXPLOIT env — same token as the ELF build.
|
||||
with open(file_map[rel], "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
Executable
+213
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the vendored exploit sources before they are served.
|
||||
|
||||
Browsers load the exploit scripts as ES modules. A `.js` file passed directly
|
||||
to `node --check` may be parsed as CommonJS, so the checker uses two passes:
|
||||
|
||||
1. A dependency-free scan for parameter/declaration name collisions.
|
||||
2. When Node is available, a module parse of every ES module.
|
||||
|
||||
The module parse is skipped with a notice when Node is absent; the SDK image
|
||||
only guarantees python3.
|
||||
|
||||
Usage: check_exploit_js.py <dir> [<dir> ...]
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
FUNC_RE = re.compile(
|
||||
r"^\s*(?:async\s+)?function\s+\w+\s*\(([^)]*)\)\s*\{", re.MULTILINE)
|
||||
DECL_RE = re.compile(r"\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)")
|
||||
|
||||
|
||||
def strip_noise(src):
|
||||
"""Blank out comments and string/template/regex literals, preserving
|
||||
offsets and newlines, so brace/identifier scanning cannot trip over them."""
|
||||
out = list(src)
|
||||
i, n = 0, len(src)
|
||||
# Tracks the last significant token so a '/' can be told apart from a regex.
|
||||
prev = ""
|
||||
while i < n:
|
||||
c = src[i]
|
||||
if c == "/" and i + 1 < n and src[i + 1] == "/":
|
||||
while i < n and src[i] != "\n":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
elif c == "/" and i + 1 < n and src[i + 1] == "*":
|
||||
out[i] = out[i + 1] = " "
|
||||
i += 2
|
||||
while i + 1 < n and not (src[i] == "*" and src[i + 1] == "/"):
|
||||
if src[i] != "\n":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
if i + 1 < n:
|
||||
out[i] = out[i + 1] = " "
|
||||
i += 2
|
||||
elif c in "\"'`":
|
||||
quote = c
|
||||
i += 1
|
||||
while i < n:
|
||||
if src[i] == "\\":
|
||||
if src[i] != "\n":
|
||||
out[i] = " "
|
||||
if i + 1 < n and src[i + 1] != "\n":
|
||||
out[i + 1] = " "
|
||||
i += 2
|
||||
continue
|
||||
if src[i] == quote:
|
||||
break
|
||||
if src[i] != "\n":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
i += 1
|
||||
prev = "str"
|
||||
continue
|
||||
elif c == "/" and prev in ("", "(", ",", "=", ":", "[", "!", "&", "|",
|
||||
"?", "{", "}", ";", "return", "typeof"):
|
||||
# regex literal: skip to the unescaped closing slash
|
||||
i += 1
|
||||
in_class = False
|
||||
while i < n and src[i] != "\n":
|
||||
if src[i] == "\\":
|
||||
i += 2
|
||||
continue
|
||||
if src[i] == "[":
|
||||
in_class = True
|
||||
elif src[i] == "]":
|
||||
in_class = False
|
||||
elif src[i] == "/" and not in_class:
|
||||
break
|
||||
if src[i] != "\n":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
i += 1
|
||||
prev = "str"
|
||||
continue
|
||||
else:
|
||||
if not c.isspace():
|
||||
prev = src[i:i + 6] if src[i:i + 6] == "return" else c
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def body_of(src, open_brace):
|
||||
"""Return the function body by brace matching on noise-stripped source."""
|
||||
depth = 0
|
||||
for i in range(open_brace, len(src)):
|
||||
if src[i] == "{":
|
||||
depth += 1
|
||||
elif src[i] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return src[open_brace + 1:i]
|
||||
return src[open_brace + 1:]
|
||||
|
||||
|
||||
def scan_collisions(path):
|
||||
"""Flag function parameters redeclared by const/let/var in the same body."""
|
||||
src = strip_noise(open(path, encoding="utf-8").read())
|
||||
problems = []
|
||||
for m in FUNC_RE.finditer(src):
|
||||
params = [p.strip() for p in m.group(1).split(",") if p.strip()]
|
||||
params = [p.split("=")[0].strip() for p in params]
|
||||
params = [p for p in params if re.fullmatch(r"[A-Za-z_$][\w$]*", p)]
|
||||
if not params:
|
||||
continue
|
||||
body = body_of(src, src.index("{", m.end() - 1))
|
||||
declared = {d for d in DECL_RE.findall(body)}
|
||||
# This scan is conservative and can flag legal shadowing in nested
|
||||
# blocks; the module parser is authoritative when Node is available.
|
||||
for name in params:
|
||||
if name in declared:
|
||||
line = src[:m.start()].count("\n") + 1
|
||||
problems.append(
|
||||
"%s:%d: parameter '%s' is redeclared by a const/let/var in "
|
||||
"the same function body" % (path, line, name))
|
||||
return problems
|
||||
|
||||
|
||||
def js_files(dirs):
|
||||
for d in dirs:
|
||||
for root, _dirs, names in os.walk(d):
|
||||
for name in sorted(names):
|
||||
if name.endswith(".js"):
|
||||
yield os.path.join(root, name)
|
||||
|
||||
|
||||
def is_module(src):
|
||||
"""True when the file uses top-level import/export, i.e. the browser will
|
||||
parse it as a module (strict mode)."""
|
||||
return re.search(r"^\s*(?:import\s|export\s|import\{|export\{)", src,
|
||||
re.MULTILINE) is not None
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2:
|
||||
print("Usage: check_exploit_js.py <dir> [<dir> ...]", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
files = list(js_files(argv[1:]))
|
||||
if not files:
|
||||
print("check_exploit_js: no .js files found in %s" % ", ".join(argv[1:]),
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
failures = []
|
||||
for path in files:
|
||||
failures.extend(scan_collisions(path))
|
||||
|
||||
if failures:
|
||||
print("check_exploit_js: FAILED", file=sys.stderr)
|
||||
for f in failures:
|
||||
print(" " + f, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Stronger pass: a real module parse, when node is available.
|
||||
try:
|
||||
subprocess.run(["node", "--version"], capture_output=True, check=True)
|
||||
have_node = True
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
have_node = False
|
||||
|
||||
if have_node:
|
||||
for path in files:
|
||||
src = open(path, encoding="utf-8").read()
|
||||
if not is_module(src):
|
||||
continue
|
||||
# Copy to a .mjs temp file so node parses it as a module (strict
|
||||
# mode), the same way the browser will. `--check` only parses, it
|
||||
# does not resolve the imports, so the temp dir does not matter.
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".mjs", delete=False,
|
||||
encoding="utf-8") as tmp:
|
||||
tmp.write(src)
|
||||
tmp_path = tmp.name
|
||||
try:
|
||||
r = subprocess.run(["node", "--check", tmp_path],
|
||||
capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
first = [l for l in r.stderr.splitlines() if "Error" in l]
|
||||
failures.append("%s: %s" % (path, first[0].strip() if first
|
||||
else "module parse failed"))
|
||||
finally:
|
||||
os.remove(tmp_path)
|
||||
if failures:
|
||||
print("check_exploit_js: FAILED (module parse)", file=sys.stderr)
|
||||
for f in failures:
|
||||
print(" " + f, file=sys.stderr)
|
||||
return 1
|
||||
module_count = sum(1 for p in files
|
||||
if is_module(open(p, encoding="utf-8").read()))
|
||||
print("check_exploit_js: OK (%d files, %d module-parsed with node)"
|
||||
% (len(files), module_count))
|
||||
else:
|
||||
print("check_exploit_js: OK (%d files, scope scan only — node not on "
|
||||
"PATH, skipping the module parse)" % len(files))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
+1
-1
@@ -70,7 +70,7 @@ def make_handler(base_dir, version, build_time):
|
||||
if rel.endswith((".html", ".htm")) or rel == "index.html":
|
||||
data = data.replace(VERSION_TOKEN, version).replace(BUILD_TIME_TOKEN, build_time)
|
||||
if rel == "app.js":
|
||||
# Build-time exploit override (auto | umtx2 | poops | p2jb),
|
||||
# Build-time exploit override (auto | umtx2 | relapse),
|
||||
# from the FORCE_EXPLOIT env — same token as the ELF/host builds.
|
||||
mode = os.environ.get("FORCE_EXPLOIT", "auto")
|
||||
data = data.replace(b"[[EXPLOIT_MODE]]", mode.encode("utf-8"))
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# third_party/ps5-elfldr -> frontend/autoloader/shared/elfldr-ps5.elf
|
||||
# third_party/ps5-unified-autoloader -> frontend/autoloader/payloads/payload.elf
|
||||
#
|
||||
# The shared elfldr is used by the slopkit chain (7.00-12.00); umtx2
|
||||
# The shared elfldr is used by the relapse chain (7.00-13.60); umtx2
|
||||
# (1.00-5.50) boots its own elfldr from the umtx2 submodule, like stock umtx2.
|
||||
# The unified-autoloader payload is the "bundled" ELF embedded in the installer:
|
||||
# after install, the homescreen app runs the exploit chain and autoloads it from
|
||||
@@ -33,7 +33,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# Shared elfldr (same ELF across all exploit chains)
|
||||
ELFLDR_SUBMODULE="$ROOT/third_party/ps5-elfldr"
|
||||
ELFLDR_REPO="itsPLK/ps5-elfldr"
|
||||
ELFLDR_TAG="v0.24-148b71c"
|
||||
ELFLDR_TAG="v0.26-bb1e117"
|
||||
ELFLDR_DEST="$ROOT/frontend/autoloader/shared/elfldr-ps5.elf"
|
||||
|
||||
# Bundled autoload payload
|
||||
|
||||
+26
-88
@@ -11,8 +11,6 @@ Usage: gen_file_registry.py <dist_dir> <header_out> <source_out>
|
||||
"""
|
||||
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
@@ -51,30 +49,20 @@ def detect_content_type(path):
|
||||
return CONTENT_TYPES.get(ext, "application/octet-stream")
|
||||
|
||||
|
||||
# slopkit ships its own payload menu servers (ftpsrv, gdbsrv, kstuff, ...) that
|
||||
# our autoloader never uses — the chains only need the elfldr they boot and
|
||||
# the kexp shellcode that loads it. poops boots the shared elfldr (served from
|
||||
# /app/<version>/shared/elfldr-ps5.elf, see tools/download_deps.sh) and p2jb
|
||||
# is patched onto it too, so only the kexp is kept (both chains use it).
|
||||
# umtx2 boots its OWN bundled elfldr (kept by
|
||||
# tools/apply_umtx2_patch.sh at umtx2/payloads/elfldr-ps5.elf, like stock
|
||||
# umtx2); the rest of its payloads are pruned, and the autoload payload comes
|
||||
# from /app/<version>/payloads/. readme.png is a slopkit repo asset, also
|
||||
# unused. The copied slopkit is a throwaway git repo
|
||||
# (tools/apply_slopkit_patch.sh), so .git must never be embedded. The payload
|
||||
# digest sidecars (payloads/*.sha256) are build-time bookkeeping and must never
|
||||
# be served. /VERSION is the staging version handoff (see Makefile) — build-time
|
||||
# bookkeeping too.
|
||||
# The exploits' payload dirs are already pruned to exactly what the chains load
|
||||
# (relapse keeps only its kexp shellcode and boots the shared elfldr from
|
||||
# /app/<version>/shared/; umtx2 keeps only its own elfldr-ps5.elf and boots that
|
||||
# one, like stock umtx2), so nothing here needs per-exploit payload filtering.
|
||||
# The autoload payload always comes from /app/<version>/payloads/.
|
||||
# The copied relapse/umtx2 are throwaway git repos (tools/apply_*_patch.sh), so
|
||||
# .git must never be embedded. The payload digest sidecars (*.sha256) are
|
||||
# build-time bookkeeping and must never be served. /VERSION is the staging
|
||||
# version handoff (see Makefile) — build-time bookkeeping too.
|
||||
def include_in_registry(path):
|
||||
if "/.git/" in path or path.endswith("/.git"):
|
||||
return False
|
||||
if path == "/VERSION":
|
||||
return False
|
||||
if "/slopkit/payloads/" in path:
|
||||
name = os.path.basename(path)
|
||||
return name.startswith("kexp") and name.endswith(".bin")
|
||||
if "/slopkit/readme.png" in path:
|
||||
return False
|
||||
if path.endswith(".sha256"):
|
||||
return False
|
||||
return True
|
||||
@@ -85,10 +73,10 @@ BUILD_TIME_PLACEHOLDER = b"[[BUILD_TIME_PLACEHOLDER]]"
|
||||
EXPLOIT_MODE_PLACEHOLDER = b"[[EXPLOIT_MODE]]"
|
||||
APP_DIR_PLACEHOLDER = b"[[APP_DIR_PLACEHOLDER]]"
|
||||
|
||||
# The build-time exploit override in app.js (auto | umtx2 | poops | p2jb).
|
||||
# The build-time exploit override in app.js (auto | umtx2 | relapse).
|
||||
# Defaults to "auto" (firmware routing) unless FORCE_EXPLOIT is set.
|
||||
DEFAULT_EXPLOIT_MODE = "auto"
|
||||
EXPLOIT_MODES = ("auto", "umtx2", "poops", "p2jb")
|
||||
EXPLOIT_MODES = ("auto", "umtx2", "relapse")
|
||||
|
||||
|
||||
def get_version_info_with_handoff(dist_dir):
|
||||
@@ -162,33 +150,14 @@ def compress_entry(data):
|
||||
return comp, True
|
||||
|
||||
|
||||
# The autoloader iframe loads poops.html with this exact query string. AppCache
|
||||
# matches URLs exactly (query included), so the manifest must list the full URL
|
||||
# or the console serves a fallback document instead of the exploit page. The
|
||||
# app now lives under /app/<version>/, so the URL is prefixed with that. Keep
|
||||
# in sync with POOPS_URL in frontend/autoloader/app.js (which resolves to the
|
||||
# same absolute path from the versioned app dir). The trailing v= matches
|
||||
# slopkit's ROUTE_VERSION cache-bust (see the patch regeneration notes in
|
||||
# ARCHITECTURE.md).
|
||||
def poops_iframe_url(app_dir):
|
||||
return (
|
||||
app_dir + "/slopkit/slopkit/poops.html"
|
||||
"?go=1&auto=1&production=1&trigger=netcontrol&attempts=8"
|
||||
"&only=ps0_preflight,ps1_prepare,ps3_stage0,ps4_validate"
|
||||
",ps5_stage1,ps6_stage2,ps8_stage3,ps9_stage4,ps10_stage5"
|
||||
"&log=debug&payload=1&autoload=payload.elf&v=final"
|
||||
)
|
||||
|
||||
|
||||
# Same for p2jb (FW 12.02-12.70): upstream's canonical production query plus
|
||||
# our autoload key (relaxed in patches/slopkit-autoload.patch). Keep in sync
|
||||
# with P2JB_URL in frontend/autoloader/app.js.
|
||||
def p2jb_iframe_url(app_dir):
|
||||
return (
|
||||
app_dir + "/slopkit/slopkit/p2jb.html"
|
||||
"?go=1&auto=1&production=1&log=debug"
|
||||
"&payload=1&autoload=payload.elf&v=final"
|
||||
)
|
||||
# The autoloader iframe loads relapse/index.html with this exact query string.
|
||||
# AppCache matches URLs exactly (query included), so the manifest must list the
|
||||
# full URL or the console serves a fallback document instead of the exploit
|
||||
# page. The app lives under /app/<version>/, so the URL is prefixed with that.
|
||||
# Keep in sync with RELAPSE_URL in frontend/autoloader/app.js (which resolves to
|
||||
# the same absolute path from the versioned app dir).
|
||||
def relapse_iframe_url(app_dir):
|
||||
return app_dir + "/relapse/index.html?autoload=payload.elf"
|
||||
|
||||
|
||||
# umtx2 auto-runs its chain on load via the 'on_load_autorun' sessionStorage
|
||||
@@ -198,44 +167,15 @@ def p2jb_iframe_url(app_dir):
|
||||
def umtx2_iframe_url(app_dir):
|
||||
return app_dir + "/umtx2/index.html?autoload=payload.elf&v=1"
|
||||
|
||||
# slopkit references its own scripts with cache-busting query strings
|
||||
# (e.g. "./core.js?v=final", "main.js?v=final", "../offsets/9.00.js?v=final").
|
||||
# AppCache matches URLs exactly, so the manifest must list those query
|
||||
# variants too or the console falls back and the module imports fail.
|
||||
CACHEBUST_RE = re.compile(r'([A-Za-z0-9_./-]+\.(?:js|css|html|png|jpg|gif))\?v=[A-Za-z0-9]+')
|
||||
|
||||
|
||||
def collect_cachebust_urls(files):
|
||||
"""Scan staged HTML/JS for query-string script imports (slopkit's ?v=
|
||||
cache-busters) and return their absolute URLs, resolved relative to the
|
||||
referencing file. Offsets are loaded dynamically as ../offsets/<fw>.js?v=final
|
||||
in main.js, so every offsets file gets the ?v=final variant as well."""
|
||||
urls = set()
|
||||
for path, full in files:
|
||||
try:
|
||||
with open(full, "r", encoding="utf-8", errors="replace") as f:
|
||||
data = f.read()
|
||||
except OSError:
|
||||
continue
|
||||
base = posixpath.dirname(path)
|
||||
for match in CACHEBUST_RE.finditer(data):
|
||||
ref, query = match.group(1), match.group(0)[len(match.group(1)):]
|
||||
resolved = posixpath.normpath(posixpath.join(base, ref))
|
||||
if resolved.startswith("/") and "/slopkit/" in resolved:
|
||||
urls.add(resolved + query)
|
||||
for path, _ in files:
|
||||
if "/slopkit/offsets/" in path and path.endswith(".js"):
|
||||
urls.add(path + "?v=final")
|
||||
return sorted(urls)
|
||||
|
||||
|
||||
def build_manifest(files, version, build_time, app_dir, pointer_path, marker_path):
|
||||
"""Build the AppCache manifest. Ordering matters for partial-cache safety:
|
||||
every versioned file is listed first, then the exploit iframe URLs and the
|
||||
slopkit cache-bust variants, then the pointer page, then the __complete__
|
||||
marker LAST. The marker being the final entry means a successfully cached
|
||||
marker implies the whole versioned directory was downloaded — and the
|
||||
pointer (frontend/pointer/index.html) verifies the marker's content before
|
||||
every versioned file is listed first, then the exploit iframe URLs (which
|
||||
carry a query string, so they are not the same cache key as the bare file),
|
||||
then the pointer page, then the __complete__ marker LAST. The marker being
|
||||
the final entry means a successfully cached marker implies the whole
|
||||
versioned directory was downloaded — and the pointer
|
||||
(frontend/pointer/index.html) verifies the marker's content before
|
||||
redirecting into it."""
|
||||
lines = [
|
||||
"CACHE MANIFEST",
|
||||
@@ -247,10 +187,8 @@ def build_manifest(files, version, build_time, app_dir, pointer_path, marker_pat
|
||||
cache_entries = [path for path, _ in files if path not in (pointer_path, marker_path)]
|
||||
cache_entries.sort()
|
||||
lines += cache_entries
|
||||
lines.append(poops_iframe_url(app_dir))
|
||||
lines.append(p2jb_iframe_url(app_dir))
|
||||
lines.append(relapse_iframe_url(app_dir))
|
||||
lines.append(umtx2_iframe_url(app_dir))
|
||||
lines += collect_cachebust_urls(files)
|
||||
lines.append(pointer_path)
|
||||
lines.append(marker_path)
|
||||
lines += [
|
||||
|
||||
Reference in new issue
Block a user