WCTF2018 truth

TL;DR

WCTF2018的一道.NET逆向题,利用了.NET即时编译(JIT)机制,动态修改了Func3函数的函数指针,写了一段脱壳代码进去,异或解密了了一段汇编并将Func4和Func5的函数指针指过去,从而修改了Func4和Func5的逻辑。

Solve

注:得到了FAKEFLAG的话可以跳过PART1

PART1 FAKEFLAG

使用dnSpy反编译程序,发现在Main函数中调用了Lib.Verify函数验证了读取的字符串:

private static void Main()
{
    Console.Write("Enter your flag: ");
    if (Lib.Verify(Console.ReadLine().Trim()))
    {
        Console.WriteLine("Great :)");
        return;
    }
    Console.WriteLine("Wrong :(");
}

进一步观察Lib.Verify函数:

public static bool Verify(string s)
{
    byte[] bytes = Encoding.ASCII.GetBytes(s);
    if (bytes.Length != 32)
    {
        return false;
    }
    byte[] array = Lib.Func2();
    Lib.Func3(array, bytes);
    Lib.Func4(array, bytes);
    Lib.Func5(array, bytes);
    for (int i = 0; i < 32; i++)
    {
        if (bytes[i] != array[3104 + i])
        {
            return false;
        }
    }
    return true;
}

我们的输入保存在bytes中,首先验证了其长度要为32,然后通过Func2函数生成了一个byte数组array,之后依次调用了Func3Func4Func5三个函数对bytesarray进行了操作,最后将bytes的最终值与array后面的一段进行了比较。
于是我们首先看Func2函数是如何生成array数组的:

public static byte[] Func2()
{
    AesCryptoServiceProvider aesCryptoServiceProvider = new AesCryptoServiceProvider();
    aesCryptoServiceProvider.BlockSize = 128;
    aesCryptoServiceProvider.KeySize = 128;
    aesCryptoServiceProvider.Mode = CipherMode.CBC;
    aesCryptoServiceProvider.Padding = PaddingMode.PKCS7;
    byte[] result;
    using (BinaryReader binaryReader = new BinaryReader(new MemoryStream(Resources.bin)))
    {
        aesCryptoServiceProvider.IV = binaryReader.ReadBytes(16);
        aesCryptoServiceProvider.Key = binaryReader.ReadBytes(16);
        using (CryptoStream cryptoStream = new CryptoStream(binaryReader.BaseStream, aesCryptoServiceProvider.CreateDecryptor(), CryptoStreamMode.Read))
        {
            using (MemoryStream memoryStream = new MemoryStream())
            {
                cryptoStream.CopyTo(memoryStream);
                result = memoryStream.ToArray();
            }
        }
    }
    return result;
}

可以看出,这段代码其中就是读取了一段资源(Resources.bin),并对其进行了AES解密。解密用的IV和key也在其中。我们可以自己写代码解密,也可以直接动态调试拿到。
之后我们再看另外三个Func:

public static void Func3(byte[] b, byte[] x)
{
    byte[] array = new byte[32];
    for (int i = 0; i < 32; i++)
    {
        array[i] = (byte)b.Skip(i * 32).Take(32).Zip(x, (byte x1, byte x2) => (int)(x1 * x2)).Sum();
    }
    Array.Copy(array, x, 32);
}
public static void Func4(byte[] b, byte[] x)
{
    byte[] array = new byte[32];
    for (int i = 0; i < 32; i++)
    {
        array[i] = (byte)b.Skip(1024 + i * 32).Take(32).Zip(x, (byte x1, byte x2) => (int)(x1 * x2)).Sum();
    }
    Array.Copy(array, x, 32);
}
public static void Func5(byte[] b, byte[] x)
{
    byte[] array = new byte[32];
    for (int i = 0; i < 32; i++)
    {
        array[i] = (byte)(b.Skip(2048 + i * 32).Take(32).Zip(x, (byte x1, byte x2) => (int)(x1 * x2)).Sum() + (int)b[3072 + i]);
    }
    Array.Copy(array, x, 32);
}

