非越狱环境下从应用重签名到微信上加载Cycript

注:这么长时间以来本文后续加入了新的内容以及一些修改,由于精力有限,所以不会同步更新到简书上。请大家移步到本项目的Github: https://github.com/Urinx/iOSAppHook

从零到一,非越狱环境下iOS应用逆向研究,从dylib注入,应用重签名到App Hook。文中用到的工具和编译好的dylib可在Github上下载。

注意!本文所有操作均在以下环境下成功进行,不同平台或环境可能存在某些问题,欢迎大家在issue中提出问题以及相互讨论。

Mac OS X 10.11.6 (15G12a)

Xcode 7.3.1 (7D1014)

iPhone 5s, iOS 9.3.3 (13G21)

免费开发者账号

示例App:微信 v6.3.19.18

前言

提到非越狱环境下App Hook大家早就已经耳熟能详,已经有很多大神研究过,这方面相关的资料和文章也能搜到很多。我最早是看到乌云知识库上蒸米的文章才对这方面有所了解,当时就想试试,整个过程看似简单(大神总是一笔带过),然而当自己真正开始动手时一路上遇到各种问题(一脸懵逼),在iOSRE论坛上也看到大家遇到的各种问题,其实阻扰大家的主要是一些环境的搭建以及相关配置没设置好,结果导致dylib编译过程各种错误,重签名不成功,各种闪退等。所以本文里的每一步操作都会很详细的交代,确保大家都能操作成功。

应用脱壳

我们知道,App Store里的应用都是加密了的,直接拿上来撸是不正确的,所以在此之前一般会有这么一个砸壳的过程,其中用到的砸壳工具就是dumpdecrypted,其原理是让app预先加载一个解密的dumpdecrypted.dylib,然后在程序运行后,将代码动态解密,最后在内存中dump出来整个程序。然而砸壳实在越狱的环境下进行的,鉴于本文主要关注点在非越狱环境下,再者我手里也没有越狱设备(有就不会这么蛋疼了)。

所以这里我们选择的是直接从PP助手等各种xx助手里面下载,注意的是这里下载的是越狱应用(不是正版应用),也就是所谓的脱过壳的应用。

为了谨慎起见,这里我们还需要确认一下从xx助手里下载的应用是否已解密,毕竟有好多应用是只有部分架构被解密,还有就是Watch App以及一些扩展依然加密了,所以最好还是确认一下,否则的话,就算hook成功,签名成功,安装成功,app还是会闪退。

首先,找到应用对应的二进制文件,查看包含哪些架构:

> file WeChat.app/WeChat
WeChat.app/WeChat: Mach-O universal binary with 2 architectures
WeChat.app/WeChat (for architecture armv7): Mach-O executable arm
WeChat.app/WeChat (for architecture arm64): Mach-O 64-bit executable

可以看到微信包含两个架构,armv7和arm64。关于架构与设备之间的对应关系可以从iOS Support Matrix上查看。理论上只要把最老的架构解密就可以了,因为新的cpu会兼容老的架构。

otool可以输出app的load commands,然后通过查看cryptid这个标志位来判断app是否被加密。1代表加密了,0代表被解密了:

> otool -l WeChat.app/WeChat | grep -B 2 crypt
          cmd LC_ENCRYPTION_INFO
      cmdsize 20
     cryptoff 16384
    cryptsize 40534016
      cryptid 0
--
          cmd LC_ENCRYPTION_INFO_64
      cmdsize 24
     cryptoff 16384
    cryptsize 43663360
      cryptid 0

可以看到微信已经被解密了,第一个对应的是较老的armv7架构,后者则是arm64架构。

鉴于微信是一个多targets的应用,包含一个Watch App和一个分享扩展。所以同理,我们还需要依次确认以下二进制文件,这里就跳过了。

WeChat.app/Watch/WeChatWatchNative.app/WeChatWatchNative
WeChat.app/Watch/WeChatWatchNative.app/PlugIns/WeChatWatchNativeExtension.appex/WeChatWatchNativeExtension
WeChat.app/PlugIns/WeChatShareExtensionNew.appex/WeChatShareExtensionNew

应用重签名

