loader: implement PASLR (disabled by default)

Faithfully implement the PASLR algorithm official Loader
uses (it's not very good). Physical address space layout
randomization means that the layout of the pages in physical
memory are randomized, but doens't randomize the virtual addresses.

Also refactor some parts of our Loader impl a little more.
This commit is contained in:
TuxSH 2022-12-29 00:23:45 +01:00
parent 7074ac1166
commit ffbd8554d5
5 changed files with 227 additions and 53 deletions

View File

@ -27,10 +27,13 @@
#include <3ds.h> #include <3ds.h>
#include <string.h> #include <string.h>
#include <assert.h> #include <assert.h>
#include "paslr.h"
#include "util.h" #include "util.h"
#include "hbldr.h" #include "hbldr.h"
#include "3dsx.h" #include "3dsx.h"
extern bool isN3DS;
static const char serviceList[32][8] = static const char serviceList[32][8] =
{ {
"APT:U", "APT:U",
@ -225,8 +228,6 @@ void hbldrPatchExHeaderInfo(ExHeader_Info *exhi)
u64 lastdep = sizeof(dependencyListNativeFirm)/8; u64 lastdep = sizeof(dependencyListNativeFirm)/8;
exhi->sci.dependencies[lastdep++] = 0x0004013000004002ULL; // nfc exhi->sci.dependencies[lastdep++] = 0x0004013000004002ULL; // nfc
strncpy((char*)&localcaps0->service_access[0x20], "nfc:u", 8); strncpy((char*)&localcaps0->service_access[0x20], "nfc:u", 8);
s64 dummy = 0;
bool isN3DS = svcGetSystemInfo(&dummy, 0x10001, 0) == 0;
if (isN3DS) if (isN3DS)
{ {
exhi->sci.dependencies[lastdep++] = 0x0004013020004102ULL; // mvd exhi->sci.dependencies[lastdep++] = 0x0004013020004102ULL; // mvd
@ -275,7 +276,7 @@ Result hbldrLoadProcess(Handle *outProcessHandle, const ExHeader_Info *exhi)
u32 tmp = 0; u32 tmp = 0;
u32 addr = 0x10000000; u32 addr = 0x10000000;
res = svcControlMemory(&tmp, addr, 0, totalSize, MEMOP_ALLOC | region, MEMPERM_READ | MEMPERM_WRITE); res = allocateProgramMemory(exhi, addr, totalSize);
if (R_FAILED(res)) if (R_FAILED(res))
{ {
IFile_Close(&file); IFile_Close(&file);

View File

@ -35,8 +35,10 @@ void hbldrHandleCommands(void *ctx);
static inline bool hbldrIs3dsxTitle(u64 tid) static inline bool hbldrIs3dsxTitle(u64 tid)
{ {
return Luma_SharedConfig->use_hbldr && tid == Luma_SharedConfig->hbldr_3dsx_tid; if (!Luma_SharedConfig->use_hbldr)
} return false;
u64 hbldrTid = Luma_SharedConfig->hbldr_3dsx_tid;
void HBLDR_RestartHbApplication(void *p); // Just like p9 clears them, ignore platform/N3DS bits
void HBLDR_HandleCommands(void *ctx); return ((tid ^ hbldrTid) & ~0xF0000000ull) == 0;
}

View File

@ -1,6 +1,7 @@
#include <3ds.h> #include <3ds.h>
#include "memory.h" #include "memory.h"
#include "patcher.h" #include "patcher.h"
#include "paslr.h"
#include "ifile.h" #include "ifile.h"
#include "util.h" #include "util.h"
#include "hbldr.h" #include "hbldr.h"
@ -9,8 +10,8 @@
extern u32 config, multiConfig, bootConfig; extern u32 config, multiConfig, bootConfig;
extern bool isN3DS, isSdMode; extern bool isN3DS, isSdMode;
static u8 g_ret_buf[sizeof(ExHeader_Info)];
static u64 g_cached_programHandle; static u64 g_cached_programHandle;
static u64 g_cached_hbldr3dsxTid;
static ExHeader_Info g_exheaderInfo; static ExHeader_Info g_exheaderInfo;
typedef struct ContentPath { typedef struct ContentPath {
@ -20,7 +21,7 @@ typedef struct ContentPath {
static const ContentPath codeContentPath = { static const ContentPath codeContentPath = {
.contentType = 1, // ExeFS (with code) .contentType = 1, // ExeFS (with code)
.fileName = ".code", // last 3 bytes have to be 0, but this is guaranteed here.s .fileName = ".code", // last 3 bytes have to be 0, but this is guaranteed here.
}; };
typedef struct prog_addrs_t typedef struct prog_addrs_t
@ -100,18 +101,16 @@ static int lzss_decompress(u8 *end)
return ret; return ret;
} }
static Result allocateSharedMem(prog_addrs_t *shared, const prog_addrs_t *vaddr, int flags) static Result allocateProgramMemoryWrapper(prog_addrs_t *mapped, const ExHeader_Info *exhi, const prog_addrs_t *vaddr)
{ {
u32 dummy; memcpy(mapped, vaddr, sizeof(prog_addrs_t));
mapped->text_addr = 0x10000000;
memcpy(shared, vaddr, sizeof(prog_addrs_t)); mapped->ro_addr = mapped->text_addr + (mapped->text_size << 12);
shared->text_addr = 0x10000000; mapped->data_addr = mapped->ro_addr + (mapped->ro_size << 12);
shared->ro_addr = shared->text_addr + (shared->text_size << 12); return allocateProgramMemory(exhi, mapped->text_addr, mapped->total_size << 12);
shared->data_addr = shared->ro_addr + (shared->ro_size << 12);
return svcControlMemory(&dummy, shared->text_addr, 0, shared->total_size << 12, (flags & 0xF00) | MEMOP_ALLOC, MEMPERM_READ | MEMPERM_WRITE);
} }
static Result loadCode(const ExHeader_Info *exhi, u64 programHandle, const prog_addrs_t *shared) static Result loadCode(const ExHeader_Info *exhi, u64 programHandle, const prog_addrs_t *mapped)
{ {
IFile file; IFile file;
FS_Path archivePath; FS_Path archivePath;
@ -123,7 +122,7 @@ static Result loadCode(const ExHeader_Info *exhi, u64 programHandle, const prog_
const ExHeader_CodeSetInfo *csi = &exhi->sci.codeset_info; const ExHeader_CodeSetInfo *csi = &exhi->sci.codeset_info;
bool isCompressed = csi->flags.compress_exefs_code; bool isCompressed = csi->flags.compress_exefs_code;
if(!CONFIG(PATCHGAMES) || !loadTitleCodeSection(titleId, (u8 *)shared->text_addr, (u64)shared->total_size << 12)) if(!CONFIG(PATCHGAMES) || !loadTitleCodeSection(titleId, (u8 *)mapped->text_addr, (u64)mapped->total_size << 12))
{ {
archivePath.type = PATH_BINARY; archivePath.type = PATH_BINARY;
archivePath.data = &programHandle; archivePath.data = &programHandle;
@ -132,30 +131,27 @@ static Result loadCode(const ExHeader_Info *exhi, u64 programHandle, const prog_
filePath.type = PATH_BINARY; filePath.type = PATH_BINARY;
filePath.data = &codeContentPath; filePath.data = &codeContentPath;
filePath.size = sizeof(codeContentPath); filePath.size = sizeof(codeContentPath);
Result res;
if (R_FAILED(res = IFile_Open(&file, ARCHIVE_SAVEDATA_AND_CONTENT2, archivePath, filePath, FS_OPEN_READ)))
*(u64 *)0x1238 = programHandle;//panic(programHandle);//svcBreak(USERBREAK_ASSERT);
// get file size assertSuccess(IFile_Open(&file, ARCHIVE_SAVEDATA_AND_CONTENT2, archivePath, filePath, FS_OPEN_READ));
assertSuccess(IFile_GetSize(&file, &size)); assertSuccess(IFile_GetSize(&file, &size));
// check size // check size
if (size > (u64)shared->total_size << 12) if (size > (u64)mapped->total_size << 12)
{ {
IFile_Close(&file); IFile_Close(&file);
return 0xC900464F; return 0xC900464F;
} }
// read code // read code
assertSuccess(IFile_Read(&file, &total, (void *)shared->text_addr, size)); assertSuccess(IFile_Read(&file, &total, (void *)mapped->text_addr, size));
IFile_Close(&file); // done reading IFile_Close(&file); // done reading
// decompress // decompress
if (isCompressed) if (isCompressed)
lzss_decompress((u8 *)shared->text_addr + size); lzss_decompress((u8 *)mapped->text_addr + size);
} }
patchCode(titleId, csi->flags.remaster_version, (u8 *)shared->text_addr, shared->total_size << 12, csi->text.size, csi->rodata.size, csi->data.size, csi->rodata.address, csi->data.address); patchCode(titleId, csi->flags.remaster_version, (u8 *)mapped->text_addr, mapped->total_size << 12, csi->text.size, csi->rodata.size, csi->data.size, csi->rodata.address, csi->data.address);
return 0; return 0;
} }
@ -171,7 +167,7 @@ static inline bool IsHioId(u64 id)
return R_LEVEL(FSREG_CheckHostLoadId(id)) == RL_SUCCESS; // check if this is an alias to an HIO-loaded title return R_LEVEL(FSREG_CheckHostLoadId(id)) == RL_SUCCESS; // check if this is an alias to an HIO-loaded title
} }
static Result GetProgramInfo(ExHeader_Info *exheaderInfo, u64 programHandle) static Result GetProgramInfoImpl(ExHeader_Info *exheaderInfo, u64 programHandle)
{ {
Result res; Result res;
TRY(IsHioId(programHandle) ? FSREG_GetProgramInfo(exheaderInfo, 1, programHandle) : PXIPM_GetProgramInfo(exheaderInfo, programHandle)); TRY(IsHioId(programHandle) ? FSREG_GetProgramInfo(exheaderInfo, 1, programHandle) : PXIPM_GetProgramInfo(exheaderInfo, programHandle));
@ -191,13 +187,31 @@ static Result GetProgramInfo(ExHeader_Info *exheaderInfo, u64 programHandle)
return res; return res;
} }
static Result GetProgramInfo(u64 programHandle)
{
Result res = 0;
u64 cachedTid = g_exheaderInfo.aci.local_caps.title_id;
u64 hbldr3dsxTid = Luma_SharedConfig->hbldr_3dsx_tid;
bool hbTitleChanged = hbldr3dsxTid != g_cached_hbldr3dsxTid;
g_cached_hbldr3dsxTid = Luma_SharedConfig->hbldr_3dsx_tid;
if (programHandle != g_cached_programHandle || (hbldrIs3dsxTitle(cachedTid) && hbTitleChanged))
{
res = GetProgramInfoImpl(&g_exheaderInfo, programHandle);
g_cached_programHandle = R_SUCCEEDED(res) ? programHandle : 0;
}
return res;
}
static Result LoadProcessImpl(Handle *outProcessHandle, const ExHeader_Info *exhi, u64 programHandle) static Result LoadProcessImpl(Handle *outProcessHandle, const ExHeader_Info *exhi, u64 programHandle)
{ {
const ExHeader_CodeSetInfo *csi = &exhi->sci.codeset_info; const ExHeader_CodeSetInfo *csi = &exhi->sci.codeset_info;
Result res = 0; Result res = 0;
u32 dummy; u32 dummy;
prog_addrs_t sharedAddr; prog_addrs_t mapped;
prog_addrs_t vaddr; prog_addrs_t vaddr;
Handle codeset; Handle codeset;
CodeSetInfo codesetinfo; CodeSetInfo codesetinfo;
@ -223,11 +237,11 @@ static Result LoadProcessImpl(Handle *outProcessHandle, const ExHeader_Info *exh
vaddr.data_size = (csi->data.size + 4095) >> 12; vaddr.data_size = (csi->data.size + 4095) >> 12;
dataMemSize = (csi->data.size + csi->bss_size + 4095) >> 12; dataMemSize = (csi->data.size + csi->bss_size + 4095) >> 12;
vaddr.total_size = vaddr.text_size + vaddr.ro_size + vaddr.data_size; vaddr.total_size = vaddr.text_size + vaddr.ro_size + vaddr.data_size;
TRY(allocateSharedMem(&sharedAddr, &vaddr, region)); TRY(allocateProgramMemoryWrapper(&mapped, exhi, &vaddr));
// load code // load code
u64 titleId = exhi->aci.local_caps.title_id; u64 titleId = exhi->aci.local_caps.title_id;
if (R_SUCCEEDED(res = loadCode(exhi, programHandle, &sharedAddr))) if (R_SUCCEEDED(res = loadCode(exhi, programHandle, &mapped)))
{ {
memcpy(&codesetinfo.name, csi->name, 8); memcpy(&codesetinfo.name, csi->name, 8);
codesetinfo.program_id = titleId; codesetinfo.program_id = titleId;
@ -240,7 +254,7 @@ static Result LoadProcessImpl(Handle *outProcessHandle, const ExHeader_Info *exh
codesetinfo.rw_addr = vaddr.data_addr; codesetinfo.rw_addr = vaddr.data_addr;
codesetinfo.rw_size = vaddr.data_size; codesetinfo.rw_size = vaddr.data_size;
codesetinfo.rw_size_total = dataMemSize; codesetinfo.rw_size_total = dataMemSize;
res = svcCreateCodeSet(&codeset, &codesetinfo, (void *)sharedAddr.text_addr, (void *)sharedAddr.ro_addr, (void *)sharedAddr.data_addr); res = svcCreateCodeSet(&codeset, &codesetinfo, (void *)mapped.text_addr, (void *)mapped.ro_addr, (void *)mapped.data_addr);
if (R_SUCCEEDED(res)) if (R_SUCCEEDED(res))
{ {
// There are always 28 descriptors // There are always 28 descriptors
@ -250,25 +264,14 @@ static Result LoadProcessImpl(Handle *outProcessHandle, const ExHeader_Info *exh
} }
} }
svcControlMemory(&dummy, sharedAddr.text_addr, 0, sharedAddr.total_size << 12, MEMOP_FREE, 0); svcControlMemory(&dummy, mapped.text_addr, 0, mapped.total_size << 12, MEMOP_FREE, 0);
return res; return res;
} }
static Result LoadProcess(Handle *process, u64 programHandle) static Result LoadProcess(Handle *process, u64 programHandle)
{ {
Result res; Result res = 0;
TRY(GetProgramInfo(programHandle));
// make sure the cached info corresponds to the current programHandle
if (g_cached_programHandle != programHandle || hbldrIs3dsxTitle(g_exheaderInfo.aci.local_caps.title_id))
{
res = GetProgramInfo(&g_exheaderInfo, programHandle);
g_cached_programHandle = programHandle;
if (R_FAILED(res))
{
g_cached_programHandle = 0;
return res;
}
}
if (hbldrIs3dsxTitle(g_exheaderInfo.aci.local_caps.title_id)) if (hbldrIs3dsxTitle(g_exheaderInfo.aci.local_caps.title_id))
return hbldrLoadProcess(process, &g_exheaderInfo); return hbldrLoadProcess(process, &g_exheaderInfo);
@ -348,16 +351,11 @@ void loaderHandleCommands(void *ctx)
break; break;
case 4: // GetProgramInfo case 4: // GetProgramInfo
memcpy(&programHandle, &cmdbuf[1], 8); memcpy(&programHandle, &cmdbuf[1], 8);
if (programHandle != g_cached_programHandle || hbldrIs3dsxTitle(g_exheaderInfo.aci.local_caps.title_id)) res = GetProgramInfo(programHandle);
{
res = GetProgramInfo(&g_exheaderInfo, programHandle);
g_cached_programHandle = R_SUCCEEDED(res) ? programHandle : 0;
}
memcpy(&g_ret_buf, &g_exheaderInfo, sizeof(ExHeader_Info));
cmdbuf[0] = IPC_MakeHeader(4, 1, 2); cmdbuf[0] = IPC_MakeHeader(4, 1, 2);
cmdbuf[1] = res; cmdbuf[1] = res;
cmdbuf[2] = IPC_Desc_StaticBuffer(sizeof(ExHeader_Info), 0); //0x1000002; cmdbuf[2] = IPC_Desc_StaticBuffer(sizeof(ExHeader_Info), 0); //0x1000002;
cmdbuf[3] = (u32)&g_ret_buf; cmdbuf[3] = (u32)&g_exheaderInfo; // official Loader makes a copy here, but this is isn't necessary
break; break;
default: // error default: // error
cmdbuf[0] = IPC_MakeHeader(0, 1, 0); cmdbuf[0] = IPC_MakeHeader(0, 1, 0);

View File

@ -0,0 +1,167 @@
#include <3ds.h>
#include "paslr.h"
#include "util.h"
#include "hbldr.h"
static const u32 titleUidListForPaslr[] = {
// JPN/USA/EUR/KOR/TWN or GLOBAL
0x0209, 0x0219, 0x0229, 0x0269, 0x0289, // Nintendo eShop
0x0334, 0x0335, 0x0336, // The Legend of Zelda: Ocarina of Time 3D
0x0343, 0x0465, 0x04B3, // Cubic Ninja
0x0913, 0x09FA, 0x0933, // Freakyforms Deluxe
0x07FD, 0x0961, // VVVVVV
0x0A5D, 0x0A5E, 0x0A6F, 0x0C61, 0x0C81, // Paper Mario: Sticker Star
0x0D7C, 0x0D7D, 0x0D7E, // Steel Diver: Sub Wars
0x11C4, // Pokemon Omega Ruby (GLOBAL)
0x11C5, // Pokemon Alpha Sapphire (GLOBAL)
0x149B, 0x1746, 0x1744, // Pokemon Super Mystery Dungeon
0x1773, 0x12C1, 0x136E, // Citizens of Earth
0x17C1, // Pokemon Picross (GLOBAL)
};
static u32 getRandomInt(u32 maxNum)
{
// As reverse-engineered from latest Loader
static u64 state = 0;
s64 tick = (s64)svcGetSystemTick();
u64 tmp = (state >> 7) - (tick >> 2);
state = (tmp ^ state) & 0x49CC9BA7FC61CA67ull;
tmp = 0x5D588B656C078965ull * state + 0x29EC3;
return (u32)(((tmp >> 32) * maxNum) >> 32);
}
static bool needsPaslr(u32 *outRegion, const ExHeader_Info *exhi)
{
u32 region = 0;
u16 minKernelVer = 0;
for (u32 i = 0; i < 28; i++)
{
u32 desc = exhi->aci.kernel_caps.descriptors[i];
if (desc >> 23 == 0x1FE)
region = desc & 0xF00;
else if (desc >> 25 == 0x7E)
minKernelVer = (u16)desc;
}
*outRegion = region;
#ifdef LOADER_ENABLE_PASLR
// Only applications and system applets (HM, Internet Browser...) are eligible for PASLR
if (region != MEMOP_REGION_APP && region != MEMOP_REGION_SYSTEM)
return false;
else if (region == MEMOP_REGION_SYSTEM && exhi->aci.local_caps.reslimit_category == RESLIMIT_CATEGORY_LIB_APPLET)
return false;
// All titles requiring 11.2+ kernel get PASLR
if (minKernelVer >= (SYSTEM_VERSION(2, 57, 0) >> 16))
return true;
// Otherwise, only games with known exploits and eShop get it
u64 titleId = exhi->aci.local_caps.title_id;
// Check if this is indeed a CTR title ID (high u32 = 00040xxx)
if (titleId >> 46 != 0x10)
return false;
for (u32 i = 0; i < sizeof(titleUidListForPaslr)/sizeof(titleUidListForPaslr[0]); i++)
{
if (((titleId >> 8) & 0xFFFFF) == titleUidListForPaslr[i])
return true;
}
#else
(void)minKernelVer;
(void)titleUidListForPaslr;
#endif
return false;
}
Result allocateProgramMemory(const ExHeader_Info *exhi, u32 vaddr, u32 size)
{
Result res = 0;
u32 region = 0;
bool doPaslr = needsPaslr(&region, exhi);
u32 tmp = 0;
if (region == 0)
return MAKERESULT(RL_PERMANENT, RS_INVALIDARG, 1, 2);
u32 allocFlags = MEMOP_ALLOC | region;
if (!doPaslr)
return svcControlMemory(&tmp, vaddr, 0, size, allocFlags, MEMPERM_READWRITE);
// Divide the virtual address space into up to 7 chunks, each a random multiple (between 1 and 16) of 64KB.
// 64KB corresponds to a "large page" for the Armv6 MMU, hence the optimization choice.
// An additional 8th chunk gets the rest (if anything).
u32 totalNumPages = size >> 12;
u32 curPage = 0;
u32 numChunks;
u32 chunkStartAddrs[8];
u32 chunkSizes[8];
for (numChunks = 0; numChunks < 7; numChunks++)
{
u32 maxUnits = (totalNumPages - curPage) / 16;
if (maxUnits == 0)
break;
else if (maxUnits > 15)
maxUnits = 15;
u32 numPages = 16 * (1 + getRandomInt(maxUnits));
chunkStartAddrs[numChunks] = vaddr + (curPage << 12);
chunkSizes[numChunks] = numPages << 12;
curPage += numPages;
}
if (curPage < totalNumPages)
{
u32 numPages = totalNumPages - curPage;
chunkStartAddrs[numChunks] = vaddr + (curPage << 12);
chunkSizes[numChunks] = numPages << 12;
curPage += numPages;
++numChunks;
}
// Randomize the order the VA chunks are allocated in physical memory, from last to first
u32 chunkOrder[8] = {0, 1, 2, 3, 4, 5, 6, 7};
u32 numChunksToRandomize = numChunks;
if (totalNumPages % 16 != 0)
{
// For MMU optimization reasons, we only randomize chunks that are multiples of 64KB (large page)
// in size. This means that if the process image (.text, .rodata, .data) is not a muliple of 64KB
// in size, then its last few pages are not randomized at all.
// In pratice, under normal circumstances, this means that the last few pages of applications are
// guaranteed to be located in <APPLICATION memregion end> - (image_size & ~0xFFFF).
--numChunksToRandomize;
}
for (s32 i = numChunksToRandomize - 1; i >= 0; i--)
{
u32 j = getRandomInt(i + 1);
tmp = chunkOrder[i];
chunkOrder[i] = chunkOrder[j];
chunkOrder[j] = tmp;
}
// Allocate the memory
u32 i;
for (i = 0; i < numChunks; i++)
{
u32 idx = chunkOrder[i];
res = svcControlMemory(&tmp, chunkStartAddrs[idx], 0, chunkSizes[idx], allocFlags, MEMPERM_READWRITE);
if (R_FAILED(res))
break;
}
// Success
if (i == numChunks)
return res;
// Clean up on failure
for (u32 j = 0; j < i; j++)
{
u32 idx = chunkOrder[i];
svcControlMemory(&tmp, chunkStartAddrs[idx], 0, chunkSizes[idx], MEMOP_FREE, MEMPERM_DONTCARE);
}
return res;
}

View File

@ -0,0 +1,6 @@
#pragma once
#include <3ds/types.h>
#include <3ds/exheader.h>
Result allocateProgramMemory(const ExHeader_Info *exhi, u32 vaddr, u32 size);