OpenSSL之SSL用法

什么是SSL?

SSL(Secure Sockets Layer 安全套接字协议),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层与应用层之间对网络连接进行加密,它最早为Netscape所研发,用以保障在Internet上数据传输的安全,利用数据加密(Encryption)技术,可确保数据在网络上的传输过程中不会被截取及窃听。

SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。
SSL协议可分为两层:
SSL记录协议(SSL Record Protocol)—— 它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。
SSL握手协议(SSL Handshake Protocol)—— 它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

SSL提供的服务:
1)认证用户和服务器,确保数据发送到正确的客户机和服务器;
2)加密数据以防止数据中途被窃取;
3)维护数据的完整性,确保数据在传输过程中不被改变。

开发流程:

我从网上摘了几个图,分别介绍如下。

图1:SSL的通用开发流程示意:
image.png

无论是服务端还是客户端,进行一次完整的SSL通讯,大致可以抽像为以下几个步骤:
1)初使化SSL环境。
2)创建SSL上下文。
3)配置SSL上下文证书及公钥信息。
4)创建SSL上下文。
5)创建TCP通讯端口。
6)建立SSL和TCP通讯端口的关联。
7)执行SSL握手。
8)执行SSL数据读写交互。
9)关闭SSL连接。
10)关于TCP通讯端口。
11)释放SSL上下文。

图2:SSL服务端接口调用流程示意:
image.png
图3:SSL客户端接口调用流程示意:
image.png

图2、图3表达得非常好,非常具体地说明了程序员应该调用哪些函数,以及执行的步骤。

主要接口:

SSL接口的主要头文件在ssl中。我们根据SSL通讯的流程摘录如下:

1)初使化SSL环境。

通常用得比较多的是这三个函数:
SSL_library_init()
OpenSSL_add_ssl_algorithms()
SSL_load_error_strings()

在1.1.1版本的定义中是这样的:

# if OPENSSL_API_COMPAT < 0x10100000L
#  define SSL_library_init() OPENSSL_init_ssl(0, NULL)
# endif

# if OPENSSL_API_COMPAT < 0x10100000L
#  define SSL_load_error_strings() \
    OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS \
                     | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL)
# endif

# if OPENSSL_API_COMPAT < 0x10100000L
#  define OpenSSL_add_ssl_algorithms()   SSL_library_init()
#  define SSLeay_add_ssl_algorithms()    SSL_library_init()
# endif

int OPENSSL_init_ssl(uint64_t opts, const OPENSSL_INIT_SETTINGS *settings);

可以看到,上面图示中的OpenSSL_add_ssl_algorithms()这一步是多余的,通常初使化只需要调用这两个就可以了:
SSL_library_init()
SSL_load_error_strings()

在初使化过程中,SSL_library_init()注册了所有在SSL APIs中的加密算法和哈希算法,SSL_load_error_strings()则加载了所有的错误描述字符串。

2)创建SSL上下文。

SSL_CTX *SSL_CTX_new(const SSL_METHOD *meth);
这个函数用于创建SSL上下文,其参数SSL_METHOD用于传入SSL的抽象方法集合。

对于每个SSL/TSL来说,有三种APIs可以用来创建一个SSL_METHOD:
一个可以用于服务端和客户端,一个只能用于服务端,另外一个只能由于客户端。定义如下:

const SSL_METHOD *TLS_method(void);
const SSL_METHOD *TLS_server_method(void);
const SSL_METHOD *TLS_client_method(void);

#define SSLv23_method           TLS_method
#define SSLv23_server_method    TLS_server_method
#define SSLv23_client_method    TLS_client_method

int SSL_CTX_set_cipher_list(SSL_CTX *, const char *str);
这个函数用于设置SSL上下文的算法套件信息。
成功返回1,失败返回0。
可用的算法如:
EDH-RSA-DES-CBC3-SHA
EDH-DSS-DES-CBC3-SHA
DES-CBC3-SHA
DHE-DSS-RC4-SHA
IDEA-CBC-SHA
RC4-SHA
RC4-MD5
EXP1024-DHE-DSS-RC4-SHA
EXP1024-RC4-SHA
EXP1024-DHE-DSS-DES-CBC-SHA
EXP1024-DES-CBC-SHA
EXP1024-RC2-CBC-MD5
EXP1024-RC4-MD5
EDH-RSA-DES-CBC-SHA
EDH-DSS-DES-CBC-SHA
DES-CBC-SHA
EXP-EDH-RSA-DES-CBC-SHA
EXP-EDH-DSS-DES-CBC-SHA
EXP-DES-CBC-SHA
EXP-RC2-CBC-MD5
EXP-RC4-MD5
这些算法按一定优先级排列,如果不作任何指定,将选用DES-CBC3-SHA.用SSL_CTX_set_cipher_list可以指定自己希望用的算法(实际上只是 提高其优先级,是否能使用还要看对方是否支持)。

