How To Create an Xcode Plugin: Part 3/3

Wrap up your Rayrolling Xcode plugin by getting your hands dirty with more assembly language and Cycript in this final instalment of the three-part tutorial series. By Derek Selander.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Cycript Swizzling

You found the class that will perform your Rayroll, but you need to figure out how the class works. Cocoa’s WebView has many methods, so you need to find out which ones Xcode uses behind the scenes.

Perusing the the documentation for WebView indicates that WebFrame performs much of the network interaction with URLs. That would be a good place to start looking.

Perhaps a quick Dtrace script will do the trick. After all, you only care about what the WebView‘s WebFrame is doing, not what it’s interacting with.

Open a new session in Terminal that does not have Cycript or LLDB. From there, type the following:

sudo dtrace -n 'objc$target:WebFrame::entry { @[probefunc] = count() }' -p `pgrep -xo Xcode`

With this script running, click around the documentation, and search for something like NSView. Stop the Dtrace script and look through the results.

It should be pretty obvious that WebView‘s WebFrame performs loadRequest: when fetching new data.

You’re now one step closer to pulling this off. You’ve found the view controller, the class of interest, and the method of interest. Instead of the usual code, swizzle, build cycle, you can use Cycript to dynamically swizzle this method without having to restart Xcode at all. That way you can see if this trick works, without all the implementation overhead in case it fails.

Back in Cycript, hunt down loadRequest: and assign it to a global Javascript variable:

cy# original_WebFrame_method = WebFrame.messages['loadRequest:']
0x11f4141e0

Now paste the following contents in Cycript:

function swizzled_loadRequest(request) { 
 var swizzledURL = [new NSURL initWithString:@"https://www.youtube.com/watch?v=ce-_0opZzh0"];
 var swizzledRequest = [new NSURLRequest initWithURL: swizzledURL];
 original_WebFrame_method.call(this, swizzledRequest); 
}

This is the Javascript equivalent function for a swizzled loadRequest:. You throw out the original request and make a new request to the lovely Rayrolling Video.

Now swap the methods:

WebFrame.messages['loadRequest:'] = swizzled_loadRequest 

Be very careful that there are no syntax errors in your swizzled function. The smallest typo will cause Xcode to crash. Which, come to think of it, is pretty much par for the course for Xcode. :]

ragecomic2

Try searching for something like NSObject in the documentation and press Enter.

But wait… instead of opening up the YouTube page in the Xcode documentation’s WebView, Xcode decided to dump you to your default browser and open it from there. This is clearly unacceptable — Xcode must bend to your will, not the other way around! :]

You’ll need to figure out what class is preventing the WebView from doing this and correct its behavior.

Since you will be viewing assembly, it would be best to see the code execution path when actually testing against the failed YouTube URL. Now that you’ve done the initial testing with Cycript swizzling, it’s time to implement it “for real” in your plugin.

Create a new WebFrame Category and name it Rayrolling WebFrame.

In WebFrame+Rayrolling_WebFrame.h, add the following header to the top so you can access this class correctly:

#import <WebKit/WebKit.h>

Now open WebView+Rayrolling_WebFrame.m and replace its contents with the following:

#import "WebFrame+Rayrolling_WebFrame.h"
#import "NSObject+MethodSwizzler.h" 
#import "Rayrolling.h"

@implementation WebFrame (Rayrolling_WebFrame)

+ (void)load {
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{ 
    [self swizzleWithOriginalSelector:@selector(loadRequest:) swizzledSelector:@selector(Rayrolling_loadRequest:) isClassMethod:NO];
  });
}

- (void)Rayrolling_loadRequest:(NSURLRequest *)request {
  if ([Rayrolling isEnabled]) {
    NSURL *url = [NSURL URLWithString:@"https://www.youtube.com/watch?v=ce-_0opZzh0"];
    NSURLRequest *rickrollingRequest = [NSURLRequest requestWithURL:url];
    [self Rayrolling_loadRequest:rickrollingRequest];
  } else {
    [self Rayrolling_loadRequest:request];
  }
}
@end 

Build and run this to update the plugin, then quit and relaunch Xcode so the updated contents are loaded into memory.

Wading Through Assembly

Typically, when either an iOS or OS X application launches another application in this sort of manner, there are a very small set of APIs that could be the culprit. One very common one is openURL:.

Fire up a new tab in the Terminal, launch LLDB, and attach it to Xcode. From there, set a breakpoint on any class that has this particular method:

lldb
(lldb) pro at -n Xcode 
...
(lldb) rb openURL: 
...
(lldb) c