这三个函数的逻辑都非常清晰,在32次循环中,每次从b数组(即之前aes解密出的array)读取32个byte,分别与x(即我们输入的bytes)的对应byte做乘法,用最终的sum构建一个新数组,最后复制回x数组。所以很明显,这就是将array中的一段数据作为一个32*32矩阵,将我们的输入作为一个32*1的向量,做了一个矩阵乘法。注意Func5最后在结果上又加了一个向量。
有了这些逻辑,我们就很容易实现出一个逆过程,求出三个矩阵的逆,把过程反过来进行一次就好了。然而,最后我们的程序得到了一段FAKEFLAGFAKEFLAGFAKEFLAGFAKEFLAG,尝试在程序中输入也无法通过。那么是哪里出了问题呢?

PART2 - REAL FLAG

为了寻找问题所在,我们首先尝试用DNSPY动态调试,发现Func3的返回值虽然与我们预期的一样,但是在步入Func3时却没有进Func3,反而是进了Func2。另外Func4和Func5中的断点都没有进去,函数的返回结果也与我们的预期不一样。
于是我们猜测,一定有某个地方对这几个函数做了手脚。于是我们从头把代码看了一遍,最终在Resources.Resources()中看到了一段可疑的代码:

unsafe static Resources()
{
    IntPtr intPtr = ldftn(Func) - 16;
    long num = *intPtr;
    IntPtr intPtr2 = ldftn(Func) - 8;
    long num2 = *intPtr2;
    ref long ptr = ldftn(Func) - 16;
    IntPtr intPtr3 = ldftn(Func) + 5;
    long num3 = (long)(*(intPtr3 + 1));
    ptr = *(intPtr3 + (IntPtr)(((int)(*(intPtr3 + 2)) << 3) + 3)) + (num3 << 3);
    ref long ptr2 = ldftn(Func) - 8;
    object obj = *(ldftn(Func) - 16);
    object obj2;
    for (;;)
    {
        obj2 = obj;
        if (*obj2 == 5)
        {
            break;
        }
        obj = obj2 + 16;
    }
    ptr2 = *(obj2 + 8);
    long num4 = *(ldftn(Func) - 8);
    *num4 = 6293447916875450697L;
    long num5 = num4 + 8L;
    *num5 = 996842507592L;
    long num6 = num5 + 8L;
    *num6 = -5023708761407594752L;
    long num7 = num6 + 8L;
    *num7 = 2247216228701921188L;
    long num8 = num7 + 8L;
    *num8 = 5195160555404404409L;
    long num9 = num8 + 8L;
    *num9 = 543045289092056715L;
    long num10 = num9 + 8L;
    *num10 = 612363414786457928L;
    long num11 = num10 + 8L;
    *num11 = 5245003925894368584L;
    long num12 = num11 + 8L;
    *num12 = 3816147333L;
    long num13 = num12 + 8L;
    object obj3 = *(ldftn(Func) - 16);
    object obj4;
    for (;;)
    {
        obj4 = obj3;
        if (*obj4 == 6)
        {
            break;
        }
        obj3 = obj4 + 16;
    }
    *(obj4 + 8) = *(ldftn(Func) - 8);
    object obj5 = *(ldftn(Func) - 16);
    object obj6;
    for (;;)
    {
        obj6 = obj5;
        if (*obj6 == 7)
        {
            break;
        }
        obj5 = obj6 + 16;
    }
    *(obj6 + 8) = *(ldftn(Func) - 8) + 89L;
    object obj7 = *(ldftn(Func) - 16);
    object obj8;
    for (;;)
    {
        obj8 = obj7;
        if (*obj8 == 8)
        {
            break;
        }
        obj7 = obj8 + 16;
    }
    *(obj8 + 8) = *(ldftn(Func) - 8) + 170L;
    *intPtr2 = num2;
    *intPtr = num;
}

