使用BlackHook(黑鉤) 可以Hook一切java或者kotlin方法

前言

之前做內(nèi)存優(yōu)化的時候愧口,為了實現(xiàn)對線程的使用監(jiān)控,借助了一個第三方的hook框架(epic)授滓,這個框架可以hook一切java方法羞海,使用也簡單,但是最大的問題是它有較嚴重的兼容性問題筛婉,部分機型會出現(xiàn)閃退的現(xiàn)象,這就導致它不能被帶到線上使用癞松,只能在線下使用爽撒,為了實現(xiàn)在線上監(jiān)控線程的使用,于是我便開發(fā)了BlackHook插件响蓉,也可以hook一切java方法硕勿,而且很穩(wěn)定,沒有兼容性問題枫甲,真是十足的黑科技

簡介

BlackHook 是一個實現(xiàn)編譯時插樁的gradle插件源武,基于ASM+Tranfrom實現(xiàn),理論上可以hook任意一個java方法或者kotlin方法想幻,只要代碼對應的字節(jié)碼可以在編譯階段被Tranfrom掃描到粱栖,就可以使用ASM在代碼對應的字節(jié)碼處插入特定字節(jié)碼,從而hook該方法

優(yōu)點

  1. 用DSL(領域特定語言)使用該插件脏毯,使用簡單闹究,配置靈活,而且插入的字節(jié)碼可以使用
    ASM Bytecode Viewer Support Kotlin 插件自動生成食店,上手難度低
  2. 理論上可以hook任意一個java方法渣淤,只要代碼對應的字節(jié)碼可以在編譯階段被Tranfrom掃描到
  3. 基于ASM+Tranfrom實現(xiàn),在編譯階段直接修改字節(jié)碼吉嫩,效率高价认,沒有兼容性問題

使用

在app下面的build.gradle文件添加如下代碼

apply plugin: 'com.blackHook'

/**
 * 返回hook線程構造函數(shù)的字節(jié)碼,Hook 線程的構造函數(shù)自娩,讓每次在調(diào)用Thread的構造函數(shù)的時候就會調(diào)用
 * ThreadCheck類的 printThread方法用踩,從而在控制臺打印線程的構造函數(shù)的調(diào)用堆棧,這些代碼可以借助
 * ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一個類捶箱,用于修改字節(jié)碼
 */
void createHookThreadByteCode(MethodVisitor mv, String className) {
    mv.visitTypeInsn(Opcodes.NEW, "com/quwan/tt/asmdemoapp/ThreadCheck")
    mv.visitInsn(Opcodes.DUP)
    mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "<init>", "()V", false)
    mv.visitLdcInsn(className)
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "printThread", "(Ljava/lang/String;)V", false)
}

/**
 * 返回需要被hook的方法,需要被hook的方法是Thread的構造函數(shù)
 */
List<HookMethod> getHookMethods() {
    List<HookMethod> hookMethodList = new ArrayList<>()
    hookMethodList.add(new HookMethod("java/lang/Thread", "<init>", "()V", { MethodVisitor mv -> createHookThreadByteCode(mv, "java/lang/Thread") }))
    return hookMethodList
}

blackHook {
    //表示要處理的數(shù)據(jù)類型是什么动漾,CLASSES 表示要處理編譯后的字節(jié)碼(可能是 jar 包也可能是目錄)丁屎,RESOURCES 表示要處理的是標準的 java 資源
    inputTypes BlackHook.CONTENT_CLASS
    //表示Transform 的作用域,這里設置的SCOPE_FULL_PROJECT代表作用域是全工程
    scopes BlackHook.SCOPE_FULL_PROJECT
    //表示是否支持增量編譯旱眯,false不支持
    isIncremental false
    //表示hook的方法
    hookMethodList = getHookMethods()
}

