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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Create an Xcode Plugin: Part 3/3
30 mins
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. :]
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.
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.
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:
- 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.
- The $rdi register, which holds the
WebPolicyDecisionListener
(aka theIDEDocWebViewContentViewController
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 thenpo
in LLDB. - 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 theNSURLRequest
instance. - 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
.
- Remember that $rbx contains the
NUSRLRequest
at this point. - This section is easy; just by looking at the disassembly’s comments, you can tell that the URL is being compared to
@"about:blank"
- 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 aBOOL
value in 64 bits. - 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.
- 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
- It looks like
ignore
is called on the listener. This happens if the URLabsoluteString
is equal to@"about:blank"
. - If
absoluteString
is equal to@"about:blank"
, there’s some further stack movement followed by a jump to some address further down. - 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:]'
. -
_allowURLRequest:webView:
is now being called. - This tests whether
_allowURLRequest:webView:
returned 1 or 0 - The code jumps if ZF is set to 1. That is, if
_allowURLRequest:webView:
returnsfalse
, 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!
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
- The
WebActionModifierFlagsKey
is loaded into $rdx. - The ‘$rbp – 0x40’ address contained the
navigationAction
NSDictionary
parameter. It’s loaded into the “self” register forobjc_msgSend
. - The result seems to be sending
unsignedIntegerValue
to another object, probably anNSNumber
. - This tests
unsignedIntegerValue
against 0x100000. This is likely some internalint
orenum
hidden by the compiler. - If the value is not equal to 0x100000, then jump to the specified address.
- 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.
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:
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:
- You could augment
_allURLRequest:webView:
to execute and then returnYES
. - 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.