在第二部分我们将要进行的是应用重签名,注意这里并不是按照整个操作流程的顺序来讲,而是跳过编译dylib,因为我觉得如果现在你没有把重签名拿下的话,写tweak写hook都是白搭。所以从这里在开始,我们不需要对App进行任何修改和处理,仅仅对其进行重签名,然后将其安装到设备上能够正常的运行。再次提醒的是重签名用的是脱壳后的App,加密的App重签名成功安到设备上也会闪退。

关于iOS应用重签名的文章有很多,签名方法都是一样,个别地方会有些小出入,然后按照里面给出的步骤手动一步一步的来操作,不知道是由于免费的开发者证书的原因还是哪一步漏掉了或者是什么其它的原因,总之就是不成功。就在一筹莫展的时候,偶然发现了一个名为iOS App Signer的Mac上的应用,这款重签名工具能够用免费的开发者账号重签名应用。我试了下,用这个工具重签名了一个应用,并且成功安装到手机上,尽管打开时闪退,但至少总算能安装到设备上去了。

看到签名成功心里一阵高兴,并且由于该应用开源,于是就到Github上阅读源码看其具体的实现。该应用是用Swift语音所写,代码量也不多,阅读起来没有问题。由于整个操作都是在终端下进行的,从终端到图形界面来回切换实在是麻烦,所以在其代码基础上稍作修改写了一个Command Line Tool工具在命令行下使用。

下面就来具体交代下这个重签名工具到底做了什么。

1. 获取到本地上的开发者签名证书和所有的Provisioning文件

获取本机上的Provisioning文件主要是调用了populateProvisioningProfiles()方法,该方法调用了ProvisioningProfile.getProfiles()将结果封装到ProvisioningProfile结构体中以数组的形式返回,并对其结果做了一个刷选,去掉了那些过期的证书。

struct ProvisioningProfile {
    ...
    static func getProfiles() -> [ProvisioningProfile] {
        var output: [ProvisioningProfile] = []
        
        let fileManager = NSFileManager()
        if let libraryDirectory = fileManager.URLsForDirectory(.LibraryDirectory, inDomains: .UserDomainMask).first, libraryPath = libraryDirectory.path {
            let provisioningProfilesPath = libraryPath.stringByAppendingPathComponent("MobileDevice/Provisioning Profiles") as NSString
            
            if let provisioningProfiles = try? fileManager.contentsOfDirectoryAtPath(provisioningProfilesPath as String) {
                for provFile in provisioningProfiles {
                    if provFile.pathExtension == "mobileprovision" {
                        let profileFilename = provisioningProfilesPath.stringByAppendingPathComponent(provFile)
                        if let profile = ProvisioningProfile(filename: profileFilename) {
                            output.append(profile)
                        }
                    }
                }
            }
        }
        return output
    }
    ...
}

简单点用shell表示就是,先列出所有的~/Library/MobileDevice/Provisioning Profiles路径下所有的后缀为.mobileprovision的文件,然后依次获取文件的信息(plist格式)。

security cms -D -i "~/Library/MobileDevice/Provisioning Profiles/xxx.mobileprovision"

获取开发者签名证书:

func populateCodesigningCerts() {
    var output: [String] = []
    
    let securityResult = NSTask().execute(securityPath, workingDirectory: nil, arguments: ["find-identity","-v","-p","codesigning"])
    if securityResult.output.characters.count >= 1 {
        let rawResult = securityResult.output.componentsSeparatedByString("\"")
        
        for index in 0.stride(through: rawResult.count - 2, by: 2) {
            if !(rawResult.count - 1 < index + 1) {
                output.append(rawResult[index+1])
            }
        }
    }
    self.codesigningCerts = output
    
    Log("Found \(output.count) Codesigning Certificates")
}

以上代码相当于:

> security find-identity -v -p codesigning
1) 1234567890123456789012345678901234567890 "iPhone Developer: XXX (xxxxxxxxxx)"
2) 1234567890123456789012345678901234567890 "Mac Developer: XXX (xxxxxxxxxx)"

2. 一些准备工作

创建临时目录,makeTempFolder()方法:

> mktemp -d -t com.eular.test
/var/folders/qr/8_n21zhd4f993khcsh_qll000000gp/T/com.eular.test.6aHPpdBZ

处理不同格式的输入文件,包括debipaappxcarchive

