震惊! 99%的人都不知道MacQQ竟然可以这样防撤回!


#1

iOSRe论坛一直很友好,没有屏蔽无帐号的用户。对于知识传播这是非常利好的。
作为一个站在众多前辈无私分险干货的肩膀上的菜鸟,分享此基础教程混个脸熟。

感谢列表:
@Aimer
@0xBBC
iOSRE官方交流群的众多热心人士


概述:
MacQQ防消息撤回 源自BlueCocoa分享的干货代码。官方链接在此:
https://blog.0xbbc.com/2017/04/prevent-qq-message-recall-in-macos/

因之前正好问过张总,在macOS下有啥好的Hook框架,张总介绍过一款神器。
因为没有应用场景,就一直没有玩儿过。
正好BlueCocoa在官方交流群分享了代码,借着BlueCocoa的代码
来完成一次动手实践活动,学习技术,顺便分享技能骗个TL2帐号:)

  1. substitute 下载,解决问题,编译
  2. 创建macOS dylib工程,抄袭BlueCocoa的工作成果
  3. DYLD_INSERT_LIBRARIES 与 load command
  4. insert_dylib 下载,编译
  5. 注入QQ
  6. @executable_path @loader_path @rpath
  7. install_name_tool 修复路径
  8. 完结撒花

0x01

开发环境

   CocoaPods : 1.2.0
        Ruby : ruby 2.2.6p396 (2016-11-15 revision 56800) [x86_64-darwin16]
    RubyGems : 2.6.11
        Host : Mac OS X 10.12.4 (16E195)
       Xcode : 8.3.1 (8E1000a)
         Git : git version 2.11.0 (Apple Git-81)
          QQ : QQ for Mac V5.5.1(25725)

张总介绍的macOS上面的Hook神器为:substitute
下载并查看readme.md,发现只有 To compile for iOS 描述,没有for macOS。。。
先执行一次万能的make试试水深。

davis-MBP:substitute-master daviyang$ ./configure
Xcode SDK platform path: u'/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform'
Using architectures for 'host': (native)
Found cpp for 'host': /usr/bin/xcrun --sdk macosx cc -E
Xcode SDK platform path: u'/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform'
Using architectures for 'asm-x86_64': ['x86_64']
Xcode SDK platform path: u'/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform'
Using architectures for 'asm-i386': ['i386']
Xcode SDK platform path: u'/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform'
Using architectures for 'asm-arm': ['armv7']
Xcode SDK platform path: u'/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform'
Using architectures for 'asm-arm64': ['arm64']
Found cc for 'host': /usr/bin/xcrun --sdk macosx cc
Found dsymutil for 'host': /usr/bin/xcrun --sdk macosx dsymutil
Found cc for 'asm-x86_64': /usr/bin/xcrun --sdk macosx cc -arch x86_64
Found cc for 'asm-i386': /usr/bin/xcrun --sdk macosx cc -arch i386
Found cc for 'asm-arm': /usr/bin/xcrun --sdk iphoneos cc -arch armv7
Found cc for 'asm-arm64': /usr/bin/xcrun --sdk iphoneos cc -arch arm64
Writing out/main.mk
Writing Makefile
Writing config.status

接下来就是make。这命令不要笑,是readme.md推荐的,跟我没关系:)

davis-MBP:substitute-master daviyang$ make -j8
··· 省略一些内容
./lib/darwin/find-syms.c:157:13: error: 'syscall' is deprecated: first deprecated in macOS 10.12 - syscall(2) is unsupported; please switch to a supported interface. For
      SYS_kdebug_trace use kdebug_signpost(). [-Werror,-Wdeprecated-declarations]
        if (syscall(294, &start_address)) /* shared_region_check_np */
            ^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/unistd.h:733:6: note: 'syscall' has been explicitly marked
      deprecated here
int      syscall(int, ...);
         ^
1 error generated.
make: *** [out/lib/darwin/find-syms.o] Error 1
make: *** Waiting for unfinished jobs....

出错了,但是平台确实选择了macOS,有得有失。
通过咨询一个不存在的网站找到一个解决方案
到这里下载老的MacOSX10.11.sdk,放入 /Applications/XCode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs

友情提醒,仓库的Release中有独立的下载压缩包。

再次make -j8 :slight_smile: ,得到相同的错误,这肯定得改编译参数了。

davis-MBP:substitute-master daviyang$ ./configure --help
*省略版面

  Xcode SDK options (host):

  --xcode-sdk ...       Use Xcode SDK - `xcodebuild -showsdks` lists; typical
                        values: macosx, iphoneos, iphonesimulator, watchos,
                        watchsimulator
*省略版面

找到了–xcode-sdk改变编译SDK,并且提示了 xcodebuild -showsdks 命令可以查看列表。

davis-MBP:substitute-master daviyang$ xcodebuild -showsdks
iOS SDKs:
	iOS 10.3                      	-sdk iphoneos10.3

