/* Copyright (C) 2025 etaHEN / LightningMods This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, see . */ #include #include #include #include #include "hijacker.hpp" #include "launcher.hpp" #include "globalconf.hpp" #include "ipc.hpp" #include "../../extern/tiny-json/tiny-json.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include pid_t elfldr_spawn(const char* cwd, int stdio, uint8_t* elf, const char* name); } using namespace std; extern struct daemon_settings global_conf; // Global variables std::string dump_path; std::string dump_title; std::string dumping_tid; bool is_dumper_enabled = false; Dump_Option dump_opt = DUMP_ALL; atomic_bool cmd_srv_Running = false; atomic_bool rest_mode_action = false; extern atomic_bool sce_cmd_srv_Running; extern atomic_bool ipc_server_2_running; extern atomic_int ipc_2_ret; extern pthread_t klog_srv; extern pthread_t discordRpcServerThread; extern int shellui_pid_for_comp; extern int DISCORD_RPC_SERVER_PORT; // Locks and synchronization objects pthread_mutex_t jb_lock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t lock; // Command enum enum Commands : int { INVALID_CMD = -1, ACTIVE_CMD = 0, LAUNCH_CMD, PROCLIST_CMD, KILL_CMD, KILL_APP_CMD, JAILBREAK_CMD, REMOUNT_FOLDER_CMD, ETAHEN_VER_CMD, PATCH_LNC_DEBUG_CMD, ACTIVATE_DUMPER_CMD, TEST_CMD, SYMLINK_CMD, }; // Constants #define MAX_TID_SIZE 10 // Network structures typedef struct SceNetEtherAddr { uint8_t data[6]; } SceNetEtherAddr; typedef union SceNetCtlInfo { uint32_t device; SceNetEtherAddr ether_addr; uint32_t mtu; uint32_t link; SceNetEtherAddr bssid; char ssid[33]; uint32_t wifi_security; int32_t rssi_dbm; uint8_t rssi_percentage; uint8_t channel; uint32_t ip_config; char dhcp_hostname[256]; char pppoe_auth_name[128]; char ip_address[16]; char netmask[16]; char default_route[16]; char primary_dns[16]; char secondary_dns[16]; uint32_t http_proxy_config; char http_proxy_server[256]; uint16_t http_proxy_port; } SceNetCtlInfo; // App information structure typedef struct app_info { uint32_t app_id; uint64_t unknown1; uint32_t app_type; char title_id[10]; char unknown2[0x3c]; } app_info_t; // Function declarations void etaHEN_log(const char *fmt, ...); void crash_log(const char *fmt, ...); bool if_exists(const char *path); void jailbreak_proc(int pid); bool isProcessAlive(int pid) noexcept; bool GetFileContents(const char *path, char **buffer); void notify(bool show_watermark, const char *text, ...); bool rmtree(const char *path); int get_ip_address(char *ip_address); bool Get_Running_App_TID(std::string& title_id, int& BigAppid); bool is_whitelisted_app(const std::string &tid); void *Play_time_thread(void *args) noexcept; bool enable_toolbox(); bool isUserLoggedIn(); pid_t find_pid(const char *name); bool Open_Utility_Elf(const char *path, uint8_t **buffer); void *fifo_and_dumper_thread(void *args) noexcept; void *runDirectPKGInstaller(void *args); void *startDiscordRpcServerThread(void *arg); void Start_Dumper(const char *source, const char *destination, const char *title_id, Dump_Option opt); void activate_shellui_patch(void); // External function declarations extern "C" { int sceNetCtlGetInfo(int32_t s, SceNetCtlInfo *b); void sceNetCtlTerm(void); int sceKernelLoadStartModule(const char *name, size_t argc, const void *argv, uint32_t flags, void *unknown, int *result); int sceKernelDlsym(uint32_t lib, const char *name, void **fun); int PS5Debug_connect(); int unmount(const char *dir, int flags); int sceLncUtilLaunchApp(const char *tid, const char *argv[], LncAppParam *param); int sceSysUtilSendSystemNotificationWithText(int messageType, const char *message); int sceNotificationSendById(int userid, bool logged_in, const char *useCaseId, const char *message); int sceUserServiceGetForegroundUser(int *userId); int sceSystemServiceGetAppIdOfRunningBigApp(); int sceSystemServiceGetAppTitleId(int app_id, char *title_id); int32_t sceKernelPrepareToSuspendProcess(pid_t pid); int32_t sceKernelSuspendProcess(pid_t pid); int sceSystemServiceGetAppId(const char *title_id); int sceUserServiceGetUserName(const int userId, char *userName, const size_t size); int sceKernelGetAppInfo(int pid, app_info_t *title); int sceKernelGetProcessName(int pid, char *name); } // Global variables char ip_address[20]; int last_BigAppid = -1; bool shown_dead_noti = false; // Function implementations bool if_exists(const char *path) { struct stat buffer; return stat(path, &buffer) == 0; } void jailbreak_proc(int pid) { kernel_set_proc_rootdir(pid, kernel_get_root_vnode()); } bool isProcessAlive(int pid) noexcept { int mib[]{CTL_KERN, KERN_PROC, KERN_PROC_PID, pid}; return sysctl(mib, 4, nullptr, nullptr, nullptr, 0) == 0; } static bool writeRecord(const char *filename, const char *tid, uint64_t duration) { FILE *file = fopen(filename, "a+b"); // Open in append mode to add new records without deleting old ones if (file == NULL) { etaHEN_log("Failed to open file for writing: %s", strerror(errno)); return false; } char tid_padded[MAX_TID_SIZE] = {0}; // Initialize all to zero strncpy(tid_padded, tid, MAX_TID_SIZE); // Safely copy the TID if (fwrite(tid_padded, sizeof(char), MAX_TID_SIZE, file) < MAX_TID_SIZE) { etaHEN_log("Failed to write TID to file: %s", strerror(errno)); fclose(file); return false; } if (fwrite(&duration, sizeof(uint64_t), 1, file) < 1) { etaHEN_log("Failed to write duration to file: %s", strerror(errno)); fclose(file); return false; } fclose(file); return true; } static bool modifyRecordDuration(const char *filename, const char *target_tid, uint64_t &new_duration) { FILE *file = fopen(filename, "r+b"); // Read/Write mode, binary if (!file) { etaHEN_log("Failed to open file for reading and writing: %s", strerror(errno)); return false; } char tid[MAX_TID_SIZE]; uint64_t duration = 0; bool found = false; while (fread(tid, sizeof(char), MAX_TID_SIZE, file) == MAX_TID_SIZE) { if (fread(&duration, sizeof(uint64_t), 1, file) == 1) { if (strncmp(tid, target_tid, MAX_TID_SIZE) == 0) { found = true; // Move the file pointer back to the beginning of the duration to overwrite it fseek(file, -((long)sizeof(uint64_t)), SEEK_CUR); fwrite(&new_duration, sizeof(uint64_t), 1, file); break; } } } fclose(file); return found; } static bool getDurationForTID(const char *filename, const char *target_tid, uint64_t &duration) { if (!if_exists(filename)) { return writeRecord(filename, target_tid, duration); } FILE *file = fopen(filename, "rb"); if (file == NULL) { etaHEN_log("Failed to open file for reading: %s", strerror(errno)); return false; } char tid[MAX_TID_SIZE]; bool found = false; while (fread(tid, sizeof(char), MAX_TID_SIZE, file) == MAX_TID_SIZE) { if (fread((void *)&duration, sizeof(uint64_t), 1, file) == 1) { if (strncmp(tid, target_tid, MAX_TID_SIZE) == 0) { found = true; break; } } else { // If we fail to read the duration after the TID, break the loop duration = 0; break; } } fclose(file); return (found == true) ? true : writeRecord(filename, target_tid, 0); } bool GetFileContents(const char *path, char **buffer) { FILE *fp = fopen(path, "rb"); if (fp == NULL) { etaHEN_log("failed to open %s", path); return false; } fseek(fp, 0, SEEK_END); long size = ftell(fp); fseek(fp, 0, SEEK_SET); if (size == 0) { fclose(fp); etaHEN_log("size is 0"); return false; } *buffer = (char *)malloc(size + 1); // Allocate memory for the file content plus null terminator if (*buffer == NULL) { etaHEN_log("failed to allocate memory (OOM)"); fclose(fp); return false; } if (fread(*buffer, size, 1, fp) != 1) { fclose(fp); free(*buffer); return false; } fclose(fp); (*buffer)[size] = '\0'; // Null-terminate the buffer return true; } void notify(bool show_watermark, const char *text, ...) { OrbisNotificationRequest req; (void)memset(&req, 0, sizeof(OrbisNotificationRequest)); char buff[3075]; va_list args{}; va_start(args, text); vsnprintf(buff, sizeof(buff), text, args); va_end(args); if (show_watermark) snprintf(req.message, sizeof(req.message), "[etaHEN] %s", buff); else snprintf(req.message, sizeof(req.message), "[Itemzflow] %s", buff); req.type = 0; req.unk3 = 0; req.use_icon_image_uri = 1; req.target_id = -1; strcpy(req.uri, "cxml://psnotification/tex_icon_system"); etaHEN_log("Notify: %s\n", req.message); sceKernelSendNotificationRequest(0, &req, sizeof(req), 0); } int get_ip_address(char *ip_address) { int ret; SceNetCtlInfo info; ret = sceNetCtlGetInfo(14, &info); if (ret < 0) goto error; memcpy(ip_address, info.ip_address, sizeof(info.ip_address)); return ret; error: memcpy(ip_address, "IP NOT FOUND", sizeof(info.ip_address)); return -1; } bool Get_Running_App_TID(std::string &title_id, int &BigAppid) { char tid[255]; BigAppid = sceSystemServiceGetAppIdOfRunningBigApp(); if (BigAppid < 0) { return false; } (void)memset(tid, 0, sizeof tid); if (sceSystemServiceGetAppTitleId(BigAppid, &tid[0]) != 0) { return false; } title_id = std::string(tid); return true; } bool is_whitelisted_app(const std::string &tid) { // Static set of exactly matched title IDs (only initialized once) static const std::unordered_set whitelist = { "ITEM00001", "NPXS39041", "DUMP00000", "PKGI13337", "TOOL00001", }; // Check for exact matches if (whitelist.find(tid) != whitelist.end()) { return true; } // Check for partial match with "LAPY" if (tid.find("LAPY") != std::string::npos) { return true; } return false; } void *Play_time_thread(void *args) noexcept { const char *filename = "/data/etaHEN/playtime.bin"; std::string tid; uint64_t duration = 0; int appid; while (true) { if (!Get_Running_App_TID(tid, appid)) { continue; } etaHEN_log("getting duration for %s", tid.c_str()); if (!getDurationForTID(filename, tid.c_str(), duration)) { continue; } etaHEN_log("got duration for %s: %llu", tid.c_str(), duration); duration++; if (!modifyRecordDuration(filename, tid.c_str(), duration)) { etaHEN_log("Failed to modify record duration for %s", tid.c_str()); continue; } etaHEN_log("Record duration for %s changed to %llu", tid.c_str(), duration); sleep(59); } return nullptr; } bool isUserLoggedIn() { bool isLoggedIn = false; UserServiceLoginUserIdList userIdList; (void)memset(&userIdList, 0, sizeof(UserServiceLoginUserIdList)); if (sceUserServiceGetLoginUserIdList(&userIdList) < 0) { return false; } for (int i = 0; i < 4; i++) { char username[500] = {0}; int userid = userIdList.user_id[i]; if (userid != -1) { int ret = sceUserServiceGetUserName(userid, &username[0], sizeof(username)); etaHEN_log("sceUserServiceGetUserName returned %d", ret); if (ret == 0) { isLoggedIn = true; break; } } } sleep(5); return isLoggedIn; } pid_t find_pid(const char *name) { int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0 }; app_info_t appinfo; size_t buf_size; void *buf; int pid = -1; // determine size of query response if (sysctl(mib, 4, NULL, &buf_size, NULL, 0)) { etaHEN_log("sysctl failed: %s", strerror(errno)); return -1; } // allocate memory for query response if (!(buf = malloc(buf_size))) { etaHEN_log("malloc failed %s", strerror(errno)); return -1; } // query the kernel for proc info if (sysctl(mib, 4, buf, &buf_size, NULL, 0)) { etaHEN_log("sysctl failed: %s", strerror(errno)); free(buf); return -1; } for (char *ptr = static_cast(buf); ptr < (static_cast(buf) + buf_size);) { struct kinfo_proc *ki = reinterpret_cast(ptr); ptr += ki->ki_structsize; if (sceKernelGetAppInfo(ki->ki_pid, &appinfo)) { memset(&appinfo, 0, sizeof(appinfo)); } if (strlen(ki->ki_comm) < 2) continue; if (strstr(ki->ki_comm, name) != NULL) { pid = ki->ki_pid; break; } } free(buf); return pid; } bool Open_Utility_Elf(const char *path, uint8_t **buffer) { if (!path || !buffer) { etaHEN_log("Invalid arguments: path or buffer is null."); return false; } int fd = open(path, O_RDONLY); if (fd < 0) { etaHEN_log("Failed to open file: %s (error: %s)", path, strerror(errno)); return false; } struct stat st; if (fstat(fd, &st) != 0) { etaHEN_log("Failed to get file stats for %s (error: %s)", path, strerror(errno)); close(fd); return false; } if (st.st_size == 0) { etaHEN_log("File %s is empty.", path); close(fd); return false; } // Allocate buffer and check for allocation failure uint8_t *buf = (uint8_t *)malloc(st.st_size); if (!buf) { etaHEN_log("Failed to allocate memory for file %s (size: %ld bytes).", path, st.st_size); close(fd); return false; } ssize_t bytes_read = read(fd, buf, st.st_size); if (bytes_read != st.st_size) { etaHEN_log("Failed to read the entire file %s (read: %ld bytes, expected: %ld bytes).", path, bytes_read, st.st_size); free(buf); close(fd); return false; } close(fd); *buffer = buf; // Pass the buffer back to the caller return true; } bool cmd_enable_fps(int appid); void *fifo_and_dumper_thread(void *args) noexcept { char *json_str = nullptr; constexpr uint32_t MAX_TOKENS = 256; json_t pool[MAX_TOKENS]{}; std::string tid, sandbox_dir_base; int retries = 0; bool fifo_found = false; #define MAX_RETIRES 5 uint8_t* util_elf = nullptr; while (true) { std::string sandbox_dir; // restart the util services daemon if it crashes or exits if (find_pid("etaHEN Utility") < 0 && retries < MAX_RETIRES) { if (retries == 0 || !util_elf) { notify(true, "etaHEN Utility is not running, restarting..."); if (!Open_Utility_Elf("/data/etaHEN/daemons/util.elf", &util_elf)) { if (++retries >= MAX_RETIRES) notify(true, "Failed to open etaHEN Utility, please resend the payload or restart the console"); continue; } } if (++retries >= MAX_RETIRES) { notify(true, "etaHEN Utility services failed to restart, please resend the payload or restart the console"); free(util_elf); continue; } if (elfldr_spawn("/", STDOUT_FILENO, util_elf, "etaHEN Utility Daemon") >= 0) { etaHEN_log(" Launched!"); notify(true, "etaHEN Utility services successfully restarted"); retries = 0; } else { etaHEN_log("failed to launch utility daemon, retry: %d", retries); } free(util_elf); } pthread_mutex_lock(&jb_lock); int bappid; if (!Get_Running_App_TID(tid, bappid)) { pthread_mutex_unlock(&jb_lock); continue; } #if 0 if(tid.rfind("CUSA") != std::string::npos || tid.rfind("SCUS") != std::string::npos) cmd_enable_fps(bappid); #endif if (is_dumper_enabled) { if (strstr(tid.c_str(), "ITEM00001") != 0) { pthread_mutex_unlock(&jb_lock); continue; } sandbox_dir = "/mnt/sandbox/pfsmnt/" + tid + "-app0/"; while (!if_exists(sandbox_dir.c_str())) { puts("waiting for Game filesystem ..."); sleep(1); } etaHEN_log("Game filesystem mounted @ %s", sandbox_dir.c_str()); int id = 0; uint32_t res = sceUserServiceGetForegroundUser(&id); if (res != 0) { printf("sceUserServiceGetForegroundUser failed: 0x%x\n", res); pthread_mutex_unlock(&jb_lock); continue; } etaHEN_log("[LA] user id %u", id); // the thread will clean this up Flag flag = Flag_None; LncAppParam param{sizeof(LncAppParam), id, 0, 0, flag}; char buffer[255]; snprintf(buffer, sizeof(buffer), "%d", dump_opt); const char *argv[5] = {dump_title.c_str(), dump_path.c_str(), tid.c_str(), &buffer[0], nullptr}; dumping_tid = tid; // LAUNCH THE DUMP UTIL WITH ARGS FROM ITEMZFLOW int err = sceLncUtilLaunchApp("DUMP00000", argv, ¶m); if (0 < err) { etaHEN_log("sceLncUtilLaunchApp returned 0x%x", (uint32_t)err); } is_dumper_enabled = false; } if (!is_whitelisted_app(tid)) { pthread_mutex_unlock(&jb_lock); continue; } sandbox_dir_base = "/mnt/sandbox/" + tid + "_"; fifo_found = false; // Try different suffixes (e.g., 000 to 50) for (int i = 0; i <= 50; ++i) { std::ostringstream oss; oss << std::setw(3) << std::setfill('0') << i; // Generate suffix with leading zeros sandbox_dir = sandbox_dir_base + oss.str() + "/download0/etahen_jailbreak"; if (if_exists(sandbox_dir.c_str())) { // Found the directory fifo_found = true; break; } } if (!fifo_found) { // Log and unlock if no directory was found pthread_mutex_unlock(&jb_lock); continue; } if (!GetFileContents(sandbox_dir.c_str(), &json_str)) { etaHEN_log("Failed to get command from %s", sandbox_dir.c_str()); pthread_mutex_unlock(&jb_lock); continue; } etaHEN_log("\nfound. %s for %s", json_str, tid.c_str()); json_t const *my_json = json_create(json_str, pool, MAX_TOKENS); if (my_json == NULL) { puts("Error parsing JSON"); etaHEN_log("Error parsing JSON"); pthread_mutex_unlock(&jb_lock); continue; } const char *PID = json_getPropertyValue(my_json, "PID"); if (!PID) { etaHEN_log("PID is null"); notify(true, "Jailbreak failed, PID is null"); pthread_mutex_unlock(&jb_lock); continue; } int reserved_value = atoi(PID); etaHEN_log("reserved_value: %d", reserved_value); int retries = 0; // limit the hijackers scope UniquePtr spawned = nullptr; do { spawned = Hijacker::getHijacker(reserved_value); if (!spawned) { if (++retries > 30 || isProcessAlive(reserved_value)) { notify(true, "Jailbreak failed, PID is invaild"); etaHEN_log("Jailbreak failed, PID is invaild"); break; } } etaHEN_log("is null for PID %d", reserved_value); } while (spawned == nullptr); if (spawned) { etaHEN_log("RIGHT Jailbreak command received: jailbreaking..."); if(global_conf.debug_app_jb_msg) notify(true, "App (PID %i) has been granted a jailbreak", reserved_value); spawned->jailbreak(true); spawned.release(); // jailbreak_proc(reserved_value); unlink(sandbox_dir.c_str()); } free(json_str); json_str = nullptr; pthread_mutex_unlock(&jb_lock); } return nullptr; }