以上的代碼其實是hook的Thread的構造函數(shù)晨川,將ThreadCheck的printThread方法hook到了Thread的構造函數(shù)中,每次調(diào)用線程的構造函數(shù)的時候就會調(diào)用ThreadCheck的printThread方法删豺,這個方法會打印出Thread的構造函數(shù)的調(diào)用堆棧共虑,從而可以在控制臺知道哪個頁面的哪行代碼實例化了Thread,ThreadCheck的代碼如下

class ThreadCheck {

    var isCanAppendLog = false
    private val tag = "====>ThreadCheck"

    fun printThread(name : String){

        println("====>printThread:${name}")

        val es = Thread.currentThread().stackTrace

        val normalInfo = StringBuilder(" \nThreadTrace:")
            .append("\nthreadName:${name}")
            .append("\n====================================threadTraceStart=======================================")

        for (e in es) {

            if (e.className == "dalvik.system.VMStack" && e.methodName == "getThreadStackTrace") {
                isCanAppendLog = false
            }

            if (e.className.contains("ThreadCheck") && e.methodName == "printThread") {
                isCanAppendLog = true
            } else {
                if (isCanAppendLog) {
                    normalInfo.append("\n${e.className}(lineNumber:${e.lineNumber})")
                }
            }
        }
        normalInfo.append("\n=====================================threadTraceEnd=======================================")

        Log.i(tag, normalInfo.toString())
    }

}

上面的代碼獲取了調(diào)用堆棧呀页,并且打印到控制臺

實現(xiàn)原理

首先它是一個gradle 的自定義Plugin妈拌,其次它是通過在編譯階段修改字節(jié)碼實現(xiàn)Hook,在編譯階段通過Tranfrom掃描所有的字節(jié)碼蓬蝶,然后根據(jù)在使用插件的時候設置的需要被Hook的方法尘分,插入需要被插入的字節(jié)碼,
需要被插入的字節(jié)碼也是在使用的時候設置的丸氛,例如下面的代碼

/**
 * 返回hook線程構造函數(shù)的字節(jié)碼培愁,Hook 線程的構造函數(shù),讓每次在調(diào)用Thread的構造函數(shù)的時候就會調(diào)用
 * ThreadCheck的 printThread方法缓窜,從而在控制臺打印線程的構造函數(shù)的調(diào)用堆棧定续,這些代碼可以借助
 * ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一個類禾锤,用于修改字節(jié)碼
 */
void createHookThreadByteCode(MethodVisitor mv, String className) {
    mv.visitTypeInsn(Opcodes.NEW, "com/quwan/tt/asmdemoapp/ThreadCheck")
    mv.visitInsn(Opcodes.DUP)
    mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "<init>", "()V", false)
    mv.visitLdcInsn(className)
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "printThread", "(Ljava/lang/String;)V", false)
}

準備過程

實現(xiàn)這個gradle插件需要我們有足夠的預備知識私股,如下:

實現(xiàn)過程

1.自定義gradle plugin

因為這是一個gradle插件螃成,所以需要我們自定義一個gradle的plugin

1. 新建一個模塊

在工程中新建一個模塊旦签,命名為"buildSrc",注意寸宏,一定要命名為buildSrc宁炫,否則在工程中必須要將代碼發(fā)布到本地或者遠程maven倉庫中才能正常使用,這樣調(diào)試不方便氮凝,如下所示:

[圖片上傳失敗...(image-88703d-1634713340383)]

2. 然后配置gradle腳本羔巢,代碼如下所示:

plugins {
    id 'java-library'
    id 'maven'
    id 'groovy'
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

dependencies {
    implementation gradleApi()//gradle sdk
    implementation localGroovy()
    implementation "com.android.tools.build:gradle:3.4.1"
    implementation 'org.ow2.asm:asm:9.1'
    implementation 'org.ow2.asm:asm-commons:9.1'
}

3. 實現(xiàn)Plugin類

新建groovy文件夾,新建BlackHookPlugin類,繼承Transform類竿秆,實現(xiàn)Plugin接口

[圖片上傳失敗...(image-843eb6-1634713340383)]

BlackHookPlugin代碼如下所示:

package com.blackHook.plugin

class BlackHookPlugin extends Transform implements Plugin<Project> {

    ....此處省略了很多代碼

    @Override
    void apply(Project target) {
        println("注冊了")
        project = target
        target.extensions.getByType(BaseExtension).registerTransform(this)
        target.extensions.create("blackHook", BlackHook.class)
    }
    
     ....此處省略了很多代碼
}

新建resources文件夾启摄,新建com.blackHook.properties文件,如下所示

[圖片上傳失敗...(image-2c65fe-1634713340383)]

com.blackHook.properties文件的代碼如下:

implementation-class=com.blackHook.plugin.BlackHookPlugin

implementation-class的值即是BlackHookPlugin的完整路徑幽钢,另外歉备,com.blackHook.properties文件的文件名既是使用插件的時候的插件名,如下代碼:

apply plugin: 'com.blackHook'

2. 實現(xiàn)BlackHook擴展類

新建BlackHook類匪燕,代碼如下

public class BlackHook {

    Closure methodHooker;

    List<HookMethod> hookMethodList = new ArrayList<>();

    public static final String CONTENT_CLASS = "CONTENT_CLASS";
    public static final String CONTENT_JARS = "CONTENT_JARS";
    public static final String CONTENT_RESOURCES = "CONTENT_RESOURCES";

    public static final String SCOPE_FULL_PROJECT = "SCOPE_FULL_PROJECT";
    public static final String PROJECT_ONLY = "PROJECT_ONLY";

    String inputTypes = CONTENT_CLASS;

    String scopes = SCOPE_FULL_PROJECT;

    boolean isNeedLog = false;

    boolean isIncremental = false;

    public Closure getMethodHooker() {
        return methodHooker;
    }

    public void setMethodHooker(Closure methodHooker) {
        this.methodHooker = methodHooker;
    }

    public List<HookMethod> getHookMethodList() {
        return hookMethodList;
    }

    public void setHookMethodList(List<HookMethod> hookMethodList) {
        this.hookMethodList = hookMethodList;
    }

    public String getInputTypes() {
        return inputTypes;
    }

    public void setInputTypes(String inputTypes) {
        this.inputTypes = inputTypes;
    }

    public String getScopes() {
        return scopes;
    }

    public void setScopes(String scopes) {
        this.scopes = scopes;
    }

    public boolean getIsIncremental() {
        return isIncremental;
    }

    public void setIsIncremental(boolean incremental) {
        isIncremental = incremental;
    }

    public boolean getIsNeedLog() {
        return isNeedLog;
    }

    public void setIsNeedLog(boolean needLog) {
        isNeedLog = needLog;
    }
}

這個類用于接收開發(fā)人員使用插件的時候設置的參數(shù)和需要被Hook的方法以及參與Hook的字節(jié)碼蕾羊,我們在使用blackHook插件的時候可以使用DSL的方式來使用,如下代碼所示:

blackHook {
    //表示要處理的數(shù)據(jù)類型是什么帽驯,CLASSES 表示要處理編譯后的字節(jié)碼(可能是 jar 包也可能是目錄)龟再, RESOURCES 表示要處理的是標準的 java 資源
    inputTypes BlackHook.CONTENT_CLASS
    //表示Transform 的作用域,這里設置的SCOPE_FULL_PROJECT代表作用域是全工程
    scopes BlackHook.SCOPE_FULL_PROJECT
    //表示是否支持增量編譯尼变,false不支持
    isIncremental false
    //表示hook的方法
    hookMethodList = getHookMethods()
}

之所以可以這么做是因為我們在BlackHookPlugin將BlackHook類添加到了target.extensions(擴展屬性)中利凑,
如下代碼:

class BlackHookPlugin extends Transform implements Plugin<Project> {
    @Override
    void apply(Project target) {
        target.extensions.create("blackHook", BlackHook.class)
    }
}

3.開始實現(xiàn)掃描

需要在BlackHookPlugin的transform()方法中掃描全局代碼,代碼如下:

  @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        Collection<TransformInput> inputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
        if (blackHook == null) {
            blackHook = new BlackHook()
            blackHook.methodHooker = project.extensions.blackHook.methodHooker
            blackHook.isNeedLog = project.extensions.blackHook.isNeedLog
            for (int i = 0; i < project.extensions.blackHook.hookMethodList.size(); i++) {
                HookMethod hookMethod = new HookMethod()
                hookMethod.className = project.extensions.blackHook.hookMethodList.get(i).className
                hookMethod.methodName = project.extensions.blackHook.hookMethodList.get(i).methodName
                hookMethod.descriptor = project.extensions.blackHook.hookMethodList.get(i).descriptor
                hookMethod.createBytecode = project.extensions.blackHook.hookMethodList.get(i).createBytecode
                blackHook.hookMethodList.add(hookMethod)
            }
        }
        inputs.each { input ->
            input.directoryInputs.each { directoryInput ->
                handleDirectoryInput(directoryInput, outputProvider)
            }
            //遍歷jarInputs
            input.jarInputs.each { JarInput jarInput ->
                //處理jarInputs
                handleJarInputs(jarInput, outputProvider)
            }
        }
        super.transform(transformInvocation)
    }

    void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        if (directoryInput.file.isDirectory()) {
            directoryInput.file.eachFileRecurse { file ->
                String name = file.name
                if (name.endsWith(".class") && !name.startsWith("R$drawable")
                        && !"R.class".equals(name) && !"BuildConfig.class".equals(name)) {
                    ClassReader classReader = new ClassReader(file.bytes)
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor classVisitor = new AllClassVisitor(classWriter, blackHook)
                    classReader.accept(classVisitor, EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    FileOutputStream fos = new FileOutputStream(
                            file.parentFile.absolutePath + File.separator + name)
                    fos.write(code)
                    fos.close()
                }
            }
        }

        //處理完輸入文件之后嫌术,要把輸出給下一個任務
        def dest = outputProvider.getContentLocation(directoryInput.name,
                directoryInput.contentTypes, directoryInput.scopes,
                Format.DIRECTORY)
        FileUtils.copyDirectory(directoryInput.file, dest)
    }

    void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
        if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
            //重名名輸出文件,因為可能同名,會覆蓋
            def jarName = jarInput.name

            def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4)
            }
            JarFile jarFile = new JarFile(jarInput.file)
            Enumeration enumeration = jarFile.entries()
            File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
            //避免上次的緩存被重復插入
            if (tmpFile.exists()) {
                tmpFile.delete()
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
            //用于保存
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = jarFile.getInputStream(jarEntry)
                //插樁class
                if (entryName.endsWith(".class") && !entryName.startsWith("R$")
                        && !"R.class".equals(entryName) && !"BuildConfig.class".equals(entryName)) {
                    //class文件處理
                    jarOutputStream.putNextEntry(zipEntry)
                    ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor cv = new AllClassVisitor(classWriter, blackHook)
                    classReader.accept(cv, EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    jarOutputStream.write(code)
                } else {
                    jarOutputStream.putNextEntry(zipEntry)
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                jarOutputStream.closeEntry()
            }
            //結束
            jarOutputStream.close()
            jarFile.close()
            def dest = outputProvider.getContentLocation(jarName + md5Name,
                    jarInput.contentTypes, jarInput.scopes, Format.JAR)
            FileUtils.copyFile(tmpFile, dest)
            tmpFile.delete()
        }
    }