iOS Simulator SDKs:
	Simulator - iOS 10.3          	-sdk iphonesimulator10.3

macOS SDKs:
	OS X 10.11                    	-sdk macosx10.11
	macOS 10.12                   	-sdk macosx10.12

tvOS SDKs:
	tvOS 10.2                     	-sdk appletvos10.2

tvOS Simulator SDKs:
	Simulator - tvOS 10.2         	-sdk appletvsimulator10.2

watchOS SDKs:
	watchOS 3.2                   	-sdk watchos3.2

watchOS Simulator SDKs:
	Simulator - watchOS 3.2       	-sdk watchsimulator3.2

重建正确的编译环境并编译。

davis-MBP:substitute-master daviyang$ ./configure --xcode-sdk macosx10.11 && make -j8
davis-MBP:substitute-master daviyang$ ls out/
_calc_darwin_target_conditionals.c	lib					libsubstitute.dylib			mconfig-hashes.txt
generated				libsubstitute.0.dylib			main.mk
davis-MBP:substitute-master daviyang$ file out/libsubstitute.dylib
out/libsubstitute.dylib: Mach-O 64-bit dynamically linked shared library x86_64

我们成功编译出了x86_64平台的libsubstitute.dylib,同时把substrate目录下的头文件拷贝备用。


#2

0x02

启动xcode ,创建工程。

macOS -》 Framework & Library -》Library
Framework:Cocoa
Type:Dynamic

将substitute.dylib与substitute.h添加到工程中。
删除自动创建的.h文件。
得到如下结构的工程

根据BlueCocoa的工作成果,防止QQ消息撤回,是nop掉了一个方法。
在dylib的构造函数中hook掉目标就OK。
编辑QQMessageRevoke.m的内容为:

#import <Foundation/Foundation.h>
#import "substrate.h"

void handleRecallNotifyIsOnline(id self, SEL _cmd, void * pVoid, BOOL isXXX)
{
    // nop
    // 可以考虑加NSLog用于测试
}

__attribute__((constructor))
static void initialize()
{
    // 使用@selector 会有编译警告,用NSSelectorFromString可消除。
    // 我们的目标是:0 warning 0 error
    SEL selector = NSSelectorFromString(@"handleRecallNotify:isOnline:");
    MSHookMessageEx(NSClassFromString(@"QQMessageRevokeEngine"),selector,(IMP)&handleRecallNotifyIsOnline,NULL);
}

编辑Scheme,将默认的编译Debug改为编译Release。
成功得到:libQQMessageRevoke.dylib


#3

0x03

BlueCocoa的文章 提到,有两种方案可以实现注入:

  1. DYLD_INSERT_LIBRARIES
  2. 修改可执行文件,增加load_commands命令

感觉第一种方案要简单一些,第二种方案牵扯到MachO与Load Command。
那我肯定就要选难的那个方案了 :sunglasses:

0x04

因长期混迹于iosre官方QQ群,见大神们讨论的多了多少还是知道一点。
注入dylib存在现成的工具的,这次使用的工具是 insert_dylib

下载编译出Release版的insert_dylib可执行文件,放 /usr/local/bin/ 并赋予可执行权限。

davis-MBP:substitute-master daviyang$ which insert_dylib
/usr/local/bin/insert_dylib
davis-MBP:substitute-master daviyang$ insert_dylib
Usage: insert_dylib dylib_path binary_path [new_binary_path]
Option flags: --inplace --weak --overwrite --strip-codesig --no-strip-codesig --all-yes

#4

0x05

将 0x01 与 0x02的成果:libsubstitute.dylib libQQMessageRevoke.dylib
放到QQ的可执行文件目录 /Applications/QQ.app/Contents/MacOS
拷贝一份原版的QQ防止意外

davis-MBP:MacOS daviyang$ ls
QQ				QQ的副本			libQQMessageRevoke.dylib	libsubstitute.dylib
davis-MBP:MacOS daviyang$ insert_dylib
Usage: insert_dylib dylib_path binary_path [new_binary_path]
Option flags: --inplace --weak --overwrite --strip-codesig --no-strip-codesig --all-yes

insert_dylib命令参数是:
insert_dylib <插入的dylib路径> <二进制路径> [新的二进制路径]

把 libQQMessageRevoke.dylib 注入到 QQ中

davis-MBP:MacOS daviyang$ insert_dylib libQQMessageRevoke.dylib QQ
LC_CODE_SIGNATURE load command found. Remove it? [y/n] y
Added LC_LOAD_DYLIB to QQ_patched
davis-MBP:MacOS daviyang$ otool -L QQ_patched | grep libQQMessageRevoke
	libQQMessageRevoke.dylib (compatibility version 0.0.0, current version 0.0.0)

libQQMessageRevoke成功的被注入到了QQ中

davis-MBP:MacOS daviyang$ ./QQ_patched
dyld: Library not loaded: /usr/local/lib/libsubstitute.0.dylib
  Referenced from: /Applications/QQ.app/Contents/MacOS/libQQMessageRevoke.dylib
  Reason: image not found
