I was upgrading my tweak CustomNotificationSound for iOS 9 the other day, and class-dumped the headers of /System/Library/PreferenceBundles/NotificationsSettings.bundle/NotificationsSettings
from my iPad Air. One private method I used in my tweak on iOS 8 was:
@interface BulletinBoardAppDetailController : PSListController
- (NSNumber *)_valueOfNotificationType:(unsigned long long)arg1;
@end
And it was:
- (NSNumber *)_valueOfNotificationType:(unsigned long long)arg1 forSectionInfo:(BBSectionInfo *)arg2;
on iOS 9. The extra arg on iOS 9 was quite easy to get via:
BBSectionInfo *sectionInfo = [BulletinBoardAppDetailController.specifier propertyForKey:@"BBSECTION_INFO_KEY"]
and it worked like a charm on my iPad Air. When I was testing this tweak on my iPhone 5, something strange has happened:
Whatever value I set with _setValue:notificationType:forSectionInfo:
, the above code always returned a “constant” value in my tweak, but the same thing was not happening on my iPad Air; what was even stranger was that the same code snippet was returning the correct value when I debugged with Cycript. The description could be explained with a few lines of code:
// Code in my tweak
@interface PSSpecifier : NSObject
@property (retain, nonatomic) NSString *identifier;
- (id)propertyForKey:(NSString *)key;
@end
@interface PSViewController : UIViewController
- (PSSpecifier *)specifier;
@end
@interface PSListController : PSViewController
- (UITableView *)table;
@end
@interface BulletinBoardAppDetailController : PSListController
- (NSNumber *)_valueOfNotificationType:(unsigned long long)arg1 forSectionInfo:(id)arg2;
@end
%group NotificationSettingsHook
%hook BulletinBoardAppDetailController
%new
- (void)CNSLogger
{
NSLog(@"CNSLog: %@, %@, %@", [self _valueOfNotificationType:0x10 forSectionInfo:[[self specifier] propertyForKey:@"BBSECTION_INFO_KEY"]], self, [[self specifier] propertyForKey:@"BBSECTION_INFO_KEY"]);
[self performSelector:@selector(CNSLogger) withObject:nil afterDelay:1.0];
}
- (void)tableView:(UITableView *)arg1 didSelectRowAtIndexPath:(NSIndexPath *)arg2
{
[self performSelector:@selector(CNSLogger) withObject:nil afterDelay:1.0];
return %orig;
}
%end
%end
%hook PSListController
- (void)lazyLoadBundle:(id)arg1
{
%orig;
if ([[arg1 identifier] isEqualToString:@"NOTIFICATIONS_ID"]) %init(NotificationSettingsHook);
}
%end
%ctor
{
%init;
}
When the hooked method was called, there’d be continuous CNSLogs every 1 second, as shown below:
Oct 25 20:25:20 FunMaker-5 Preferences[14721]: CNSLog: 0, <BulletinBoardAppDetailController 0x16a16000: navItem <UINavigationItem: 0x17878700>, view <UITableView: 0x17278e00; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x1785f190>; layer = <CALayer: 0x16699120>; contentOffset: {0, -64}; contentSize: {320, 659.5}>>, <BBSectionInfo: 0x165c5570> Section com.alipay.iphoneclient 'Alipay': shows in NC = NO, Alert style = Banner, Lockscreen = YES, External = YES, Push settings = [s:BSA] [e:B-A], allows notifications = YES
Oct 25 20:25:21 FunMaker-5 Preferences[14721]: CNSLog: 0, <BulletinBoardAppDetailController 0x16a16000: navItem <UINavigationItem: 0x17878700>, view <UITableView: 0x17278e00; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x1785f190>; layer = <CALayer: 0x16699120>; contentOffset: {0, -64}; contentSize: {320, 659.5}>>, <BBSectionInfo: 0x165c5570> Section com.alipay.iphoneclient 'Alipay': shows in NC = NO, Alert style = Banner, Lockscreen = YES, External = YES, Push settings = [s:BSA] [e:B-A], allows notifications = YES
...
While CNSLogger was called recursively, I changed the value and checked it with Cycript:
FunMaker-5:~ root# cycript -p Preferences
cy# controller = #0x16a16000
#"<BulletinBoardAppDetailController 0x16a16000: navItem <UINavigationItem: 0x17878700>, view <UITableView: 0x17278e00; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x1785f190>; layer = <CALayer: 0x16699120>; contentOffset: {0, -64}; contentSize: {320, 659.5}>>"
cy# sectionInfo = #0x165c5570
#"<BBSectionInfo: 0x165c5570> Section com.alipay.iphoneclient 'Alipay': shows in NC = NO, Alert style = Banner, Lockscreen = YES, External = YES, Push settings = [s:BSA] [e:B-A], allows notifications = YES"
cy# [controller _setValue:@1 notificationType:0x10 forSectionInfo:sectionInfo]
cy# [controller _valueOfNotificationType:0x10 forSectionInfo:sectionInfo]
@true
As you may have already noticed, controller
and sectionInfo
were taken from the exact same addresses to the ones in CNSLogs, to make sure the caller and args were all the same. Now let’s take a look at CNSLog after I’ve changed the value with Cycript:
FunMaker-5:~ root# grep CNSLog /var/log/syslog | grep 1,
The return values were different! How could that happen?
As a 5-year noob writing tweaks and reversing iOS, I was totally lost on this bug. But I believed the bug could be fixed, with the help from our code wrangler, a.k.a. rpetrich. And he hit the point in less than 10 seconds:
Let’s run the command he mentioned on the fat NotificationsSettings binary and see what was there:
< - (id)_valueOfNotificationType:(unsigned int)arg1 forSectionInfo:(id)arg2;
---
> - (id)_valueOfNotificationType:(unsigned long long)arg1 forSectionInfo:(id)arg2;
As he further explained, this was a typedef issue:
The bug were bubbling to the surface then:
The 1st arg of _valueOfNotificationType:forSectionInfo:
was unsigned int
on 32-bit devices and unsigned long long
on 64-bit devices. The method was dumped from a 64-bit device, i.e. my iPad Air, and was then used on a 32-bit device, i.e. my iPhone 5. unsigned long long
was 64-bit long on both 32 and 64-bit devices, and required 2 32-bit registers to hold it. As I said on the book, _valueOfNotificationType:forSectionInfo:
's 2 args were stored in R2 and R3 respectively on 32-bit devices, but if the 1st arg was 64-bit long, R2 (a 32-bit register) would not be large enough to hold such a long arg, it had to be divided into 2 args and “will spill into the next register”, i.e. R3. Since the arg was 0x10, the lower 32-bit happened to be 0, it filled R3 with 0 a.k.a. nil. So, even if I called the method like this:
[controller _valueOfNotificationType:0x10 forSectionInfo:anExisitingSectionInfo];
It was actually executing like this:
[controller _valueOfNotificationType:0x10 forSectionInfo:nil];
Ultimately, it was my fault to have told clang the wrong arg type so clang extended a correct 32-bit value into a “wrong” 64-bit value, overwriting the other correct arg and causing the bug. What a stupid mistake!
Now that we know the root cause of the bug, let’s check it with LLDB.
Let’s set a breakpoint on the 1st instruction of
[BulletinBoardAppDetailController _valueOfNotificationType:forSectionInfo:]
and print all the args:
(lldb) br s -a 0x02ba0000+0x98D0
Breakpoint 2: where = NotificationsSettings`___lldb_unnamed_function67$$NotificationsSettings, address = 0x02ba98d0
Process 14721 stopped
* thread #1: tid = 0x3b009, 0x02ba98d0 NotificationsSettings`___lldb_unnamed_function67$$NotificationsSettings, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
frame #0: 0x02ba98d0 NotificationsSettings`___lldb_unnamed_function67$$NotificationsSettings
NotificationsSettings`___lldb_unnamed_function67$$NotificationsSettings:
-> 0x2ba98d0 <+0>: push {r4, r7, lr}
0x2ba98d2 <+2>: add r7, sp, #0x4
0x2ba98d4 <+4>: movw r0, #0xc876
0x2ba98d8 <+8>: mov r4, r2
(lldb) po $r0
<BulletinBoardAppDetailController 0x16a16000: navItem <UINavigationItem: 0x17878700>, view <UITableView: 0x17278e00; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x1785f190>; layer = <CALayer: 0x16699120>; contentOffset: {0, -64}; contentSize: {320, 659.5}>>
(lldb) p (char *)$r1
(char *) $1 = 0x00490f98 "_valueOfNotificationType:forSectionInfo:"
(lldb) po $r2
16
(lldb) po $r3
<nil>
R3, storing the 2nd arg, was nil. It wasn’t nil in our previous CNSLogs, right?
The fix to the bug is quite straightforward:
@interface BulletinBoardAppDetailController : PSListController
// - (BBSectionInfo *)effectiveSectionInfo; // iOS 8
// - (BBSectionInfo *)_effectiveSectionInfoForSectionInfo:(id)arg1 pushSetting:(NSUInteger)arg2; // iOS 9
#if __LP64__
- (NSNumber *)_valueOfNotificationType:(unsigned long long)arg1; // iOS 8
- (NSNumber *)_valueOfNotificationType:(unsigned long long)arg1 forSectionInfo:(BBSectionInfo *)arg2; // iOS 9
- (void)_setValue:(NSNumber *)arg1 notificationType:(unsigned long long)arg2; // iOS 8
- (void)_setValue:(NSNumber *)arg1 notificationType:(unsigned long long)arg2 forSectionInfo:(BBSectionInfo *)arg3; // iOS 9
#else
- (NSNumber *)_valueOfNotificationType:(unsigned int)arg1; // iOS 8
- (NSNumber *)_valueOfNotificationType:(unsigned int)arg1 forSectionInfo:(BBSectionInfo *)arg2; // iOS 9
- (void)_setValue:(NSNumber *)arg1 notificationType:(unsigned int)arg2; // iOS 8
- (void)_setValue:(NSNumber *)arg1 notificationType:(unsigned int)arg2 forSectionInfo:(BBSectionInfo *)arg3; // iOS 9
#endif
@end
Or, more elegant as it should be:
@interface BulletinBoardAppDetailController : PSListController
// - (BBSectionInfo *)effectiveSectionInfo; // iOS 8
// - (BBSectionInfo *)_effectiveSectionInfoForSectionInfo:(id)arg1 pushSetting:(NSUInteger)arg2; // iOS 9
- (NSNumber *)_valueOfNotificationType:(NSUInteger)arg1; // iOS 8
- (NSNumber *)_valueOfNotificationType:(NSUInteger)arg1 forSectionInfo:(BBSectionInfo *)arg2; // iOS 9
- (void)_setValue:(NSNumber *)arg1 notificationType:(NSUInteger)arg2; // iOS 8
- (void)_setValue:(NSNumber *)arg1 notificationType:(NSUInteger)arg2 forSectionInfo:(BBSectionInfo *)arg3; // iOS 9
@end
Note that NSUInteger
equals to unsigned long
/unsigned int
on 64/32-bit devices, and unsigned long
equals to unsigned long long
, according to this link, this link and this link
Learning from our own mistakes is always the best way to get better. Happy hacking!
P.S. According to rpetrich, NSUInteger, NSInteger and CGFloat may all subject to this problem, please pay special attention!
References:
- rpetrich
- https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Cocoa64BitGuide/64BitChangesCocoa/64BitChangesCocoa.html
- https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaTouch64BitGuide/Major64-BitChanges/Major64-BitChanges.html
- http://stackoverflow.com/questions/2107544/types-in-objective-c-on-iphone/2107549#2107549