3)配置SSL上下文证书及公钥信息。

int SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx, const char *file, int type);
int SSL_CTX_use_certificate_file(SSL_CTX *ctx, const char *file, int type);
这两个函数用于加载私钥和证书文件。
成功返回1,失败返回0。
其中,type的取值:

# define SSL_FILETYPE_ASN1       X509_FILETYPE_ASN1
# define SSL_FILETYPE_PEM        X509_FILETYPE_PEM
即
# define X509_FILETYPE_PEM       1
# define X509_FILETYPE_ASN1      2

也可以直接使用二进制结构和ASN1序列化的内存数据:
int SSL_CTX_use_RSAPrivateKey(SSL_CTX *ctx, RSA *rsa);
int SSL_CTX_use_RSAPrivateKey_ASN1(SSL_CTX *ctx, const unsigned char *d, long len);
int SSL_CTX_use_certificate(SSL_CTX *ctx, X509 *x);
int SSL_CTX_use_certificate_ASN1(SSL_CTX *ctx, int len, const unsigned char *d);

int SSL_CTX_check_private_key(const SSL_CTX *ctx);
在完成私钥和证书加载后,这个函数由于检查二者是否匹配。
成功返回1,失败返回0。

4)创建SSL上下文。

SSL *SSL_new(SSL_CTX *ctx);
创建SSL结构,SSL的连接信息都保存在SSL结构中。
新的SSL结构会从SSL_CTX结构中继承包括,连接类型、选项、验证方式以及超时。

5)创建TCP通讯端口。

本步省略,不在本文的介绍范围。

6)建立SSL和TCP通讯端口的关联。

int SSL_set_fd(SSL *s, int fd);
成功返回1,失败返回0。

也可以直接使用BIO代替。
BIO* bio = BIO_new_socket(socket, BIO_NOCLOSE);
SSL_set_bio(ssl, bio, bio);

7)执行SSL握手。

SSL握手过程是一个复杂过程,涉及到重要的加密秘钥交换。
但是握手过程可以通过服务端调用SSL_accept()和客户端调用SSL_connect()完成。

int SSL_accept(SSL *ssl);
int SSL_connect(SSL *ssl);
成功返回1,失败返回<=0。
这两个函数是可以重复调用的,这个特性在非阻塞模式下尤为明显。

此外,在握手完成后,可通过调用SSL_get_peer_certificate来获取对端的证书。
X509 *SSL_get_peer_certificate(const SSL *s);

如果对方存在证书,就可以调用X509的相关函数提取证书的身份信息,比如:
X509_NAME *X509_get_subject_name(X509 *a);

8)执行SSL数据读写交互。

在SSL握手完成后,数据就可以通过已经建立好的连接安全的发送了。
不要再使用send、recv函数,而是要使用SSL_write和SSL_read。

int SSL_read(SSL *ssl, void *buf, int num);
int SSL_write(SSL *ssl, const void *buf, int num);
与send、recv用法相似。
成功返回发送或接收的字节数,失败返回<=0。
当返回值<0(通常为-1)时,应检查错误码,尤其是在非阻塞模式时。

错误码可使用SSL_get_error()函数获取,其定义为:
int SSL_get_error(const SSL *s, int ret_code);

9)关闭SSL连接。

当关闭SSL连接时,SSL客户端和服务端需要发送close_notify消息,通知对端SSL将要关闭了,调用SSL_shutdown函数来发送close_notify消息。

int SSL_shutdown(SSL *s);
成功返回1,失败返回<=0。

关闭过包含以下两个步骤:
1)发送一个close_notify关闭告警。
2)从对端接收一个close_notify的关闭消息。

发起关闭的客户端或者服务端可以调用SSL_shutdown一次或者两次。
如果调用了两次,一次调用用于发送close_notify消息,另外一次用于响应对端的。
如果只调用一次,发起关闭一端将不会等待对端的响应(发起关闭的一端不需要等待对端的关闭响应,一旦收到对端关闭消息就要马上发送关闭响应。

10)关于TCP通讯端口。

本步省略,不在本文的介绍范围。

11)释放SSL上下文。

