文官洗碗安天下,武将打怪定乾坤。多么美好的年代,思之令人泪落。

恢复Mac版QQ一些隐藏的小功能

时间:2016-09-16 / 分类:优雅的iOS,学习园地,逆向工程 / 浏览:8922 / 4个评论 发表评论

零、前言

很多人问我为什么这么久不更新文章,其实我暑假去参加了大牛哥Mac逆向入门夏令营,可惜Mac逆向技术太高深,我只学到了一点皮毛。

回到宿舍后,我打开了电脑,发现Mac版QQ弹出了一个版本更新提示,说最新版v5.1.2正式版已在官网发布,于是我便更新了。

不过更新完我就后悔了,因为我发现5.x版本有些功能用不了:

1、不能选择忙碌状态:
image
要知道,我每次上QQ都会把状态设为忙碌,这样才能假装成很忙的样子。

2、不能发送本地图片,只能发送文件:
image
从上图可以看到,发送图片的按钮不见了。虽然可以把图片拖到聊天窗口发送,但操作还是感觉很麻烦。

因此,我便利用了从大牛哥那里学来的逆向基础知识,来给QQ加上这两个功能。

一、恢复忙碌状态选项

Interface Inspector附加QQ的进程,然后定位到选择状态的按钮,如下图所示:
image
可以很明显地看出,菜单列表里有一个忙碌的菜单项,后面有个划了一条斜线的眼睛,表示该菜单项是隐藏状态。

因此可以猜想程序员只是把该菜单项隐藏了,具体作用应该还保留着,如果将该菜单项设置为不隐藏的话,那么就很有可能可以使用忙碌的功能了。

那么怎样才能获取到这个菜单列表呢?首先我们可以看到状态按钮的类名是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
image

然后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,已经可以看到忙碌状态菜单了:
image

选择忙碌状态后,登录另一个QQ查看我当前的状态,确实变成忙碌了:
image

三、恢复发送图片功能

Interface Inspector定位到聊天框的工具栏,一共有7个按钮,这些按钮都没有设置隐藏属性:
image

可以看到聊天界面的工具栏也是7个按钮:
image

也就是说,如果要增加发送图片的按钮的话,就只能自己创建一个新的按钮,再加到工具栏上面了。

至于点击按钮会调用什么方法,可以通过逆向旧版本的QQ来获得。下面介绍一种方法,可以比较快速获取到按钮调用的方法:

首先下载4.x版本的QQ,然后打开插件工程,按command + shift + ,快捷键打开Edit scheme界面,Executable那一项选择4.x版本QQ的路径:
image
然后再按command + R运行插件工程,Xcode会自动运行QQ。

登录QQ后随便进入一个聊天窗口,接着在Xcode的debug工具栏里点击界面调试按钮:
image

当视图层次界面加载完毕之后,在层次界面里选中QQ聊天框工具栏的发送图片按钮:
image

在Xcode的右侧边栏可以看到该按钮的属性:
image
可以看到按钮调用的方法是-[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个。那么怎么才能获取到这些按钮的数量呢?

看一下前面出现过的图片:
image
可以看到这些按钮都是加在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,可以看到出现了发送图片按钮:
image
点击后也确实可以发送图片了。

插件工程可以在QQPlugin下载。

很惭愧,就做了一点微小的工作,谢谢大家。

标签: , ,
本文链接: 恢复Mac版QQ一些隐藏的小功能
版权所有: 破博客, 转载请注明本文出处。

4个评论

  1. 指尖上的艺术
    2016/09/21 23:03:57

    我也开始喜欢上逆向了,想你学习 :neutral:

    • admin
      2016/09/23 00:20:22

      很惭愧,就做了一点微小的工作。

  2. 4ch0nery
    2016/09/18 11:49:36

    :evil: 坐等更新

发表评论

*

* (显示gravatar头像)

Ctrl+Enter快捷回复

© 2019 破博客 all rights reserved.