在iOS 8中,Notes原始的阅览界面是这样的,如图7-2所示。
要在这个界面显示这条note的字数,在哪里显示比较好看呢?不知道你对iOS 6上的Notes有没有印象,那时的每条note都有一个居中显示的标题,如图7-3所示。
图7-2 iOS 8的Notes阅览界面
图7-3 iOS 6的Notes阅览界面
iOS 8把这个标题给去掉了,整个导航栏显得空空荡荡的,不如我们就把字数加在标题所在的位置,如图7-4所示。
效果还不赖!要把Notes改造成这样,需要做些什么工作呢?还记得第5章里说过,在iOS里你看见的每个东西都是一个对象吗?记住这个准则,一起来思考。
1)每条note都是一个对象,阅览界面涵盖了一条note的内容及修改时间等信息,这些信息都来源于这条note。阅览界面是一个view,可以通过nextResponder追溯到它的controller,用controller又可以访问note的相关数据,可以用于在刚刚进入阅览界面的时候初始化字数标题。
2)当编辑一条note时,阅览界面的右上方会出现一个“Done”的按钮,如图7-5所示。
点击“Done”之后,这条note被保存下来。这个现象说明一条note在编辑过程中是不会实时保存的,不然就不需要这个按钮了。而字数标题随着内容的编辑实时变化的效果是最好的,要达到这种效果,就需要一个实时监测note内容变化的方法,且要从这个方法里拿到当前的字数,实时更新标题——因为这种方法一般是定义在protocol里的,所以要留意各种protocol里有没有出现这类函数。
3)如果已经搞定了字数,要怎么把它放在导航栏上呢?阅览界面的controller想必是一个UIViewController的子类,而UIViewController有一个名为title的property,因此只要简简单单地调用setTitle:就可以了。
图7-4 加上字数之后的阅览界面
图7-5 阅览界面出现“Done”按钮
如果能解决上面3个问题,Characount for Notes的技术难点就算全部拿下。没有更多需要解释的了,我们开始动手吧~
在“/Applications/”下踅摸了一圈,没有名为“Notes.app”的文件夹。这种情况下,该怎么定位Notes所在的文件夹呢?还记得在dumpdecrypted章节里找App目录的小技巧吗?是的,就是用ps命令:先关掉所有的App,然后打开Notes。接着ssh到iOS中,用ps命令看看当前有哪些进程来自“/Applications/”,如下:
FunMaker-5:~ root# ps -e | grep /Applications/ 592 ?? 0:37.70 /Applications/MobileMail.app/MobileMail 761 ?? 0:02.78 /Applications/MessagesNotificationViewService.app/MessagesNotificationViewService 1807 ?? 0:00.55 /private/var/db/stash/_.29LMeZ/Applications/MobileSafari.app/webbookmarksd 2016 ?? 0:05.23 /Applications/InCallService.app/InCallService 2619 ?? 0:02.66 /Applications/MobileSMS.app/MobileSMS 2672 ?? 0:01.20 /Applications/MobileNotes.app/MobileNotes 2678 ttys000 0:00.01 grep /Applications/
其中,最可疑的当然就是MobileNotes了,怎么验证呢?kill掉它,看看已经打开的Notes会不会闪退,如下:
FunMaker-5:~ root# killall MobileNotes
Notes果然退出了,说明“/Applications/MobileNotes.app/MobileNotes”就是Notes的可执行文件,而且同时还知道了“/Applications/”下运行在后台的一些应用。把MobileNotes拷贝到OSX中,准备class-dump。
因为Notes不是从AppStore下载的,没有加壳,所以可以直接使用class-dump,如下:
snakeninnys-MacBook:~ snakeninny$ class-dump -S -s -H /Users/snakeninny/Code/iOSSystemBinaries/8.1_iPhone5/MobileNotes.app/MobileNotes -o /Users/snakeninny/Code/iOSPrivateHeaders/8.1/MobileNotes
一共有88个头文件,粗略扫一眼,看看能发现什么,如图7-6所示。
图7-6 class-dump头文件
看到图7-6中选中的文件了吗?是不是它,我们现在不知道,也不用急于猜测,结果马上就会揭晓了。
百试不爽的recursiveDescription又要派上用场了,如下:
FunMaker-5:~ root# cycript -p MobileNotes cy# ?expand expand == true cy# [[UIApp keyWindow] recursiveDescription] @"<UIWindow: 0x17688db0; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x17689620>; layer = <UIWindowLayer: 0x17688fc0>> | <UILayoutContainerView: 0x175bb880; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x175bb900>> | | <UILayoutContainerView: 0x17699350; frame = (0 0; 320 568); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x1769cf60>; layer = <CALayer: 0x17699530>> | | | <UINavigationTransitionView: 0x176564c0; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x17658ec0>> | | | | <UIViewControllerWrapperView: 0x176d13b0; frame = (0 0; 320 568); layer = <CALayer: 0x176d1530>> | | | | | <UILayoutContainerView: 0x1769dd80; frame = (0 0; 320 568); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x176a16f0>; layer = <CALayer: 0x1769de00>> | | | | | | <UINavigationTransitionView: 0x1769ebb0; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x1769ec40>> | | | | | | | <UIViewControllerWrapperView: 0x175109e0; frame = (0 0; 320 568); layer = <CALayer: 0x175109b0>> | | | | | | | | <NotesBackgroundView: 0x175ee3e0; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x17510a70>; layer = <CALayer: 0x175ee580>> | | | | | | | | | <NotesTextureBackgroundView: 0x175ee5b0; frame = (0 0; 320 568); clipsToBounds = YES; layer = <CALayer: 0x175ee630>> | | | | | | | | | | <NotesTextureView: 0x175ee940; frame = (0 -64; 320 640); layer = <CALayer: 0x175ee9c0>> | | | | | | | | | <NoteContentLayer: 0x176c5110; frame = (0 0; 320 568); layer = <CALayer: 0x176ca850>> | | | | | | | | | | <UIView: 0x175f2130; frame = (16 0; 288 0); hidden = YES; layer = <CALayer: 0x175dd2b0>> | | | | | | | | | | <NotesScrollView: 0x175f2a10; baseClass = UIScrollView; frame = (0 0; 320 568); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x175f1b70>; layer = <CALayer: 0x175f28d0>; contentOffset: {0, -64}; contentSize: {320, 460}> | | | | | | | | | | | <UIView: 0x175f09a0; frame = (0 0; 320 0); layer = <CALayer: 0x175f2790>> | | | | | | | | | | | <UIView: 0x175f27e0; frame = (0 0; 0 460); layer = <CALayer: 0x175f2850>> | | | | | | | | | | | <NoteDateLabel: 0x175f3400; baseClass = UILabel; frame = (69 5.5; 182 18); text = 'November 24, 2014, 20:44'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x175f3560>> | | | | | | | | | | | <NoteTextView: 0x175ee3e0; baseClass = _UICompatibilityTextView; frame = (6 28; 308 418); text = 'Secret'; clipsToBounds = YES; gestureRecognizers = <NSArray: 0x176c7ed0>; layer = <CALayer: 0x176d88e0>; contentOffset: {0, 0}; contentSize: {308, 52}> ……
果不其然有一个NoteTextView,且“Secret”就位于其中。持续调用nextResponder,找出它的controller,如下:
cy# [#0x175ee3e0 nextResponder] #"<NotesScrollView: 0x17d307c0; baseClass = UIScrollView; frame = (0 0; 320 568); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x17e502a0>; layer = <CALayer: 0x17d30b60>; contentOffset: {0, -64}; contentSize: {320, 251}>" cy# [#0x17d307c0 nextResponder] #"<NoteContentLayer: 0x17e505b0; frame = (0 0; 320 568); layer = <CALayer: 0x17e50470>>" cy# [#0x17e505b0 nextResponder] #"<NotesBackgroundView: 0x17e52320; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x17d0c940>; layer = <CALayer: 0x17e522f0>>" cy# [#0x17e52320 nextResponder] #"<NotesDisplayController: 0x17edc340>"
好的,就是NoteDisplayController了。看看如下直接调用setTitle:能否改变阅览界面的标题:
cy# [#0x17edc340 setTitle:@"Characount = Character count"]
效果如图7-7所示。
图7-7 setTitle:的效果
没有任何问题,第一目标达成!
趁热打铁,到NoteDisplayController.h里看看它的定义,如下:
@interface NotesDisplayController : UIViewController <NoteContentLayerDelegate, UIActionSheetDelegate, AFContextProvider, UIPopoverPresentationControllerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate, NotesQuickLookActivityItemDelegate, ScrollViewKeyboardResizerDelegate, NSUserActivityDelegate, NotesStateArchiving> { …… @property(nonatomic, getter=isVisible) BOOL visible; // @synthesize visible=_visible; - (void)loadView; @property(retain, nonatomic) NoteObject *note; // @synthesize note=_note; …… }
文件的内容很多,通览之后,我们发现了一个NoteObject类型的属性。虽说一个note就是一个对象,但NoteObject的名称含义是不是也太明显了……在Cycript里把它打印出来看看,如下:
cy# [#0x17edc340 note] #'<NoteObject: 0x176aa170> (entity: Note; id: 0x176a9040 <x-coredata://4B88CC7C-7A5F-4F15-9275-53C6D0ABE0C3/Note/p15> ; data: {\n attachments = (\n );\n author = nil;\n body = "0x176a8b20 <x-coredata://4B88CC7C-7A5F-4F15-9275-53C6D0ABE0C3/NoteBody/p15>";\n containsCJK = 0;\n contentType = 0;\n creationDate = "2014-11-24 05:00:59 +0000";\n deletedFlag = 0;\n externalFlags = 0;\n externalSequenceNumber = 0;\n externalServerIntId = "-4294967296";\n guid = "781B6C87-2855-4512-8864-50618754333A";\n integerId = 3865;\n isBookkeepingEntry = 0;\n modificationDate = "2014-11-24 12:44:08 +0000";\n serverId = nil;\n store = "0x175a2b60 <x-coredata://4B88CC7C-7A5F-4F15-9275-53C6D0ABE0C3/Store/p1>";\n summary = nil;\n title = Secret;\n})'
很明显,NoteObject就是当前显示的note,各个字段含义都比较清晰,现在去看看它的定义,如下:
@interface NoteObject : NSManagedObject { } - (BOOL)belongsToCollection:(id)arg1; @property(nonatomic) unsigned long long sequenceNumber; - (BOOL)containsAttachments; @property(retain, nonatomic) NSString *externalContentRef; @property(retain, nonatomic) NSData *externalRepresentation; @property(readonly, nonatomic) BOOL hasValidServerIntId; @property(nonatomic) long long serverIntId; @property(nonatomic) unsigned long long flags; @property(readonly, nonatomic) NSURL *noteId; @property(readonly, nonatomic) BOOL isBeingMarkedForDeletion; @property(readonly, nonatomic) BOOL isMarkedForDeletion; - (void)markForDeletion; @property(nonatomic) BOOL isPlainText; - (id)contentAsPlainTextPreservingNewlines; @property(readonly, nonatomic) NSString *contentAsPlainText; @property(retain, nonatomic) NSString *content; // Remaining properties @property(retain, nonatomic) NSSet *attachments; // @dynamic attachments; @property(retain, nonatomic) NSString *author; // @dynamic author; @property(retain, nonatomic) NoteBodyObject *body; // @dynamic body; @property(retain, nonatomic) NSNumber *containsCJK; // @dynamic containsCJK; @property(retain, nonatomic) NSNumber *contentType; // @dynamic contentType; @property(retain, nonatomic) NSDate *creationDate; // @dynamic creationDate; @property(retain, nonatomic) NSNumber *deletedFlag; // @dynamic deletedFlag; @property(retain, nonatomic) NSNumber *externalFlags; // @dynamic externalFlags; @property(retain, nonatomic) NSNumber *externalSequenceNumber; // @dynamic externalSequenceNumber; @property(retain, nonatomic) NSNumber *externalServerIntId; // @dynamic externalServerIntId; @property(readonly, retain, nonatomic) NSString *guid; // @dynamic guid; @property(retain, nonatomic) NSNumber *integerId; // @dynamic integerId; @property(retain, nonatomic) NSNumber *isBookkeepingEntry; // @dynamic isBookkeepingEntry; @property(retain, nonatomic) NSDate *modificationDate; // @dynamic modificationDate; @property(retain, nonatomic) NSString *serverId; // @dynamic serverId; @property(retain, nonatomic) NoteStoreObject *store; // @dynamic store; @property(retain, nonatomic) NSString *summary; // @dynamic summary; @property(retain, nonatomic) NSString *title; // @dynamic title; @end
非常好,这么多的property表明NoteObject是个非常标准的model。如何获取它的文字内容呢?在上面的代码中,看到了一个名为contentAsPlainText的property,像下面这样调用它看看是什么效果:
cy# [#0x176aa170 contentAsPlainText] @"Secret"
为了进一步确认,改一下这条note的文字,再配一张图,如图7-8所示。
图7-8 重新编辑这条note
然后重新调用contentAsPlainText,如下:
cy# [#0x176aa170 contentAsPlainText] @"bbs.iosre.com"
基本可以确定这个函数能够正确返回当前note的文字内容了,对它调用length就可以拿到这条note的文字个数,如下:
cy# [[#0x176aa170 contentAsPlainText] length] 13
还有最后一项任务,咱们快马加鞭,把它搞定!
在本章开头部分已经提到,“实时监测note内容变化的方法一般是定义在protocol里的”。因为设置标题的函数,以及获取note对象的操作都是通过NotesDisplayController类完成的,所以如果能在这个类里找到一个符合条件的方法,那另两项操作就可以放在这个方法里完成了,可以极大地简化代码。打开NotesDisplayController.h,看看它实现了哪些协议,如下:
@interface NotesDisplayController:UIViewController<NoteContentLayerDelegate,UIActionSheetDelegate,AFContextProvider,UIPopoverPresentationControllerDelegate,UINavigationControllerDelegate,UIImagePickerControllerDelegate,NotesQuickLookActivityItemDelegate,ScrollViewKeyboardResizerDelegate,NSUserActivityDelegate,NotesStateArchiving> …… @end
其中UIActionSheetDelegate、UIPopoverPresentationControllerDelegate、UINavigation-ControllerDelegate、UIImagePickerControllerDelegate都是公开协议,明显跟note内容的变化没关系,可以直接排除掉了。剩下的NoteContentLayerDelegate、AFContextProvider、NotesQu-ickLookActivityItemDelegate、ScrollViewKeyboardResizerDelegate、NSUserActivityDelegate和NotesStateArchiving都不能轻易放过,需要逐个排查。先看NoteContentLayerDelegate-Protocol.h,如下:
@protocol NoteContentLayerDelegate <NSObject> - (BOOL)allowsAttachmentsInNoteContentLayer:(id)arg1; - (BOOL)canInsertImagesInNoteContentLayer:(id)arg1; - (void)insertImageInNoteContentLayer:(id)arg1; - (BOOL)isNoteContentLayerVisible:(id)arg1; - (BOOL)noteContentLayer:(id)arg1 acceptContentsFromPasteboard:(id)arg2; - (BOOL)noteContentLayer:(id)arg1 acceptStringIncreasingContentLength:(id)arg2; - (BOOL)noteContentLayer:(id)arg1 canHandleLongPressOnElement:(id)arg2; - (void)noteContentLayer:(id)arg1 containsCJK:(BOOL)arg2; - (void)noteContentLayer:(id)arg1 contentScrollViewWillBeginDragging:(id)arg2; - (void)noteContentLayer:(id)arg1 didChangeContentSize:(struct CGSize)arg2; - (void)noteContentLayer:(id)arg1 handleLongPressOnElement:(id)arg2 atPoint:(struct CGPoint)arg3; - (void)noteContentLayer:(id)arg1 setEditing:(BOOL)arg2 nimated:(BOOL)arg3; - (void)noteContentLayerContentDidChange:(id)arg1 updatedTitle:(BOOL)arg2; - (BOOL)noteContentLayerShouldBeginEditing:(id)arg1; @optional - (void)noteContentLayerKeyboardDidHide:(id)arg1; @end
其中,noteContentLayer:didChangeContentSize:和noteContentLayerContentDidChange:u-pdatedTitle:这两个方法有些可疑,编辑一条note时,这条note的内容和内容所占的尺寸都在实时变化,因此这两个方法确实有被实时调用的可能性。查看NotesDisplayController.h,这两个协议方法也都被实现了。为了确定它们有没有被实时调用,考虑用LLDB验证一下。
用LLDB附加MobileNotes,看看MobileNotes的ASLR偏移,如下:
(lldb) image list -o -f [ 0] 0x00035000 /private/var/db/stash/_.29LMeZ/Applications/MobileNotes.app/MobileNotes(0x0000000000039000) [ 1] 0x00197000 /Library/MobileSubstrate/MobileSubstrate.dylib (0x0000000000197000) [ 2] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/Frameworks/QuickLook.framework/QuickLook ……
ASLR偏移是0x35000。然后把MobileNotes拖进IDA,待初始分析完成后,查看[NotesDisplay-Controller noteContentLayer:didChangeContentSize:]和[NotesDisplayController noteContent-LayerContent-DidChange:updatedTitle:]的基地址,如图7-9和图7-10所示。
图7-9 [NotesDisplayController noteContentLayer:didChangeContentSize:]
图7-10 [NotesDisplayController noteContentLayerContentDidChange:updatedTitle:]
两者的基地址分别是0x16E70和0x1AEB8,因此断点地址分别是0x4BE70和0x4FEB8。下2个断点,然后随便打开一条note并编辑它,看看断点会不会停,如下:
(lldb) br s -a 0x4BE70 Breakpoint 1: where = MobileNotes`___lldb_unnamed_function382$$MobileNotes, address = 0x0004be70 (lldb) br s -a 0x4FEB8 Breakpoint 2: where = MobileNotes`___lldb_unnamed_function458$$MobileNotes, address = 0x0004feb8
你得到的结果肯定跟笔者的一模一样——2个断点都会被触发很多次!协议方法被调用,一般是因为方法名中提到的那个事件发生了;而那件事发生的对象,一般是协议方法的参数。在当前情况下,则表明发生了didChangeContentSize和ContentDidChange事件,而content本身很可能就是参数。接着来看看这两个函数的第一个参数是什么,如下:
(lldb) br com add 1 Enter your debugger command(s). Type 'DONE' to end. > po $r2 > c > DONE (lldb) br com add 2 Enter your debugger command(s). Type 'DONE' to end. > po $r2 > c > DONE (lldb) c
可以看到,输出中有很多的NoteContentLayer,如下:
Process 24577 resuming Command #2 'c' continued the target. <NoteContentLayer: 0x14ecdf50; frame = (0 0; 320 568); animations = { bounds.origin=<CABasicAnimation: 0x16fee090>; bounds.size=<CABasicAnimation: 0x16fee4a0>; position=<CABasicAnimation: 0x16fee500>; }; layer = <CALayer: 0x14eca900>> Process 24577 resuming Command #2 'c' continued the target. <NoteContentLayer: 0x14ecdf50; frame = (0 0; 320 568); animations = { bounds.origin=<CABasicAnimation: 0x16fee090>; bounds.size=<CABasicAnimation: 0x16fee4a0>; position=<CABasicAnimation: 0x16fee500>; }; layer = <CALayer: 0x14eca900>> Process 24577 resuming Command #2 'c' continued the target. <NoteContentLayer: 0x14ecdf50; frame = (0 0; 320 568); layer = <CALayer: 0x14eca900>> Process 24577 resuming Command #2 'c' continued the target.
既然能拿到NoteContentLayer,多半能够从中获取NoteContent。打开NoteContentLayer.h,看看它提供了些什么方法,如下:
@interface NoteContentLayer : UIView <NoteTextViewActionDelegate, Note TextViewLayoutDelegate, UITextViewDelegate> …… @property(retain, nonatomic) NoteTextView *textView; // @synthesize textView=_textView; …… @end
NoteContentLayer有一个NoteTextView的属性,而在本章的开头,用Cycript打印UI层次的时候,发现一条note的文字就是显示在NoteTextView之上的。更改一下断点命令,把NoteTextView打印出来看看,如下:
(lldb) br com add 1 Enter your debugger command(s). Type 'DONE' to end. > po [$r2 textView] > c > DONE (lldb) br com add 2 Enter your debugger command(s). Type 'DONE' to end. > po [$r2 textView] > c > DONE
然后继续编辑这条note,发现对note文字的改动全都体现在了LLDB的输出上,如下:
Process 24577 resuming Command #2 'c' continued the target. <NoteTextView: 0x15aace00; baseClass = _UICompatibilityTextView; frame = (6 28; 308 209); text = 'Secre'; clipsToBounds = YES; gestureRecognizers = <NSArray: 0x14eddfc0>; layer = <CALayer: 0x14ee7da0>; contentOffset: {0, 0}; contentSize: {308, 52}> Process 24577 resuming Command #2 'c' continued the target. <NoteTextView: 0x15aace00; baseClass = _UICompatibilityTextView; frame = (6 28; 308 209); text = 'Secret'; clipsToBounds = YES; gestureRecognizers = <NSArray: 0x14eddfc0>; layer = <CALayer: 0x14ee7da0>; contentOffset: {0, 0}; contentSize: {308, 52}>
最后一个步骤,就是从NoteTextView拿到text。打开NoteTextView.h,如下:
@interface NoteTextView : _UICompatibilityTextView <UIGestureRecognizerDelegate> { id <NoteTextViewActionDelegate> _actionDelegate; id <NoteTextViewLayoutDelegate> _layoutDelegate; …… } …… @property(nonatomic) __weak id <NoteTextViewActionDelegate> actionDelegate; // @synthesize actionDelegate=_actionDelegate; …… @property(nonatomic) __weak id <NoteTextViewLayoutDelegate> layoutDelegate; // @synthesize layoutDelegate=_layoutDelegate; …… @end
它的实现并不长,但里面唯一含有text字样的,是2个delegate,显然不会返回NSString对象。text不在它自己实现里,就一定在它的父类里,打开_UICompatibilityTextView.h,如下:
@interface _UICompatibilityTextView : UIScrollView <UITextLinkInteraction, UITextInput> …… @property(nonatomic) int textAlignment; @property(copy, nonatomic) NSString *text;- (BOOL)hasText; @property(retain, nonatomic) UIColor *textColor; @property(retain, nonatomic) UIFont *font; @property(copy, nonatomic) NSAttributedString *attributedText; ……
原来text在这里。用LLDB做最后的确认,如下:
(lldb) br com add 1 Enter your debugger command(s). Type 'DONE' to end. > po [[$r2 textView] text] > c > DONE (lldb) br com add 2 Enter your debugger command(s). Type 'DONE' to end. > po [[$r2 textView] text] > c > DONE Secret Process 24577 resuming Command #2 'c' continued the target. Secret i Process 24577 resuming Command #2 'c' continued the target.
至此,我们成功找到了2个实时监测note内容变化的方法(随便选一个就好了,我们选第二个),并且可以拿到note的实时文本数据,早前设计的3个功能现在已经全部实现。不难吧?