iOS-代码混淆加固策略

技 术 文 章 / 超 人


对于IOS来说,由于系统是封闭的,APP上架需要通过App Store,安全性来说相当高。但是对于大厂和知名APP而言,别人给的安全保障永远没有自己做的来得踏实。所以对于大厂、少部分企业级和金融支付类应用来说加固是相当重要的。

下面是目前几个专业加固大厂提供的加固策略

  • 网易


    网易

    网易安全三板斧:

  1. 第一板斧是防静态分析,这里包括字符串加密、符号混淆、代码逻辑混淆和游戏存档加密;

2.第二板斧是防动态调试、反调试和通信安全(数据加密);

  1. 第三板斧是外挂检测、加速挂、内存修改挂和自动任务挂等
  • 爱加密


    爱加密
  • safengine


    afengine
  • 几维安全


    几维安全
  • 梆梆安全


    梆梆安全

本文将针对以上几点进行实现,对于一些不太容易实现的将会做方向性讨论

  • 字符串加密
  • 代码混淆(方法命,类命,变量名,符号表)
  • 代码逻辑混淆
  • 反调试

字符串加密

对字符串加密的方式目前我所了解到掌握到的最可靠方式就是用脚本将代码中的所有标记需要加密的字符串进行异或转换,这样代码中就不存在明文字符串了。当然第三方的字符串加密不可能这么简单,具体怎么做的我也不太清楚。不过为了增加字符串加密的难度复杂性,我们可以先将字符串用加密工具转换(例如AES、base64等)后的把加字符串放在工程中,并且把解密的钥匙放在工程中,用异或转换,把解密钥匙和加密后的字符串转换,这样就有2层保障,增加了复杂度。

  • 首先 我们创建任意一个工程,在工程中写入下面的代码,并在每句打上断点,再选择Xcode工具栏的Debug --> Debug Workflow --> Always Show Disassembly。这样你就可以在断点处进入汇编模式界面,最后运行程序
/* 加密NSString字符串 */
    NSString *str = @"Hello World";
    NSLog(@"%@",str);
    /* 加密char*字符串 */
    char* cStr = "Super Man";
    NSLog(@"%s",cStr);
断点处进入汇编模式界面

你会发现,你的字符串内容暴露在了汇编模式中,这会导致别人在逆向分析你的工程时能看见你的字符串内容,我们一般接口、域名、加解密钥匙串、AppKey、AppId等比较重要的东西会放在客户端用作字符串,这就很容易暴露出来。

  • 步骤1 首先需要在工程代码中进行修改,把下面的宏和decryptConfusionCS,decryptConstString函数放入代码中,用宏包含每个需要转换的字符串。

/* 字符串混淆解密函数,将char[] 形式字符数组和 aa异或运算揭秘 */
extern char* decryptConfusionCS(char* string)
{
    char* origin_string = string;
    while(*string) {
        *string ^= 0xAA;
        string++;
    }
    return origin_string;
}

/* 解密函数,返回的是NSString类型的 */
extern NSString* decryptConstString(char* string)
{
    /* 先执行decryptConfusionString函数解密字符串 */
    char* str = decryptConfusionCS(string);
    /* 获取字符串的长度 */
    unsigned long len = strlen(str);
    NSUInteger length = [[NSString stringWithFormat:@"%lu",len] integerValue];
     NSString *resultString = [[NSString alloc]initWithBytes:str length:length encoding:NSUTF8StringEncoding];
    return resultString;
}


/*
 * 使用heyujia_confusion宏控制加密解密
 * 当heyujia_confusion宏被定义的时候,执行加密脚本,对字符串进行加密
 * 当heyujia_confusion宏被删除或为定义时,执行解密脚本,对字符串解密
 */
#define heyujia_confusion