首先我们可以注意到9个奇怪的long,这些数值被写到了*(ldftn(Func) - 8)开始的一段地址中。
之后的3个for循环,分别从*(ldftn(Func) - 16)开始,找到06、07和08开始的16个byte,并把后8byte分别改为*(ldftn(Func) - 8)*(ldftn(Func) - 8) + 89*(ldftn(Func) - 8) + 170的地址。联想到之前Func3、Func4、Func5函数无法正常调试且逻辑被修改,我们就可以自然地想到就是在这里修改了这三个函数的指针。而且06、07、08也分别对应了Func3、Func4、Func5 token的最后一个byte:

image.png

进一步分析函数指针被修改后指向的位置,Func3的指针被改到了*(ldftn(Func) - 16),而这就是之前9个long写到的地方。可以猜测这9个long就是Func3函数的实际代码。我们再计算一下这9个long的总长度:9*8=72,而Func4指向的是*(ldftn(Func) - 8) + 89,显然是在修改的9个long的后面。那这部分的代码是从哪里来的呢?为了搞清具体逻辑,我们再进行动态调试,把断点下在Resources构造函数后面(不知道为什么Resources函数里面无法下断点),在内存中搜索找到这9个long的地址:

我们发现了一段疑似的汇编代码。重新调试一次,把断点设在Resources函数前面,将这段内存的内容前后对比一下:

发现确实就是修改了这段汇编代码的开头,并且把三个函数的指针指到了这段汇编代码中。
于是我们Dump下这段内存,用IDA查看,发现这段汇编其实就是Func2函数(那个AES解密函数),只是修改了开头的一部分。但是很显然这两部分并无法拼合在一起,而且也无法想象Func4和Func5其实只是执行Func2的一部分,所以这段代码应该不是最终运行的代码。
于是我们这次把断点下载Func4和Func5执行后,然后再尝试Dump这段汇编:

发现后面的代码被更新了,然后Func4和Func5的入口看起来也比较正常了。这次我们再用IDA打开,逻辑就十分清晰了:

Func3前面一段代码不会影响返回结果(实际上是脱壳代码,后面会详细分析这部分),只在最后做了一个矩阵乘法,跟原来的Func3逻辑是一样的。

Func4对矩阵做了转置,并进行了5次矩阵乘法,并在最后将把向量里的值都异或了0x5A。

Func5中则是将向量异或了Func4中矩阵的前32个Byte,再进行矩阵乘法。这两个操作循环重复10次。
整体逻辑跟之前相差不大,贴一份robin大佬的解题代码,运行得到真Flag:flag{884RN4SoqUt9Cu87pVSPG0ndA8}

I=Integers(256)

def func1(mat,input):
    
    return mat*input.T

def func1_inv(mat,input):
    return (~mat)*input

def func2(mat,input):
    for i in range(5):
        input=mat.T*input
    input=[int(x[0]) for x in input]
    return [x^^0x5a for x in input]

def func2_inv(mat,input):
    input=[x^^0x5a for x in input]
    input=matrix(I,input)
    input=input.T
    for i in range(5):
        input=(~mat.T)*input
    return input
def func3(mat,input,iv):
    
    for i in range(10): 
        input=[x^^y for x,y in zip(input,iv)]
        input=matrix(I,input)
        input=mat*input.T
        input=[int(x[0]) for x in input]
    return input
def func3_inv(mat,input,iv):
    for i in range(10):
        input=matrix(I,input)
        input=(~mat)*(input.T)
        input=[int(x[0]) for x in input]
        input=[x^^y for x,y in zip(input,iv)]
    return input