Go back to the Documentation window and try searching for a new item, say, NSString. Immediately upon hitting Enter in the Documentation Window, LLDB breaks on -[IDEWorkspace openURL:]. Bingo!

Just for giggles, make sure this is the correct URL:

(lldb) po $rdx 
<iframe width="500" height="375" src="https://www.youtube.com/embed/ce-_0opZzh0?feature=oembed" frameborder="0" allowfullscreen></iframe>

Good. So now you need to see how it was called:

(lldb) bt 5
* thread #1: tid = 0x2ea1d, 0x00007fff88b529bf AppKit`-[NSWorkspace openURL:], queue = 'com.apple.main-thread', stop reason = breakpoint 1.6
  * frame #0: 0x00007fff88b529bf AppKit`-[NSWorkspace openURL:]
    frame #1: 0x0000000119f5ab47 IDEDocViewer`-[IDEDocWebViewContentViewController haveWorkspaceOpenOrRevealURL:] + 577
    frame #2: 0x0000000119f5a5e1 IDEDocViewer`-[IDEDocWebViewContentViewController webView:decidePolicyForNavigationAction:request:frame:decisionListener:] + 607
    frame #3: 0x000000011d19cbd2 Rickrolling`-[NSViewController(self=0x00007fa3d75b3f20, _cmd=0x00007fff89c18f65, webView=0x00007fa3d75c46c0, actionInformation=0x00007fa3dadf5f40, request=0x00007fa3d2dae450, frame=0x00007fa3d73cbee0, listener=0x00007fa3dceee600) Rr_swzl_webView:decidePolicyForNavigationAction:request:frame:decisionListener:] + 274 at NSViewController+IDEDocWebViewContentViewController_Swizzler.m:35
    frame #4: 0x00007fff86b49ebc CoreFoundation`__invoking___ + 140

Looking at frame #2, it seems that this would be the method which makes the decision to launch it internally in the Documentation window or to hand it off to the default browser.

Doing a search on this method indicates that webView:decidePolicyForNavigationAction:request:frame:decisionListener: is defined in the WebPolicyDelegate protocol.

According to the Apple documentation on this protocol, the decisionListener implements WebPolicyDecisionListener which means, based upon some internal logic, you will call [listener ignore] if you don’t want to not load the content at all, [listener download] if you want to download the content, or [listener use] if you want to open the URL right in the WebView.

There’s no other way around it. You need to see what’s happening inside this method. Back to the magical land of assembly you thought you escaped in the first tutorial! :]

Note: If you aren’t fully caffeinated yet and want to skip the disassembly analysis, skip ahead to the final reconstructed method implementation.

In LLDB:

(lldb) di -n '-[IDEDocWebViewContentViewController webView:decidePolicyForNavigationAction:request:frame:decisionListener:]'

This dumps out a fair bit of assembly. It’s OK — don’t assume the fetal position and huddle in the corner. You’ll go through this assembly section by section and you’ll see that it’s not that bad.

Note: You’ll notice that the addresses are different on your workstation — this is to be expected.
0x114da2382 <+0>:   pushq  %rbp
0x114da2383 <+1>:   movq   %rsp, %rbp
0x114da2386 <+4>:   pushq  %r15
0x114da2388 <+6>:   pushq  %r14
0x114da238a <+8>:   pushq  %r13
0x114da238c <+10>:  pushq  %r12
0x114da238e <+12>:  pushq  %rbx                     ; // 1
0x114da238f <+13>:  subq   $0x58, %rsp
0x114da2393 <+17>:  movq   %r8, %r12
0x114da2396 <+20>:  movq   %rcx, %r13
0x114da2399 <+23>:  movq   %rdi, -0x58(%rbp)
0x114da239d <+27>:  movq   0x10(%rbp), %r15
0x114da23a1 <+31>:  movq   %rdi, -0x30(%rbp)         ; // 2 
0x114da23a5 <+35>:  movq   %rsi, -0x38(%rbp)
0x114da23a9 <+39>:  movq   0x61f20(%rip), %r14       ; (void *)0x00007fff95678050: objc_retain
0x114da23b0 <+46>:  movq   %rdx, %rdi
0x114da23b3 <+49>:  callq  *%r14
0x114da23b6 <+52>:  movq   %rax, -0x40(%rbp)
0x114da23ba <+56>:  movq   %r13, %rdi
0x114da23bd <+59>:  callq  *%r14
0x114da23c0 <+62>:  movq   %rax, -0x48(%rbp)
0x114da23c4 <+66>:  movq   %r12, %rdi
0x114da23c7 <+69>:  callq  *%r14
0x114da23ca <+72>:  movq   %rax, %rbx               ; // 3 
0x114da23cd <+75>:  movq   %rbx, -0x50(%rbp)
0x114da23d1 <+79>:  movq   %r15, %rdi
0x114da23d4 <+82>:  callq  *%r14
0x114da23d7 <+85>:  movq   %rax, %r14                ; // 4