#ifdef heyujia_confusion
/* heyujia_confusion 宏被定义,那么就进行执行解密脚本 */
/* confusion_NSSTRING宏的返回结果是NSString 类型的 */
#define confusion_NSSTRING(string) decryptConstString(string)
/* confusion_CSTRING宏的返回结果是char* 类型的 */
#define confusion_CSTRING(string) decryptConfusionCS(string)
#else
/* heyujia_confusion 宏没有被定义,那么就执行加密脚本 */
/* 加密NSString类型的 */
#define confusion_NSSTRING(string) @string
/* 加密char *类型的 */
#define confusion_CSTRING(string) string
#endif

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    /* 使用confusion_NSSTRING宏包含需要加密的NSString字符串 */
    NSString *str = confusion_NSSTRING("Hello World");
    NSLog(@"%@",str);
    /* 使用confusion_NSSTRING宏包含需要加密的char*字符串 */
    char* cStr = confusion_CSTRING("Super Man");
    NSLog(@"%s",cStr);    
}
  • 步骤2 使用终端cd 到需要加密的工程目录下 执行touch confusion.pytouch decrypt.py 命令,生产加密和解密脚本文件

  • 步骤3 把下面代码加入解密脚本confusion.py中

#!/usr/bin/env python
# encoding=utf8
# -*- coding: utf-8 -*-
# author by heyujia
# 脚本将会用于对指定目录下的.h .m源码中的字符串进行转换
# 替换所有字符串常量为加密的char数组,形式((char[]){1, 2, 3, 0})

import importlib
import os
import re
import sys


# replace替换字符串为((char[]){1, 2, 3, 0})的形式,同时让每个字节与0xAA异或进行加密
# 当然可以不使用0xAA 使用其他的十六进制也行 例如0XBB、0X22、0X11
def replace(match):
    string = match.group(2) + '\x00'
    replaced_string = '((char []) {' + ', '.join(["%i" % ((ord(c) ^ 0xAA) if c != '\0' else 0) for c in list(string)]) + '})'
    return match.group(1) + replaced_string + match.group(3)


# obfuscate方法是修改传入文件源代码中用confusion_NSSTRING标记的所有字符串
# 使用replace函数对字符串进行异或转换
def obfuscate(file):
    with open(file, 'r') as f:
        code = f.read()
        f.close()
        code = re.sub(r'(confusion_NSSTRING\(|confusion_CSTRING\()"(.*?)"(\))', replace, code)
        code = re.sub(r'//#define ggh_confusion', '#define ggh_confusion', code)
        with open(file, 'w') as f:
            f.write(code)
            f.close()


# openSrcFile方法是读取源码路径下的所有.h和.m 文件
# 对每个文件执行obfuscate函数
def openSrcFile(path):
    print("混淆的路径为 "+ path)
    # this folder is custom
    for parent,dirnames,filenames in os.walk(path):
        #case 1:
        #        for dirname in dirnames:
        #            print((" parent folder is:" + parent).encode('utf-8'))
        #            print((" dirname is:" + dirname).encode('utf-8'))
        #case 2
        for filename in filenames:
            extendedName = os.path.splitext(os.path.join(parent,filename))
            if (extendedName[1] == '.h' or extendedName[1] == '.m'):
                print("处理源代码文件: "+ os.path.join(parent,filename))
                obfuscate(os.path.join(parent,filename))


#这里需要修改源码的路径为自己工程的文件夹名称
srcPath = '../daimahunxiao'

if __name__ == '__main__':
    print("本脚本用于对源代码中被标记的字符串进行加密")
    
    if len(srcPath) > 0:
        openSrcFile(srcPath)
    else:
        print("请输入正确的源代码路径")
        sys.exit()

  • 步骤4 把下面的解密代码放入decrypt.py解密脚本中
#!/usr/bin/env python
# encoding=utf8
# -*- coding: utf-8 -*-
# author by heyujia
# 解密脚本
# 替换所有标记过的加密的char数组为字符串常量,""

import importlib
import os
import re
import sys


# 替换((char[]){1, 2, 3, 0})的形式为字符串,同时让每个数组值与0xAA异或进行解密
def replace(match):
    string = match.group(2)
    decodeConfusion_string = ""
    for numberStr in list(string.split(',')):
        if int(numberStr) != 0:
            decodeConfusion_string = decodeConfusion_string + "%c" % (int(numberStr) ^ 0xAA)
    replaced_string = '\"' + decodeConfusion_string + '\"'

    print("replaced_string = " + replaced_string)
    
    return match.group(1) + replaced_string + match.group(3)


# 修改源代码,加入字符串加密的函数
def obfuscate(file):
    with open(file, 'r') as f:
        code = f.read()
        f.close()
        code = re.sub(r'(confusion_NSSTRING\(|confusion_CSTRING\()\(\(char \[\]\) \{(.*?)\}\)(\))', replace, code)
        code = re.sub(r'[/]*#define ggh_confusion', '//#define ggh_confusion', code)
        with open(file, 'w') as f:
            f.write(code)
            f.close()