deb -> ar -x xxx.deb -> tar -xf xxx.tar -> mv Applications/ -> Payload/
ipa -> unzip -> Payload/
app -> copy -> Payload/
xcarchive -> copy .xcarchive/Products/Applications/ -> Payload/

3. 重签名

首先,复制provisioning文件到app目录里:

cp xxx.mobileprovision Payload/xxx.app/embedded.mobileprovision

根据provisioning文件导出entitlements.plist:

if provisioningFile != nil || useAppBundleProfile {
    print("Parsing entitlements")
                    
    if let profile = ProvisioningProfile(filename: useAppBundleProfile ? appBundleProvisioningFilePath : provisioningFile!){
        if let entitlements = profile.getEntitlementsPlist(tempFolder) {
            Log("–––––––––––––––––––––––\n\(entitlements)")
            Log("–––––––––––––––––––––––")
            do {
                try entitlements.writeToFile(entitlementsPlist, atomically: false, encoding: NSUTF8StringEncoding)
                Log("Saved entitlements to \(entitlementsPlist)")
            } catch let error as NSError {
                Log("Error writing entitlements.plist, \(error.localizedDescription)")
            }
        } else {
            Log("Unable to read entitlements from provisioning profile")
            warnings += 1
        }
        if profile.appID != "*" && (newApplicationID != "" && newApplicationID != profile.appID) {
            Log("Unable to change App ID to \(newApplicationID), provisioning profile won't allow it")
            cleanup(tempFolder); return
        }
    } else {
        Log("Unable to parse provisioning profile, it may be corrupt")
        warnings += 1
    }
    
}

修复可执行文件的权限:

if let bundleExecutable = getPlistKey(appBundleInfoPlist, keyName: "CFBundleExecutable"){
    NSTask().execute(chmodPath, workingDirectory: nil, arguments: ["755", appBundlePath.stringByAppendingPathComponent(bundleExecutable)])
}

替换所有Info.plist里的CFBundleIdentifier

if let oldAppID = getPlistKey(appBundleInfoPlist, keyName: "CFBundleIdentifier") {
                        
    func changeAppexID(appexFile: String){
        
        func shortName(file: String, payloadDirectory: String) -> String {
            return file.substringFromIndex(payloadDirectory.endIndex)
        }
        
        let appexPlist = appexFile.stringByAppendingPathComponent("Info.plist")
        if let appexBundleID = getPlistKey(appexPlist, keyName: "CFBundleIdentifier"){
            let newAppexID = "\(newApplicationID)\(appexBundleID.substringFromIndex(oldAppID.endIndex))"
            print("Changing \(shortName(appexFile, payloadDirectory: payloadDirectory)) id to \(newAppexID)")
            
            setPlistKey(appexPlist, keyName: "CFBundleIdentifier", value: newAppexID)
        }
        if NSTask().execute(defaultsPath, workingDirectory: nil, arguments: ["read", appexPlist,"WKCompanionAppBundleIdentifier"]).status == 0 {
            setPlistKey(appexPlist, keyName: "WKCompanionAppBundleIdentifier", value: newApplicationID)
        }
        recursiveDirectorySearch(appexFile, extensions: ["app"], found: changeAppexID)
    }
    
    recursiveDirectorySearch(appBundlePath, extensions: ["appex"], found: changeAppexID)
}
...

然后对所有的dylibso0vispvrframeworkappexapp以及egg文件用codesign命令进行签名。代码略长,此处不写。

4. 打包

最后将上述目录用zip打包成ipa文件就可以了。

5. 安装ipa文件

这里用到的是mobiledevice工具,执行下列命令安装ipa文件到手机上:

./mobiledevice install_app xxx.ipa

当然,你也可以使用ideviceinstaller工具。

./ideviceinstaller -i xxx.ipa

总之,上述步骤较多,主要集中在前4个步骤上,不建议自己操作,你可以选择使用图形界面的iOS App Signer应用,也可以使用我提供的根据其开源代码写的命令行工具,AppResign,你可以直接下载编译好的二进制文件

使用方法:

./AppResign input out

这里以微信为例,我们一开始直接对其重签名,总是不成功,我猜问题主要在里面的Watch App上。于是乎便采取的最简单粗暴的方法,解压ipa文件,将WeChat.app里面的Watch文件夹,连同PlugIns文件夹一起删去。再用AppResign重签名,如图所示:

