How to tweak existing Medium iOS app features with Theos & Logos - part 2
In this post we will continue to enhance Medium tweak. We will learn how to create a preference bundle and hook into Settings.app to configure default claps. If you have not read part 1 yet, I suggest to have a look first before continuing.
Disclaimer
This post is for educational purposes only. How you use this information is your responsibility. I will not be held accountable for any illegal activities, so please use it at your discretion and contact the app’s author if you find issues.
Prerequisites
Below tools are used during this post:
- A jailbroken device.
- FLEX Loader Cydia package
- Theos development setup
- Install Medium iOS app
- Hopper Disassembler
Overview
We will fix the problems encountered in the previous post. Let me list down things we will cover today:
- What are
clapButtonPressed:andclapButtonReleased:doing? - Are there any alternative methods to hook?
- How to create a preference bundle that allows changing a number of claps instead of hard coding.
- Spoil alert another thing we can tweak Medium
I bet it will be more hands-on than the previous post, please get our hands dirty!! 👌👌
Clapping function behind the scene
Static analysis using Hopper Disassembler
As promised in the previous post, we will reveal what’s going on in clapButtonPressed: and clapButtonReleased: methods. We need to do some analysis on the Medium binary file.
With the help of Frida iOS Dump or CrackerXI, we can easily pull out .ipa file of Medium app on a jailbroken device, unzip .ipa and navigate to Payload/hangtag.app folder.
Drag and drop hangtag binary (MachO) file into Hopper Disassembler and wait for a while for it to disassemble. When it finishes, in the left panel make sure Labels tab is selected, let search for clapButtonPressed and click on -[ClapButton clapButtonPressed:] result you will be navigated to method implementation on the right, assembly instructions again!!! But don’t worry, this time we don’t need to read every instruction, we only need to understand what method is doing in general. To do that, switch on Pseudo-code mode tab, you will be impressed with how great it is:
1
2
3
4
5
6
/* @class ClapButton */
-(void)clapButtonPressed:(void *)arg2 {
[self prepareFeedbackGenerator];
[self->_longPressClapController startTimer];
return;
}
As you can see, it invoked timer when button is pressed or tapped ([self->_longPressClapController startTimer]). Just hover your mouse over startTimer and double click on it, it should navigate you to method implementation of this selector. In this case, startTimer selector is referenced by multiple classes, so let select -[LongPressClapController startTimer] option when popup appears to proceed, and this is implementation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* @class LongPressClapController */
-(void)startTimer {
[self tryToPerformClap]; // 1. [self tryToPerformClap]
[*(self + 0x8) invalidate]; // 2. [_clapTimer invalidate]
r22 = [[WeakProxy proxyWithTarget:self] retain];
r0 = [NSTimer scheduledTimerWithTimeInterval:r22
target:@selector(tryToPerformClap)
selector:0x0
userInfo:0x1
repeats:r6]; // 3.
r0 = [r0 retain];
r8 = *(self + 0x8);
*(self + 0x8) = r0; // 4. _clapTimer = r0
[r8 release];
[r22 release];
return;
}
I put some inline comments, let focus on that, and ignore the rest:
- It invokes method
tryToPerformClap, by the name we can guess it perform a clap, so it should be count as 1 clap - It tries to invoke the method
invalidatefrom an unknown object at address *(self + 0x8). This is the common syntax to access ivars, in this case it’s _clapTimer ivar (NSTimer). From apple document, invoke invalidate will stop the timer from ever firing again and request its removal from its run loop. - It creates a new NSTimer instance, but it seems the decompiler is taking wrong arguments, i.e. r22 suppose to be number but it’s
WeakProxy… We have no choice, and we will find out soon. - Store new timer instance to _clapTimer
Switch to assembly mode and focus on these instructions:
1
2
3
4
5
6
7
8
9
10
11
...
ldr x1, [x8, #0xce0] ; @selector(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:)
adrp x8, #0x10060f000
ldr d0, [x8, #0xae8] ; 0x10060fae8, timerInterval = 0.2
mov x0, x21 ; _OBJC_CLASS_$_NSTimer
mov x2, x22 ; target = self = LongPressClapController
mov x3, x20 ; "tryToPerformClap",@selector(tryToPerformClap)
movz x4, #0x0 ; userInfo
movz w5, #0x1 ; repeats, argument "instance" for method imp___stubs__objc_msgSend
bl imp___stubs__objc_msgSend ; objc_msgSend
...
They are instructions of NSTimer creation and my inline comments for each one. As you can see register d0 holding address 0x10060fae8, double click on this address it holding value 0.2 (000000010060fae8 dq 0.2), so it should be timer interval (200 milliseconds). All can be rewritten like this:
1
2
3
4
5
r0 = [NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(tryToPerformClap)
userInfo:nil
repeats:YES]; // 3.
Base on this, we know that timer will be fired every 200 milliseconds to invoke [self tryToPerformClap]
Check references to clapButtonPressed and clapButtonReleased:, it’s showing this:
1
2
3
4
5
6
7
8
9
10
/* @class ClapButton */
-(void)addActions {
[self addTarget:self
action:@selector(clapButtonPressed:)
forControlEvents:0x1];
[self addTarget:self
action:@selector(clapButtonReleased:)
forControlEvents:0x1c0];
return;
}
ClapButton is subclass of UIButton so it will inherit addTarget:action:forControlEvents: method. This is the place to register actions for events of type UIControl.Event. Let figure out what are events (0x1 and 0x1c0 are in hexadecimal) of each action.
Let have a look inside UIControl.Event declaration (I stripped public static modifiers for short) and put inline comments for raw values of each event in decimal and binary.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension UIControl {
public struct Event : OptionSet {
public init(rawValue: UInt)
var touchDown: UIControl.Event { get } // 1 = 0000 0000 0001
var touchDownRepeat: UIControl.Event { get } // 2 = 0000 0000 0010
var touchDragInside: UIControl.Event { get } // 4 = 0000 0000 0100
var touchDragOutside: UIControl.Event { get } // 8 = 0000 0000 1000
var touchDragEnter: UIControl.Event { get } // 16 = 0000 0001 0000
var touchDragExit: UIControl.Event { get } // 32 = 0000 0010 0000
var touchUpInside: UIControl.Event { get } // 64 = 0000 0100 0000
var touchUpOutside: UIControl.Event { get } // 128 = 0000 1000 0000
var touchCancel: UIControl.Event { get } // 256 = 0001 0000 0000
...
}
As you can see the raw values pattern, they are all bitmask constants. With this kind of bitmask represents, multiple events can be represented in one number. Let examine it!!
The control event registered for clapButtonPressed: is easy to guess 0x1 = 1 = .touchDown. But for clapButtonReleased: it is a bit tricky. 0x1c0 = 448 does not match any defined events, is Hopper Disassembler decompiler wrong? Convert 0x1c0 to binary will be 0001 1100 0000. There are 3 bits set, check with above constants it will be this combination: 0001 0000 0000 | 0000 1000 0000 | 0000 0100 0000 or human-readable would be .touchCancel | .touchUpOutside | .touchUpInside, so it’s revealed:
1
2
3
4
5
6
7
8
9
10
/* @class ClapButton */
-(void)addActions {
[self addTarget:self
action:@selector(clapButtonPressed:)
forControlEvents:.touchDown];
[self addTarget:self
action:@selector(clapButtonReleased:)
forControlEvents:.touchCancel | .touchUpOutside | .touchUpInside];
return;
}
Let summarize where we are:
1
2
3
4
-[ClapButton clapButtonPressed:]
| -[LongPressClapController startTimer]
| -[LongPressClapController tryToPerformClap]
I bet we can hook into -[LongPressClapController tryToPerformClap] method and handle clapping stuff. You can delve into tryToPerformClap to reverse more thing, I will leave it to you (spoil alert: look for a method that allows to hook and bypass max 50 claps, it will be only 2 or 3 levels deeper from tryToPerformClap, below is the example 😊)
Figure 4: Hook to bypass max 50 claps per post (client-side working only)
Alternative method to hook
We found out that -[LongPressClapController tryToPerformClap] is possible to hook for clapping, let comment out hooking methods of ClapButton class and add below new code:
1
2
3
4
5
6
7
8
9
10
11
12
%hook LongPressClapController
int numberOfClaps = 4;
-(void)tryToPerformClap {
%log; // log method name and argument, you will find this log in `Console` app
for (int i = 0; i < numberOfClaps; i++) {
%orig; // invoke original method
}
}
%end
Compile and install the tweak again, you will you it will work as expected.
Preference bundle
The only thing we feel not clean is that we are hard coding numberOfClaps in the tweak source code, the end-users have no option to change it unless they have source code. Theos has a template that allows you to create preference bundle to hook into Settings.app and add a new setting item as you want. We plan to use this preference bundle template to create a new Medium Tweak item with a slider that allows users to set the number of claps as they want. How do you feel? 🤯🤯
Create Preference bundle project
From the root folder of your tweak, run Theos command to create new preference bundle template as below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
MBP# $THEOS/bin/nic.pl
NIC 2.0 - New Instance Creator
------------------------------
[1.] iphone/activator_event
[2.] iphone/application_modern
[3.] iphone/application_swift
[4.] iphone/flipswitch_switch
[5.] iphone/framework
[6.] iphone/library
[7.] iphone/preference_bundle_modern
[8.] iphone/tool
[9.] iphone/tool_swift
[10.] iphone/tweak
[11.] iphone/xpc_service
Choose a Template (required): 7
Project Name (required): Medium Tweak Pref
Package Name [com.yourcompany.mediumtweakpref]:com.reversethatapp.mediumtweakpref
Author/Maintainer Name [ReverseThatApp]: ReverseThatApp
[iphone/preference_bundle_modern] Class name prefix (three or more characters unique to this project) [XXX]: RTA
Instantiating iphone/preference_bundle_modern in mediumtweakpref/...
Adding 'Medium Tweak Pref' as an aggregate subproject in Theos makefile 'Makefile'.
Done.
When it’s done, you will see new folder mediumtweakpref created and new lines added into your Makefile:
1
2
SUBPROJECTS += mediumtweakpref
include $(THEOS_MAKE_PATH)/aggregate.mk
Your project structure will look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|- mediumenhanceclapstweak
| |- mediumtweakpref
| | |- Resources
| | | |- Info.plist
| | | |- Root.plist // declare your UI here
| | |- entry.plist // setup entry icon, title
| | |- Makefile
| | |- RTARootListController.h
| | |- RTARootListController.m // handle code logic
| |- control
| |- Makefile
| |- mediumenhanceclapstweak.plist
| |- Tweak.xm
Design preference layout
Our target to build the preference UI like Figure 1. Let make the entry first (left panel on iPad).
Entry item
For entry item, we need an icon and change the entry title a bit. Let open entry.plist file and make it like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>entry</key>
<dict>
<key>bundle</key>
<string>MediumTweakPref</string>
<key>cell</key>
<string>PSLinkCell</string>
<key>detail</key>
<string>RTARootListController</string>
<key>icon</key>
<string>entry-icon.png</string><!-- change icon name here-->
<key>isController</key>
<true/>
<key>label</key>
<string>Medium Tweak</string><!-- change entry title here-->
</dict>
</dict>
</plist>
For entry icon, I downloaded from flaticon.com and put same location as entry.plist file, name it entry-icon.png(size 32x32) and entry-icon@2x.png(size 64x64).
For the entry label, I changed the value to Medium Tweak, it’s up to you.
Preference UI
For preference UI, I’m using slider cell and static text cell to display slider value (we call it specifiers). Let open Resources/Root.plist file and modify like this (you might have a look other preferences specifier plist to understand how to use it)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>title</key>
<string>Medium Tweak</string>
<key>items</key>
<array>
<dict>
<key>cell</key>
<string>PSGroupCell</string>
<key>label</key>
<string>Default claps</string>
</dict>
<dict>
<key>cell</key>
<string>PSSliderCell</string>
<key>default</key>
<integer>1</integer> <!-- Default value of the slider -->
<key>defaults</key>
<string>com.reversethatapp.mediumtweakpref</string> <!-- entry path no extension -->
<key>key</key>
<string>DEFAULT_CLAPS</string> <!-- Your key name here -->
<key>max</key>
<real>50</real> <!-- Enter your maximum value here -->
<key>min</key>
<real>1</real> <!-- Enter your minimum value here -->
<key>showValue</key> <!-- Show the value of the slider. true or false. -->
<false/>
<key>isSegmented</key>
<true/>
<key>segmentCount</key>
<integer>50</integer>
<key>leftImage</key>
<string>clapping-hands-left.png</string>
<key>rightImage</key>
<string>clapping-hands-right.png</string>
</dict>
</array>
</dict>
</plist>
We are using PSGroupCell (you can think it like section) and PSSliderCell belongs to PSGroupCell. For PSGroupCell I will add it programmatically so you can understand how to add specifiers in plist or the code file.
For PSSliderCell we are using leftImage and rightImage key, you can download those images and put in Resources folder. In my case I made leftImage size smaller than rightImage size to illustrate increment order.
Now it’s time for you to run and see how preference bundle look like, from Terminal navigate to Tweak root folder (not preference root folder) then compile and install the tweak as normal, it will compile and install our preference together, would look like this:
Figure 5: Medium Tweak Preference draft
It looks nice and slider working fine, let add one more specifier programmatically right below using PSStaticTextCell to show value of slider. Open RTARootListController.m and modify existing - (NSArray *)specifiers as below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// This capture slider value
static NSInteger currentClaps = 1;
- (NSArray *)specifiers {
if (!_specifiers) {
_specifiers = [self loadSpecifiersFromPlistName:@"Root" target:self];
}
// 1. load persisted default claps to currentClaps
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"identifier == %@", @"DEFAULT_CLAPS"];
NSArray *filteredArray = [_specifiers filteredArrayUsingPredicate:predicate];
PSSpecifier* sliderSpec = [filteredArray objectAtIndex:0];
NSString *path = [NSString stringWithFormat:@"/User/Library/Preferences/%@.plist", sliderSpec.properties[@"defaults"]];
NSMutableDictionary *settings = [NSMutableDictionary dictionary];
[settings addEntriesFromDictionary:[NSDictionary dictionaryWithContentsOfFile:path]];
NSString *key = sliderSpec.properties[@"key"];
if (key != nil && [key isEqualToString:@"DEFAULT_CLAPS"]) {
NSInteger currentClapsNumber = [[settings objectForKey:key] intValue];
currentClaps = currentClapsNumber;
}
// 2. create new PSStaticTextCell
PSSpecifier* spec = [PSSpecifier preferenceSpecifierNamed: [self generateClapsText]
target:self
set:@selector(setPreferenceValue:specifier:)
get:@selector(readPreferenceValue:)
detail:Nil
cell:PSStaticTextCell
edit:Nil];
// 3. alignment center
[spec setProperty:@2 forKey:@"alignment"];
// 4. Insert below slider
[_specifiers insertObject:spec atIndex:2];
return _specifiers;
}
The idea is that it loaded defined specifiers in Root.plist file then add one more PSStaticTextCell at index 2 (after slider specifier).
Figure 6: Medium Tweak Preference with static cell
Read/Write preference
We’ve just added new static cell to display value of segment. Because segment cell value only can display decimal number, we will need another static cell to round up and display as an integer number. Open RTARootListController.m and add 2 more methods that read preference value from plist file and store value into plist file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
- (id)readPreferenceValue:(PSSpecifier*)specifier {
NSString *path = [NSString stringWithFormat:@"/User/Library/Preferences/%@.plist", specifier.properties[@"defaults"]];
NSMutableDictionary *settings = [NSMutableDictionary dictionary];
[settings addEntriesFromDictionary:[NSDictionary dictionaryWithContentsOfFile:path]];
return (settings[specifier.properties[@"key"]]) ?: specifier.properties[@"default"];
}
- (void)setPreferenceValue:(id)value specifier:(PSSpecifier*)specifier {
NSString *path = [NSString stringWithFormat:@"/User/Library/Preferences/%@.plist", specifier.properties[@"defaults"]];
NSMutableDictionary *settings = [NSMutableDictionary dictionary];
[settings addEntriesFromDictionary:[NSDictionary dictionaryWithContentsOfFile:path]];
NSString *key = specifier.properties[@"key"];
// Handle if this is segment cell
if ([key isEqualToString:@"DEFAULT_CLAPS"]) {
// 1. Get current segment decimal value
CGFloat defaultClapsFloat = [value floatValue];
// 2. Convert to integer value and store to global variable
NSInteger defaultClaps = (NSInteger) defaultClapsFloat;
currentClaps = defaultClaps;
[settings setObject:[NSNumber numberWithInteger:defaultClaps] forKey:key];
cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]];
// 3. Refresh static cell to display currentClaps
PSTableCell1 *staticCell = (PSTableCell1 *)[super tableView:(UITableView *)self.table cellForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]];
[staticCell setTitle:[self generateClapsText]];
} else {
[settings setObject:value forKey:key];
}
// 4. Store preference values to file
[settings writeToFile:path atomically:YES];
}
- (NSString *)generateClapsText {
return [NSString stringWithFormat:@"%ld 👏", currentClaps];
}
Clean build and install the tweak, you will see the number of claps change when drag segment. To double confirm if new segment value is persisted to file, open Filza app and navigate to /User/Library/Preferences/com.reversethatapp.mediumtweakpref.plist you will see there is an entry DEFAULT_CLAPS that reflects new segment value.
The codes require for preference bundle are complete, now we just need to read this DEFAULT_CLAPS value from Tweak.xm and simulate the number of taps on UI.
Using Preference
Which method to hook?
Switch back to Hopper Disassembler and navigate to tryToPerformClap method and have a look:
1
2
3
4
5
6
7
8
/* @class LongPressClapController */
-(void)tryToPerformClap {
r0 = [self delegate];
r0 = [r0 retain];
[r0 clapViewWasTapped];
[r0 release];
return;
}
Just make it short this method will call [[self delegate] clapViewWasTapped], so what’s the delegate here? Double click on selector clapViewWasTapped you will be prompted this dialog:
Figure 3: Methods implementing selector clapViewWasTapped
It has 3 classes implements this selector, the usual way is dynamic analysis with lldb to set breakpoint and print out delegate instance. But there is another way I want to introduce in this post is using frida-trace (I will have another post how to use it in details soon). From Terminal just run frida-ps -Ua to see which is the process ID of Medium app:
1
2
3
4
5
6
MBP# frida-ps -Ua
PID Name Identifier
----- ----------- -----------------------------
...
14389 Medium com.medium.reader
...
Using this PID 14389 for next command to trace how method clapViewWasTapped is called:
1
2
3
4
5
6
7
MBP# frida-trace -U -p 14389 -m "-[* clapViewWasTapped]"
Instrumenting functions...
-[StickyFullPostFooterActionBar clapViewWasTapped]: Auto-generated handler at "/__handlers__/__StickyFullPostFooterActionBar__6b41bd3e.js"
-[NowPlayingViewController clapViewWasTapped]: Auto-generated handler at "/__handlers__/__NowPlayingViewController_clapV_52240ab8.js"
-[hangtag.IcelandPostActionBarCell clapViewWasTapped]: Auto-generated handler at "/__handlers__/__hangtag.IcelandPostActionBarCe_0381b398.js"
-[PostPreviewActionBar clapViewWasTapped]: Auto-generated handler at "/__handlers__/__PostPreviewActionBar_clapViewWasTapped_.js"
Started tracing 4 functions. Press Ctrl+C to stop.
We are tracing all classes that implement selector clapViewWasTapped (-U is usb, -p is process ID). Let clap any post, you will see some logs in Terminal console
1
2
3
/* TID 0x403 */
3371 ms -[StickyFullPostFooterActionBar clapViewWasTapped]
3372 ms | -[PostPreviewActionBar clapViewWasTapped]
BINGO!! StickyFullPostFooterActionBar is the delegate, and it also means we can hook this method like this in the Tweak.xm file (take note to remove existing %hook ClapButton section in Tweak.xm file before adding new hook below):
Using preference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Define preference path, it would be /var/mobile/Library/Preferences/prefernceBundleId.plist
#define PLIST_PATH "/var/mobile/Library/Preferences/com.reversethatapp.mediumtweakpref.plist"
%hook StickyFullPostFooterActionBar
- (void)clapViewWasTapped {
%log;
// 1. Load info stored to device in `.plist` file into dictionary
NSDictionary *settings = [[NSMutableDictionary alloc] initWithContentsOfFile:@PLIST_PATH];
// 2. Load default clap setup in preference
CGFloat defaultClapsFloat = [[settings objectForKey:@"DEFAULT_CLAPS"] floatValue];
NSInteger defaultClaps = (NSInteger) defaultClapsFloat;
// 3. Simulate clap
for (int i = 0; i < defaultClaps; i++) {
NSLog(@"Clap no. %d", i);
%orig;
}
}
%end
Preference bundle will persist settings in .plist file at above location, and from Tweak.xm we just need to fetch that file and extract the correct key that defined for each cell in the Preference Resources/Root.plist file, in this case DEFAULT_CLAPS is the key of clap segment setting.
Testing
Spoil alert
Next posts we will continue to reverse Medium app and enable unlimited read feature for free user, is that cool? Follow my Twitter to get notified for up comming posts :P
Figure 8: Unlimited read
Final thoughts
- Again hooking is a fast way to change existing behaviors of the app, you can find there are so many useful tweaks out there to install and experience yourself.
- From a research perspective, hooking to methods can help change behaviors of the app, for example disable jailbreak detection, disable SSL Pinning… which allow you to inspect the app in runtime.