掃描的過程中會將掃描到的所有類的信息(包含類名截碴,父類名,方法名等)交給AllClassVisitor類蛉威,AllClassVisitor類代碼如下所示:

public class AllClassVisitor extends ClassVisitor {
    private String className;
    private BlackHook blackHook;
    private String superClassName;

    public AllClassVisitor(ClassVisitor classVisitor, BlackHook blackHook) {
        super(ASM6, classVisitor);
        this.blackHook = blackHook;
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        className = name;
        superClassName = superName;
    }

    // 掃描到每個類中的方法的時候會回調(diào)到這個方法
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        // 新建AllMethodVisitor類日丹,將掃描到類和方法的信息以及BlackHook類存儲的參數(shù)交給    AllMethodVisitor對象,由AllMethodVisitor來判斷是否需要Hook指定的方法
        return new AllMethodVisitor(blackHook, mv, access, name, descriptor, className, superClassName);
    }

然后在AllClassVisitor類中會將將掃描到的類和方法的信息以及BlackHook擴展類存儲的參數(shù)交給AllMethodVisitor對象蚯嫌,由AllMethodVisitor來判斷是否需要Hook指定的方法哲虾,AllMethodVisitor代碼如下:

class AllMethodVisitor extends AdviceAdapter {
    private final String methodName;
    private final String className;
    private BlackHook blackHook;
    private String superClassName;

