Old bug reveals new knowledge: don't confuse armv7 and arm64 headers

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 :smile:
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:

  1. rpetrich :relaxed:
  2. https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Cocoa64BitGuide/64BitChangesCocoa/64BitChangesCocoa.html
  3. https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaTouch64BitGuide/Major64-BitChanges/Major64-BitChanges.html
  4. http://stackoverflow.com/questions/2107544/types-in-objective-c-on-iphone/2107549#2107549
2 个赞

Thank you for this explanation! This was super helpful… My only question is how did you get cycript working on your 64 bit iPad air? I seriously need it to fix some things :frowning:

edit: actually now that I re-read it, it seems like you were only debugging with cycript on your iPhone 5 and not the iPad air :’(

Yes, according to this thread, Cycript doesn’t work on arm64 devices with iOS 9 yet