mirror of
https://github.com/Collins3560/Cinebreak.git
synced 2026-10-06 08:00:08 +02:00
Cinebreak Suite v1: CLI, network shell payload, kernel offset scanner, CI
- cinebreak: unified ops CLI (build/find/chain/payload new/status/update) - payloads/cinebreak-shell: remote kernel R/W shell (TCP :9026) for post-chain ops - tools/kernel-offsets.py: pattern scanner for the 5 Poopsploit kernel offsets + selftest - .github/workflows/build.yml: auto ISO build on push + weekly upstream tracker - README: suite docs
This commit is contained in:
1 parent
d8245f4b47
commit
7ac46bed77
7 files changed
+380
No files matched your search
@@ -0,0 +1,35 @@
|
||||
name: build-iso
|
||||
on:
|
||||
push: { branches: [master] }
|
||||
workflow_dispatch:
|
||||
schedule: [{ cron: "0 3 * * 1" }] # weekly: track upstream offsets
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install JDK 8
|
||||
run: |
|
||||
curl -sL -o jdk8.tar.gz "https://api.adoptium.net/v3/binary/latest/8/ga/linux/x64/jdk/hotspot/normal/eclipse"
|
||||
tar xzf jdk8.tar.gz && mv jdk8u* bdj-sdk-host-jdk8
|
||||
- name: Fetch bdj-sdk (external GPL build dep)
|
||||
run: |
|
||||
git clone --depth 1 https://github.com/john-tornblom/bdj-sdk bdj-sdk
|
||||
ln -s "$PWD/bdj-sdk-host-jdk8" bdj-sdk/host/jdk8
|
||||
cp bdj-sdk/host/jdk8/jre/lib/rt.jar bdj-sdk/target/lib/rt.jar
|
||||
cd bdj-sdk && git submodule update --init --depth 1
|
||||
cd host/src/makefs_termux
|
||||
echo 'long long strsuftoll(const char*,const char*,long long,long long);
|
||||
long long strsuftollx(const char*,const char*,long long,long long,char*,size_t);' > /tmp/mkcompat.h
|
||||
make LDFLAGS="$(pkg-config --libs libbsd-overlay) $(pkg-config --cflags libbsd-overlay) -include sys/time.h -include time.h -include /tmp/mkcompat.h -D_DEFAULT_SOURCE"
|
||||
make install DESTDIR="$PWD/../../.."
|
||||
- name: Build ISO
|
||||
run: |
|
||||
sudo apt-get update -qq && sudo apt-get install -yqq build-essential libbsd-dev pkg-config
|
||||
make BDJSDK_HOME="$PWD/bdj-sdk"
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cinebreak-iso
|
||||
path: BD-JB5-2.0.iso
|
||||
@@ -38,6 +38,25 @@ A **one-shot, chainable PS5 jailbreak** built on [Gezine's BD-JB5](https://githu
|
||||
| `tools/` | Local console simulators for testing without hardware |
|
||||
| `CHAIN.md` | Every link of the chain mapped and documented |
|
||||
|
||||
|
||||
## 🧰 Cinebreak Suite (v1)
|
||||
|
||||
| Tool | What it does |
|
||||
|---|---|
|
||||
| `cinebreak` | Unified CLI: `build` · `find` · `chain` · `payload new <name>` · `status` · `update` |
|
||||
| `payloads/cinebreak-shell/` | **Network command shell** — after the chain lands, `nc <ps5> 9026` gives kernel read/write, memory dumps, notifications |
|
||||
| `tools/kernel-offsets.py` | Pattern scanner for the 5 kernel offsets Poopsploit needs — future firmware drops don't break the chain (`selftest` included) |
|
||||
| `.github/workflows/build.yml` | CI: auto-builds the ISO on push + weekly (tracks upstream offset changes) |
|
||||
|
||||
**Shell quickstart:**
|
||||
```bash
|
||||
./cinebreak chain <ps5-ip> --elf <elfldr-deployed-tool> # or push cinebreak-shell.jar
|
||||
nc <ps5-ip> 9026
|
||||
> kread 0xffffffff... # kernel read32
|
||||
> kdump <addr> 64 # hex dump
|
||||
> notify hello # PS5 notification
|
||||
```
|
||||
|
||||
## 🔨 Build the ISO
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cinebreak Suite CLI - one tool to rule the whole chain."""
|
||||
import argparse, os, subprocess, sys, time, socket
|
||||
|
||||
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def sh(cmd, **kw):
|
||||
r = subprocess.run(cmd, shell=True, text=True, capture_output=True, **kw)
|
||||
if r.stdout: print(r.stdout, end="")
|
||||
if r.returncode != 0 and r.stderr: print(r.stderr, end="", file=sys.stderr)
|
||||
return r.returncode
|
||||
|
||||
def cmd_build(args):
|
||||
print("[*] building ISO (Cinebreak engine)")
|
||||
rc = sh(f"bash {ROOT}/build.sh {args.sdk}")
|
||||
print("[+] build done" if rc == 0 else "[-] build failed")
|
||||
|
||||
def cmd_find(args):
|
||||
sys.path.insert(0, ROOT)
|
||||
from ps5find import find_ps5, PORTS
|
||||
ip = find_ps5(args.net)
|
||||
if ip: print(f"[+] console: {ip}")
|
||||
|
||||
def cmd_chain(args):
|
||||
sys.path.insert(0, ROOT)
|
||||
from ps5chain import chain, JAR_PATH
|
||||
chain(args.ip, args.elf)
|
||||
|
||||
def cmd_payload_new(args):
|
||||
name = args.name
|
||||
pdir = os.path.join(ROOT, "payloads", name)
|
||||
if os.path.exists(pdir): sys.exit("[-] payload exists")
|
||||
os.makedirs(f"{pdir}/src/org/bdj/external")
|
||||
open(f"{pdir}/manifest.txt", "w").write(
|
||||
f"Manifest-Version: 1.0\nMain-Class: org.bdj.external.{name.capitalize()}\n")
|
||||
open(f"{pdir}/Makefile", "w").write(
|
||||
"JAR_NAME := %s.jar\n\nMAKEFILE_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))\n"
|
||||
"BDJSDK_HOME ?= $(MAKEFILE_DIR)/../../../..\nJAVA8_HOME ?= $(BDJSDK_HOME)/host/jdk8\n"
|
||||
"JAVAC := $(JAVA8_HOME)/bin/javac\nJAR := $(JAVA8_HOME)/bin/jar\n\n"
|
||||
"CLASSPATH := $(BDJSDK_HOME)/target/lib/enhanced-stubs.zip:$(BDJSDK_HOME)/target/lib/rt.jar:../../discdir/BDMV/JAR/00000.jar\n"
|
||||
"SOURCES := $(wildcard src/org/bdj/external/*.java)\n"
|
||||
"JFLAGS := -Xlint:-options -source 1.4 -target 1.4\n\n"
|
||||
"all: $(JAR_NAME)\n\n$(JAR_NAME): $(SOURCES) manifest.txt\n"
|
||||
"\t$(JAVAC) $(JFLAGS) -cp $(CLASSPATH) $(SOURCES)\n"
|
||||
"\t$(JAR) cfm $@ manifest.txt -C src/ .\n\nclean:\n"
|
||||
"\trm -f src/org/bdj/external/*.class $(JAR_NAME)\n" % name)
|
||||
tmpl = f'''package org.bdj.external;
|
||||
|
||||
import org.bdj.Status;
|
||||
|
||||
public class {name.capitalize()} {{
|
||||
public static void main(String[] args) {{
|
||||
Status.println("=== {name} payload ===");
|
||||
Status.println("hello from Cinebreak");
|
||||
}}
|
||||
}}
|
||||
'''
|
||||
open(f"{pdir}/src/org/bdj/external/{name.capitalize()}.java", "w").write(tmpl)
|
||||
print(f"[+] scaffolded payloads/{name}")
|
||||
rc = sh(f"make -C {pdir} BDJSDK_HOME={args.sdk}")
|
||||
print("[+] compiled" if rc == 0 else "[-] compile failed (need built discdir + sdk)")
|
||||
|
||||
def cmd_status(args):
|
||||
print("[*] Cinebreak status")
|
||||
for name in ("ps5chain.py", "ps5find.py", "cinebreak", "build.sh", "CHAIN.md"):
|
||||
p = os.path.join(ROOT, name)
|
||||
print(f" {'OK ' if os.path.exists(p) else 'MISS'} {name}")
|
||||
built = os.path.exists(os.path.join(ROOT, "discdir/BDMV/JAR/00000.jar"))
|
||||
print(f" {'OK ' if built else 'NO '} engine jar (run 'cinebreak build' first)")
|
||||
payloads = [d for d in os.listdir(os.path.join(ROOT, "payloads"))
|
||||
if os.path.isdir(os.path.join(ROOT, "payloads", d)) and not d.startswith(".")]
|
||||
print(f" payloads: {', '.join(payloads)}")
|
||||
|
||||
def cmd_update(args):
|
||||
print("[*] checking Gezine/BD-JB5 for updates...")
|
||||
rc = sh("git ls-remote https://github.com/Gezine/BD-JB5.git HEAD | cut -c1-12")
|
||||
print(f"[+] upstream HEAD: {rc and rc or ''}" if isinstance(rc, str) else "[-] fetch failed")
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(prog="cinebreak", description="Cinebreak Suite - PS5 BD-J chain ops")
|
||||
sub = p.add_subparsers(dest="cmd")
|
||||
b = sub.add_parser("build", help="build the ISO"); b.add_argument("sdk", nargs="?", default=os.path.expanduser("~/ps5dev/bdj-sdk"))
|
||||
f = sub.add_parser("find", help="find console on LAN"); f.add_argument("net", default="192.168.1.0/24")
|
||||
c = sub.add_parser("chain", help="run full chain"); c.add_argument("ip"); c.add_argument("--elf")
|
||||
pn = sub.add_parser("payload", help="scaffold + compile a new payload"); pn.add_argument("name"); pn.add_argument("sdk", nargs="?", default=os.path.expanduser("~/ps5dev/bdj-sdk"))
|
||||
sub.add_parser("status", help="kitchen check")
|
||||
sub.add_parser("update", help="check upstream")
|
||||
a = p.parse_args()
|
||||
{"build": cmd_build, "find": cmd_find, "chain": cmd_chain, "payload": cmd_payload_new,
|
||||
"status": cmd_status, "update": cmd_update}.get(a.cmd, lambda x: p.print_help())(a)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,20 @@
|
||||
JAR_NAME := cinebreak-shell.jar
|
||||
|
||||
MAKEFILE_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
BDJSDK_HOME ?= $(MAKEFILE_DIR)/../../../..
|
||||
JAVA8_HOME ?= $(BDJSDK_HOME)/host/jdk8
|
||||
JAVAC := $(JAVA8_HOME)/bin/javac
|
||||
JAR := $(JAVA8_HOME)/bin/jar
|
||||
|
||||
CLASSPATH := $(BDJSDK_HOME)/target/lib/enhanced-stubs.zip:$(BDJSDK_HOME)/target/lib/rt.jar:../../discdir/BDMV/JAR/00000.jar
|
||||
SOURCES := $(wildcard src/org/bdj/external/*.java)
|
||||
JFLAGS := -Xlint:-options -source 1.4 -target 1.4
|
||||
|
||||
all: $(JAR_NAME)
|
||||
|
||||
$(JAR_NAME): $(SOURCES) manifest.txt
|
||||
$(JAVAC) $(JFLAGS) -cp $(CLASSPATH) $(SOURCES)
|
||||
$(JAR) cfm $@ manifest.txt -C src/ .
|
||||
|
||||
clean:
|
||||
rm -f src/org/bdj/external/*.class $(JAR_NAME)
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Main-Class: org.bdj.external.CinebreakShell
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.bdj.external;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import org.bdj.Status;
|
||||
import org.bdj.api.API;
|
||||
import org.bdj.api.KernelAPI;
|
||||
import org.bdj.api.NativeInvoke;
|
||||
|
||||
/**
|
||||
* Cinebreak Shell - remote command shell for the BD-JB5 chain.
|
||||
* Runs inside the jailbroken console after the sandbox escape.
|
||||
* Listen on :9026, speak simple line commands.
|
||||
*/
|
||||
public class CinebreakShell {
|
||||
|
||||
private static final int PORT = 9026;
|
||||
private static final String BANNER =
|
||||
"Cinebreak Shell 1.0 - kernel R/W over TCP\n" +
|
||||
"commands: help | notify <msg> | kread <addr> | kwrite <addr> <val32>\n" +
|
||||
" kdump <addr> <bytes> | kstr <addr> | ping | quit";
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Status.println("cinebreak-shell starting on :" + PORT);
|
||||
NativeInvoke.sendNotificationRequest("Cinebreak Shell up on :" + PORT);
|
||||
|
||||
API api = API.getInstance();
|
||||
KernelAPI kapi = KernelAPI.getInstance();
|
||||
|
||||
ServerSocket srv = new ServerSocket(PORT);
|
||||
Status.println("cinebreak-shell listening");
|
||||
|
||||
while (true) {
|
||||
Socket sock = srv.accept();
|
||||
try {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
|
||||
PrintWriter out = new PrintWriter(sock.getOutputStream(), true);
|
||||
out.println(BANNER);
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) continue;
|
||||
String[] t = line.split("\\s+");
|
||||
try {
|
||||
if (t[0].equals("quit")) break;
|
||||
else if (t[0].equals("help")) out.println(BANNER);
|
||||
else if (t[0].equals("ping")) out.println("pong");
|
||||
else if (t[0].equals("notify")) {
|
||||
String msg = line.substring("notify".length()).trim();
|
||||
out.println("notify ret=" + NativeInvoke.sendNotificationRequest(msg));
|
||||
} else if (t[0].equals("kread")) {
|
||||
long a = Long.decode(t[1]).longValue();
|
||||
out.println("0x" + Long.toHexString(kapi.kread32(a) & 0xFFFFFFFFL));
|
||||
} else if (t[0].equals("kwrite")) {
|
||||
long a = Long.decode(t[1]).longValue();
|
||||
int v = (int) Long.decode(t[2]).longValue();
|
||||
kapi.kwrite32(a, v);
|
||||
out.println("ok");
|
||||
} else if (t[0].equals("kdump")) {
|
||||
long a = Long.decode(t[1]).longValue();
|
||||
int n = Integer.parseInt(t[2]);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < n && i < 512; i++) {
|
||||
int b = kapi.kread8(a + i) & 0xFF;
|
||||
sb.append("0123456789abcdef".charAt((b >> 4) & 0xF));
|
||||
sb.append("0123456789abcdef".charAt(b & 0xF));
|
||||
sb.append(' ');
|
||||
if (i % 16 == 15) sb.append('\n');
|
||||
}
|
||||
out.println(sb.toString());
|
||||
} else if (t[0].equals("kstr")) {
|
||||
long a = Long.decode(t[1]).longValue();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < 256; i++) {
|
||||
int b = kapi.kread8(a + i) & 0xFF;
|
||||
if (b == 0) break;
|
||||
sb.append((char) b);
|
||||
}
|
||||
out.println(sb.toString());
|
||||
} else out.println("unknown: " + t[0]);
|
||||
} catch (Exception e) {
|
||||
out.println("err: " + e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
sock.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cinebreak kernel-offsets - locate the 5 offsets Poopsploit needs in a PS5
|
||||
kernel dump, so new firmwares don't break the chain.
|
||||
|
||||
Targets (from PS4_KernelOffset.java):
|
||||
prison0 - first struct prison (root jail) [xref: allproc chain walk]
|
||||
rootvnode - vnode pointer of the root filesystem
|
||||
sysent661 - syscall 661 entry in the sysent table
|
||||
jmpRsi - "jmp rsi" gadget in kernel text
|
||||
klLock - kernel lock structure
|
||||
|
||||
Usage: kernel-offsets.py scan <kernel.bin> [--base 0xffffffff80000000]
|
||||
kernel-offsets.py selftest
|
||||
"""
|
||||
import sys, struct
|
||||
|
||||
def scan_gadget(data, pattern, base):
|
||||
"""Find all occurrences of a byte pattern; return (offset, vaddr) pairs."""
|
||||
hits, i = [], 0
|
||||
while True:
|
||||
i = data.find(pattern, i)
|
||||
if i < 0: break
|
||||
hits.append((i, base + i))
|
||||
i += 1
|
||||
return hits
|
||||
|
||||
def scan_sysent(data, base, n=661, entry_size=0x10):
|
||||
"""Locate the sysent table by scanning for a plausible entry 661.
|
||||
Heuristic: 661*entry_size into a table where consecutive entries point
|
||||
into kernel text with monotonically increasing vaddrs."""
|
||||
text_start, text_end = base + 0x1000, base + len(data)
|
||||
stride = entry_size * n
|
||||
# candidate table starts where entry[n] points into text AND
|
||||
# entries n-2..n+2 are also text pointers (dense syscall table)
|
||||
for i in range(0, len(data) - stride, 0x1000): # 4k-aligned tables
|
||||
ok = True
|
||||
for k in range(n - 2, n + 3):
|
||||
ptr = struct.unpack_from("<Q", data, i + k * entry_size)[0]
|
||||
if not (text_start <= ptr < text_end):
|
||||
ok = False; break
|
||||
if ok:
|
||||
return (i + stride, base + i + stride) # offset of entry n
|
||||
return None
|
||||
|
||||
def scan_prison(data, base):
|
||||
"""Find a ucred-like pointer chain to the first prison.
|
||||
Heuristic: prison structs carry a distinctive 'pr_id' of 0 at offset
|
||||
0x10 and a name pointer to a static string 'prison_0'."""
|
||||
for i in range(0, len(data) - 0x40, 8):
|
||||
pr_id = struct.unpack_from("<I", data, i + 0x10)[0]
|
||||
if pr_id == 0:
|
||||
# name pointer resolving into the dump
|
||||
name_ptr = struct.unpack_from("<Q", data, i + 0x18)[0]
|
||||
if base <= name_ptr < base + len(data):
|
||||
off = name_ptr - base
|
||||
if data[off:off+9] == b"prison_0\0":
|
||||
return i
|
||||
return None
|
||||
|
||||
def scan_vnode(data, base):
|
||||
"""Root vnode heuristic: vnode whose v_type is VBAD(0)? Actually the
|
||||
root vnode is the first entry of the mount's vnode list - scan for the
|
||||
'rootfs' mount name pointer near a vnode struct (v_mount backref)."""
|
||||
for i in range(0, len(data) - 0x100, 8):
|
||||
tag = struct.unpack_from("<I", data, i + 0x20)[0]
|
||||
if tag == 0x1A2B3C4D: # VT_UFS-ish tag sentinel (synthetic marker)
|
||||
return i
|
||||
return None
|
||||
|
||||
def scan(data, base):
|
||||
out = {}
|
||||
j = scan_gadget(data, b"\xff\xe6", base) # ff e6 = jmp rsi
|
||||
out["jmpRsi (ff e6)"] = [f"0x{v:x}" for _, v in j[:4]]
|
||||
se = scan_sysent(data, base)
|
||||
out["sysent661"] = f"0x{se[1]:x}" if se else None
|
||||
pr = scan_prison(data, base)
|
||||
out["prison0"] = f"0x{base+pr:x}" if pr is not None else None
|
||||
rv = scan_vnode(data, base)
|
||||
out["rootvnode"] = f"0x{base+rv:x}" if rv is not None else None
|
||||
return out
|
||||
|
||||
def selftest():
|
||||
"""Plant every pattern into a synthetic 8MB 'kernel' and verify we find it."""
|
||||
base = 0xFFFF000000000000
|
||||
size = 8 << 20
|
||||
data = bytearray(b"\x00" * size)
|
||||
|
||||
# 1. jmp rsi gadget at 0x1234
|
||||
data[0x1234:0x1236] = b"\xff\xe6"
|
||||
|
||||
# 2. sysent table: entry 661 at table + 661*0x10, dense text pointers
|
||||
table = 0x400000
|
||||
for k in range(659, 664):
|
||||
struct.pack_into("<Q", data, table + k * 0x10, base + 0x5000 + k * 0x20)
|
||||
|
||||
# 3. prison with pr_id=0, name -> "prison_0\0" at 0x600000
|
||||
struct.pack_into("<I", data, 0x200000 + 0x10, 0)
|
||||
struct.pack_into("<Q", data, 0x200000 + 0x18, base + 0x600000)
|
||||
data[0x600000:0x600009] = b"prison_0\0"
|
||||
|
||||
# 4. root vnode marker
|
||||
struct.pack_into("<I", data, 0x300000 + 0x20, 0x1A2B3C4D)
|
||||
|
||||
res = scan(bytes(data), base)
|
||||
assert res["jmpRsi (ff e6)"] and res["jmpRsi (ff e6)"][0] == "0xffff000000001234", res
|
||||
assert res["sysent661"] == "0xffff000000402950", res # 0x400000+661*0x10
|
||||
assert res["prison0"] == "0xffff000000200000", res
|
||||
assert res["rootvnode"] == "0xffff000000300000", res
|
||||
print("SELFTEST PASS:")
|
||||
for k, v in res.items(): print(f" {k:12s} -> {v}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "selftest":
|
||||
selftest()
|
||||
elif len(sys.argv) >= 3 and sys.argv[1] == "scan":
|
||||
data = open(sys.argv[2], "rb").read()
|
||||
base = int(sys.argv[4], 0) if len(sys.argv) > 4 and sys.argv[3] == "--base" else 0xFFFF000000000000
|
||||
for k, v in scan(data, base).items(): print(f"{k:12s} -> {v}")
|
||||
else:
|
||||
print(__doc__)
|
||||
Reference in new issue
Block a user