Take a deep breath; here’s what all that means:

  1. After this instruction, all the scratchspace registers are now pushed. The pushq operand saves the state of the register so it can be popq‘d at a later time when leaving the function.
  2. The $rdi register, which holds the WebPolicyDecisionListener (aka the IDEDocWebViewContentViewController instance) is set to an address that is -0x30 below the address of $rbp. You can access it in LLDB lke so: x/gx '$rbp - 0x30'. The address spat out will be the address which you can then po in LLDB.
  3. Here’s the fun part of assembly: navigating which register stores what, and where. The contents of the return register $rax are copied to $rbx. $rax was set by the retain call in $r14 with an object passed in by $r12. $r12 was set by $r8. As you learned earlier, $r8 is the 3rd parameter passed into a function (not including the “self” $rdi register parameter nor the $rsi Selector register). Looking at the documentation again for this method implies that as of right now, $rbx should contain the NSURLRequest instance.
  4. Yep, the assembly is still performing the setup. As you can see, $r14 is passed the retain Selector for memory management, so the assembly is simply going through and retaining the items so they don’t disappear and cause a crash.

Ok…that wasn’t so bad. Onto the next section:

0x114da23da <+88>:  movq   0x7fc17(%rip), %rsi     ; "URL"
0x114da23e1 <+95>:  movq   0x61ed8(%rip), %r12     ; (void *)0x00007fff956700c0: objc_msgSend
0x114da23e8 <+102>: movq   %rbx, %rdi              ; // 1 
0x114da23eb <+105>: callq  *%r12
0x114da23ee <+108>: movq   %rax, %rdi
0x114da23f1 <+111>: callq  0x114de6bee             ; symbol stub for: objc_retainAutoreleasedReturnValue
0x114da23f6 <+116>: movq   %rax, %r15
0x114da23f9 <+119>: movq   0x7fb28(%rip), %rsi     ; "absoluteString"
0x114da2400 <+126>: movq   %r15, %rdi
0x114da2403 <+129>: callq  *%r12
0x114da2406 <+132>: movq   %rax, %rdi
0x114da2409 <+135>: callq  0x114de6bee             ; symbol stub for: objc_retainAutoreleasedReturnValue
0x114da240e <+140>: movq   %rax, %rbx
0x114da2411 <+143>: movq   0x7fde8(%rip), %rsi     ; "isEqualToString:" // 2 
0x114da2418 <+150>: leaq   0x641a1(%rip), %rdx     ; @"about:blank"
0x114da241f <+157>: movq   %rbx, %rdi
0x114da2422 <+160>: callq  *%r12
0x114da2425 <+163>: movb   %al, %r12b              ; // 3 
0x114da2428 <+166>: movq   0x61e99(%rip), %r13     ; (void *)0x00007fff95678440: objc_release
0x114da242f <+173>: movq   %rbx, %rdi
0x114da2432 <+176>: callq  *%r13
0x114da2435 <+179>: movq   %r15, %rdi
0x114da2438 <+182>: callq  *%r13
0x114da243b <+185>: testb  %r12b, %r12b            ; // 4 
0x114da243e <+188>: je     0x114da246e             ; <+236> // 5

.

  1. Remember that $rbx contains the NUSRLRequest at this point.
  2. This section is easy; just by looking at the disassembly’s comments, you can tell that the URL is being compared to @"about:blank"
  3. The result of isEqualToString: is now passed from $al to $r12b. $al is a register only 8 bits long. It wouldn’t make sense to store a BOOL value in 64 bits.
  4. The testb instruction compares the destination with the source operand. If the value in $r12b is a 1, then the ZF register flag will be a 0.
  5. Based upon the ZF register this instruction jumps to 0x119f5a46e if the value is 1. Another way to summarize the last two operands: if the value is not equal to @"about:blank", then jump to 0x119f5a46e

You’re starting to see the method come together! So far you have the following:

- (void)webView:(WebView *)webView
decidePolicyForNavigationAction:(NSDictionary *)actionInformation
        request:(NSURLRequest *)request
          frame:(WebFrame *)frame
decisionListener:(id<WebPolicyDecisionListener>)listener
{
  if ([[[request URL] absoluteString] isEqualToString:@"about:blank"]) {
    // TODO 
  } else {
    // jump to 0x119f5a46e
  }
}

