If you’re a frequent user of the Open Quickly feature in Xcode, and you use a dark menu bar and Dock, then you might have noticed that the appearance of the Open Quickly window changed in Xcode 9.3. In versions prior to Xcode 9.3 the Open Quickly window would use a dark theme if you used a dark menu bar and Dock, but the Open Quickly window in Xcode 9.3 always uses a light theme.
Turns out that the dark theme is still there, and it’s possible to re-enable it! Re-enabling it involves unsigning Xcode and byte patching a binary, but it’s possible! :-)
In the rest of this post I’ll explain how we can reverse engineer Xcode to find the code that’s responsible for the styling of the Open Quickly feature, and then how we can byte patch the relevant binary.
Note: Everything in this post has only been tested on Xcode 9.3 (9E145) downloaded from the Apple Developer Portal. It will probably not work with any other version.
Finding the code responsible for the dark theme
To understand if the dark theme for the Open Quickly feature is still available in Xcode 9.3 or not we need to disassemble the Xcode binary. However, seeing that the Xcode binary is only 35 KB there’s probably not a lot going on in there.
I remember reading somewhere (probably in a tweet by @stroughtonsmith) that basically all of the Xcode functionality
is implemented in a bunch of frameworks that can be found in the Xcode.app bundle.
If we take a look around in Xcode.app/Contents/Frameworks
we’ll see a framework
called IDEKit
. This jumped out at me (probably also from seeing it in a tweet
sometime) and seems like a good a place as any to start, so let’s disassemble
the IDEKit
binary using Hopper.
Using Hopper to inspect IDEKit
If we use Hopper to search for labels matching openquickly
in the IDEKit
binary we can see that there’s a bunch of classes that seem to implement the
Open Quickly functionality. This means that we probably picked the right framework!
Great!
Let’s see if there’s any references to a dark theme in any of the openquickly
classes by searching for openquickly dark
. Well well well, the top result of
-[IDEAbstractOpenQuicklyWindowController inDarkMode]
certainly seems like a
great indication that there’s still code in here for a dark theme (or mode).
We can find even more evidence by looking at what code references the -inDarkMode
method and using Hopper’s pseudo-code feature to inspect the
-[IDEAbstractOpenQuicklyWindowController prepareToShowWindow]
method, which
contains this snippet:
// ...
COND = [r15 inDarkMode] == 0x0;
rax = *_NSAppearanceNameVibrantDark;
if (COND) {
rax = *_NSAppearanceNameVibrantLight;
}
// ...
The return value of -inDarkMode
seems to decide which visual effect the
Open Quickly window uses. Similar conditionals can be found in the other methods
that reference -inDarkMode
.
Let’s find out how -inDarkMode
is implemented using the handy pseudo-code
feature again:
char -[IDEAbstractOpenQuicklyWindowController inDarkMode](void * self, void * _cmd) {
r14 = [[self window] retain];
rbx = [[r14 dvt_theme] retain];
r15 = [rbx isDark];
[rbx release];
[r14 release];
rax = sign_extend_64(r15);
return rax;
}
This is basically just doing return [[[self window] dvt_theme] isDark];
which
doesn’t really tell us a whole lot. Our next step is to figure out what type of
object -dvt_theme
returns and how its -isDark
method is implemented.
Unfortunately, the -dvt_theme
method and its related return type aren’t declared
in IDEKit
. They must be implemented in one of the other frameworks in the
Xcode bundle, so let’s have another look in there.
There’s nothing in the Frameworks
folder that seems related to the dvt
prefix,
but in the SharedFrameworks
folder there’s a bunch of frameworks that are
named using a DVT
prefix. One of them is called DVTKit
and since we had
success with the similarly named IDEKit
let’s open DVTKit
in Hopper!
Using Hopper to inspect DVTKit
This time we know a bit more what we’re looking for, so let’s search for labels
matching dvt_theme
. It turns out we’re pretty good at guessing which frameworks
are relevant, because right there at the top of the search results is -[NSWindow dvt_theme]
!
void * -[NSWindow dvt_theme](void * self, void * _cmd) {
r14 = [objc_getAssociatedObject(self, @selector(dvt_theme)) retain];
if (r14 != 0x0) {
rbx = [r14 retain];
}
else {
r15 = [[DVTThemeManager shared] retain];
rbx = [[r15 defaultTheme] retain];
[r15 release];
}
[r14 release];
[rbx autorelease];
rax = rbx;
return rax;
}
Unfortunately the pseudo-code doesn’t tell us what the return type of the method
is (it just says void *
), but based on the name of the method and the fact that
DVTThemeManager
is mentioned in the implementation I think a good bet is
DVTTheme
.
Searching for DVTTheme
lets us know that we’ve guessed correctly, and looking
through the search results there’s a -[DVTTheme isDark]
method. That’s very
likely the method that the Open Quickly code in IDEKit
is invoking to see
if it should use a light or dark theme for its window. What does the
pseudo-code for the -isDark
method look like?
char -[DVTTheme isDark](void * self, void * _cmd) {
return 0x0;
}
Well, that definitely seems like it could be a reason for why we’re not seeing the dark Open Quickly window in Xcode 9.3 anymore :-)
We can compare the above implementation to the implementation in the version of
DVTKit
that ships with an older version of Xcode (9.2) to see if something
changed:
char -[DVTTheme isDark](void * self, void * _cmd) {
var_30 = self;
var_28 = _cmd;
r15 = [[self->_contents objectForKey:@"isDark"] retain];
if (r15 != 0x0) {
rdx = [NSString class];
if ([r15 isKindOfClass:rdx] == 0x0) {
// ... stripped for brevity
}
rbx = [r15 boolValue];
}
else {
rbx = 0x0;
}
[r15 release];
rax = sign_extend_64(rbx);
return rax;
}
Hey, it definitely changed! We could dig deeper and try to understand how the
_contents
instance variable is populated and why this method would return
true
in previous versions of Xcode, but that might not lead anywhere. Besides,
it’s more fun to just change the current implementation of -[DVTTheme isDark]
to see if something happens to the Open Quickly window!
Byte patching Xcode/DVTKit for fun and profit
By now we know that the -[DVTTheme isDark]
method was modified in the latest version
of Xcode (9.3), and we know that the Open Quickly-related class in IDEKit
indirectly references that method when deciding which window background to use.
All that’s left to do is to figure out how we can modify the DVTKit
binary so
that -[DVTTheme isDark]
returns true
instead of false
.
I’m absolutely no expert at reverse engineering or assembly, but I’m pretty sure
that by replacing one or more bytes in the DVTKit
binary using a hex editor
we should be able to change the behaviour of the aforementioned method.
The first step in our journey towards byte patching DVTKit
is to leave the
comfortable world of Hopper’s pseudo-code mode and start looking at the implementation
of -[DVTTheme isDark]
in Hopper’s assembly mode:
-[DVTTheme isDark]:
00000000000163c9 push rbp
00000000000163ca mov rbp, rsp
00000000000163cd xor eax, eax
00000000000163cf pop rbp
00000000000163d0 ret
After looking at this method for some time and Googling the names of x86 registers
we come to the conclusion that the xor eax, eax
operation is probably what’s
making the method return 0
(or false
). The eax
register seems to be what
holds the return value of the method, and xor eax, eax
is equivalent to
eax = eax ^ eax;
in C. XORing a value with itself always gives a result of
0
, which explains the pseudo-code we saw earlier in Hopper.
If we want to change the return value of -[DVTTheme isDark]
it would seem
that modifying the xor eax, eax
operation is the way forward. But what should we
change it to? Since we can only replace bytes in the binary (changing the length
of the binary would break other offsets and addresses), and the length in bytes of
the xor eax, eax
operation is 2 bytes (which Scripts > Disassemble instruction
in Hopper helpfully tells us) we’re pretty limited in our options.
Being pretty inexperienced with assembly, I couldn’t think of a
1- or 2-byte change that would guarantee that eax
would be populated with a non-zero
value. However, by changing the XOR
instruction to an OR
instruction we can
at least avoid modifying the eax
register (since eax = eax | eax
is a no-op).
If we’re lucky the eax
register might have a non-zero value in it when the
-[DVTTheme isDark]
method is called, which means it will return a non-zero
value just like we wanted!
Using Hopper’s hex editor to replace a byte
Hopper doesn’t just have a pseudo-code mode and an assembly mode, it also has a
hexadecimal mode that can be used as a hex editor. Just what we need! If we switch
to the hexadecimal mode with the xor eax, eax
instruction selected then Hopper
will helpfully highlight the corresponding bytes (31 C0
).
We can verify that these are the bytes that encode the xor eax, eax
instruction
by referring to an x86 instruction set reference
and looking up the XOR instruction.
There’s a bunch of different opcodes
for the different combinations of operands, and one of them is indeed encoded
as 31
. Looks like that’s the byte we’re replacing!
To find the correct hexadecimal value to replace 31
with we’ll refer to the
OR instruction reference on the same website
and look for an opcode that takes the same combination of operands (two registers):
That looks right! The leftmost column tells us that the first byte of this OR
instruction is encoded as 09
, so that’s what we’ll replace 31
with. We can
do that by double-clicking 31
in Hopper’s hexadecimal mode and typing 09
.
Hopper will highlight the new byte value in red to indicate that the byte has
been changed.
With the editing done all that’s left is getting the modified DVTKit
binary
into the Xcode 9.3 bundle.
Exporting the modified binary
When we edit the disassembled binary in Hopper we aren’t actually modifying the
original binary. Hopper keeps its own internal representation of the binary, and
that’s what we’re editing. To get a binary with our changes applied we need to use
Hopper’s Produce New Executable…
option from the File
menu. Since DVTKit
is
a signed binary Hopper will ask us if we want the new binary to have the old,
invalid signature or if we want to remove it.
I believe both options will give us the same result in our case, but let’s go
ahead and remove the signature. We probably want to keep the original DVTKit
binary around, so we should also make sure to save the modified binary somewhere
where we don’t overwrite the original binary.
Getting the modified binary into Xcode
Before we move our new and improved DVTKit
binary into Xcode I would recommend
that we make a copy of Xcode 9.3 for reasons that will soon become clear.
Then, when we have a copy of Xcode 9.3, we can go ahead and overwrite the existing
DVTKit
binary in Xcode 9.3 with our modified binary by moving or copying the
modified binary into the DVTKit
framework bundle. The full path to the DVTKit
binary is Xcode.app/Contents/SharedFrameworks/DVTKit.framework
/Versions/A/DVTKit
.
Now all we have to do is launch the copy of Xcode 9.3 that contains our modified binary and savour the fruits of our labour!
Oh. That’s not what we were expecting. Did we make a mistake when editing the
binary? Nope! If we look closely at the crash report it says code signing
blocked mmap() of 'Xcode.app/…/DVTKit'
. Remember when Hopper told us that we
had modified a signed application and the new binary would have an invalid
signature? Well this is the result of the modified DVTKit
binary having an
invalid or missing signature :-)
To get around this we’ll need to unsign Xcode. Fortunately there are tools that
will do this for us! This is also why I recommended we make a copy of Xcode
before replacing the DVTKit
binary; I wouldn’t feel safe using an unsigned
version of Xcode in my day-to-day work.
Unsigning Xcode
To unsign Xcode we’ll use the free unsign
utility. When we have downloaded the repository and have run make
we’ll have an
unsign
binary that’s ready to use. Using it is really simple:
# From the "unsign" directory
$ ./unsign /Applications/Xcode\ copy.app/Contents/MacOS/Xcode
$ cd /Applications/Xcode\ copy.app/Contents/MacOS
$ mv Xcode.unsigned Xcode # unsign outputs a copy of the input binary
We should now, finally, be able to launch our unsigned and modified version of Xcode 9.3 and rejoice in the return of the dark Open Quickly menu!
Conclusions
Phew. What a ride! At the start of this adventure I wasn’t sure we would be able to find references to a dark theme in Xcode 9.3, let alone be able to byte patch a binary to get it back, but we did! That’s pretty cool.
I hope that I’ve been able to show that reverse engineering and byte patching a binary is something that’s been made relatively accessible through tools like Hopper. Even though there’s a ton more features in Hopper that I don’t understand, we can still do cool things like modifying Xcode by just being curious, being patient, and being good at Google :-)
Have fun exploring!