手把手教你逆向微信之朋友圈小视频转发(上)

LeonLei的博客 欢迎来访!

前言

此文为逆向微信二进制文件,实现朋友圈小视频转发的教程,从最开始的汇编代码入手到最后重签名安装等操作,手把手教你玩转微信!学会之后再去逆向微信其他功能易如反掌。
本篇文章由于篇幅太长分成了两篇,上篇讲解的是逆向工作,也就是怎么找到相关的函数和方法实现,下篇讲解的是怎么在非越狱机重签名安装和越狱机tweak安装的详细过程。
正文的第二部分还提供了微信自动抢红包、修改微信步数的代码,这些都可以照葫芦画瓢按照本文的套路一步步逆向找到,这里就不再赘述。
在实践之前,需要准备好一部越狱的手机,然后将下文列出的所有工具安装好。IDA跟Reveal都是破解版,IDA的正版要2000多刀,对于这么牛逼的逆向工具确实物有所值,不过不是专门研究逆向的公司也没必要用正版的,下个Windows的破解版就好,Mac上暂时没找到。Mac上可以用hopper代替IDA,也是一款很牛逼的逆向工具。废话不多说,正式开始吧!

转载请注明出处:来自LeonLei的博客http://www.gaoshilei.com

逆向微信朋友圈(上篇)

一、获取朋友圈的小视频

注意:本文逆向的微信的二进制文件为6.3.28版本,如果是不同的微信版本,二进制文件中的基地址也不相同

本文涉及到的工具

  1. cycript
  2. LLDB与debugserver(Xcode自带)
  3. OpenSSH
  4. IDA
  5. Reveal
  6. theos
  7. CydiaSubstrate
  8. iOSOpenDev
  9. ideviceinstaller
  10. tcprelay(本地端口映射,USB连接SSH,不映射可通过WiFi连接)
  11. dumpdecrypted
  12. class-dump
  13. iOS App Signer
  14. 编译好的yololib

逆向环境为MacOS + iPhone5S 9.1越狱机
先用dumpdecrypted给微信砸壳(不会的请我写的看这篇教程),获得一个WeChat.decrypted文件,先把这个文件扔到IDA中分析(60MB左右的二进制文件,IDA差不多40分钟才能分析完),用class-dump导出所有头文件

LeonLei-MBP:~ gaoshilei$ class-dump -S -s -H /Users/gaoshilei/Desktop/reverse/binary_for_class-dump/WeChat.decrypted -o /Users/gaoshilei/Desktop/reverse/binary_for_class-dump/class-Header/WeChat

我滴个亲娘!一共有8000个头文件,微信果然工程量浩大!稳定一下情绪,理一理思路继续搞。要取得小视频的下载链接,找到播放视频的View,顺藤摸瓜就能找到小视频的URL。用Reveal查看小视频的播放窗口

Reveal

可以看出来WCContentItemViewTemplateNewSigh这个对象是小视频的播放窗口,它的subView有WCSightView,SightView、SightPlayerView,这几个类就是我们的切入点。
保存视频到favorite的时候是长按视频弹出选项的,那么在WCContentItemViewTemplateNewSight这个类里面可能有手势相关的方法,去刚才导出的头文件中找线索。

- (void)onLongTouch;
- (void)onLongPressedWCSight:(id)arg1;
- (void)onLongPressedWCSightFullScreenWindow:(id)arg1;

这几个方法跟长按手势相关,再去IDA中找到这些函数,逐个查看。onLongPressedWCSight和onLongPressedWCSightFullScreenWindow都比较简单,onLongTouch比较长,而且发现了内部调用了方法Favorites_Add,因为长按视频的时候出来一个选项就是Favorites,并且我看到这个函数调用