Onto the next section:

0x114da2440 <+190>: movq   0x7fdc1(%rip), %rsi     ; "ignore"
0x114da2447 <+197>: movq   %r14, %rdi
0x114da244a <+200>: callq  *0x61e70(%rip)          ; (void *)0x00007fff956700c0: objc_msgSend // 1 
0x114da2450 <+206>: movq   -0x40(%rbp), %r13
0x114da2454 <+210>: movq   -0x48(%rbp), %rax
0x114da2458 <+214>: movq   -0x50(%rbp), %r15
0x114da245c <+218>: movq   %r14, %r12
0x114da245f <+221>: movq   %rax, %r14
0x114da2462 <+224>: movq   0x61e5f(%rip), %rbx     ; (void *)0x00007fff95678440: objc_release
0x114da2469 <+231>: jmp    0x114da2621             ; <+671> // 2 
0x114da246e <+236>: movq   %r14, -0x60(%rbp)
0x114da2472 <+240>: movq   0x7fd9f(%rip), %rsi     ; "_allowURLRequest:webView:" // 3 
0x114da2479 <+247>: movq   -0x58(%rbp), %r15
0x114da247d <+251>: movq   %r15, %rdi
0x114da2480 <+254>: movq   -0x50(%rbp), %r14
0x114da2484 <+258>: movq   %r14, %rdx
0x114da2487 <+261>: movq   -0x40(%rbp), %rbx
0x114da248b <+265>: movq   %rbx, %rcx
0x114da248e <+268>: callq  *0x61e2c(%rip)          ; (void *)0x00007fff956700c0: objc_msgSend // 4
0x114da2494 <+274>: testb  %al, %al  				       ; // 5
0x114da2496 <+276>: movq   %rbx, %r13
0x114da2499 <+279>: je     0x114da25a4             ; <+546> // 6 
  1. It looks like ignore is called on the listener. This happens if the URL absoluteString is equal to @"about:blank".
  2. If absoluteString is equal to @"about:blank", there’s some further stack movement followed by a jump to some address further down.
  3. This is a new interesting Selector of IDEDocWebViewContentViewController. Disassembling this class is an exercise left to the reader. :] To get started in LLDB: di -n '-[IDEDocWebViewContentViewController _allowURLRequest:webView:]'.
  4. _allowURLRequest:webView: is now being called.
  5. This tests whether _allowURLRequest:webView: returned 1 or 0
  6. The code jumps if ZF is set to 1. That is, if _allowURLRequest:webView: returns false, the jump instruction will be executed.

You can now build out your pseudo-code a little bit further.

- (void)webView:(WebView *)webView
decidePolicyForNavigationAction:(NSDictionary *)actionInformation
        request:(NSURLRequest *)request
          frame:(WebFrame *)frame
decisionListener:(id<WebPolicyDecisionListener>)listener
{
  if ([[[request URL] absoluteString] isEqualToString:@"about:blank"]) {
    [listener ignore];
    // Do some stack cleanup logic
    // 0x114da2469 <+231>: jmp    0x114da2621      

  } else if ([self _allowURLRequest:request webView:webView]) {

  } else {
  	// 0x114da2499 <+279>: je     0x114da25a4               ; <+546>
  }
}

On to the final section — you’re almost there!

ragecomic

0x114da249f <+285>: movq   %r14, %r13
0x114da24a2 <+288>: movq   0x61d87(%rip), %rax     ; (void *)0x00007fff788d7800: WebActionModifierFlagsKey  						    ; 
0x114da24a9 <+295>: movq   (%rax), %rdx				     ; // 1 
0x114da24ac <+298>: movq   0x7f5dd(%rip), %rsi     ; "objectForKey:"
0x114da24b3 <+305>: movq   -0x48(%rbp), %rdi       ; // 2 
0x114da24b7 <+309>: movq   0x61e02(%rip), %r12     ; (void *)0x00007fff956700c0: objc_msgSend
0x114da24be <+316>: callq  *%r12
0x114da24c1 <+319>: movq   %rax, %rdi
0x114da24c4 <+322>: callq  0x114de6bee             ; symbol stub for: objc_retainAutoreleasedReturnValue
0x114da24c9 <+327>: movq   %rax, %rbx
0x114da24cc <+330>: movq   0x7fd4d(%rip), %rsi     ; "unsignedIntegerValue" // 3
0x114da24d3 <+337>: movq   %rbx, %rdi
0x114da24d6 <+340>: callq  *%r12
0x114da24d9 <+343>: movq   %rax, %r14
0x114da24dc <+346>: movq   %rbx, %rdi
0x114da24df <+349>: callq  *0x61de3(%rip)          ; (void *)0x00007fff95678440: objc_release
0x114da24e5 <+355>: testl  $0x100000, %r14d        ; // 4
0x114da24ec <+362>: je     0x114da25fb             ; <+633> // 5