f=open("bin","rb")
data=f.read()
f.close()
m1=map(ord,data[:1024])
m2=map(ord,data[1024:1024*2])
m3=map(ord,data[1024*2:1024*3])
m4=map(ord,data[1024*3:1024*3+32])
m5=map(ord,data[1024*3+32:1024*32+64])
input=[48]*32
input=matrix(I,input)
ma1=[m1[i*32:i*32+32] for i in range(32)]
ma2=[m2[i*32:i*32+32] for i in range(32)]
ma3=[m3[i*32:i*32+32] for i in range(32)]
ma1=matrix(I,ma1)
ma2=matrix(I,ma2)
ma3=matrix(I,ma3)
rm1= func1(ma1,input)
assert( input.T == func1_inv(ma1,rm1))
rm2=func2(ma2, rm1)
assert( func2_inv(ma2,rm2)==rm1)
rm3=func3(ma3,rm2,[int(x) for x in ma2[0]])
assert( func3_inv(ma3,rm3,[int(x) for x in ma2[0]])==rm2)

re2=func3_inv(ma3,m5,[int(x) for x in ma2[0]])
re1=func2_inv(ma2,re2)
re0=func1_inv(ma1,re1)
re=[int(x[0]) for x in re0]
flag=map(chr,re)
print "".join(flag)

Appendix

这道题虽然解决了,但其中还有一些细节我们还是没有搞懂。例如为什么.NET程序的函数最终指向一段汇编,Func5和Func6的汇编代码是从哪里出来等。在比赛后,我们又进行了一点简单的探索。

.NET即时编译机制

.NET中的C#、VB.NET、F#等语言的编译过程并不是像C/C++一样直接编译出原生代码,而是首先编译成IL中间语言,在运行时,再由JIT (Just-In-Time) compiler根据需要,将IL编译成汇编代码。我们运行的.NET的exe中实际上保存的就是IL,而不是汇编代码:


image.png

实际执行中,函数的调用需要通过Method Table来找到method的地址。在程序运行的开始,函数指针指向一个Stub。在函数被调用时,JIT compiler会编译这个method并将原本指向Stub的指针指向编译好的代码。于是,之后调用该函数就直接执行这段汇编代码。
我们可以使用WinDbg工具来帮助我们理解这一过程:

> !DumpMT -md 0007ff9ec805b00
EEClass:         00007ff9ec992068
Module:          00007ff9ec804118
Name:            WCTF2018Rev.Lib
mdToken:         0000000002000003
File:            D:\ctf\wctf\WCTF2018Rev_Release.exe
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 11
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ffa4ae3c000 00007ffa4a997fb8 PreJIT System.Object.ToString()
00007ffa4aea40f0 00007ffa4a997fc0 PreJIT System.Object.Equals(System.Object)
00007ffa4af14490 00007ffa4a997fe8 PreJIT System.Object.GetHashCode()
00007ffa4ae6f390 00007ffa4a998000 PreJIT System.Object.Finalize()
00007ff9ec9100c0 00007ff9ec805af8   NONE WCTF2018Rev.Lib..ctor()
00007ff9ec910500 00007ff9ec805a98    JIT WCTF2018Rev.Lib.Verify(System.String)
00007ff9ec910098 00007ff9ec805aa8   NONE WCTF2018Rev.Lib.Func(Int32)
00007ff9ec9100a0 00007ff9ec805ab8   NONE WCTF2018Rev.Lib.Func2()
00007ff9ec9100a8 00007ff9ec805ac8   NONE WCTF2018Rev.Lib.Func3(Byte[], Byte[])
00007ff9ec9100b0 00007ff9ec805ad8   NONE WCTF2018Rev.Lib.Func4(Byte[], Byte[])
00007ff9ec9100b8 00007ff9ec805ae8   NONE WCTF2018Rev.Lib.Func5(Byte[], Byte[])

观察Verify函数中,在运行Func2之前的Method Table。内存中Method Table的格式是每个函数8个byte的MethodDesc加上后面8个byte的Method Entry(就是函数指针,之后称Method Entry):

