零、前言
很多人问我为什么这么久不更新文章,其实我暑假去参加了大牛哥Mac逆向入门夏令营,可惜Mac逆向技术太高深,我只学到了一点皮毛。
回到宿舍后,我打开了电脑,发现Mac版QQ弹出了一个版本更新提示,说最新版v5.1.2
正式版已在官网发布,于是我便更新了。
不过更新完我就后悔了,因为我发现5.x
版本有些功能用不了:
1、不能选择忙碌状态:
要知道,我每次上QQ都会把状态设为忙碌,这样才能假装成很忙的样子。
2、不能发送本地图片,只能发送文件:
从上图可以看到,发送图片的按钮不见了。虽然可以把图片拖到聊天窗口发送,但操作还是感觉很麻烦。
因此,我便利用了从大牛哥那里学来的逆向基础知识,来给QQ加上这两个功能。
一、恢复忙碌状态选项
用Interface Inspector附加QQ的进程,然后定位到选择状态的按钮,如下图所示:
可以很明显地看出,菜单列表里有一个忙碌
的菜单项,后面有个划了一条斜线的眼睛,表示该菜单项是隐藏状态。
因此可以猜想程序员只是把该菜单项隐藏了,具体作用应该还保留着,如果将该菜单项设置为不隐藏的话,那么就很有可能可以使用忙碌的功能了。
那么怎样才能获取到这个菜单列表呢?首先我们可以看到状态按钮的类名是OnlineStateImagePopUpButton
,父类为NSPopUpButton
,查看NSPopUpButton
类的头文件,可以发现该类有一个itemArray
的属性,能够获取到菜单列表:
@interface NSPopUpButton : NSButton
// Accessing the items
@property (readonly, copy) NSArray<NSMenuItem *> *itemArray;
@property (readonly) NSInteger numberOfItems;
//......
@end
也就是说,如果能获取到状态按钮对象的话,就能获取到菜单列表了。由图片可知,状态按钮在MQAIOSelfInfoViewController2
视图控制器里。
用class-dump
获取QQ的头文件,找到MQAIOSelfInfoViewController2.h
,部分内容如下:
@interface MQAIOSelfInfoViewController2 : NSViewController <NSMenuDelegate>
{
unsigned long long _status;
NSButton *_avatarButton;
NSPopUpButton *_statusPopUpButton;
}
//......
@end
可以看到该视图控制器有一个_statusPopUpButton
属性,看名字基本可以肯定是状态按钮了。
那么我们可以hook该视图控制器的viewDidLoad
方法,这时状态按钮已经创建完毕,通过遍历状态按钮的菜单列表,将每个菜单的hidden
属性设为NO
。
二、创建插件工程
按照《使用EasySIMBL为Mac应用加载插件》教程里的方法安装EasySIMBL模板,然后用Xcode新建一个EasySIMBL插件工程,工程名为QQPlugin
:
然后hook-[MQAIOSelfInfoViewController2 viewDidLoad]
方法,代码如下所示:
@implementation NSObject (MQAIOSelfInfoViewController2BusyStatus)
+ (void)busyStatus_MQAIOSelfInfoViewController2
{
[self jr_swizzleMethod:@selector(viewDidLoad)
withMethod:@selector(busyStatus_viewDidLoad)
error:NULL];
}
- (void)busyStatus_viewDidLoad
{
[self busyStatus_viewDidLoad];
NSPopUpButton *popUpButton = [self valueForKey:@"_statusPopUpButton"];
for (NSMenuItem *menuItem in popUpButton.itemArray) {
menuItem.hidden = NO;
}
}
@end
编译后重新运行QQ,已经可以看到忙碌状态菜单了:
选择忙碌状态后,登录另一个QQ查看我当前的状态,确实变成忙碌了:
三、恢复发送图片功能
用Interface Inspector
定位到聊天框的工具栏,一共有7个按钮,这些按钮都没有设置隐藏属性:
可以看到聊天界面的工具栏也是7个按钮:
也就是说,如果要增加发送图片的按钮的话,就只能自己创建一个新的按钮,再加到工具栏上面了。
至于点击按钮会调用什么方法,可以通过逆向旧版本的QQ来获得。下面介绍一种方法,可以比较快速获取到按钮调用的方法:
首先下载4.x
版本的QQ,然后打开插件工程,按command + shift + ,快捷键打开Edit scheme
界面,Executable
那一项选择4.x
版本QQ的路径:
然后再按command + R运行插件工程,Xcode会自动运行QQ。
登录QQ后随便进入一个聊天窗口,接着在Xcode的debug工具栏里点击界面调试按钮:
当视图层次界面加载完毕之后,在层次界面里选中QQ聊天框工具栏的发送图片按钮:
在Xcode的右侧边栏可以看到该按钮的属性:
可以看到按钮调用的方法是-[MQAIOChatTootKitViewController onPicture:]
。
查看MQAIOChatTootKitViewController.h
文件,可以看到如下方法:
@interface MQAIOChatTootKitViewController : NSViewController
//......
@property(readonly) MQFaceButton *faceBtn;
@property(readonly) TXHoverButton *grabBtn;
@property(readonly) TXHoverButton *shakeBtn;
@property(readonly) TXHoverButton *pictureBtn;
@property(readonly) TXHoverButton *historyBtn;
@property(readonly) TXHoverButton *disruptBtn;
@property(readonly) MQSwitchButton *switchBtn;
- (void)onFaceButtonClick;
- (void)onGrabScreen:(id)arg1;
- (void)onShakeWindow:(id)arg1;
- (void)onPicture:(id)arg1;
- (void)onBlock:(id)arg1;
- (void)onMsgRecord:(id)arg1;
- (void)onClickSwitchButton:(id)arg1;
//......
@end
可以猜想pictureBtn
就是发送图片的按钮。
在5.x
版QQ的头文件里也可以看到这些方法,把5.x
版本QQ的可执行文件拖到Hopper里分析,然后查看-[MQAIOChatTootKitViewController pictureBtn]
方法的伪代码:
void * -[MQAIOChatTootKitViewController pictureBtn](void * self, void * _cmd) {
rbx = self;
rax = rbx->_pictureBtn;
if (rax == 0x0) {
r12 = *_OBJC_IVAR_$_MQAIOChatTootKitViewController._pictureBtn;
r13 = *objc_msgSend;
rax = [rbx buttonOfClass:[TXHoverButton class]];
rax = [rax retain];
*(rbx + r12) = rax;
[rbx setConstraintsForButton:rax];
[*(rbx + r12) setTarget:rbx];
[*(rbx + r12) setAction:@selector(onPicture:)];
[*(rbx + r12) setToolTip:[[NSBundle mainBundle] localizedStringForKey:@"Send pictures" value:@"" table:0x0]];
[*(rbx + r12) setNormalImage:[NSImage imageNamed:@"toolbar_pictures_normal"]];
[*(rbx + r12) setHoverImage:[NSImage imageNamed:@"toolbar_pictures_hover"]];
[*(rbx + r12) setAlternateImage:[NSImage imageNamed:@"toolbar_pictures_down"]];
rax = *(rbx + r12);
}
return rax;
}
可以看到该方法用懒加载的方式创建了一个按钮,也就是说代码还是保留的,只是没使用而已。那么我们可以先看其它按钮是怎么创建的,就看历史记录按钮好了。
按照《Hopper Disassembler批量导出反编译的伪代码》里的方法,反编译出MQAIOChatTootKitViewController
类所有方法的伪代码。
打开~/ClassDecompiles/QQ/MQAIOChatTootKitViewController.m
文件,搜索historyBtn
,可以很快发现以下代码:
void -[MQAIOChatTootKitViewController setupUI:](void * self, void * _cmd, int arg2) {
//......
loc_100086744:
if (LODWORD(r13) != 0x80) goto loc_100086dad;
rax = (r12)(r15, @selector(fileBtn));
r14 = var_A0;
(r12)(r14, @selector(addSubview:), rax);
rax = (r12)(r15, @selector(historyBtn));
(r12)(r14, @selector(addSubview:), rax);
rsi = r15->_fileBtn;
rax = _NSDictionaryOfVariableBindings(@"_fileBtn, _historyBtn", rsi);
var_A8 = rax;
rax = (r12)(NSLayoutConstraint, @selector(constraintsWithVisualFormat:options:metrics:views:), @"V:|-(top)-[_fileBtn]", 0x0, var_98, rax);
(r12)(r14, @selector(addConstraints:), rax);
rbx = (r12)(r15, @selector(historyBtn));
rax = (r12)(r15, @selector(fileBtn));
rax = (r12)(r15, @selector(topAlignView:andView:), rbx, rax);
(r12)(r14, @selector(addConstraint:), rax);
rdi = NSLayoutConstraint;
rdx = @"H:|-(leading)-[_fileBtn]-(gap1)-[_historyBtn]-(>=gap3)-|";
LODWORD(rcx) = 0x0;
rsi = @selector(constraintsWithVisualFormat:options:metrics:views:);
r8 = var_98;
//......
}
可以看到这些按钮是用自动布局的方法写的,因此可以考虑hook-[MQAIOChatTootKitViewController setupUI:]
方法,在这些按钮添加完毕后,把发送图片的按钮加到最后面。
要先知道前面按钮的数量,才能计算出最后一个按钮的位置。考虑到不同的聊天框可能会出现不同数量的按钮,所以不能写死7个。那么怎么才能获取到这些按钮的数量呢?
看一下前面出现过的图片:
可以看到这些按钮都是加在MQEventForwardView
类的实例对象里的,在MQAIOChatTootKitViewController.m
文件里搜索MQEventForwardView
,可以发现以下代码:
void * -[MQAIOChatTootKitViewController init](void * self, void * _cmd) {
var_28 = self;
var_20 = *0x1011d6e70;
r14 = @selector(init);
rbx = [[var_28 super] init];
if (rbx != 0x0) {
r15 = *objc_msgSend;
r14 = [[[MQEventForwardView alloc] init] autorelease];
[r14 setTranslatesAutoresizingMaskIntoConstraints:0x0];
[r14 setDelegate:rbx];
[rbx setView:r14];
MQRegisterNotificationInMainThread(@"kMQGroupEventNotification+weqe", rbx, @selector(handleGroupEventNotification:));
MQRegisterNotificationInMainThread(@"kMQDiscussEventNotification+dfsdf", rbx, @selector(handleDiscussEventNotification:));
MQRegisterNotificationInMainThread(@"kMQContactIMInfoEvtNotification", rbx, @selector(handleIMInfoEventNotification:));
}
rax = rbx;
return rax;
}
也就是说,当试图控制器初始化的时候,就将MQEventForwardView
实例对象赋值给了view
属性。那么使用self.view.subviews.count
方法就能获取到按钮的数量了。
最后的示例代码如下:
@implementation NSObject (MQAIOChatTootKitViewControllerSendPicture)
+ (void)sendPicture_MQAIOChatTootKitViewController
{
[self jr_swizzleMethod:@selector(setupUI:)
withMethod:@selector(sendPicture_setupUI:)
error:NULL];
}
- (void)sendPicture_setupUI:(int)arg1
{
[self sendPicture_setupUI:arg1];
MQAIOChatTootKitViewController *vc = (MQAIOChatTootKitViewController *)self;
NSInteger buttonCount = vc.view.subviews.count;
NSButton *pictureBtn = (NSButton *)vc.pictureBtn;
[vc.view addSubview:pictureBtn];
NSDictionary *metrics = @{
@"left" : @(20 + buttonCount * 40),
@"top" : @(10),
};
NSDictionary *views = NSDictionaryOfVariableBindings(pictureBtn);
[vc.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[pictureBtn]"
options:0
metrics:metrics
views:views]];
[vc.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[pictureBtn]"
options:0
metrics:metrics
views:views]];
}
@end
编译后重启QQ,可以看到出现了发送图片按钮:
点击后也确实可以发送图片了。
插件工程可以在QQPlugin下载。
很惭愧,就做了一点微小的工作,谢谢大家。
2016/09/21 23:03:57
我也开始喜欢上逆向了,想你学习
2016/09/23 00:20:22
很惭愧,就做了一点微小的工作。
2016/09/18 11:49:36
2016/09/23 00:19:24
赶快更新吧