void SSL_free(SSL *ssl);
void SSL_CTX_free(SSL_CTX *);

使用举例:

阻塞模式用法:

基本上只要按照前面介绍的开发流程,组织好整个程序,通常就能正常工作了。下面是示例代码。

服务端代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include <openssl/ssl.h>
#include <openssl/err.h>

#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define SSL_print_error_and_freectx() \
    ERR_print_errors_fp(stdout); \
    SSL_CTX_free(ctx);

int listenLocal(unsigned short uPort)
{
    int sockS = socket(AF_INET, SOCK_STREAM, 0);
    if (sockS < 0)
    {
        printf("socket() error! \n");
        return -1;
    }

    struct sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_port = htons(uPort);

    int ret = bind(sockS, (struct sockaddr*)&sin, sizeof(sin));
    if (ret < 0)
    {
        printf("bind() error! \n");
        close(sockS);
        return -1;
    }

    listen(sockS, 50);

    return sockS;
}

int main(int argc, char* argv[])
{
    SSL_library_init();
    SSL_load_error_strings();

    SSL_CTX* ctx = SSL_CTX_new( SSLv23_server_method() );
    if (ctx == NULL)
    {
        ERR_print_errors_fp(stdout);
        return -1;
    }

    int ret = SSL_CTX_use_certificate_file(ctx, "test.crt", SSL_FILETYPE_PEM);
    if (ret != 1)
    {
        SSL_print_error_and_freectx();
        return -1;
    }

    ret = SSL_CTX_use_PrivateKey_file(ctx, "test.key", SSL_FILETYPE_PEM);
    if (ret != 1)
    {
        SSL_print_error_and_freectx();
        return -1;
    }

    ret = SSL_CTX_check_private_key(ctx);
    if (ret != 1)
    {
        SSL_print_error_and_freectx();
        return -1;
    }

    int sockS = listenLocal(9999);
    if (sockS < 0)
    {
        SSL_CTX_free(ctx);
        return -1;
    }

    while (true)
    {
        struct sockaddr_in sinfrom;
        socklen_t sinfromlen = sizeof(sinfrom);
        int sockC = accept(sockS, (struct sockaddr*)&sinfrom, &sinfromlen);
        if (sockC < 0)
        {
            printf("accept() error! \n");
            break;
        }

        printf("accpet connect:[%d - %s:%d] \n", sockC, inet_ntoa(sinfrom.sin_addr), ntohs(sinfrom.sin_port));

        SSL* ssl = SSL_new(ctx);
        SSL_set_fd(ssl, sockC);

        do
        {
            ret = SSL_accept(ssl);
            if (ret != 1)
            {
                printf("SSL handshake failed! \n");
                break;
            }
            printf("SSL handshake success! \n");

            SSL_write(ssl, "hello", 5);

            while (true)
            {
                char sBuf[1024] = {0};
                int bytesin = SSL_read(ssl, sBuf, sizeof(sBuf)-1);
                if (bytesin <= 0)
                {
                    printf("error or disconnect! \n");
                    break;
                }

                printf("read:[%s] \n", sBuf);
            }

            SSL_shutdown(ssl);
        } while(0);

        SSL_free(ssl);
        close(sockC);
    }

    close(sockS);
    SSL_CTX_free(ctx);

    return 0;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include <openssl/ssl.h>
#include <openssl/err.h>

#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int connectPeer(const char* sIp, unsigned short uPort)
{
    int sockC = socket(AF_INET, SOCK_STREAM, 0);
    if (sockC < 0)
    {
        printf("socket() errror! \n");
        return -1;
    }

    struct sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = inet_addr(sIp);
    sin.sin_port = htons(uPort);

    int ret = connect(sockC, (struct sockaddr*)&sin, sizeof(sin));
    if (ret < 0)
    {
        printf("connect() failed! \n");
        close(sockC);
        return -1;
    }

    return sockC;
}

void showCert(SSL* ssl)
{
    X509* x509 = SSL_get_peer_certificate(ssl);

    if (x509)
    {
        const char* subjectname = X509_NAME_oneline(X509_get_subject_name(x509), 0, 0);
        printf("subject name:[%s] \n", subjectname);

        const char* issuername = X509_NAME_oneline(X509_get_issuer_name(x509), 0, 0);
        printf("issuer name:[%s] \n", issuername);
        
        X509_free(x509);
    }
}

int main(int argc, char* argv[])
{
    SSL_library_init();
    SSL_load_error_strings();

    SSL_CTX* ctx = SSL_CTX_new( SSLv23_client_method() );
    if (ctx == NULL)
    {
        ERR_print_errors_fp(stdout);
        SSL_CTX_free(ctx);
        return -1;
    }

    int sockC = connectPeer("127.0.0.1", 9999);
    if (sockC < 0)
    {
        SSL_CTX_free(ctx);
        return -1;
    }

    SSL* ssl = SSL_new(ctx);
    SSL_set_fd(ssl, sockC);

    do
    {
        int ret = SSL_connect(ssl);
        if (ret != 1)
        {
            printf("SSL handshake failed! \n");
            break;
        }
        printf("SSL handshake success! \n");

        showCert(ssl);

        char sBuf[1024] = {0};
        int bytesin = SSL_read(ssl, sBuf, sizeof(sBuf)-1);
        if (bytesin <= 0)
        {
            printf("disconnect! \n");
            SSL_shutdown(ssl);
            break;
        }

        printf("read:[%s] \n", sBuf);

        for (int i = 0; i < 3; ++i)
        {
            SSL_write(ssl, "hello", 5);
            sleep(1);
        }

        SSL_shutdown(ssl);
    } while (0);

    SSL_free(ssl);
    close(sockC);

    SSL_CTX_free(ctx);

    return 0;
}
非阻塞模式用法:

对非阻塞套接字来说,SSL_read()和SSL_write()的调用通常会返回-1,这并不是真正表示收发失败了,大多可能的原因是套接字的缓冲不可用,我们需要稍后进行尝试。只不过,在非阻塞模式下,采用主动尝试的方法通常都不可取,正式的场合应该使用异步事件模型。

下面的代码出于简单性,暂时采用稍后尝试的方法。代码示例如下:

服务端代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include <openssl/ssl.h>
#include <openssl/err.h>

#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define SSL_print_error_and_freectx() \
    ERR_print_errors_fp(stdout); \
    SSL_CTX_free(ctx);

int listenLocal(unsigned short uPort)
{
    int sockS = socket(AF_INET, SOCK_STREAM, 0);
    if (sockS < 0)
    {
        printf("socket() error! \n");
        return -1;
    }

    struct sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_port = htons(uPort);

    int ret = bind(sockS, (struct sockaddr*)&sin, sizeof(sin));
    if (ret < 0)
    {
        printf("bind() error! \n");
        close(sockS);
        return -1;
    }

    listen(sockS, 50);

    return sockS;
}

int main(int argc, char* argv[])
{
    SSL_library_init();
    SSL_load_error_strings();

    SSL_CTX* ctx = SSL_CTX_new( SSLv23_server_method() );
    if (ctx == NULL)
    {
        ERR_print_errors_fp(stdout);
        return -1;
    }

    int ret = SSL_CTX_use_certificate_file(ctx, "test.crt", SSL_FILETYPE_PEM);
    if (ret != 1)
    {
        SSL_print_error_and_freectx();
        return -1;
    }

    ret = SSL_CTX_use_PrivateKey_file(ctx, "test.key", SSL_FILETYPE_PEM);
    if (ret != 1)
    {
        SSL_print_error_and_freectx();
        return -1;
    }

    ret = SSL_CTX_check_private_key(ctx);
    if (ret != 1)
    {
        SSL_print_error_and_freectx();
        return -1;
    }

    int sockS = listenLocal(9999);
    if (sockS < 0)
    {
        SSL_CTX_free(ctx);
        return -1;
    }

    while (true)
    {
        struct sockaddr_in sinfrom;
        socklen_t sinfromlen = sizeof(sinfrom);
        int sockC = accept(sockS, (struct sockaddr*)&sinfrom, &sinfromlen);
        if (sockC < 0)
        {
            printf("accept() error! \n");
            break;
        }

        printf("accpet connect:[%d - %s:%d] \n", sockC, inet_ntoa(sinfrom.sin_addr), ntohs(sinfrom.sin_port));

        fcntl(sockC, F_SETFL, fcntl(sockC, F_GETFL) | O_NONBLOCK);

        SSL* ssl = SSL_new(ctx);
        SSL_set_fd(ssl, sockC);

        do
        {
            bool bHandShake = false;
            while (true)
            {
                ret = SSL_accept(ssl);
                printf("SSL_accept() ret:[%d] \n", ret);
                if (ret != 1)
                {
                    int err = SSL_get_error(ssl, ret);
                    if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE)
                    {
                        printf("want read or write... \n");
                        usleep(1000);
                        continue;
                    }

                    break;
                }

                bHandShake = true;
                break;
            }

            if (!bHandShake)
            {
                printf("SSL handshake failed! \n");
                break;
            }

            printf("SSL handshake success! \n");

            SSL_write(ssl, "hello", 5);

            while (true)
            {
                char sBuf[1024] = {0};
                int bytesin = SSL_read(ssl, sBuf, sizeof(sBuf)-1);
                if (bytesin < 0)
                {
                    int err = SSL_get_error(ssl, -1);
                    if (err == SSL_ERROR_WANT_READ)
                    {
                        printf("want read... \n");
                        usleep(100000);
                        continue;
                    }

                    printf("read error! \n");
                    break;
                }

                if (bytesin == 0)
                {
                    printf("disconnect! \n");
                    break;
                }

                printf("read:[%s] \n", sBuf);
            }

            SSL_shutdown(ssl);
        } while(0);

        SSL_free(ssl);
        close(sockC);
    }

    close(sockS);
    SSL_CTX_free(ctx);

    return 0;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include <openssl/ssl.h>
#include <openssl/err.h>

#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int connectPeer(const char* sIp, unsigned short uPort)
{
    int sockC = socket(AF_INET, SOCK_STREAM, 0);
    if (sockC < 0)
    {
        printf("socket() errror! \n");
        return -1;
    }

    struct sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = inet_addr(sIp);
    sin.sin_port = htons(uPort);

    int ret = connect(sockC, (struct sockaddr*)&sin, sizeof(sin));
    if (ret < 0)
    {
        printf("connect() failed! \n");
        close(sockC);
        return -1;
    }

    return sockC;
}

void showCert(SSL* ssl)
{
    X509* x509 = SSL_get_peer_certificate(ssl);

    if (x509)
    {
        const char* subjectname = X509_NAME_oneline(X509_get_subject_name(x509), 0, 0);
        printf("subject name:[%s] \n", subjectname);

        const char* issuername = X509_NAME_oneline(X509_get_issuer_name(x509), 0, 0);
        printf("issuer name:[%s] \n", issuername);

        X509_free(x509);
    }
}

int main(int argc, char* argv[])
{
    SSL_library_init();
    SSL_load_error_strings();

    SSL_CTX* ctx = SSL_CTX_new( SSLv23_client_method() );
    if (ctx == NULL)
    {
        ERR_print_errors_fp(stdout);
        SSL_CTX_free(ctx);
        return -1;
    }

    int sockC = connectPeer("127.0.0.1", 9999);
    if (sockC < 0)
    {
        SSL_CTX_free(ctx);
        return -1;
    }

    fcntl(sockC, F_SETFL, fcntl(sockC, F_GETFL) | O_NONBLOCK);

    SSL* ssl = SSL_new(ctx);
    SSL_set_fd(ssl, sockC);

    do
    {
        bool bHandShake = false;
        while (true)
        {
            int ret = SSL_connect(ssl);
            printf("SSL_connect() ret:[%d] \n", ret);
            if (ret != 1)
            {
                int err = SSL_get_error(ssl, -1);
                if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE)
                {
                    printf("want read or write... \n");
                    usleep(1000);
                    continue;
                }

                break;
            }

            bHandShake = true;
            break;
        }

        if (!bHandShake)
        {
            printf("SSL handshake failed! \n");
            break;
        }

        printf("SSL handshake success! \n");

        showCert(ssl);

        bool bShutDown = false;
        while (true)
        {
            char sBuf[1024] = {0};
            int bytesin = SSL_read(ssl, sBuf, sizeof(sBuf)-1);
            if (bytesin < 0)
            {
                int err = SSL_get_error(ssl, -1);
                if (err == SSL_ERROR_WANT_READ)
                {
                    printf("want read... \n");
                    usleep(100000);
                    continue;
                }

                printf("read error! \n");
                SSL_shutdown(ssl);
                bShutDown = true;
                break;
            }

            if (bytesin == 0)
            {
                printf("disconnect! \n");
                SSL_shutdown(ssl);
                bShutDown = true;
                break;
            }

            printf("read:[%s] \n", sBuf);
            break;
        }

        if (bShutDown)
            break;

        for (int i = 0; i < 3; ++i)
        {
            SSL_write(ssl, "hello", 5);
            sleep(1);
        }

        SSL_shutdown(ssl);
    } while (0);

    SSL_free(ssl);
    close(sockC);

    SSL_CTX_free(ctx);

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

推荐阅读更多精彩内容