Syscall internals

I assume you understand what is a syscall and how syscalls are called from user space in PSP. If not, please read Freddy’s post.

We are going to take a closer look at what happens in kernel space, where syscalls are intercepted and served. This is a pretty technical post so get some coffee.

When a syscall is issued (called) from user space, the syscall exception handler which is installed in interruptmgr will take control. The hardware will put a pointer in coprocessor register r12, which leads to a fixed size (0×4010) table in memory. Let’s call it syscall table. The first 4 words of the table are the header, and the rest are slots for function pointers.
The struct of the syscall table header is like this:
struct syscall_table {
u32 unk_0;
u32 seed;
u32 table_size; // always 0x4000
u32 total_size; // always 0x4010
u32 func[0]; // 0x1000 slots of function pointers

The most important field is the seed, which is generated randomly when the system boots up. Meanwhile, register r21 of the coprocessor has a value by which you can index into the function pointer slot. The Kernel then reads the value of that slot, and jalr (jumps) to it, which is the real implementation of the syscall.

We, as a user space process, have no privilege to write or read the syscall table directly, because the table is within kernel memory space.

The syscall table is initialized in loadcore, when system/game boots up. The seed is generated at that moment, and never changes until next bootup. All the slots in the function pointers table will be initialized to an address in loadcore, which does nothing but returns an error number (0×80020001).

When a module is loaded, it has to register its exported function in loadcore. The registration in a word is to find a proper slot(s) to store the function pointers, then assign a syscall number to it.

The Kernel first decides how many slots reserved for the module. The algo is as below:
reserved_slots_cnt = (random() & 3) + 1 + exported_syscall_cnt;

Then kernel will decide which slot index to start, with this algo:
start_index = random() % exported_syscall_cnt;

BTW, the random() function is reading the Count register.

Then from the start_index, the kernel copies the function addr into the table in a
fixed sequence (the order follows the sequence of exports).
At this point, the exported function has already been assigned a syscall number, which is calculated as:
syscall_num = seed / 4 + slot_index;

Note that at this moment, all function pointers are masked by 0×80000000, i.e., any attempt to call this syscall will be trapped by a Bus error. This is how Sony protects un-imported syscalls.

Note also the reserved slots count for certain module is larger than the total amount of exported functions. Therefore, the spare slots are filled with a fixed address, which triggers break exception. It brings more trouble for syscall estimation.

As a sum up, the algo for filling the syscall table is as below:
for (i = start_index; i < reserved_slots_cnt + start_index; i++) {
int index = i;

  if (index > reserved_slots_cnt)
index = index % reserved_slots_cnt; // wrap at the end

  if (index >= exported_syscall_cnt)
slots[index] = break_0_trap; // dead slots
slots[index] = next_func_addr & (~0x80000000);

A subtle point here is that, kernel also maintains an NID to syscall mapping, when registering modules.

When a module is loaded from user space, the kernel will check its stub section, find out all needed NIDs, by which it can tell what is the correct syscall
number for this specific NID, then fix the stub section. Of course the kernel will also set back the MSB bit (Most Significant Bit) in corresponding function, so as to activate that syscall.

As I mentioned just now, without kernel privilege we can’t touch the syscall table. But assuming you have a kexploit, there’s something interesting you can do:

Imagine you want to call a un-exported NID. All you need to do is to search in syscall table to find that function addr (remember to mask the MSB), then set back the MSB.

What about a function that doesn’t even have NID? Let’s say it’s called sub_XXXXXXXX.

All you need to do is to follow how loadcore assigns the syscall number:

  1. Find either an empty or dead slot in the table
  2. Set the function address to the slot
  3. Calcaulate the syscall number by the slot index

Then you can issue a syscall with the new syscall number just like any other exported functions. You don’t need to bother with NID etc. at all.

The only missing piece in this story is how the seed is generated. It’s not so important because you can read the value of seed if you get kernel privilege; on the other hand, the whole syscall table is not interesting in that case.

  1. lostarot’s avatar

    Hello Jigsaw,

    You don’t know me, but I know you.I wanna play a game. The rules of the game are simple but the consequnce for breaking them are great. :D

    Nice article sir.


  2. naki’s avatar

    wow… totally understood that :)


  3. freddy_156’s avatar

    I just wanted to add something:
    The random value is actually the return value of sceKernelGetInitialRandomValue with some extra calculation, this value is conserved trough kernel reboots (or should? I’m just guessing here, haven’t looked at reboot.bin yet), and it’s set at boot, passed as an argument to sysmem module_bootstart by IPL, it’s generated by kirk command 0xE. (Thanks to Davee and Draan for information about IPL)


  4. Alex’s avatar

    Thanks for the article, very interesting to know the internal workings :D


  5. Dovahkiin’s avatar

    Thanks jigsaw!!! :) I need all the information I can get…

    then have fun with the vulnerability in psheet.prx at 5.00 OFW :D



Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>