Abort trap: 6

不过执行却。。。 :scream:
从输出可以看出,dyld试着从 dyld: Library not loaded: /usr/local/lib/libsubstitute.0.dylib 路径加载动态库
我明明就放在了当前可执行文件路径下了啊,干嘛还找不到!(没错,Windows思想害的)

davis-MBP:MacOS daviyang$ otool -L libQQMessageRevoke.dylib | grep subs
	/usr/local/lib/libsubstitute.0.dylib (compatibility version 0.0.0, current version 0.0.0)

进一步跟踪问题,发现libQQMessageRevoke依赖的libsubstitute路径存在问题。

可执行文件中要链接的动态库在连接时已经写死在可执行文件中了,这个路径来自dylib内部。
我们需要调整这个路径,相对于可执行文件的当前目录下


#5

0x06 @executable_path @loader_path @rpath

上面的错误明显是路径错误,和Windows平台思想不同,不会尝试性的从几个地方搜索。
而是在链接时,就指定了一个固定的路径。
这里有篇文章讲解了路径问题:
http://www.cppblog.com/lauer3912/archive/2012/04/18/171814.aspx

根据我们的需求,我们要将Absolute paths更改为@executable_path。好消息是更改程序这次是xcode自带的。

0x07 install_name_tool

davis-MBP:MacOS daviyang$ install_name_tool
Usage: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/install_name_tool [-change old new] ... [-rpath old new] ... [-add_rpath new] ... [-delete_rpath old] ... [-id name] input

我们需要把 libQQMessageRevoke 对libsubstitute.0.dylib 的依赖路径改为
@executable_path/libsubstitute.dylib

davis-MBP:MacOS daviyang$ otool -L libQQMessageRevoke.dylib | grep subs
	/usr/local/lib/libsubstitute.0.dylib (compatibility version 0.0.0, current version 0.0.0)
davis-MBP:MacOS daviyang$ install_name_tool -change /usr/local/lib/libsubstitute.0.dylib @executable_path/libsubstitute.dylib libQQMessageRevoke.dylib
davis-MBP:MacOS daviyang$ otool -L libQQMessageRevoke.dylib | grep subs
	@executable_path/libsubstitute.dylib (compatibility version 0.0.0, current version 0.0.0)

成功改变了依赖路径,在控制台中执行QQ_patched。

davis-MBP:MacOS daviyang$ ./QQ_patched

可以正确的启动,如果在Hook函数中写了NSLog,那么可以测试的时候查看控制台输出。

我发现修改了QQ的签名后,之前记住的帐号密码是无法使用的(将QQ_patched改为QQ一样),不清楚原理。
输入帐号密码登录,无法撤回

功能测试通过,把QQ_patched改为QQ,覆盖原来的,这样子Dock图标可以直接启动啦。

davis-MBP:MacOS daviyang$ mv QQ_patched QQ
davis-MBP:MacOS daviyang$ ./QQ

点击Dock QQ图标,oh fuck! 提示无法启动!
明明命令行下都可以启动啊,而且都是QQ,这。。。不科学啊!

这种情况下基本上靠猜,结合经验来说,这多半又是路径的问题。。。

davis-MBP:MacOS daviyang$ otool -L QQ | grep voke
	libQQMessageRevoke.dylib (compatibility version 0.0.0, current version 0.0.0)

注意,结合0x06的文章,只有@开头的几个特殊路径代表相对路径,其他的都是绝对路径。
我们在控制台中能够正确执行,是因为当前工作目录就是QQ可执行文件所在的MacOS,libQQMessageRevoke.dylib也正好就在这里。

Dock启动,工作目录就不是这里了,当然无法启动。

基于这个猜测,继续修改

davis-MBP:MacOS daviyang$ install_name_tool -change libQQMessageRevoke.dylib @executable_path/libQQMessageRevoke.dylib QQ
davis-MBP:MacOS daviyang$ otool -L QQ | grep voke
	@executable_path/libQQMessageRevoke.dylib (compatibility version 0.0.0, current version 0.0.0)

把QQ的libQQMessageRevoke依赖路径也改为相对的。Dock点击,成功启动。

0x08 完结撒花

让我们再次感谢张总的答疑解惑,BlueCocoa的无私奉献。


#6

可以可以,纯技术分析


#7

很抱歉,就做了一点微小的工作.以及这个应用场景下并不需要substitute

那么问题来了,Mac微信怎么重签名

/sarcasm


#8

感觉弄复杂了,用好Xcode的framework模版和Run Script能轻松不少,妈妈再也不用担心我不会重签名了/逃


#9

技术分享,就要支持!:+1:


#11

流程略复杂,,,

  1. create dylib project
  2. DYLD_INSERT_LIBRARIES && run
  3. done

#13

mark, 找时间再回头来看。


#14

有抢红包了