craig@craigjb.com
GitHubhttps://github.com/craigjb Mastodonhttps://twitter.com/craig_jbishop
« FPGA Game Boy Part 1: SpinalHDL and Z80-ish T-Cycles Atlys FPGA reset shenanigans »

Reverse engineering away an annoying pop-up


I recently started using an app called Be Focused Pro for focusing using the Pomodoro technique to focus a bit better. The free version of the app has ads and some pop-ups, so I paid $5 for the pro version to remove the ads. Afterward, I was unhappy to discover the app still persisted in annoying its pro users. Periodically, when you enter a 5-minute break, the app triggers a pop-up notification asking to rate the app in the AppStore. While normally I would just find another app, I actually really like this one’s user interface (and I already paid $5 for it). So instead, I decided to hack away the annoying pop-up and have the app I want.

Annoying pop-up dialog that appears periodically

First, I opened the app binary in Hopper Disassembler, which is an amazingly priced alternative to IDA Pro for static analysis on Mac. To find the code that opens the stupid pop-up, we can search for the pop-up string and follow the references back to the code that actually runs the pop-up. So first, I searched for the string “Please take a moment”, and then clicked to follow the cross-references back to the code.

Finding the pop-up string in the disassembly
Finding the pop-up string in the disassembly

Following the XREFs by double-clicking, I ended up in the middle of an Objective-C method called -[XWRate initialize]:. It looks like this app was built using Objective-C, and it looks like this XWRate class probably handles the annoying pop-up. To get a better look at what’s happening, I ran the binary through class-dump, which outputs the class declarations so we can see what methods are in the XWRate class.

@interface XWRate : NSWindowController
{
    BOOL _previewMode;
    NSTextField *_titleField;
    NSTextView *_descriptionField;
    NSButton *_rateButton;
    NSButton *_remindButton;
    unsigned long long _daysUntilPrompt;
    unsigned long long _remindPeriod;
    NSString *_messageTitle;
    NSString *_message;
    NSString *_rateButtonLabel;
    NSString *_dontLikeButtonLabel;
    NSString *_remindButtonLabel;
    NSString *_appStoreID;
    NSString *_appName;
}

+ (void)rateAppButtonHandler;
+ (id)instance;
@property(retain, nonatomic) NSString *appName; // @synthesize appName=_appName;
@property(retain, nonatomic) NSString *appStoreID; // @synthesize appStoreID=_appStoreID;
@property(nonatomic) BOOL previewMode; // @synthesize previewMode=_previewMode;
@property(retain, nonatomic) NSString *remindButtonLabel; // @synthesize remindButtonLabel=_remindButtonLabel;
@property(retain, nonatomic) NSString *dontLikeButtonLabel; // @synthesize dontLikeButtonLabel=_dontLikeButtonLabel;
@property(retain, nonatomic) NSString *rateButtonLabel; // @synthesize rateButtonLabel=_rateButtonLabel;
@property(retain, nonatomic) NSString *message; // @synthesize message=_message;
@property(retain, nonatomic) NSString *messageTitle; // @synthesize messageTitle=_messageTitle;
@property(nonatomic) unsigned long long remindPeriod; // @synthesize remindPeriod=_remindPeriod;
@property(nonatomic) unsigned long long daysUntilPrompt; // @synthesize daysUntilPrompt=_daysUntilPrompt;
@property(retain, nonatomic) NSButton *remindButton; // @synthesize remindButton=_remindButton;
@property(retain, nonatomic) NSButton *rateButton; // @synthesize rateButton=_rateButton;
@property(retain, nonatomic) NSTextView *descriptionField; // @synthesize descriptionField=_descriptionField;
@property(retain, nonatomic) NSTextField *titleField; // @synthesize titleField=_titleField;
- (void).cxx_destruct;
- (void)openRatingsPageInAppStore;
- (void)hideRateWindow;
- (void)rightButtonSelector:(id)arg1;
- (void)leftButtonSelector:(id)arg1;
- (BOOL)offerToRateAtPoint:(unsigned long long)arg1;
@property(nonatomic) unsigned long long pointsCount;
- (BOOL)isNewVersion;
- (void)askToRate;
- (void)generateUsedPoint;
- (unsigned long long)usedPoint;
- (id)ratingsURL;
- (void)updateMessageTtitle;
- (void)awakeFromNib;
- (void)initialize;
- (id)initWithWindowNibName:(id)arg1 owner:(id)arg2;
- (id)init;

@end

Looks like the XWRate class is a singleton, since it has a static + (id)instance; method. To disable the pop-up, it helps to be able to trigger it reliably without waiting for whatever time period the app does. Looks like the - (void)askToRate method might just do that. So, I fired up the app in lldb, lldb /Applications/Be\ Focused\ Pro.app, started it, (lldb) run, and the stopped execution with ctrl-c after all the startup messages. Then to see if these methods do what I think they do, I used lldb expression evaluation to run the askToRate method.

(lldb) e XWRate* $inst = [XWRate instance]
(lldb) e [$inst askToRate]

And, it worked! The terrible pop-up showed up. The app became unresponsive when I tried to click the button, but that’s probably a side-effect of breaking execution and running the method with the app’s event loop running. Back in Hopper, I took a look at the askToRate disassembly. Hopper makes it really easy to search for Objective-C nethod names directly in the left panel.

Finding the askToRate method in the disassembly

Looking in the disassembly, this section looks particularly guilty since it calls runModalForWindow which presumably causes the pop-up to appear.

000000010009e02d         mov        rsi, qword [0x1003ce920]                    ; @selector(runModalForWindow:), argument "selector" for method _objc_msgSend
000000010009e034         mov        rdi, r12                                    ; argument "instance" for method _objc_msgSend
000000010009e037         mov        rdx, rbx
000000010009e03a         call       r13

Alright, looks like if the call r13 is turned into a nop, the pop-up shouldn’t show up. So, next I created a copy of the app, cp -r /Applications/Be\ Focused\ Pro.app ~/Desktop/. Then, I removed code-signing from the copy so we can modify it without triggering bad things, codesign --remove-signature Be\ Focused\ Pro.app. Then I opened the copy up in Hopper, and used the Modify->Assemble Instruction menu to replaced the call r13 with three nop instructions. Then, I saved a new executable with the modifcations.

Replace the modal window call with nops

Now to test it, I did the same procedure as before with lldb:

(lldb) e XWRate* $inst = [XWRate instance]
(lldb) e [$inst askToRate]

And… no pop-up! Life is good now.


« FPGA Game Boy Part 1: SpinalHDL and Z80-ish T-Cycles Atlys FPGA reset shenanigans »

Copyright © 2017 Craig J Bishop