#读取源码路径下的所有.h和.m 文件
def openSrcFile(path):
    print("解密路径: "+ path)
    # this folder is custom
    for parent,dirnames,filenames in os.walk(path):
        #case 1:
        #        for dirname in dirnames:
        #            print((" parent folder is:" + parent).encode('utf-8'))
        #            print((" dirname is:" + dirname).encode('utf-8'))
        #case 2
        for filename in filenames:
            extendedName = os.path.splitext(os.path.join(parent,filename))
            #读取所有.h和.m 的源文件
            if (extendedName[1] == '.h' or extendedName[1] == '.m'):
                print("已解密文件:"+ os.path.join(parent,filename))
                obfuscate(os.path.join(parent,filename))


#源码路径
srcPath = '../daimahunxiao'
if __name__ == '__main__':
    print("字符串解混淆脚本,将被标记过的char数组转为字符串,并和0xAA异或。还原代码")
    if len(srcPath) > 0:
        openSrcFile(srcPath)
    else:
        print("请输入正确的源代码路径!")
        sys.exit()
  • 步骤5 根据自己的需求修改下脚本里面的代码 和 文件路径。

  • 步骤6 把步骤1中的宏heyujia_confusion注释了,然后执行加密脚本,在终端中输入python confusion.py,
    (1.如果报错,请查看下自己Mac电脑中的python版本,如果是python3就输入python3 confusion.py.
    (2.如果报Non-ASCII character '\xe8' in file confusion.py on line 2相关的错,请确定脚本的前面3行是

#!/usr/bin/env python
# encoding=utf8
# -*- coding: utf-8 -*-

必须有这三行代码,才能在脚本中输入中文
(3.如果报IndentationError: unexpected indent,请注意脚本中的每行代码的换行符和缩进格式必须标准

  • 执行完步骤6后的结果
    执行完不步骤6后的结果

    此时字符串已被加密,运行程序会发现一切正常
    输出结果

加密后汇编界面

加密后汇编界面看不见我们的字符串内容了,但是我们用来解密的方法还是暴露在了汇编界面,所以我们后期还需要对方法名,变量名,类命等做混淆。

  • 步骤7 把步骤1中的宏heyujia_confusion取消注释,然后执行解密脚本,在终端中输入python decrypt.py
    解密后

    解密后文本又变回了原样。

这里只是基本的异或转换加密,让代码中的字符串变成看不懂的char [],实际操作中远远不止这么简单
例如:

  • 首先:我们先用加密工具例如:AES.Base64等把需要转换的字符串先加密变成加密字符串
  • 然后:在用异或转换加密的脚本把加密字符串进行转换(包括解密用的钥匙串)
  • 在使用的时候:先异或解密字符串,然后根据解密钥匙串把字符串在转为可用的字符串

ps.还有一种保护字符串的方法,就是使用NSLocalizedString字符串本地化。


虽然跟着我的步骤你确实加密成功了,但是你却无法实际验证。所以要验证最终的混淆结果是否达到效果,你还需要学习如何破壳解密IPA如何动态静态逆向编程分析工程源码,大家可以先看看我这篇文章。先掌握逆向分析后在来做代码混淆,就能验证混淆结果是否有效

变量、方法名,类名混淆

对于混淆这一块,网上真的是千篇一律,基本都是copy的念大婶的内容,没有一点自己的创新和思考。网上的方法我也用过,但是有缺陷,只能混淆方法名或者说自己固定的内容去替换。第一不自动,对于大项目而言每个方法名自己添加,太麻烦。第二变量混淆有问题,因为只是单纯的字符串替换,用宏代替。当遇到使用_ 下划线访问变量时,就会出现错误。

对于变量、方法名,类名的混淆,其实跟字符串混淆差不多,都是加密混淆,然后解密混淆。不同的是,变量、方法名,类名的混淆目的是为了让别人反编译的时候不知道你的变量、方法,类是具体用来干什么的,不会想明文那样一目了然。增加逆向难度。混淆的内容不需要想字符串一样,最后程序运行时还要转成中文正常使用。由于本人对shell脚本语言也不是非常熟悉,想要按照自己的思路写一套完整的混淆脚本还不行。所以这部分也是在网上找的,算是目前最实用最完善的混淆

  • 首先 打开终端cd到需要混淆的工程目录下,输入
    touch obConfusion.sh (加密混淆脚本文件)
    touch obDecrypt.sh(解密混淆脚本文件)
    生成2个脚本文件

  • 然后在工程目录以外创建一个文件夹,用于保存加密时生成的加密文本内容,该内容会在解密是用到

  • 最后是在obConfusion.shobDecrypt.sh文件中加入脚本内容

下面是加密混淆脚本内容

#!/bin/sh
##################################
#  (该脚本是在https://github.com/heqingliang/CodeObfus 上找到的)
#  代码混淆脚本  heyujia 2018.03.15
#
##################################

#识别含有多字节编码字符时遇到的解析冲突问题
export LC_CTYPE=C
export LANG=C

#配置项:
#项目路径,会混淆该路径下的文件
ProjectPath="/Users/xieyujia/Desktop/ios/学习项目/daimahunxiao"
#这个路径是混淆成功后,原文本和替换文本解密对应的文件存放路径(该路径不能在项目目录或其子目录),混淆成功后会在该路径下生成一个解密时需要的文件,根据该文件的文本内容把混淆后的内容更换为原文本内容,该文件名的组成由$(date +%Y%m%d)"_"$(date +%H%M)及日期_小时组成,每分钟会不一样。所以解密的时候需要每次更换文件路径
SecretFile="/Users/xieyujia/Desktop/ios/学习项目/tihuan"$(date +%Y%m%d)"_"$(date +%H%M)

#第一个参数为项目路径
if [[ $1 ]]
then
if [[ $1 != "_" ]]; then
ProjectPath=$1
fi
fi
#第二个参数指定密钥文件路径及文件名
if [[ $2 ]]
then
if [[ $2 != "_" ]]; then
SecretFile=$2
fi
fi
##############################################################################

#查找文本中所有要求混淆的属性\方法\类,只会替换文本中ob_开头和_fus结尾的字符串(区分大小写,例如oB_就不会做混淆),如果注释内容有该类型的字符串,也会进行替换。对于使用 _下划线访问的变量属性,不会有影响,一样会替换成对应_的混淆内容。
resultfiles=`grep 'ob_[A-Za-z0-9_]*_fus' -rl $ProjectPath`
#查找结果为空则退出
if [[ -z $resultfiles ]]
then
echo "项目没有需要混淆的代码"
exit
else
echo "开始混淆代码..."
echo  > $SecretFile
fi

x=$(awk  '
BEGIN{srand();k=0;}
#随机数生成函数
function random_int(min, max) {
return int( rand()*(max-min+1) ) + min;
}
#随机字符串生成函数
function random_string(len) {
result="UCS"k;
alpbetnum=split("a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z", alpbet, ",");
for (i=0; i<len; i++) {
result = result""alpbet[ random_int(1, alpbetnum) ];
}
return result;
}
/ob_[A-Za-z0-9_]*_fus/{
x = $0;
#匹配需要混淆的属性变量方法
while (match(x, "ob_[A-Za-z0-9_]*_fus") > 0) {
tempstr=substr(x, RSTART, RLENGTH);
#判断是否有之前已经找过的重复字符串
for ( i = 0; i < k; i++ ){
if (strarr[i] == tempstr){break;}
}
if(i<k){
#重复字符串,直接删除。所以不用担心混淆内容过多,可能会出现重复的混淆字符串
x=substr(x, RSTART+RLENGTH);
continue;
}else{
#不是重复字符串,添加到替换数组
strarr[k++]=tempstr;
}
randomstr=random_string(20);
printf("%s:%s|", tempstr,randomstr);
#替换随机字符串
gsub(tempstr,randomstr, x);
x = substr(x, RSTART+RLENGTH);
}
}' $resultfiles )

#加密对写入密钥文件
echo $x > $SecretFile

recordnum=1
while [[ 1 == 1 ]]; do
record=`echo $x|cut -d "|" -f$recordnum`
if [[ -z $record ]]
then
break
fi
record1=`echo $record|cut -d ":" -f1`
echo "原项:"$record1
record2=`echo $record|cut -d ":" -f2`
echo "加密项:"$record2
#替换文件夹中所有文件的内容(支持正则)
#单引号不能扩展
sed -i '' "s/${record1}/${record2}/g" `grep $record1 -rl $ProjectPath`
echo "第"$recordnum"项混淆代码处理完毕"
let "recordnum = $recordnum + 1"
done

#查找需要混淆的文件名并替换
filerecordnum=1
while [[ 1 == 1 ]]; do
filerecord=`echo $x|cut -d "|" -f$filerecordnum`
if [[ -z $filerecord ]]
then
break
fi
filerecord1=`echo $filerecord|cut -d ":" -f1`
#echo "原项:"$filerecord1
filerecord2=`echo $filerecord|cut -d ":" -f2`
#echo "加密项:"$filerecord2
#改文件名

find $ProjectPath -name $filerecord1"*"| awk '
BEGIN{frecord1="'"$filerecord1"'";frecord2="'"$filerecord2"'";finish=1}
{
filestr=$0;
gsub(frecord1,frecord2,filestr);
print "mv " $0 " " filestr";echo 第"finish"个混淆文件处理完毕";
finish++;
}'|bash
let "filerecordnum = $filerecordnum + 1"
done

下面是解密混淆脚本的内容

#!/bin/sh
######################################
#
#  代码还原脚本  RyoHo 2018.03.15
#
######################################

#识别含有多字节编码字符时遇到的解析冲突问题
export LC_CTYPE=C
export LANG=C

#配置项:
#已经混淆的项目路径
ProjectPath="/Users/xieyujia/Desktop/ios/学习项目/daimahunxiao"
#这个是文件路径而不是目录,是混淆的时候生成的文本文件路径,每次不一样。所以每次加密后,解密时需要更换路径
SecretFile="/Users/xieyujia/Desktop/ios/学习项目/tihuan20180315_1456"
#第一个参数为项目路径
if [[ $1 ]]
then
if [[ $1 != "_" ]]; then
ProjectPath=$1
fi
fi
#第二个参数指定密钥文件路径及文件名
if [[ $2 ]]
then
if [[ $2 != "_" ]]; then
SecretFile=$2
fi
fi
##############################################################################
#内容还原
x=`cat $SecretFile`
recordnum=1
while [[ 1 == 1 ]]; do
record=`echo $x|cut -d "|" -f$recordnum`
if [[ -z $record ]]
then
break
fi
record1=`echo $record|cut -d ":" -f1`
echo "原项:"$record1
record2=`echo $record|cut -d ":" -f2`
echo "加密项:"$record2
#若项目中加密项与密钥文件的加密项不符合则退出程序
searchresult=`grep $record2 -rl $ProjectPath`
if [[ -z $searchresult ]]; then
echo "指定的密钥文件不能还原"
exit
fi
#替换文件夹中所有文件的内容(支持正则)
#单引号不能扩展
sed -i '' "s/${record2}/${record1}/g" $searchresult
echo "第"$recordnum"项混淆代码还原完毕"
let "recordnum = $recordnum + 1"
done
#文件还原
filerecordnum=1
while [[ 1 == 1 ]]; do
filerecord=`echo $x|cut -d "|" -f$filerecordnum`
if [[ -z $filerecord ]]
then
break
fi
filerecord1=`echo $filerecord|cut -d ":" -f1`
#echo "原项:"$filerecord1
filerecord2=`echo $filerecord|cut -d ":" -f2`
#echo "加密项:"$filerecord2
#改文件名

find $ProjectPath -name $filerecord2"*"| awk '
BEGIN{
frecord1="'"$filerecord1"'";
frecord2="'"$filerecord2"'";
finish=1;
}
{
filestr=$0;
gsub(frecord2,frecord1,filestr);
print "mv " $0 " "filestr ";echo 第"finish"个混淆文件还原完毕"
finish++;
}'|bash
let "filerecordnum = $filerecordnum + 1"
done

应大家需要把脚本源码地址放出来

建议大家看看脚本内容,有利于学习理解。该脚本是有针对性的混淆内容,可以自己修改脚本中的正则表达式来确定混淆的内容。脚本中只会替换文本中ob_开头和fus结尾的字符串(区分大小写,例如oB就不会做混淆),如果注释内容有该类型的字符串,也会进行替换。对于使用 下划线访问的变量属性,不会有影响,一样会替换成对应的混淆内容。
提供一个shell脚本学习的网站


代码逻辑混淆

下一小节代码逻辑混淆,目前还在代码逻辑混淆这块还在攻破学习中,网上大多数方法不好或者过时无效。

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