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.
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.
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.
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.
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.