前言
购买了《iOS应用逆向工程》这本书后,我只是抱着书本看了几天,但是却缺少实战经验,书本上的内容也已经忘记了差不多.最近我浏览了一些关于iOS逆向工程的技术博客,心里萌生了练练手的想法.所以就选择了微信防撤回这个题目,开始这次逆向工程的一次练手小项目.本篇博客仅作记录之用.
准备
在这里先介绍一下iOS逆向工程中用到的一些工具.
Mac端
- iFunBox : Mac端上的iPhone文件管理工具.
- Hopper Disassembler : Mac端上的反汇编工具.
- USBmuxd : Mac端上的端口转发工具.
- Theos : 越狱开发工具包.
- class-dump : 用于取得应用的头文件
iOS
- Cycript : 用作运行时运行特定的方法.
- LLDB与debugserver : 动态调试工具.
- dumpdecrypted : 砸壳工具.
- OpenSSH : 提供ssh接入的工具.
- Filza : 文件管理工具.
本次逆向使用的是不完美越狱的运行iOS 10.2的iPad Air 2.
开始
在iOS设备中对可执行文件砸壳
因为从App Store中下载回来的app都是经过加密的.所以直接使用class dump的话并没有用.所以需要用dumpdecrypted对可执行文件砸壳.
- 用iFunBox把dumpdecrypted.dylib文件拷贝到Media目录.再用Filza把dumpdecrypted.dylib拷贝到微信的documents目录.(因为iOS10.2不完美越狱不兼容AppSync和afc2,所以使用Mac端的pp助手会不稳定,所以使用iFunBox.)
- ssh到手机的终端,cd到documents目录中,执行下面的命令进行砸壳操作
xxx$ cp /usr/lib/dumpdecrypted.dylib /path/to/app/document
xxx$ DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /path/to/WeChat
- 最后砸壳完成后会在documents目录生成砸了壳后的二进制文件,拷贝出来并class-dump他的头文件备用.(建议还是使用Filza和iFunbox)
class dump二进制文件
使用以下命令提取头文件,将头文件保存到path/to/headers/Wechat/,并且头文件内容按名字排序.
xxx$ class-dump -S -s -H WeChat -o /path/to/headers/Wechat/
至此,我们得到了一个拥有8000多个头文件的文件夹.
尝试定位tweak入口
直接使用Mac上的搜索功能.既然是撤回,那么先搜索revokeMsg或者revokeMessage这种关键字:
可以发现,有多个头文件都包含这两个关键字中的一个.先观察FunctionMsgMgr
,BaseMsgContentViewController
,CMessageMgr
这几个类.从名字可以看出,MsgMgr应该就是一个对消息进行管理的类.而BaseMsgContentViewController与聊天窗口的类有关.
使用lldb和debugserver
现在我们有几个和撤回消息有关的备选的类,究竟哪个才是真正和别人撤回消息有关的呢?
为了筛选这几个类,我们可以使用使用lldb和debugserver.安装lldb与debugserver的内容请参考《iOS应用逆向工程》.
因为通过wifi进行ssh然后使用lldb速度太慢,我们可以使用usbmux进行端口转发,提高效率.新建一个终端,找到usbmux/python-client/tcprelay.py
xxx$ path/to/tcprelay.py -t 22:2222
Forwarding local port 2222 to remote port 22
因为iOS中的ssh的默认监听端口是22,这个命令的意思是把Mac的2222端口转发到iOS设备上的22端口.
再新建一个终端,ssh到iOS设备上
xxx$ ssh root@localhost -p 2222
新建第三个终端,再将一个端口转发到iOS设备上,推荐10000以后的端口
xxx$ path/to/tcprelay.py -t 12345:12345
先运行微信,然后在运行ssh的终端中打开debugserver
xxx~ root# debugserver *:12345 -a WeChat
打开第四个终端窗口,运行lldb
xxx$ /path/to/lldb
(lldb) process connect connect://localhost:12345
至此,我们在Mac上运行了lldb和在iOS上运行了debugserver.之后就要添加断点了.一般情况下,我们需要知道指令的内存地址才能添加断点.而因为ASLR
ASLR(Address Space Layout Randomization)http://theiphonewiki.com/wiki/ASLR
的关系,每次进程启动时,用一进程的所有模块在虚拟内存中的起始地址都会产生随机偏移.所以我们需要使用这条公式
实际地址 = 静态地址 + 偏移地址
来计算增加断点的实际地址.那么如何获取偏移地址呢?使用
(lldb) image list -o -f
然后ctrl + f 搜索wechat,可以获得偏移地址.下面我们将获取静态地址.
使用hopper
将砸壳后的二进制文件拖入hopper.这里先介绍一下hopper的基本使用.拖入二进制文件后并等待hopper处理完毕后,hopper的界面会这样呈现.在hopper处理完整个二进制文件之前,你所看到的很大一部分都会是乱码.然么看hopper到底处理完毕没有呢?留意上方的一个有黄色绿色灰色和紫色的长条形区域.当这个区域大部分从黄色变成灰色后并且右下角的working标志消失(图中并没有显示)后,代表二进制文件处理完毕.
留意上面的四个一组的按钮,第一个按钮显示的是汇编代码.按第二个按钮显示的是流程.按第三个按钮显示的是反汇编代码,第四个按钮显示的是机器代码.
在左侧显示的是搜索栏,可以输入我们想寻找的函数,例如onrevokeMsg
随便点击一个,右侧的显示区便会显示相应方法的汇编代码.
左侧的便是静态地址.对于大型的二进制文件,hopper一般需要运行很久才会处理完毕.所以可以在hopper处理完毕后保存,即可避免每次打开hopper都要重新处理二进制文件.
使用lldb添加断点
现在我们知道了方法的静态地址和模块的偏移地址.在lldb中输入如
(lldb) br s -a ‘0x0000000000068000+0x000000010280a408’
引号中的是两个地址,其中一个是静态地址,另一个是模块的偏移地址.
在这里,介绍一下几个常用的lldb指令:
- br delete : 删除全部断点
- br delete 1 : 删除1号断点
- br disable : 禁用全部断点
- br enable : 启用全部断点
- bt : 显示函数调用栈
- po $arg1 显示参数1
- br l : 显示断点列表.
在对上面所找到的需要筛选的方法,加上断点,观察后得出:只有[CMessageMgr onRevokeMsg:]会在别人撤回方法的时候调用.使用形如
(lldb)po $arg3
这种指令可以观察当前函数的参数.发现参数Msg的描述是:
{m_uiMesLocalID=5, m_ui64MesSvrID=111111111111111111, m_nsFromUsr=iiiiii~ii, m_nsToUsr=iiiiii~ii, m_uiStatus=4, type=1, msgSource=”
<sequence_id>iiiiiiiii</ sequence_id> “}
搜索头文件,发现Msg很可能是一个CMessageWrap对象.
编写和安装tweak
请参考《iOS应用逆向工程》.如果遇到工程名含有小写字符和+-符号除外的字符的问题的话请将theos工程的名字改为全部小写.
我们将勾住[CMessageMgr onRevokeMsg:],把原本的[CMessageMgr onRevokeMsg:]方法替换成空方法.安装tweak之后,发现的确可以将撤回消息无效化.
发现不足之处
虽然防止撤回这个功能已经实现了,可是这种实现方法的不足也是十分明显的:不知道别人究竟有没有撤回消息,因为我们并没有对被撤回的消息进行标记.
对被撤回的消息进行标记,最直观的方法就是在聊天的view中把撤回的消息使用改变backgroundColor的方法标记成不同颜色.
首先我们需要找到显示消息的类.
观察BaseMsgContentViewController的头文件,我们可以发现这样的方法:
- (id)findNodeViewByLocalId:(unsigned int)arg1;
- (id)findNodeDataByLocalId:(unsigned int)arg1;
其中的(id)findNodeViewByLocalId:(unsigned int)arg1:是否就是根据LocalId返回消息的呢?我们已经知道[CMessageMgr onRevokeMsg:]的参数里面有一个m_uiMesLocalID的成员,这个成员所记录的量是否可以作为参数调用(id)findNodeViewByLocalId:(unsigned int)arg1:呢?如果可以,那么我们就可以根据onRevokeMsg的参数标记被撤回的信息了.那么我们需要怎么测试(id)findNodeViewByLocalId:(unsigned int)arg1:方法呢?
使用cycript
使用cycript,可以在app运行的环境下调用函数.
xxx~ root# cycript -p WeChat
xxx# [[[UIWindow keyWindow] rootViewController] _printHierarchy].toString()
输出如下:
<MMTabBarController 0x10515fc00>, state: appeared, view: <UILayoutContainerView 0x1058446e0> | <MMUINavigationController 0x1050f5a00>, state: appeared, view: <UILayoutContainerView 0x10436f810> | | <NewMainFrameViewController 0x10602f200>, state: disappeared, view: <MMUIHookView 0x10597ccc0> not in the window | | <BaseMsgContentViewController 0x105256e00>, state: appeared, view: <UIView 0x1059d2120> | <MMUINavigationController 0x105150c00>, state: disappeared, view: <UILayoutContainerView 0x10438bf10> not in the window | | <ContactsViewController 0x1060f0200>, state: disappeared, view: (view not loaded) | <MMUINavigationController 0x1060f1000>, state: disappeared, view: <UILayoutContainerView 0x10438efd0> not in the window | | <FindFriendEntryViewController 0x104833e00>, state: disappeared, view: (view not loaded) | <MMUINavigationController 0x1060f6c00>, state: disappeared, view: <UILayoutContainerView 0x10597a420> not in the window | | <MoreViewController 0x1060f2800>, state: disappeared, view: (view not loaded)
取得BaseMsgContentViewController
xxx# v = #0x1191f6800
<BaseMsgContentViewController: 0x105256e00>
对BaseMsgContentViewController进行测试
xxx# [v GetMessagesWrapArray]
结果如下
@[#”{m_uiMesLocalID=1, m_ui64MesSvrID=xxxxxxxxxxxxxxxxxxx, m_nsFromUsr=xxxxxxxxxx@chatroom, m_nsToUsr=xxxxxxxxxx, m_uiStatus=4, type=1, msgSource=\”
<sequence_id>xxxxxxxxx</ sequence_id>\n\t \n\”} “,#”{m_uiMesLocalID=2, m_ui64MesSvrID=xxxxxxxxxxxxxxxxxxx, m_nsFromUsr=xxxxxxxxxx@chatroom, m_nsToUsr=xxxxxxxxxx, m_uiStatus=4, type=1, msgSource=\”1 \n \tx \n<sequence_id>xxxxxxxxx</ sequence_id>\n\t \n\”} “]1 \n \tx \n
测试findNodeViewByLocalId
xxx#h = [v findNodeViewByLocalId:2]
"<TextMessageCellView: 0x119ddfce0; frame = (0 0; 768 84); layer = <CALayer: 0x170634b40>>"
xxx# [h setBackgroundColor:[UIColor greenColor]];
界面中的最后一条信息后面的view的背景颜色已经变成绿色了.可以得出结论.接下来可以做的事情是:编写tweak在onRevokeMsg:方法中得到消息的m_uiMesLocalID,在BaseMsgContentViewController中使用findNodeViewByLocalId:为被撤回的消息做标记.可以尝试自己实现.
后记
这是逆向实验虽然基本但是对熟悉各种逆向工具的使用还是具有一定意义的.虽然现在实现了防止撤回的一部分功能,但是解决方法还有瑕疵:只有在当前聊天界面中被撤回的消息才会标记,并且如果退出后再次进入该聊天界面,标记也会消失.功能还有待完善.
参考:
Explore WeChat for Chat UI Implementation
一步一步实现iOS微信自动抢红包(非越狱)