支付宝9.3版本聊天语音保存Tweak开发笔记

背景

加入了MacTalk作者池院长的支付宝群“攻城狮之路”,已经有了三次分享,分享是以语音形式进行。近期正好看完了《iOS应用逆向工程》这本书,想来可以试试写个tweak,保存聊天中的语音。

环境

  1. iPhone 5,iOS 8.3,越狱。
  2. 支付宝9.3 。

使用iPhone5主要是因为CPU是32位,32位arm汇编。IDA免费版不能反汇编64位程序。其次也是我初学,这本书中的例子也都是32位汇编,对我来说更简单点。

去掉保护

获取 AppBundleIdentifier

进入 AlipayWallet.app 目录,

everettjfs-iPhone:/var/mobile/Containers/Bundle/Application/9DB7CE45-3B4C-42A3-9D4D-49A3A5122903/AlipayWallet.app root# cat Info.plist | grep com.
    <string>com.alipay.iphoneclient</string>

去掉 ptrace 和 __RESTRICT section 两个保护

参见:
http://everettjf.github.io/2015/12/28/simple-ios-antidebugging-and-antiantidebugging/

破掉保护后就可以使用cycript了。

分析

砸壳

使用 dumpdecripted

everettjfs-iPhone:~ root# cycript -p AlipayWallet
cy# [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]
@[#"file:///var/mobile/Containers/Data/Application/F3E3A318-3E42-40BB-B1AC-2DE3CD8ACB00/Documents/"]

class-dump

$ class-dump --list-arches AlipayWallet.decrypted
$ class-dump -s -S -H --arch armv7 AlipayWallet.decrypted -o dumpAlipay

找到语音对应的UITableViewCell

打开支付宝,切换到聊天界面。保持聊天对话的对方有语音消息。

everettjfs-iPhone:~ root# cycript -p AlipayWallet
cy# ?expand
expand == true
cy# [[UIApp keyWindow] recursiveDescription]
@"<UIWindow: 0x2db3ae0; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x2db3f60>; layer = <UIWindowLayer: 0x2db3c30>>
   | <UILayoutContainerView: 0xb4662d0; frame ....
   ......

可以把语音时长作为关键字,例如搜索1'',可以找到:

<UILabel: 0xe2a1900; frame = (141 65.316; 15 14); text = '1'''; userInteractionEnabled = NO; layer = <_UILabelLayer: 0xe2a19e0>>

进而可以找到对应的UITableViewCell:

<CTMessageCell: 0x39ecc00; baseClass = UITableViewCell; frame = (0 285; 320 99); ......

于是,从class-dump的文件中找到CTMessageCell的头文件:
这时就要火眼金睛啦。找到以下信息:

@property(retain, nonatomic) APChatMedia *voiceObj; // @synthesize voiceObj=_voiceObj;
@property(copy, nonatomic) NSString *timeLine; // @synthesize timeLine=_timeLine;

- (void)playAudio;

可以直接在cycript中调用来验证数据是不是要找的。

这个APChatMedia,很有意思。看下头文件。

@interface APChatMedia : NSObject
@property(retain, nonatomic) NSData *data; // @synthesize data=_data;
@property(retain, nonatomic) NSString *url; // @synthesize url=_url;

url可以获取到一个字符串。开始看到url我还很高兴,难道就这么简单,难道这就是下载语音文件的url,可惜不是啊)

cy# [#0x3022400 voiceObj]
#"<APChatMedia: 0xb226710>"
cy# [#0xb226710 url]
@"ww4fd1rfRDShBo_4K6rqfwAAACMAAQED"

又看到APChatMedia的data,难道这就是语音数据,可惜这个值总是nil。

看来得找别的办法,看看playAudio这个方法吧。

上lldb和IDA

lldb中找到 CTMessageCell playAudio 的地址,下断点,点击一条语音,果然断下来。

分析playAudio的代码,内部会调用 APPlayManager 的play:FinishCallback。

APChatMedia会作为第一个参数传进入。

