非越獄環境下從應用重簽名到微信上加載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閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容