    protected AllMethodVisitor(BlackHook blackHook, org.objectweb.asm.MethodVisitor methodVisitor, int access, String name, String descriptor, String className, String superClassName) {
        super(ASM5, methodVisitor, access, name, descriptor);
        this.blackHook = blackHook;
        this.methodName = name;
        this.className = className;
        this.superClassName = superClassName;
    }

    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String methodName, String descriptor, boolean isInterface) {
        super.visitMethodInsn(opcode, owner, methodName, descriptor, isInterface);
        if (blackHook.isNeedLog) {
            System.out.println("====>methodInfo:" + "className:" + owner + ",methodName:" + methodName + ",descriptor:" + descriptor);
        }
        if (blackHook != null && blackHook.hookMethodList != null && blackHook.hookMethodList.size() > 0) {
            for (int i = 0; i < blackHook.hookMethodList.size(); i++) {
                HookMethod hookMethod = blackHook.hookMethodList.get(i);
                //這里根據(jù)開發(fā)人員設置的需要hook的方法以及掃描到的方法來判斷是否需要hook
                if ((owner.equals(hookMethod.className) || superClassName.equals(hookMethod.className) || className.equals(hookMethod.className)) && methodName.equals(hookMethod.methodName) && descriptor.equals(hookMethod.descriptor)) {
                    hookMethod.createBytecode.call(mv);
                    break;
                }
            }
        }
    }
}

在這個類中根據(jù)開發(fā)人員調(diào)用插件的時候設置的需要hook的方法以及掃描到的方法來判斷是否需要hook

4.源碼

https://github.com/18824863285/BlackHook

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市择示,隨后出現(xiàn)的幾起案子束凑,更是在濱河造成了極大的恐慌,老刑警劉巖栅盲,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汪诉,死亡現(xiàn)場離奇詭異,居然都是意外死亡谈秫,警方通過查閱死者的電腦和手機扒寄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拟烫,“玉大人该编,你說我怎么就攤上這事∷妒纾” “怎么了课竣?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵嘉赎,是天一觀的道長。 經(jīng)常有香客問我于樟,道長公条,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任迂曲,我火速辦了婚禮赃份,結果婚禮上,老公的妹妹穿的比我還像新娘奢米。我一直安慰自己,他們只是感情好纠永,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布鬓长。 她就那樣靜靜地躺著,像睡著了一般尝江。 火紅的嫁衣襯著肌膚如雪涉波。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天炭序,我揣著相機與錄音啤覆,去河邊找鬼。 笑死惭聂,一個胖子當著我的面吹牛窗声,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播辜纲,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼笨觅,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了耕腾?” 一聲冷哼從身側響起见剩,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扫俺,沒想到半個月后苍苞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡狼纬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年羹呵,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疗琉。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡担巩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出没炒,到底是詐尸還是另有隱情涛癌,我是刑警寧澤犯戏,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站拳话,受9級特大地震影響先匪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜弃衍,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一呀非、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧镜盯,春花似錦岸裙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至艺糜,卻和暖如春剧董,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背破停。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工翅楼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人真慢。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓毅臊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親黑界。 傳聞我的和親對象是個殘疾皇子褂微,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容