(lldb) br s -a 0x00D3EFD6
Breakpoint 1: where = AlipayWallet`___lldb_unnamed_function68838$$AlipayWallet + 118, address = 0x00d3efd6
(lldb) c
error: Process is running.  Use 'process interrupt' to pause execution.
Process 6235 stopped
* thread #1: tid = 0x58da9, 0x00d3efd6 AlipayWallet`___lldb_unnamed_function68838$$AlipayWallet + 118, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00d3efd6 AlipayWallet`___lldb_unnamed_function68838$$AlipayWallet + 118
AlipayWallet`___lldb_unnamed_function68838$$AlipayWallet:
->  0xd3efd6 <+118>: blx    0xe0bb50                  ; ___lldb_unnamed_function73268$$AlipayWallet
    0xd3efda <+122>: mov    r0, r6
    0xd3efdc <+124>: blx    0xe0bb4c                  ; ___lldb_unnamed_function73267$$AlipayWallet
    0xd3efe0 <+128>: mov    r0, r5
(lldb) po $r0
<APPlayManager: 0xce984f0>

(lldb) p (char*)$r1
(char *) $1 = 0x021535fd "play:finishCallback:"
(lldb) po $r2
<APChatMedia: 0xc7e7b90>

到APPlayManager的play:FinishCallback看下,发现会从VoiceCache中获取音频数据。

- (id)queryVoiceDataForKey:(id)arg1 formatType:(unsigned int)arg2;
> VoiceCache.queryVoiceDataForKey:formatType:
lldb) p (char*)$r1
(char *) $18 = 0x020f55b3 "queryVoiceDataForKey:formatType:"
(lldb) po $r2
9atdHG1cSFKJyb8yqntaaQAAACMAAQED

(lldb) p $r3
(unsigned int) $20 = 1

其实到这里,queryVoiceDataForKey 返回的 NSData 就是要获取的音频数据了。保存下来,拿到电脑上就行啦。

步骤总结

  1. CTMessageCell.playAudio
  2. APPlayManager.playFinishCallback (param : [CTMessageCell voiceObj])
  3. APVoiceManager.playAudioWithCloudId:resumeActive:downLoadHandler:playHandler:
  4. VoiceCache.queryVoiceDataForKey:formatType:

如何获取语音的时间戳

有了语音数据,如何保存个合适的名字。因为分享会有很多语音,需要区分出语音的先后(支付宝的收藏又是体验很差)。

于是看CTMessageCell的头文件,再看父类PKCell,可以看到有聊天对方的信息,

@property(retain, nonatomic) APContactInfo *contactInfo; // @synthesize contactInfo=_contactInfo;

再看父类PKBaseCell,会发现有个

@property(retain, nonatomic) NSDictionary *chatDataSource; // @synthesize chatDataSource=_chatDataSource;

cycript打印出来:

