A Klingon Dictionary Fell Out of My Taskbar
2026-09-23
I wanted to learn Ghidra properly, and CLI crackmes had stopped teaching me anything. They are all the same shape: one function, one comparison, invert it, done. What I did not have was experience with a real application, the kind with a GUI, an event loop, and forty megabytes of Apple framework smeared over everything. So I grabbed the binary for uBar, a well-liked macOS dock replacement, and started reading.
I did not set out to crack it. I want to be clear about that up front, partly because it is true and partly because, as you will see, cracking it is not the interesting part and is arguably not even possible. I set out to understand one registration window. I came back with a Klingon dictionary and a story about the pettiest, most patient anti-piracy mechanism I have ever taken apart. Grab a chair.
Objective-C is a hostile witness
If you have only ever reversed C, the first thing an Objective-C binary does is refuse to make
sense. There are no function calls where you expect them. Everything, and I mean everything,
goes through a single function called objc_msgSend. A line of source like
[window setTitle:s] compiles to objc_msgSend(window, @selector(setTitle:),
s), so your clean call graph collapses into ten thousand calls to the same address.
Ghidra papers over this by inventing fake C++-looking method calls, which is how you end up
staring at things like objc_stub::setTitle: and wondering what language you are
even looking at.
The other early confusion is that instance variables are not at fixed offsets. Objective-C
uses a non-fragile ABI, so self->prefs compiles to "load the offset for
prefs from a global, then add it to self." Two
dereferences to read one field. Ghidra shows you *(ID *)(self + prefs) and you
slowly realise prefs is not a value, it is an address holding a value.
Once you know the trick, the fix is to hand Ghidra the class layout as a struct, retype
self, and watch the pointer arithmetic melt into named fields. The layout is
sitting right there in the Mach-O metadata, the ivar_list_t, which is the
runtime's own dictionary of every field's name, offset and type. That table would save me
later, in a way I did not appreciate at the time.
The registration window, and a croissant
The registration logic hangs off a class called RegistrationWindow. Reading it,
one method name stopped me cold:
- (BOOL)etDoncAvecNom:(NSString *)name etClef:(NSString *)key
That is French. "Et donc, avec nom, et clef" is roughly "and so, with name, and key."
clef is French for key. This is the license validator, and it has been given a
name in French for no reason other than that most people grepping a binary for
verify, checkLicense or validateKey will sail straight
past a method that reads like a bakery order. It is a small, cheap obstacle, and it worked on
me for a good thirty seconds. Foreshadowing: it would not be the last thing in this binary
deliberately built to waste my time.
The function itself is refreshingly boring, and it is boring on purpose. It rebuilds a public key out of about ten string fragments stitched together at runtime (so the whole PEM blob never sits in the binary as one greppable lump), then verifies the license:
- (BOOL)etDoncAvecNom:(NSString *)name etClef:(NSString *)key {
CFobLicVerifier *v = [[CFobLicVerifier alloc] init];
NSMutableString *pem = [NSMutableString string];
[pem appendString:@"MIHxMIGpBgcqhkjOOAQBMIGdAkEAms0PgLuJM7s0Fb8XHCwueCJc"];
[pem appendString:@"/YNJ9N+X7pFs"];
// ...eight more chunks...
[v setPublicKey:[CFobLicVerifier completePublicKeyPEM:pem] error:nil];
NSString *signed = [NSString stringWithFormat:@"uBar4%@", name];
return [v verifyRegCode:key forName:signed error:nil];
}
CFobLicVerifier is CocoaFob, a
well-known Mac licensing library. And this is the detail that quietly decides the entire rest
of the story: it is public-key. Your license key is a cryptographic signature
over your name (product-tagged as uBar4 + name, so a key for one CocoaFob app
will not work in another).
Public-key licensing means there is no keygen. In the CLI crackmes I had been doing, "verify a key" and "generate a valid key" were the same maths run in two directions. Here they are not. Signing requires the private key, which lives on the developer's machine and nowhere else. You cannot forge a license. You cannot even edit the name on a real one, because the signature is bound to the exact string. The only attack surface left is patching the binary, which is not what I was there to do. Remember that you cannot change the name. It matters enormously later.
The check that hides in time
So where does the app actually decide you are a filthy pirate? Not, it turns out, anywhere
near the registration window. The window just paints itself. The real decision is in the app
delegate's applicationDidFinishLaunching:, and it does not run at launch.
if (accessibilityGranted && screenRecordingGranted && dockOK) {
uint32_t hours = arc4random_uniform(6) + 6; // 6..11
dispatch_after(dispatch_time(0, hours * 3600 * NSEC_PER_SEC),
dispatch_get_main_queue(), ^{
[self deferredLicenseCheck];
});
[self setup];
}
The license check is scheduled with dispatch_after, six to eleven hours in the
future, with the exact delay randomised by arc4random_uniform. Sit on that for a
second. If you attach a debugger and watch the app start, nothing license-related happens. You
could single-step the entire launch and conclude the app has no runtime protection at all.
The check is deliberately decoupled from any event you would think to breakpoint. It is
hiding in time, not in space.
The flag wearing a disguise
The deferred check is short, and the first time I read it I completely misjudged what it did:
- (void)deferredLicenseCheck {
NSString *owner = self.prefs.owner;
if (!owner) return;
// "[" + "k]" -> "[k]", assembled at runtime
NSString *marker = [NSString stringWithFormat:@"[%@", [@"k]" uppercaseString]];
if ([owner containsString:marker]) {
self.AXStatus = 3;
}
}
It reads your registered name and checks whether it contains a marker, then sets
AXStatus = 3. And AXStatus is where the genius is, because
AXStatus is a lie. It has a getter and setter called AXStatus and
setAXStatus:, it is initialised right next to the real
AXIsProcessTrustedWithOptions accessibility-permission check, and everything
about it screams "boring macOS permissions plumbing." Any reverser glances at it and moves on.
It even has a cover story that holds up under questioning. A completely separate method, called every time the app is activated, does this:
if (self.AXStatus < 2) self.AXStatus++; // climbs 0 -> 1 -> 2, then stops
So in normal life AXStatus looks exactly like a three-state accessibility enum
that warms up as you use the app and settles at 2. It never reaches 3 on its own. The value
3 is out of band, reachable only by the deferred check, and it is the poison. The
piracy flag has been parked inside a variable whose entire personality is designed to make you
ignore it. Whether that is deliberate camouflage or a very convenient accident, it is
devastatingly effective.
The marker that isn't in the binary
So what is the marker? Look again at how it is built: stringWithFormat:@"[%@"
with the value "k]" substituted in. Glue them together and you get
[k]. That is the tag the whole machine is hunting for: a
registered name containing [k]. The k, given where this is going, is
for Klingon.
And note how it is assembled. The [ lives in one string constant. The
k] lives in a completely different one. They only become [k] at
runtime. If you had run strings over the binary looking for [k], you
would have found nothing, because the literal never exists on disk. A three-character string,
split across two allocations, specifically to defeat grep. At this point I
started to genuinely admire whoever wrote this.
The payload, and where the Klingon was hiding
Two things read AXStatus and change behaviour based on it. One is a method with
an extremely promising name, localizedTitleVersion:, which builds the title
strings the app displays. Cleaned up, it is this:
- (NSString *)localizedTitleVersion:(NSString *)title {
NSMutableArray *out = [NSMutableArray array];
for (NSString *word in [title componentsSeparatedByString:@" "]) {
if (self.AXStatus < 3) {
[out addObject:word]; // normal: keep the real word
} else {
NSUInteger i = arc4random_uniform(99); // a 99-word dictionary
[out addObject:self.klingonWords[i]]; // replace with a random Klingon word
if (firstWord) out[0] = [out.lastObject capitalizedString];
}
}
return [out componentsJoinedByString:@" "];
}
If AXStatus is 3, every word of every title gets replaced by a random word drawn
from klingonWords, with the first one politely capitalised so it still looks like
a title. That is the payload. Get flagged, and hours later your menu bar starts speaking
Klingon.
The klingonWords array is the punchline to a subplot. Earlier in launch there is
a genuinely intimidating-looking blob: a giant base64 string, decoded to bytes, decoded again
to a UTF-8 string, split on commas, and stored. It looks like the kind of thing that decodes
to a secret key. When I tried to decode the literal, I got garbage, because, of course, there
is a %@ format placeholder buried inside the base64, so the real input
is only assembled at runtime and the static string is booby-trapped against exactly what I was
trying to do. Strip the placeholder and decode the clean part and you get:
ngor,qevpob,yIv,puq,...,tlhaq,gho,rewbe',veng,...,So',wep,SoQ,ngoq,bIr,tach,...
A comma-separated Klingon vocabulary list, in English-alphabetical order.
tlhaq is Klingon for clock, which for a taskbar app is a nice touch.
Here is the part that should teach you something about your tools, though: I had earlier used
Ghidra's "find uses of field" on klingonWords and concluded it was dead code,
never read anywhere. I was wrong, and the reason I was wrong is that field-usage search is
type-based. localizedTitleVersion: reaches the array through an untyped
[NSApp delegate] pointer, so Ghidra could not connect it to the field. The tool
confidently told me the Klingon was unused right up until the actual instructions told me it
was the entire point. Do not trust an ivar cross-reference until you have checked the untyped
paths.
The whole machine, start to finish
Step back and look at the assembled contraption:
- A name containing
[k]trips the flag. The marker is split across two strings so it is not greppable. - The flag is
AXStatus = 3, hidden inside a variable that otherwise looks like accessibility state and even has a fake increment for cover. - The check runs on a timer, 6 to 11 hours after launch, randomised, so it is decoupled from anything you could breakpoint.
- The payload replaces your menu titles with random Klingon from a base64-encoded, format-string-booby-trapped dictionary.
- And because CocoaFob binds the name to the key, the mark cannot be removed. Edit
[k]out of the name and the signature no longer validates. You are stuck carrying it.
That last point is the keystone. The only way a name ends up with [k] in it is if
the developer signed it that way on purpose, then let those licenses find their way onto the
warez forums as honeypots. A pirate grabs one, it registers perfectly, everything works, they
forget all about it, and one evening their taskbar quietly turns into gibberish they cannot
debug, cannot google, and cannot scrub off. No error. No nag. No crash. Just Klingon.
The part where I discover I am ten years late
I was extremely pleased with myself, right up until I typed "uBar Klingon" into a search box and discovered this was a minor legend. The developer, Edward Brawer, went public with it in 2015. Of roughly 30,000 paid users he reckoned about 1,000 had helped themselves, and rather than fight them he set, in his words, his phaser to amusement. Frustrated pirates emailed support about the "gibberish", occasionally from corporate accounts, occasionally signing off with their job titles. The support team replied, in Klingon, that as they were using a pirated copy it was unavoidable that they must begin learning it. "It is the life you have chosen."
What the news coverage never had, because news coverage never does, was the mechanism. The
articles say the app "detected the registration mechanism was circumvented," which is the PR
version. The real version is a crypto-bound [k] honeypot disguised as an
accessibility flag on a randomised six-hour fuse. His own writeup has since rotted off the
internet into a 404. So consider this the disassembly the story never came with.
What it was actually worth
As a Ghidra lesson this was worth ten crackmes. In one binary I had to build ObjC structs from the metadata, retype pointers, chase behaviour across message sends with no direct call edges, learn that field cross-references lie when types are missing, fall back to scalar search over a raw struct offset, and hand-decode a base64 blob that was specifically engineered to punish me for decoding it. None of that is in a CLI crackme.
And I did not crack anything, because there was nothing satisfying to crack. The protection is public-key; the clever bit was never the lock, it was the trap sprung on people who got in through a door the developer left open on purpose. If you use uBar, buy it. It is about thirty dollars, it is a genuinely nice piece of software, and it is a good deal cheaper than the afternoon I spent in Ghidra proving its author has a better sense of humour than I do.
Qapla'.