And here’s the location where the above instruction was jumping to…

0x114da25fb <+633>: movq   0x7fc26(%rip), %rsi     ; "use" // 6 
0x114da2602 <+640>: movq   -0x60(%rbp), %r12
0x114da2606 <+644>: movq   %r12, %rdi
0x114da2609 <+647>: callq  *0x61cb1(%rip)          ; (void *)0x00007fff956700c0: objc_msgSend
0x114da260f <+653>: movq   %r13, %r15
0x114da2612 <+656>: movq   0x61caf(%rip), %rbx     ; (void *)0x00007fff95678440: objc_release
0x114da2619 <+663>: movq   -0x40(%rbp), %r13
0x114da261d <+667>: movq   -0x48(%rbp), %r14
0x114da2621 <+671>: movq   %r12, %rdi

  1. The WebActionModifierFlagsKey is loaded into $rdx.
  2. The ‘$rbp – 0x40’ address contained the navigationAction NSDictionary parameter. It’s loaded into the “self” register for objc_msgSend.
  3. The result seems to be sending unsignedIntegerValue to another object, probably an NSNumber.
  4. This tests unsignedIntegerValue against 0x100000. This is likely some internal int or enum hidden by the compiler.
  5. If the value is not equal to 0x100000, then jump to the specified address.
  6. Following the address jump, if the value is not equal to 0x100000, you will finally see logic that uses the use command.

Here’s what the final reconstructed method looks like now:

- (void)webView:(WebView *)webView
decidePolicyForNavigationAction:(NSDictionary *)actionInformation
        request:(NSURLRequest *)request
          frame:(WebFrame *)frame
decisionListener:(id<WebPolicyDecisionListener>)listener
{
  if ([[[request URL] absoluteString] isEqualToString:@"about:blank"]) {
    [listener ignore];
    // Do some stack cleanup logic
    // 0x114da2469 <+231>: jmp    0x114da2621      
  } else if ([self _allowURLRequest:request webView:webView]) {
    if ([[actionInformation[WebActionModifierFlagsKey] unsignedIntegerValue] != 0x100000) {
      [listener use];
    } else {
      // Unexplored
    }
  } else {
    // Unexplored
  }
}

As long as _allowsURLRequest:webView returns YES and the WebActionModifierFlags doesn’t equal 0x100000, then this video should load!

It appears you need to force _allowsURLRequest:webView to always return YES. The $rax register is responsible for this.

Note: Advanced users of LLDB would suggest using thread return 1 in LLDB; however, using this trick with breakpoint commands will cause Xcode to crash. As a result, you’ll instead break on the return of this function call and modify the $al register to return 1 instead of 0

You can search for the code yourself, but since you’re a champ for working through all that assembly inspection, I’ll just give it to you: it’s found in the 274th offset in the disassembly dump. Search for it using ⌘ + F. Dump the assembly again and take note of the address:

(lldb) b 0x1173a1494
Breakpoint 11: where = IDEDocViewer`-[IDEDocWebViewContentViewController webView:decidePolicyForNavigationAction:request:frame:decisionListener:] + 274, address = 0x00000001173a1494
(lldb) br command add
Enter your debugger command(s).  Type 'DONE' to end.
> reg write $al 1 
> c 
> DONE
(lldb) c
error: Process is running.  Use 'process interrupt' to pause execution.

Now open the Documentation window and force a new loadRequest: call by searching for a new item in the title search bar:

LLDB_Doc_Command

Wahoooo! You’ve found the secret!

So what did you learn, other than assembly looks nothing like Objective-C? :] By wading through the assembly, you’ve figured out a couple of ways to augment this particular code:

  1. You could augment _allURLRequest:webView: to execute and then return YES.
  2. You could augment webView:decidePolicyForNavigationAction:request:frame:decisionListener: to just call decision [listener use]; and return.

The best way to guide your decision is to ask: How do you think the Xcode engineers will augment these APIs in future? _allURLRequest:webView: is private and could change. On the other hand, webView:decidePolicyForNavigationAction:request:frame:decisionListener: comes from the WebPolicyDecisionListener, which is a public API so it’s unlikely to change.

The choice should be obvious: you’ll augment webView:decidePolicyForNavigationAction:request:frame:decisionListener: to always execute [listener use] then return when Rayrolling is enabled.