ADRP            X8, #selRef_sightVideoPath@PAGE
LDR             X1, [X8,#selRef_sightVideoPath@PAGEOFF]

这里拿到了小视频的地址,可以推测这个函数跟收藏有关,下面打断点测试。

(lldb) im li -o -f
[  0] 0x000000000003c000 /var/mobile/Containers/Bundle/Application/2F1D52EC-C57E-4F95-B715-EF04351232E8/WeChat.app/WeChat(0x000000010003c000)

可以看到WeChat的ASLR为0x3c000,在IDA查找到这三个函数的基地址,分别下断点

(lldb) br s -a 0x1020D3A10+0x3c000
Breakpoint 1: where = WeChat`___lldb_unnamed_symbol110094$$WeChat + 28, address = 0x000000010210fa10
(lldb) br s -a 0x1020D3370+0x3c000
Breakpoint 2: where = WeChat`___lldb_unnamed_symbol110091$$WeChat + 8, address = 0x000000010210f370
(lldb) br s -a 0x1020D33E4+0x3c000
Breakpoint 3: where = WeChat`___lldb_unnamed_symbol110092$$WeChat + 12, address = 0x000000010210f3e4

回到微信里面长按小视频,看断点触发情况

Process 3721 stopped
* thread #1: tid = 0x658fc, 0x000000010210f370 WeChat`___lldb_unnamed_symbol110091$$WeChat + 8, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x000000010210f370 WeChat`___lldb_unnamed_symbol110091$$WeChat + 8
WeChat`___lldb_unnamed_symbol110091$$WeChat:
->  0x10210f370 <+8>:  add    x29, sp, #16              ; =16 
    0x10210f374 <+12>: mov    x19, x0
    0x10210f378 <+16>: adrp   x8, 4968
    0x10210f37c <+20>: ldr    x0, [x8, #744]
(lldb) c
Process 3721 resuming
Process 3721 stopped
* thread #1: tid = 0x658fc, 0x000000010210fa10 WeChat`___lldb_unnamed_symbol110094$$WeChat + 28, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x000000010210fa10 WeChat`___lldb_unnamed_symbol110094$$WeChat + 28
WeChat`___lldb_unnamed_symbol110094$$WeChat:
->  0x10210fa10 <+28>: add    x29, sp, #96              ; =96 
    0x10210fa14 <+32>: sub    sp, sp, #96               ; =96 
    0x10210fa18 <+36>: mov    x19, x0
    0x10210fa1c <+40>: adrp   x8, 4863
……

发现断点2先被触发,接着触发断点1,后面断点2和1又各触发了1次,断点3一直很安静。可以排除onLongPressedWCSightFullScreenWindow与收藏小视频的联系。小视频的踪影就要在剩下的两个方法中寻找了。通过V找到C,顺藤摸瓜找到M屡试不爽!用cycript注入WeChat,拿到播放小视频的view所在的Controller。

cy# [#0x138c18030 nextResponder]
#"<WCTimeLineCellView: 0x138c34620; frame = (0 0; 319 249); tag = 1048577; layer = <CALayer: 0x138362ba0>>"
cy# [#0x138c34620 nextResponder]
#"<UITableViewCellContentView: 0x138223c70; frame = (0 0; 320 256); gestureRecognizers = <NSArray: 0x1384ec480>; layer = <CALayer: 0x138081dc0>>"
cy# [#0x138223c70 nextResponder]
#"<MMTableViewCell: 0x138c9f930; baseClass = UITableViewCell; frame = (0 307; 320 256); autoresize = W; layer = <CALayer: 0x1382dcd10>>"
cy# [#0x138c9f930 nextResponder]
#"<UITableViewWrapperView: 0x137b57800; frame = (0 0; 320 504); gestureRecognizers = <NSArray: 0x1383db660>; layer = <CALayer: 0x138af20c0>; contentOffset: {0, 0}; contentSize: {320, 504}>"
cy# [#0x137b57800 nextResponder]
#"<MMTableView: 0x137b8ae00; baseClass = UITableView; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x138adb590>; layer = <CALayer: 0x138956890>; contentOffset: {0, 99.5}; contentSize: {320, 3193}>"
cy# [#0x137b8ae00 nextResponder]
#"<UIView: 0x138ade5c0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x138ac9990>>"
cy# [#0x138ade5c0 nextResponder]
#"<WCTimeLineViewController: 0x1379eb000>"

通过响应者链条找到
WCContentItemViewTemplateNewSight所属的Controller为WCTimeLineViewController。在这个类的头文件中并没有发现有价值的线索,不过我们注意到小视频所在的view是属于MMTableVIewCell的(见上图Reveal分析图),这是每一个iOS最熟悉的TableView,cell的数据是通过UITableViewDataSource的代理方法- tableView:cellForRowAtIndexPath:赋值的,通过这个方法肯定能知道到M的影子。在IDA中找到[WCTimeLineViewController tableView:cellForRowAtIndexPath:],定位到基地址0x10128B6B0位置:

__text:000000010128B6B0     ADRP     X8, #selRef_genNormalCell_indexPath_@PAGE

这里的函数是WCTimeLineViewController中生成cell的方法,除了这个方法在这个类中还有另外三个生成cell的方法:

- (void)genABTestTipCell:(id)arg1 indexPath:(id)arg2;
- (void)genRedHeartCell:(id)arg1 indexPath:(id)arg2;
- (void)genUploadFailCell:(id)arg1 indexPath:(id)arg2;

通过字面意思可以猜测出normal这个应该是生成小视频cell的方法。继续在IDA中寻找线索

__text:0000000101287CC8     ADRP     X8, #selRef_getTimelineDataItemOfIndex_@PAGE

genNormalCell:IndexPath:方法中发现上面这个方法,可以大胆猜想这个方法是获取TimeLine(朋友圈)数据的方法,那小视频的数据肯定也是通过这个方法获取的,并且IDA可以看到这个方法中调用一个叫做selRef_getTimelineDataItemOfIndex_的方法,获取DataItem貌似就是cell的数据源啊!接下来用LLDB下断点验证猜想。
通过IDA可以找到这个方法对应的基地址为:0x101287CE4,先打印正在运行WeChat的ASLR偏移

LeonLei-MBP:~ gaoshilei$ lldb
(lldb) process connect connect://localhost:1234
(lldb) im li -o -f 
[0] 0x0000000000050000 /var/mobile/Containers/Bundle/Application/2DCE8F30-9B6B-4652-901C-37EB1FF2A40D/WeChat.app/WeChat(0x0000000100050000)

所以我们下断点的位置是0x50000+0x101287CE4

(lldb) br s -a 0x50000+0x101287CE4
Breakpoint 1: where = WeChat`___lldb_unnamed_symbol63721$$WeChat + 252, address = 0x00000001012d7ce4

打印x0的值

(lldb) po $x0
Class name: WCDataItem, addr: 0x15f5f03b0
tid: 12393001887435993280
username: wxid_z8twcz4o18fg12
createtime: 1477360950
commentUsers: (
)
contentObj: <WCContentItem: 0x15f57d000>

得到一个WCDataItem的对象,这里x0的值就是selRef_getTimelineDataItemOfIndex_执行完的返回值,然后把x0的值改掉

(lldb) register write $x0 0
(lldb) c

此时会发现我们要刷新的那条小视频内容全部为空

小视频内容为空

到这里已经找到了小视频的源数据获取方法,问题是我们怎么拿到这个WCDataItem呢?继续看IDA分析函数的调用情况:

WCTimeLineViewController - (void)genNormalCell:(id) indexPath:(id)

__text:0000000101287BCC                 STP             X28, X27, [SP,#var_60]!
__text:0000000101287BD0                 STP             X26, X25, [SP,#0x60+var_50]
__text:0000000101287BD4                 STP             X24, X23, [SP,#0x60+var_40]
__text:0000000101287BD8                 STP             X22, X21, [SP,#0x60+var_30]
__text:0000000101287BDC                 STP             X20, X19, [SP,#0x60+var_20]
__text:0000000101287BE0                 STP             X29, X30, [SP,#0x60+var_10]
__text:0000000101287BE4                 ADD             X29, SP, #0x60+var_10
__text:0000000101287BE8                 SUB             SP, SP, #0x80
__text:0000000101287BEC                 MOV             X19, X3
__text:0000000101287BF0                 MOV             X22, X0
__text:0000000101287BF4                 MOV             W25, #0x100000
__text:0000000101287BF8                 MOVK            W25, #1
__text:0000000101287BFC                 MOV             X0, X2
__text:0000000101287C00                 BL              _objc_retain
__text:0000000101287C04                 MOV             X28, X0
__text:0000000101287C08                 MOV             X0, X19
__text:0000000101287C0C                 BL              _objc_retain
__text:0000000101287C10                 MOV             X20, X0
__text:0000000101287C14                 STR             X20, [SP,#0xE0+var_98]
__text:0000000101287C18                 ADRP            X8, #selRef_row@PAGE
__text:0000000101287C1C                 LDR             X1, [X8,#selRef_row@PAGEOFF]
__text:0000000101287C20                 BL              _objc_msgSend
__text:0000000101287C24                 MOV             X26, X0
__text:0000000101287C28                 ADRP            X8, #selRef_section@PAGE
__text:0000000101287C2C                 LDR             X19, [X8,#selRef_section@PAGEOFF]
__text:0000000101287C30                 MOV             X0, X20
__text:0000000101287C34                 MOV             X1, X19
__text:0000000101287C38                 BL              _objc_msgSend
__text:0000000101287C3C                 STR             X0, [SP,#0xE0+var_A8]
__text:0000000101287C40                 MOV             X0, X20
__text:0000000101287C44                 MOV             X1, X19
__text:0000000101287C48                 BL              _objc_msgSend
__text:0000000101287C4C                 MOV             X2, X0
__text:0000000101287C50                 ADRP            X8, #selRef_calcDataItemIndex_@PAGE
__text:0000000101287C54                 LDR             X1, [X8,#selRef_calcDataItemIndex_@PAGEOFF]
__text:0000000101287C58                 MOV             X0, X22
__text:0000000101287C5C                 BL              _objc_msgSend
__text:0000000101287C60                 MOV             X21, X0
__text:0000000101287C64                 STR             X21, [SP,#0xE0+var_C0]
__text:0000000101287C68                 ADRP            X8, #classRef_MMServiceCenter@PAGE
__text:0000000101287C6C                 LDR             X0, [X8,#classRef_MMServiceCenter@PAGEOFF]
__text:0000000101287C70                 ADRP            X8, #selRef_defaultCenter@PAGE
__text:0000000101287C74                 LDR             X1, [X8,#selRef_defaultCenter@PAGEOFF]
__text:0000000101287C78                 STR             X1, [SP,#0xE0+var_B8]
__text:0000000101287C7C                 BL              _objc_msgSend
__text:0000000101287C80                 MOV             X29, X29
__text:0000000101287C84                 BL              _objc_retainAutoreleasedReturnValue
__text:0000000101287C88                 MOV             X19, X0
__text:0000000101287C8C                 ADRP            X8, #classRef_WCFacade@PAGE
__text:0000000101287C90                 LDR             X0, [X8,#classRef_WCFacade@PAGEOFF]
__text:0000000101287C94                 ADRP            X8, #selRef_class@PAGE
__text:0000000101287C98                 LDR             X1, [X8,#selRef_class@PAGEOFF]
__text:0000000101287C9C                 STR             X1, [SP,#0xE0+var_B0]
__text:0000000101287CA0                 BL              _objc_msgSend
__text:0000000101287CA4                 MOV             X2, X0
__text:0000000101287CA8                 ADRP            X8, #selRef_getService_@PAGE
__text:0000000101287CAC                 LDR             X1, [X8,#selRef_getService_@PAGEOFF]
__text:0000000101287CB0                 STR             X1, [SP,#0xE0+var_A0]
__text:0000000101287CB4                 MOV             X0, X19
__text:0000000101287CB8                 BL              _objc_msgSend
__text:0000000101287CBC                 MOV             X29, X29
__text:0000000101287CC0                 BL              _objc_retainAutoreleasedReturnValue
__text:0000000101287CC4                 MOV             X20, X0
__text:0000000101287CC8                 ADRP            X8, #selRef_getTimelineDataItemOfIndex_@PAGE
__text:0000000101287CCC                 LDR             X1, [X8,#selRef_getTimelineDataItemOfIndex_@PAGEOFF]
__text:0000000101287CD0                 STR             X1, [SP,#0xE0+var_C8]
__text:0000000101287CD4                 MOV             X2, X21
__text:0000000101287CD8                 BL              _objc_msgSend
__text:0000000101287CDC                 MOV             X29, X29
__text:0000000101287CE0                 BL              _objc_retainAutoreleasedReturnValue
__text:0000000101287CE4                 MOV             X21, X0
__text:0000000101287CE8                 MOV             X0, X20
......

selRef_getTimelineDataItemOfIndex_传入的参数是x2,可以看到传值给x2的x21是函数selRef_calcDataItemIndex_的返回值,是一个unsigned long数据类型。继续分析,selRef_getTimelineDataItemOfIndex_函数的调用者是上一步selRef_getService_的返回值,经过断点分析发现是一个WCFacade对象。整理一下selRef_getTimelineDataItemOfIndex_的调用:
调用者是selRef_getService_的返回值;参数是selRef_calcDataItemIndex_的返回值
下面把目光转向那两个函数,用相同的原理分析它们各自怎么实现调用

  1. 先看selRef_getService_
    在0x101287CB4这个位置可以发现,这个函数的调用者是从通过x19 MOV的,打印x19发现是一个MMServiceCenter对象,往上找x19是在0x101287C88这个位置赋值的,结果很清晰x19是[MMServiceCenter defaultCenter]的返回值。
    在0x101287CA4位置可以找到传入的参数x2,往上分析可以看出来它的参数是[WCFacade class]的返回值。
  2. 接着找selRef_calcDataItemIndex_
    在0x101287C58的位置找到它的调用者x0,x0通过x22赋值,继续向上寻找,发现在最上面0x101287BF0的位置,x22是x0赋值的,一开始的x0就是WCTimeLineViewController自身。
    在0x101287C4C位置发现传入的参数来自x2,x2是通过上一步selRef_section函数的返回值x0赋值的,在0x101287C30位置可以发现selRef_section函数的调用者是x20赋值的,如下图所示,最终找到selRef_section的调用者是x3
    selRef_section函数的调用者

    x3就是函数WCTimeLineViewController - (void)genNormalCell:(id) indexPath:(id)的第二个参数indexPath,,所以selRef_calcDataItemIndex_的参数是[IndexPath section]
    对上面的分析结果做个梳理:
    因此getTimelineDataItemOfIndex:的调用者可以通过
[[MMServiceCenter defaultCenter] getService:[WCFacade class]]

来获得,它的参数可以通过下面的函数获取

[WCTimeLineViewController calcDataItemIndex:[indexPath section]]

总感觉还少点什么?indexPath我们还没拿到呢!下一步就是拿到indexPath,这个就比较简单了,因为我们位于[WCContentItemViewTemplateNewSight onLongTouch]中,所以可以通过[self nextResponder]依次拿到MMTableViewCell、MMTableView和WCTimeLineViewController,再通过[MMTableView indexPathForCell:MMTableViewCell]拿到indexPath。
做完这些,已经拿到WCDataItem对象,接下来的重点要放在WCDataItem上,最终要获取我们要的小视频。到这个类的头文件中找线索,因为视频是下载完成后才能播放的,所以这里应该拿到了视频的路径,所以要注意url和path相关的属性或方法,然后找到下面这几个嫌疑对象

@property(retain, nonatomic) NSString *sourceUrl2; 
@property(retain, nonatomic) NSString *sourceUrl; 
- (id)descriptionForKeyPaths;
- (id)keyPaths;

回到LLDB中,用断点打印这些值,看看有什么。

(lldb) po [$x0 keyPaths]
<__NSArrayI 0x15f74e9d0>(
    tid,
    username,
    createtime,
    commentUsers,
    contentObj
)
(lldb) po [$x0 descriptionForKeyPaths]
Class name: WCDataItem, addr: 0x15f5f03b0
tid: 12393001887435993280
username: wxid_z8twcz4o18fg12
createtime: 1477360950
commentUsers: (
)
contentObj: <WCContentItem: 0x15f57d000>
(lldb) po [$x0 sourceUrl]
 nil
(lldb) po [$x0 sourceUrl2]
 nil

并没有什么有价值的线索,不过注意到WCDataItem里面有一个WCContentItem,看来只能从这儿入手了,去看一下头文件吧!

@property(retain, nonatomic) NSString *linkUrl; 
@property(retain, nonatomic) NSString *linkUrl2; 
@property(retain, nonatomic) NSMutableArray *mediaList;

在LLDB打印出来

(lldb) po [[$x0 valueForKey:@"contentObj"] linkUrl]
https://support.weixin.qq.com/cgi-bin/mmsupport-bin/readtemplate?t=page/common_page__upgrade&v=1
(lldb) po [[$x0 valueForKey:@"contentObj"] linkUrl2]
 nil
(lldb) po [[$x0 valueForKey:@"contentObj"] mediaList]
<__NSArrayM 0x15f985e10>(
<WCMediaItem: 0x15dfebdf0>
)

mediaList数组里面有一个WCMediaItem对象,Media一般用来表示视频和音频,大胆猜测就是它了!赶紧找到头文件搜索一遍。

@property(retain, nonatomic) WCUrl *dataUrl;
- (id)pathForData;
- (id)pathForSightData;
- (id)pathForTempAttachVideoData;
- (id)videoStreamForData;

上面这些属性和方法中pathForSightData是最有可能拿到小视频路径的,继续验证

(lldb) po [[[[$x0 valueForKey:@"contentObj"] mediaList] lastObject] dataUrl]
type[1], url[http://vweixinf.tc.qq.com/102/20202/snsvideodownload?filekey=30270201010420301e020166040253480410d14adcddf086f4e131d11a5b1cca1bdf0203039fa00400&bizid=1023&hy=SH&fileparam=302c0201010425302302040fde55e20204580ebd3602024eea02031e8d7d02030f42400204d970370a0201000400], enckey[0], encIdx[-1], token[]
(lldb) po [[[[$x0 valueForKey:@"contentObj"] mediaList] lastObject] pathForData]
/var/mobile/Containers/Data/Application/7C3A6322-1F57-49A0-ACDE-6EF0ED74D137/Library/WechatPrivate/6f696a1b596ce2499419d844f90418aa/wc/media/5/53/8fb0cdd77208de5b56169fb3458b45
(lldb) po [[[[$x0 valueForKey:@"contentObj"] mediaList] lastObject] pathForSightData]
/var/mobile/Containers/Data/Application/7C3A6322-1F57-49A0-ACDE-6EF0ED74D137/Library/WechatPrivate/6f696a1b596ce2499419d844f90418aa/wc/media/5/53/8fb0cdd77208de5b56169fb3458b45.mp4
(lldb) po [[[[$x0 valueForKey:@"contentObj"] mediaList] lastObject] pathForAttachVideoData]
 nil
(lldb) po [[[[$x0 valueForKey:@"contentObj"] mediaList] lastObject] videoStreamForData]
 nil

拿到小视频的网络url和本地路径了!这里可以用iFunBox或者scp从沙盒拷贝这个文件看看是不是这个cell应该播放的小视频。

LeonLei-MBP:~ gaoshilei$ scp root@192.168.0.115:/var/mobile/Containers/Data/Application/7C3A6322-1F57-49A0-ACDE-6EF0ED74D137/Library/WechatPrivate/6f696a1b596ce2499419d844f90418aa/wc/media/5/53/8fb0cdd77208de5b56169fb3458b45.mp4 Desktop/
8fb0cdd77208de5b56169fb3458b45.mp4                100%  232KB 231.9KB/s   00:00    

用QuickTime打开发现果然是我们要寻找的小视频。再验证一下url是否正确,把上面打印的dataUrl在浏览器中打开,发现也是这个小视频。分析这个类可以得出下面的结论:

  • dataUrl:小视频的网络url
  • pathForData:小视频的本地路径
  • pathForSightData:小视频的本地路径(不带后缀)

至此小视频的路径和取得方式分析已经完成,要实现转发还要继续分析微信的朋友圈发布。

二、实现转发功能

1.“走进死胡同”

这节是我在找小视频转发功能时走的弯路,扒到最后并没有找到实现方法,不过也提供了一些逆向中常用的思路和方法,不想看的可以跳到第二节。

(1)找到小视频拍摄完成调用的方法名称

打开小视频的拍摄界面,用cycript注入,我们要找到发布小视频的方法是哪个,然后查看当前的窗口有哪些window(因为小视频的拍摄并不是在UIApplication的keyWindow中进行的)

cy# [UIApp windows].toString()
(
    "<iConsoleWindow: 0x125f75e20; baseClass = UIWindow; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x125f77b70>; layer = <UIWindowLayer: 0x125df4810>>",
    "<SvrErrorTipWindow: 0x127414d40; baseClass = UIWindow; frame = (0 64; 320 45); hidden = YES; gestureRecognizers = <NSArray: 0x12740d930>; layer = <UIWindowLayer: 0x1274030b0>>",
    "<MMUIWindow: 0x127796440; baseClass = UIWindow; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x1278083c0>; layer = <UIWindowLayer: 0x127796750>>",
    "<UITextEffectsWindow: 0x1270e0d40; frame = (0 0; 320 568); opaque = NO; autoresize = W+H; layer = <UIWindowLayer: 0x1270b4ba0>>",
    "<NewYearActionSheet: 0x127797e10; baseClass = UIWindow; frame = (0 0; 320 568); hidden = YES; userInteractionEnabled = NO; layer = <UIWindowLayer: 0x1277d5490>>"
)

发现当前页面一共有5个window,其中MMUIWindow是小视频拍摄所在的window,打印它的UI树状结构

cy# [#0x127796440 recursiveDescription]

打印结果比较长,不贴了。找到这个按钮是拍摄小视频的按钮

   |    |    |    |    |    | <UIButton: 0x1277a9d70; frame = (89.5 368.827; 141 141); opaque = NO; gestureRecognizers = <NSArray: 0x1277aaeb0>; layer = <CALayer: 0x1277a9600>>
   |    |    |    |    |    |    | <UIView: 0x1277aa0a0; frame = (0 0; 141 141); userInteractionEnabled = NO; tag = 252707333; layer = <CALayer: 0x1277aa210>>
   |    |    |    |    |    |    |    | <UIImageView: 0x1277aa2e0; frame = (0 0; 141 141); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x1277aa490>>

然后执行

cy# [#0x1277a9d70 setHidden:YES]

发现拍摄的按钮消失了,验证了我的猜想。寻找按钮的响应事件,可以通过target来寻找

cy# [#0x1277a9d70 allTargets]
[NSSet setWithArray:@[#"<MainFrameSightViewController: 0x1269a4600>"]]]
cy# [#0x1277a9d70 allControlEvents]
193
cy# [#0x1277a9d70 actionsForTarget:#0x1269a4600 forControlEvent:193]
null

发现按钮并没有对应的action,这就奇怪了!UIButton必须要有target和action,不然这个Button不能响应事件。我们试试其他的ControlEvent

cy# [#0x1277a9d70 actionsForTarget:#0x1269a4600 forControlEvent:UIControlEventTouchDown]
@["btnPress"]
cy# [#0x1277a9d70 actionsForTarget:#0x1269a4600 forControlEvent:UIControlEventTouchUpOutside]
@["btnRelease"]
cy# [#0x1277a9d70 actionsForTarget:#0x1269a4600 forControlEvent:UIControlEventTouchUpInside]
@["btnRelease"]

结果发现这三个ContrlEvent有对应的action,我们再看看这三个枚举的值

typedef enum UIControlEvents : NSUInteger {
    UIControlEventTouchDown = 1 <<  0,
    UIControlEventTouchDownRepeat = 1 <<  1,
    UIControlEventTouchDragInside = 1 <<  2,
    UIControlEventTouchDragOutside = 1 <<  3,
    UIControlEventTouchDragEnter = 1 <<  4,
    UIControlEventTouchDragExit = 1 <<  5,
    UIControlEventTouchUpInside = 1 <<  6,
    UIControlEventTouchUpOutside = 1 <<  7,
    UIControlEventTouchCancel = 1 <<  8,
    ......
} UIControlEvents;

可以看出来UIControlEventTouchDown对应1,UIControlEventTouchUpInside对应128,UIControlEventTouchUpOutside对应64,三者相加正好193!原来调用[#0x1277a9d70 allControlEvents]的时候返回的应该是枚举,有多个枚举则把它们的值相加,是不是略坑?我也是这样觉得的!刚才我们把三种ControlEvent对应的action都打印出来了,继续LLDB+IDA进行动态分析。

(2)找到小视频拍摄完成跳转到发布界面的方法

因为要找到小视频发布的方法,所以对应的btnPress函数我们并不关心,把重点放在btnRelease上面,拍摄按钮松开后就会调用的方法。在IDA中找到这个方法

MainFrameSightViewController - (void)btnRelease

找到之后下个断点

(lldb) br s -a 0xac000+0x10209369C
Breakpoint 4: where = WeChat`___lldb_unnamed_symbol108894$$WeChat + 32, address = 0x000000010213f69c
Process 3813 stopped
* thread #1: tid = 0xf1ef0, 0x000000010213f69c WeChat`___lldb_unnamed_symbol108894$$WeChat + 32, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
    frame #0: 0x000000010213f69c WeChat`___lldb_unnamed_symbol108894$$WeChat + 32
WeChat`___lldb_unnamed_symbol108894$$WeChat:
->  0x10213f69c <+32>: bl     0x1028d0b60               ; symbol stub for: objc_msgSend
    0x10213f6a0 <+36>: cmp    w0, #2                    ; =2 
    0x10213f6a4 <+40>: b.ne   0x10213f6dc               ; <+96>
    0x10213f6a8 <+44>: adrp   x8, 5489

用手机拍摄小视频然后松开,触发了断点,说明我们的猜想是正确的。继续分析发现代码是从上图的右边走的,看了一下没有什么方法是跳转到发布视频的,不过仔细看一下有一个block,是系统的延时block,位置在0x102093760。然后我们跟着断点进去,在0x1028255A0跳转到x16所存的地址

(lldb) si
Process 3873 stopped
* thread #1: tid = 0xf62c4, 0x00000001028d9598 WeChat`dispatch_after, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001028d9598 WeChat`dispatch_after
WeChat`dispatch_after:
->  0x1028d9598 <+0>: adrp   x16, 1655
    0x1028d959c <+4>: ldr    x16, [x16, #1056]
    0x1028d95a0 <+8>: br     x16

WeChat`dispatch_apply:
    0x1028d95a4 <+0>: adrp   x16, 1655
(lldb) po $x2
<__NSStackBlock__: 0x16fd49f88>

发现传入的参数x2是一个block,我们再回顾一下dispatch_after函数

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

这个函数有三个参数,分别是dispatch_time_t、dispatch_queue_t、dispatch_block_t,那这里打印的x2就是要传入的block,所以我们猜测拍摄完小视频会有一个延时,然后执行刚才传入的block,所以x2中肯定有其他方法调用,下一步就是要知道这个block的位置。

(lldb) memory read --size 8 --format x 0x16fd49f88
0x16fd49f88: 0x000000019f8fd218 0x00000000c2000000
0x16fd49f98: 0x000000010214777c 0x0000000102fb0e60
0x16fd49fa8: 0x000000015da32600 0x000000015ea1a430
0x16fd49fb8: 0x000000015cf5fee0 0x000000016fd49ff0

0x000000010214777c就是block所在的位置,当然要减掉当前WeChat的ASLR偏移,最终在IDA中的地址为0x10209377C,突然发现这就是btnRelease的子程序sub_10209377C。这个子程序非常简单,只有一个方法selRef_logicCheckState_有可能是我们的目标。先看看这个方法是谁调用的

(lldb) br s -a 0xb4000+0x1020937BC
......
Process 3873 stopped
* thread #1: tid = 0xf62c4, 0x00000001021477bc WeChat`___lldb_unnamed_symbol108895$$WeChat + 64, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
    frame #0: 0x00000001021477bc WeChat`___lldb_unnamed_symbol108895$$WeChat + 64
WeChat`___lldb_unnamed_symbol108895$$WeChat:
->  0x1021477bc <+64>: adrp   x8, 5489
    0x1021477c0 <+68>: ldr    x1, [x8, #1552]
    0x1021477c4 <+72>: orr    w2, wzr, #0x1
    0x1021477c8 <+76>: ldp    x29, x30, [sp, #16]
(lldb) po $x0
<MainFrameSightViewController: 0x15d1f0c00>

发现还是MainFrameSightViewController这个对象调用的,那selRef_logicCheckState_肯定也在这个类的头文件中,寻找一下果然发现了

- (void)logicCheckState:(int)arg1;

在IDA左侧窗口中寻找[MainFrameSightViewController logicCheckState:],发现这个方法超级复杂,逻辑太多了,不着急慢慢捋。
在0x102094D6C位置我们发现一个switch jump,思路就很清晰了,我们只要找到小视频拍摄完成的这条线往下看就行了,LLDB来帮忙看看走的那条线。在0x102094D6C位置下个断点,这个断点在拍摄小视频的时候会多次触发,可以在拍摄之前把断点dis掉,拍摄松手之前再启用断点,打印此时的x8值

(lldb) p/x $x8
(unsigned long) $38 = 0x0000000102174e10

x8是一个指针,它指向的地址是0x102174e10,用这个地址减去当前ASLR的偏移就可以找到在IDA中的基地址,发现是0x102094E10,拍摄完成的逻辑处理这条线找到了,一直走到0x102094E24位置之后跳转0x1020951C4,这个分支的内容较少,里面有三个函数

loc_1020951C4
ADRP            X8, #selRef_hideTips@PAGE
LDR             X1, [X8,#selRef_hideTips@PAGEOFF]
MOV             X0, X19
BL              _objc_msgSend
ADRP            X8, #selRef_finishWriter@PAGE
LDR             X1, [X8,#selRef_finishWriter@PAGEOFF]
MOV             X0, X19
BL              _objc_msgSend
ADRP            X8, #selRef_turnCancelBtnForFinishRecording@PAGE
LDR             X1, [X8,#selRef_turnCancelBtnForFinishRecording@PAGEOFF]
MOV             X0, X19
BL              _objc_msgSend
B               loc_102095288

其中selRef_finishWriterselRef_turnCancelBtnForFinishRecording需要重点关注,这两个方法看上去都是小视频录制结束的意思,线索极有可能就在这两个函数中。通过查看调用者发现这两个方法都属于MainFrameSightViewController,继续在IDA中查看这两个方法。在selRef_finishWriter中靠近末尾0x102094248的位置发现一个方法名叫做f_switchToSendingPanel,下个断点,然后拍摄视频,发现这个方法并没有被触发。应该不是通过这个方法调用发布界面的,继续回到selRef_finishWriter方法中;在0x1020941DC的位置调用方法selRef_stopRecording,打印它的调用者发现这个方法属于SightFacade,继续在IDA中寻找这个方法的实现。在这个方法的0x101F9BED4位置又调用了selRef_stopRecord,同样打印调用者发现这个方法属于SightCaptureLogicF4,有点像剥洋葱,继续在寻找这个方法的实现。在这个方法内部0x101A98778位置又调用了selRef_finishWriting,同样的原理找到这个方法是属于SightMovieWriter。已经剥了3层了,继续往下:
SightMovieWriter - (void)finishWriting中的0x10261D004位置分了两条线,这个位置下个断点,然后拍摄完小视频触发断点,打印x19的值

(lldb) po $x19
<OS_dispatch_queue: CAPTURE.CALLBACK[0x13610bcd0] = { xrefcnt = 0x4, refcnt = 0x4, suspend_cnt = 0x0, locked = 1, target = com.apple.root.default-qos.overcommit[0x1a0aa3700], width = 0x0, running = 0x0, barrier = 1 }>

所以代码不会跳转到loc_10261D054而是走的左侧,在左侧的代码中发现启用了一个block,这个block是子程序sub_10261D0AC,地址为0x10261D0AC,找到这个地址,结构如下图所示:

sub_10261D0AC

可以看出来主要分两条线,我们在第一个方框的末尾也就是0x10261D108位置下个断点,等拍摄完毕触发断点之后打印x0的值为1,这里的汇编代码为

__text:000000010261D104                 CMP             X0, #2
__text:000000010261D108                 B.EQ            loc_10261D234

B.EQ是在上一步的结果为0才会跳转到loc_10261D234,但是这里的结果是不为0的,将x0的值改为2让上一步的结果为0

(lldb) po $x0
1
(lldb) register write $x0 2
(lldb) po $x0
2

此时放开断点,等待跳转到小视频发布界面,结果是一直卡在这个界面没有任何反应,所以猜测实现跳转的逻辑应该在右边的那条线,继续顺着右边的线寻找:
在右侧0x10261D3AC位置发现调用了下面的这个方法

- (void)finishWritingWithCompletionHandler:(void (^)(void))handler;

这个方法是系统提供的AVAssetWriter里面的方法,在视频写入完成之后要做的操作,这个里是要传入一个block的,因为只有一个参数所以对应的变量是x2,打印x2的值

(lldb) po $x2
<__NSStackBlock__: 0x16e086c78>
(lldb) memory read --size 8 --format x 0x16e086c78
0x16e086c78: 0x00000001a0aa5218 0x00000000c2000000
0x16e086c88: 0x00000001026d94b0 0x0000000102fc98c0
0x16e086c98: 0x0000000136229fd0 0x000000016e086d00
0x16e086ca8: 0x00000001997f5318 0xfffffffec9e882ff

并且通过栈内存找到block位置为0x10261D4B0(需要减去ASLR的偏移)

sub_10261D4B0
var_20= -0x20
var_10= -0x10
STP             X20, X19, [SP,#var_20]!
STP             X29, X30, [SP,#0x20+var_10]
ADD             X29, SP, #0x20+var_10
MOV             X19, X0
LDR             X0, [X19,#0x20]
ADRP            X8, #selRef_stopAmr@PAGE
LDR             X1, [X8,#selRef_stopAmr@PAGEOFF]
BL              _objc_msgSend
LDR             X0, [X19,#0x20]
ADRP            X8, #selRef_compressAudio@PAGE
LDR             X1, [X8,#selRef_compressAudio@PAGEOFF]
LDP             X29, X30, [SP,#0x20+var_10]
LDP             X20, X19, [SP+0x20+var_20],#0x20
B               _objc_msgSend
; End of function sub_10261D4B0

只调用了两个方法,一个是selRef_stopAmr停止amr(一种音频格式),另一个是selRef_compressAudio压缩音频,拍摄完成的下一步操作应该不会放在这两个方法里面,找了这么久也没有头绪,这个路看来走不通了,不要钻牛角尖,战略性撤退寻找其他入口。
逆向的乐趣就是一直寻找真相的路上,能体会到成功的乐趣,也有可能方向错了离真相反而越来越远,不要气馁调整方向继续前进!

2.“另辟蹊径”

(由于微信在后台偷偷升级了,下面的内容都是微信6.3.30版本的ASLR,上面的分析基于6.3.28版本)

注意到在点击朋友圈右上角的相机按钮底部会弹出一个Sheet,第一个就是Sight小视频,从这里入手,用cycript查看Sight按钮对应的事件是哪个

iPhone-5S:~ root# cycript -p "WeChat"
cy# [UIApp windows].toString()
`(
    "<iConsoleWindow: 0x14d6ccc00; baseClass = UIWindow; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x14d7df110>; layer = <UIWindowLayer: 0x14d7d6f60>>",
    "<SvrErrorTipWindow: 0x14eaa5800; baseClass = UIWindow; frame = (0 0; 320 45); hidden = YES; gestureRecognizers = <NSArray: 0x14e9e8950>; layer = <UIWindowLayer: 0x14e9e6510>>",
    "<UITextEffectsWindow: 0x14ec38ba0; frame = (0 0; 320 568); opaque = NO; autoresize = W+H; layer = <UIWindowLayer: 0x14ec39360>>",
    "<UITextEffectsWindow: 0x14e9c67a0; frame = (0 0; 320 568); layer = <UIWindowLayer: 0x14d683ff0>>",
    "<UIRemoteKeyboardWindow: 0x14f226e40; frame = (0 0; 320 568); opaque = NO; autoresize = W+H; layer = <UIWindowLayer: 0x14d6f4de0>>",
    "<NewYearActionSheet: 0x14f1704a0; baseClass = UIWindow; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x14ef9bf90>; layer = <UIWindowLayer: 0x14ef61a20>>"
)`
cy# [#0x14f1704a0 recursiveDescription].toString()

底部的Sheet是NewYearActionSheet,然后打印NewYearActionSheet的UI树状结构图(比较长不贴了)。然后找到Sight对应的UIButton是0x14f36d470

cy# [#0x14f36d470 allTargets]
[NSSet setWithArray:@[#"<NewYearActionSheet: 0x14f1704a0; baseClass = UIWindow; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x14ef9bf90>; layer = <UIWindowLayer: 0x14ef61a20>>"]]]
cy# [#0x14f36d470 allControlEvents]
64
cy# [#0x14f36d470 actionsForTarget:#0x14f1704a0 forControlEvent:64]
@["OnDefaultButtonTapped:"]

通过UIControl的actionsForTarget:forControlEvent:方法可以找到按钮绑定的事件,Sight按钮的触发方法为OnDefaultButtonTapped:,回到IDA中在NewYearActionSheet中找到这个方法们继续往下分析只有这个方法selRef_dismissWithClickedButtonIndex_animated,通过打印它的调用者发现还是NewYearActionSheet,继续点进去找到newYearActionSheet_clickedButtonAtIndex方法,看样子不是NewYearActionSheet自己的,打印调用者x0发现它属于类WCTimeLineViewController。跟着断点走下去在0x1012B78CC位置调用了方法#selRef_showSightWindowForMomentWithMask_byViewController_scene
通过观察发现这个方法的调用者是0x1012B78AC这个位置的返回值x0,这是一个类SightFacade,猜测这个方法在SightFacade里面,去头文件里找一下果然发现这个方法

- (void)showSightWindowForMomentWithMask:(id)arg1 byViewController:(id)arg2 scene:(int)arg3;

这个方法应该就是跳转到小视频界面的方法了。下面分别打印它的参数

(lldb) po $x2
<UIImage: 0x14f046660>, {320, 568}
(lldb) po $x3
<WCTimeLineViewController: 0x14e214800>
(lldb) po $x4
2
(lldb) po $x0
<SightFacade: 0x14f124b40>

其中x2、x3、x4分别对应三个参数,x0是调用者,跳到这个方法内部查看怎么实现的。发现在这个方法中进行了小视频拍摄界面的初始化工作,首先初始化一个MainFrameSightViewController,再创建一个UINavigationController将MainFrameSightViewController放进去,接下来初始化一个MMWindowController调用

- (id)initWithViewController:(id)arg1 windowLevel:(int)arg2;

方法将UINavigationController放了进去,完成小视频拍摄界面的所有UI创建工作。
拍摄完成之后进入发布界面,此时用cycript找到当前的Controller是SightMomentEditViewController,由此萌生一个想法,跳过前面的拍摄界面直接进入发布界面不就可以了吗?我们自己创建一个SightMomentEditViewController然后放到UINavigationController里面,然后再将这个导航控制器放到MMWindowController里面。(这里我已经写好tweak进行了验证,具体的tweak思路编写在后文有)结果是的确可以弹出发布的界面,但是导航栏的NavgationBar遮住了原来的,整个界面是透明的,很难看,而且发布完成之后无法销毁整个MMWindowController,还是停留在发布界面。我们要的结果不是这个,不过确实有很大的收获,最起码可以直接调用发布界面了,小视频也能正常转发。我个人猜测,当前界面不能被销毁的原因是因为MMWindowController新建了一个window,它跟TimeLine所在的keyWindow不是同一个,SightMomentEditViewController的按钮触发的方法是没有办法销毁这个window的,所以有一个大胆的猜想,我直接在当前的WCTimeLineViewController上把SightMomentEditViewController展示出来不就可以了吗?

[WCTimelineVC presentViewController:editSightVC animated:YES completion:^{
}];

像这样展示岂不妙哉?不过通过观察SightMomentEditViewController的头文件,结合小视频发布时界面上的元素,推测创建这个控制器至少需要两个属性,一个是小视频的路径,另一个是小视频的缩略图,将这两个关键属性给了SightMomentEditViewController那么应该就可以正常展示了

SightMomentEditViewController *editSightVC = [[%c(SightMomentEditViewController) alloc] init];
NSString *localPath = [[self iOSREMediaItemFromSight] pathForSightData];
UIImage *image = [[self valueForKey:@"_sightView"] getImage];
[editSightVC setRealMoviePath:localPath];
[editSightVC setMoviePath:localPath];
[editSightVC setRealThumbImage:image];
[editSightVC setThumbImage:image];
[WCTimelineVC presentViewController:editSightVC animated:YES completion:^{
}];

小视频的发布界面可以正常显示并且所有功能都可以正常使用,唯一的问题是返回按钮没有效果,并不能销毁SightMomentEditViewController。用cycript查看左侧按钮的actionEvent找到它的响应函数是- (void)popSelf;,点击左侧返回触发的是pop方法,但是这个控制器并不在navgationController里面,所以无效,我们要对这个方法进行重写

- (void)popSelf
{
    [self dismissViewControllerAnimated:YES completion:^{

    }];
}

此时再点击返回按钮就可以正常退出了,此外,在WCContentItemViewTemplateNewSight中发现了一个方法叫做- (void)sendSightToFriend;,可以直接将小视频转发给好友,至此小视频转发的功能已经找到了。

手把手教你逆向微信之朋友圈小视频转发(下)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容