chatDataSource
cy# [#0x319c400 chatDataSource]
@{"alignmentType":0,"templateData":@{"l":12,"v":"FGAegsGqTTaAB3n80shI_gAAACMAAQED"},"id":"12","originId":"12","data":@{"displayName":"everettjf","sessionId":"2088002664597371","bizImage":"Local_Image_CHAT.left","hintName":"everettjf","timeLine":#"2015-12-24 15:56:27 +0000","HeadIcon":"http://tfs.alipayobjects.com/images/partner/T1IJphXc4XXXXXXXXX_160X160","action":"0","seed":"2088002664597371@145097258940385","cellSelected":"0","userType":"1","userID":"2088122631212590","v":"FGAegsGqTTaAB3n80shI_gAAACMAAQED","bizMemo":"[\xe8\xaf\xad\xe9\x9f\xb3]","fromUId":"2088002664597371","l":12,"fromLoginId":"eve***@outlook.com","bizType":"CHAT","bizRemind":"","toUId":"2088122631212590","clientMsgID":"2088002664597371@145097258940385","link":"","msgID":151224235627370001,"toLoginId":"","localId":6},"headerText":"Thursday 11:56 PM","msgType":0}

可以看到 timeLine 就是这条信息的时间戳。

源码

好了,大功告成,写个tweak吧。

把保存的代码放到了收藏按钮中。hook 收藏的方法 - (void)collectMenu:(id)arg1

参见代码:

https://github.com/everettjf/AlipayWalletChatVoiceSaver

补充

音频格式

语音格式,把语音复制出来后,用二进制编辑工具iHex打开,可以看到是wav格式。

Wifi下自动下载语音

上面我们每次都从VoiceCache中获取音频数据,应该是因为在Wifi下,语音会自动下载,下载后自动就放到了VoiceCache中。

通过可以在 APVoiceManager 的下面方法增加断点,就可以发现每次都自动下载,具体就不分析了。

- (void)downLoadAudioWithCloudId:(id)arg1 downLoadHandler:(CDUnknownBlockType)arg2;

总结

虽然远没有《iOS应用逆向工程》最后一章的例子复杂,但也作为自己看完这本书的小小的句号吧,既是句号,也是新的起点。这本书的内容也只是引导自己走进了iOS逆向的大门,入门而已。

期待未来……

(文章也发布到俺的博客了 http://everettjf.github.io/2015/12/29/tweak-for-alipay-wallet-chat-voice-save/

6 个赞

好厉害,我今天实在iosre的微博上看到的,然后再过来仔细学习了。
想请教一个问题,这样下载的音频是连续的还是离散的一段一段的?
我自己正在微信上做类似的保存语音尝试~,看了你的帖子觉得有了方向哈哈哈~谢谢!

这样是提取出每条消息的语音数据。最后都是wav格式。

可以自己用ffmpeg合并多个语音。

感谢分享,很详细,学习了

问个问题,ptrace这么破?我看你那个链接里的并没明白,麻烦你指导一下,还有就是ptrace没破的话是不是用cycript和Reveal和lldb都用不了?

ptrace 主要是防止加载调试器。
restrict section 会妨碍 cycript的使用。

我去掉了Restrict section的保护后还是用cycript加载不了,偶然有一次加载成功了,而后无效了,版本也是支付宝9.3

ptrace 也同时解决了吗?

trace没有解决, 加载不上报的错是;MS:Error: _krncall(task_for_pid(self, pid, &task)) =5
*** _assert(system(inject.str().c_str()) == 0):…/Inject.cpp(119):InjectLibrary

而且程序闪退

要先解决程序闪退的问题。
需要重新签名。用书上说的那个放到theos/bin目录下的ldid重新签名。ldid -S AppBinary。我当时闪退问题也卡了一阵子。

我没找到书上说的那个ldid,我用的是Xcode自带的那个

这个呢 http://joedj.net/ldid

我刚才从零开始做了以下步骤:1.去掉__RESTRICT sectio保护,2.用你给的链接的ldid签名,3.在手机上点击支付宝,闪退。手机是iPhone 4s iOS 8.4

AppSync 安装了吗

这是闪退的日志:
Jan 14 01:18:09 AA kernel[0] : AppleFairplayTextCrypterSession::fairplayOpen() failed, error -42004
Jan 14 01:18:09 AA com.apple.xpc.launchd[1] (UIKitApplication:com.alipay.iphoneclient[0x350d][2647]) : FairPlay decryption failed on binary.
Jan 14 01:18:09 AA SpringBoard[713] : Unable to get pid for ‘UIKitApplication:com.alipay.iphoneclient[0x350d]’: No such process (err 3)
Jan 14 01:18:09 AA SpringBoard[713] : Application ‘UIKitApplication:com.alipay.iphoneclient[0x350d]’ exited due to FairPlay failure.
Jan 14 01:18:09 AA SpringBoard[713] : Application “com.alipay.iphoneclient” exited due to FairPlay failure. Attempting keybag refetch and app relaunch.
Jan 14 01:18:09 AA locationd[118] : Gesture EnabledForTopCLient: 0, EnabledInDaemonSettings: 0
Jan 14 01:18:10 AA locationd[118] : Gesture EnabledForTopCLient: 0, EnabledInDaemonSettings: 0
Jan 14 01:18:10 AA itunesstored[1005] : libMobileGestalt MGBasebandSupport.c:196: No kCTMobileEquipmentInfoMEID in CT mobile equipment info dictionary
Jan 14 01:18:15 AA locationd[118] : Gesture EnabledForTopCLient: 0, EnabledInDaemonSettings: 0
Jan 14 01:18:21 AA locationd[118] : Gesture EnabledForTopCLient: 0, EnabledInDaemonSettings: 0

看起来像是restrict没有修改好。这方面我没太多经验。
明天我重新操作一遍,看看有哪个遗漏。

好的,谢谢你

我重新试了下最新版本的支付宝。重新签名后可以启动。

  1. ps -e | grep /var 找到AppBinary路径
  2. 把AppBinary复制出
  3. 二进制编辑器(iHex等)修改__RESTRICT和__restrict为其他值。(比如:__RRRRRRRR 和 __rrrrrrrr。保证长度不变就行啦)【这里长度要保持不变】
  4. ldid -S AppBinary 重签名。 【使用的这个 http://joedj.net/ldid1 ,下载后chmod 777 ldid 】
  5. Cydia中安装 AppSync。(安装后 重新启动springboard或者重启手机)

然后就可以 cycript -p AlipayWallet 了

你好,我重新学习了,但是ihex没用过,求教程或一些指点,thx T T