00007ff9`ec805a98 03 00 00 21 05 00 28 00 00 05 91 ec f9 7f 00 00  (Verify)
00007ff9`ec805aa8 04 00 02 20 06 00 28 00 98 00 91 ec f9 7f 00 00  (Func)
00007ff9`ec805ab8 05 00 04 20 07 00 28 20 a0 00 91 ec f9 7f 00 00  (Func2)
00007ff9`ec805ac8 06 00 06 20 08 00 28 00 a8 00 91 ec f9 7f 00 00  (Func3)
00007ff9`ec805ad8 07 00 08 20 09 00 28 00 b0 00 91 ec f9 7f 00 00  (Func4)
00007ff9`ec805ae8 08 00 0a 20 0a 00 28 20 b8 00 91 ec f9 7f 00 00  (Func5)

还没有被JIT编译的函数,其Method Entry指向一个Stub(在WinDbg中显示为NONE)

> dd 00007ff9ec910098
00007ff9`ec910098  614313e8 05025e5f (Stub of Func) 61430be8 04045e5f (Stub of Func2)
00007ff9`ec9100a8  614303e8 03065e5f (Stub of Func3) 6142fbe8 02085e5f (Stub of Func4)
00007ff9`ec9100b8  6142f3e8 010a5e5f (Stub of Func5) 6142ebe8 000c5e5f

由于Verify函数已经被执行,所以已经被JIT编译,其Method Entry指向其汇编实现,而其他函数都指向Stub。
如果我们完整地运行完程序(Func3、Func4、Func5均已执行完):

> !DumpMT -md 00007ff9ec835b00
EEClass:         00007ff9ec9c2068
Module:          00007ff9ec834118
Name:            WCTF2018Rev.Lib
mdToken:         0000000002000003
File:            D:\ctf\wctf\WCTF2018Rev_Release.exe
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 11
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ffa4ae3c000 00007ffa4a997fb8 PreJIT System.Object.ToString()
00007ffa4aea40f0 00007ffa4a997fc0 PreJIT System.Object.Equals(System.Object)
00007ffa4af14490 00007ffa4a997fe8 PreJIT System.Object.GetHashCode()
00007ffa4ae6f390 00007ffa4a998000 PreJIT System.Object.Finalize()
00007ff9ec9400c0 00007ff9ec835af8   NONE WCTF2018Rev.Lib..ctor()
00007ff9ec940500 00007ff9ec835a98    JIT WCTF2018Rev.Lib.Verify(System.String)
00007ff9ec940098 00007ff9ec835aa8   NONE WCTF2018Rev.Lib.Func(Int32)
00007ff9ec9405c0 00007ff9ec835ab8    JIT WCTF2018Rev.Lib.Func2()
00007ff9ec9405c0 00007ff9ec835ab8    JIT WCTF2018Rev.Lib.Func2() (shoule be Func3)
00007ff9ec940619 00007ff9ec835ab8    JIT WCTF2018Rev.Lib.Func2() (shoule be Func4)
00007ff9ec94066a 00007ff9ec835ab8    JIT WCTF2018Rev.Lib.Func2() (shoule be Func5)

这里由于Method Table被改掉了,WinDbg显示有点问题。不过Entry还是可以正常看,说明我们之前的分析结果是正确的:
修改之后,Func3指向了Func2的汇编地址(Func2汇编的前一部分是被9个long修改过的),Func4指向了Func2 + 0x59, Func5指向了Func2 + 0xaa。
到这里我们的第一个问题就解决了。顺便附上WinDbg用到的相关命令:

  1. 加载SOS.dll
> .load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\SOS.dll
  1. 设断点并运行到断点
> bp 7ffa9bcece5d
> g
  1. 加载sos模块
> .loadby sos clr
  1. 找到Method Table地址
> !Name2EE * WCTF2018Rev.Lib
--------------------------------------
Module:      00007ff9ec834118
Assembly:    WCTF2018Rev_Release.exe
Token:       0000000002000003
MethodTable: 00007ff9ec835b00
EEClass:     00007ff9ec9c2068
Name:        WCTF2018Rev.Lib
--------------------------------------

  1. 打印Method Table
!DumpMT -md 00007ff9ec835b00
EEClass:         00007ff9ec9c2068
Module:          00007ff9ec834118
Name:            WCTF2018Rev.Lib
mdToken:         0000000002000003
File:            D:\ctf\wctf\WCTF2018Rev_Release.exe
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 11
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ffa4ae3c000 00007ffa4a997fb8 PreJIT System.Object.ToString()
00007ffa4aea40f0 00007ffa4a997fc0 PreJIT System.Object.Equals(System.Object)
00007ffa4af14490 00007ffa4a997fe8 PreJIT System.Object.GetHashCode()
00007ffa4ae6f390 00007ffa4a998000 PreJIT System.Object.Finalize()
00007ff9ec9400c0 00007ff9ec835af8   NONE WCTF2018Rev.Lib..ctor()
00007ff9ec940500 00007ff9ec835a98    JIT WCTF2018Rev.Lib.Verify(System.String)
00007ff9ec940098 00007ff9ec835aa8   NONE WCTF2018Rev.Lib.Func(Int32)
00007ff9ec9405c0 00007ff9ec835ab8    JIT WCTF2018Rev.Lib.Func2()
00007ff9ec9405c0 00007ff9ec835ab8    JIT WCTF2018Rev.Lib.Func2()
00007ff9ec940619 00007ff9ec835ab8    JIT WCTF2018Rev.Lib.Func2()
00007ff9ec94066a 00007ff9ec835ab8    JIT WCTF2018Rev.Lib.Func2()
  1. 打印Method Description
> !DumpMD 00007ff9ec835ab8
Method Name:  WCTF2018Rev.Lib.Func2()
Class:        00007ff9ec9c2068
MethodTable:  00007ff9ec835b00
mdToken:      0000000006000005
Module:       00007ff9ec834118
IsJitted:     yes
CodeAddr:     00007ff9ec9405c0
Transparency: Critical

参考:
浅谈.NET中的IL代码
Debugging .NET with WinDbg

Method加壳

另一个问题就是Func4和Func5的汇编是如何生成的。
我们已经知道Method在运行时,根据Method Entry找到Method的汇编代码。而在Resources函数中,Func4和Func5的Method Entry指针都被指向了Func2中一段无意义代码中,只有Func3指向的是一段修改过的代码(看起来比较像脱壳代码)。所以我们再来仔细看一下这段代码的实现:


我们可以看到,在for循环中,v5指向的地址被修改了共38*8=0x130个字节,而v5指向的恰恰就是Func3函数的结束部分(从0x44开始):
脱壳前后对比

修改的这部分就是Func4和Func5的汇编代码。而这些代码是从哪里来的呢?
我们继续观察脱壳代码,发现其实就是把array数组从0x8开始的部分与一个常数进行了0x1F2FB740F5455FA4异或解密(实际上每次异或都会把这个常数按位循环移动一下)。
就是说,这个array数组(就是resource.bin那个资源)不仅构造出了个FAKEFLAG,构造出了个真FLAG,还特么藏了两个函数在里面(你以为我是Flag,其实我还是个函数)。不得不说出题人还真的有点想法(可惜线下赛基本被所有队做出来了‧_‧)。

未解决问题

有一个没搞明白的地方,就是在Resources函数中,遍历Method Table的指针:

IntPtr intPtr = ldftn(Func) + 5;
long num = (long)(*(intPtr + 1));
ptr = *(intPtr + (IntPtr)(((int)(*(intPtr + 2)) << 3) + 3)) + (num << 3);

这个ptr指针是如何计算出来的,还是没弄明白……

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