这时候将其安装到设备上,成功,并能够正常的打开。至此,这一步就大功告成了。

最后一点要注意的是,由于中国的开发者利用免费的证书大量对应用进行重签名,所以目前苹果加上了许多限制,免费开发者的provisioning证书有效时间从之前的30天改为7天,过期后需要重新签名。另外就是一个星期内最多只能申请到10个证书。

安装iOSOpenDev

如果上面的重签名步骤你能够成功的进行,接下来便开始考虑搭建编写dylib的越狱开发环境了。这里我们选择的是iOSOpenDev这个越狱开发平台工具,该工具集成到Xcode里面,提供了编写越狱应用插件的各种模版。所以下面就讲讲如何以正确的姿势安装iOSOpenDev。

一般一开始我们会去其官网上下载了一个pkg安装文件然后点击安装,结果一般会安装失败,接下来就开始尝试了各种姿势。其实那个pkg安装文件也没干啥,就是执行了一个iod-setup脚本,好吧于是就手工执行了这个脚本,发现其中下载github上的东西老是失败,然后翻了个墙,然后就好了。。。

sudo ./iod-setup base
sudo ./iod-setup sdk -sdk iphoneos

执行第二步会出现一个错误就是链接iOS 9.3的private framework失败,主要是在9.3的SDK里去掉了private framework。

PrivateFramework directory not found: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.3.sdk/System/Library/PrivateFrameworks

解决办法:

1. 在[这里](https://jbdevs.org/sdks/)下载9.2的SDK。
2. 解压后放到/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs。
3. 你可以把9.2里面的PrivateFrameworks文件夹复制到9.3对应的位置里path/to/iPhoneOS9.3.sdk/System/Library/。
4. 或者是修改/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Info.plist文件,将`MinimumSDKVersion`改为9.2。
5. 再次运行`sudo ./iod-setup sdk -sdk iphoneos`命令。

新建个CaptainHook连上设备编译一下,就可以发现编译成功了。

App Hook

在这部分里,我们便开始动手编写dylib并将其注入到目标应用中。首先打开Xcode,新建一个项目,模版选择iOSOpenDev里的CaptainHook Tweak,项目名为hook。

删除hook.mm文件里的模版内容,替换为以下内容:

__attribute__((constructor)) static void entry() {
    NSLog(@"hello, world!");
}

Cmd+B编译,打开LatestBuild文件夹,得到编译好的dylib文件。

使用yololib工具对对二进制文件进行dylib的注入。

>./yololib WeChat.app/WeChat hook.dylib
2016-06-16 07:35:06.014 yololib[49519:642763] dylib path @executable_path/hook.dylib
2016-06-16 07:35:06.016 yololib[49519:642763] dylib path @executable_path/hook.dylib
Reading binary: WeChat.app/WeChat

2016-06-16 07:35:06.016 yololib[49519:642763] FAT binary!
2016-06-16 07:35:06.016 yololib[49519:642763] Injecting to arch 9
2016-06-16 07:35:06.016 yololib[49519:642763] Patching mach_header..
2016-06-16 07:35:06.016 yololib[49519:642763] Attaching dylib..

2016-06-16 07:35:06.016 yololib[49519:642763] Injecting to arch 0
2016-06-16 07:35:06.016 yololib[49519:642763] 64bit arch wow
2016-06-16 07:35:06.016 yololib[49519:642763] dylib size wow 56
2016-06-16 07:35:06.017 yololib[49519:642763] mach.ncmds 75
2016-06-16 07:35:06.017 yololib[49519:642763] mach.ncmds 76
2016-06-16 07:35:06.017 yololib[49519:642763] Patching mach_header..
2016-06-16 07:35:06.017 yololib[49519:642763] Attaching dylib..

2016-06-16 07:35:06.017 yololib[49519:642763] size 51
2016-06-16 07:35:06.017 yololib[49519:642763] complete!

注入成功后可以用MachOView程序查看整个MachO文件的结构,比如在Load Commands这个数据段里,可以看到LC_LOAD_DYLIB加载动态库。我们使用MachOView打开,可以看到已经被注入hook.dylib,如图所示。

别忘了,我们还需要将我们注入的dylib文件放到WeChat.app目录下。

cp hook.dylib WeChat.app/

接下来就是之前的老套路了,先重签名然后安装到设备上。安装完后,我们用idevicesyslog查看log信息。在命令行中输入命令:

idevicesyslog

然后打开我们修改过的微信应用,可以看到如下图的log输出信息。

可以看到hello, world!成功输出,表示我们的dylib的代码已经能够执行。

一个简单的CaptainHook载入Cycript

当然不可能一个NSLog结束了,所以接下来我们将编写一个dylib来载入Cycript工具方便我们以后调试目标应用。

新建一个CaptainHook,项目名为loadCycript。首先,导入Cycript.framework,另外还需要导入其依赖的JavaScriptCore.frameworklibsqlite3.0.tbdlibstdc++.6.0.9.tbd

然后,在Build Settings里面的搜索bitcode,将Enable Bitcode选项设为NO

一开始编写loadCycript.mm文件的内容是这样的:

#import <Cycript/Cycript.h>
#import <CaptainHook/CaptainHook.h>

#define CYCRIPT_PORT 8888

CHDeclareClass(AppDelegate);
CHDeclareClass(UIApplication);

CHOptimizedMethod2(self, void, AppDelegate, application, UIApplication *, application, didFinishLaunchingWithOptions, NSDictionary *, options)
{
    CHSuper2(AppDelegate, application, application, didFinishLaunchingWithOptions, options);
    
    NSLog(@"## Start Cycript ##");
    CYListenServer(CYCRIPT_PORT);
}

__attribute__((constructor)) static void entry() {
    CHLoadLateClass(AppDelegate);
    CHHook2(AppDelegate, application, didFinishLaunchingWithOptions);
}

上述代码hook了AppDelegate里的application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)方法,在该方法里开启Cycript并绑定到8888端口。

编译该项目生成dylib文件,注入到微信App中,重签名后安装到手机里,然后在idevicesyslog输出的日志里看看有没有我们输出的信息,结果找了半天,嗯哼,还真没有。。。

问题出在哪了呢,难道是application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)这个方法没有hook成功吗?好吧,随后又试了试hookapplicationDidEnterBackground等方法,结果没一个成功,然后就一脸懵逼了,此时第一反应就是不会没有AppDelegate这个类吧。

没办法,只有导出微信的头文件看看了。

class-dump -H -o header WeChat.app/WeChat

结果一看,还真没有(°_°)… 通过搜索关键字AppDelegate终于找到了其真身MicroMessengerAppDelegate,虽说穿了个马甲,但依然还是它。

该有的方法都有,这样我就放心了。于是对之前的代码稍作修改就可以了。

#import <Cycript/Cycript.h>
#import <CaptainHook/CaptainHook.h>

#define CYCRIPT_PORT 8888

CHDeclareClass(UIApplication);
CHDeclareClass(MicroMessengerAppDelegate);

CHOptimizedMethod2(self, void, MicroMessengerAppDelegate, application, UIApplication *, application, didFinishLaunchingWithOptions, NSDictionary *, options)
{
    CHSuper2(MicroMessengerAppDelegate, application, application, didFinishLaunchingWithOptions, options);
    
    NSLog(@"## Start Cycript ##");
    CYListenServer(CYCRIPT_PORT);
}


CHConstructor {
    @autoreleasepool {
        CHLoadLateClass(MicroMessengerAppDelegate);
        CHHook2(MicroMessengerAppDelegate, application, didFinishLaunchingWithOptions);
    }
}

再跑一遍,这会我们就能在log日志里能看到Cycript成功开启的消息了。

我们用Cycript远程连上去调试看看,比如说修改发现页的tableView背景颜色。

注:本文中用到的AppResign重签名工具,以及编译好的loadCycript.dylib可以在这里下载。

后续

至于之后该做什么,你想干嘛就干嘛。Cycript在手,天下我有,你可以使用Class-dump工具dunp出应用的头文件,或者是将二进制文件拖到ida或hopper里面反汇编分析,写tweak插件,实现各种姿势抢红包等等,本文就不讨论这些了。

参考链接

之前看的都没记录,下列都是后来想到才记下来的。

蒸米的iOS冰与火之歌系列:

微信重签名相关:

iOSOpenDev:

其它:

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

推荐阅读更多精彩内容