SSH协议详解 2021-08-24

SSH 协议架构

ssh协议 =
The SSH Transport Layer Protocol [SSH-TRANS] + The User Authentication Protocol [SSH-USERAUTH] + The Connection Protocol [SSH-CONNECT]
rfc4251: The Secure Shell (SSH) Protocol Architecture

SSH protocol stack

ssh protocol stack:

SSH-CONNECT
SSH-USERAUTH
SSH-TRANS
TCP

SSH协议栈说明,经过服务器认证和用户认证后,才会最后建立SSH-CONNECTION

  • SSH传输层协议SSH Transport Layer Protocol:它负责认证服务器,加密数据,确保数据完整性, 虽然它运行在TCP之上,但其实它可以运行在任意可靠的数据流之上;
  • SSH用户认证协议SSH User Authentication Protocol:它负责认证使用者是否是ssh服务器的用户, Public Key Authentication登陆ssh就将在这一层实现;
  • SSH连接协议SSH Connection Protocol:它把多路(Multiplex)加密的通道转换成逻辑上的Channel

SSH-CONNECT protocol

rfc4254: SSH-CONNECT protocol
The Connection Protocol [SSH-CONNECT] multiplexes the encrypted tunnel into several logical channels.

[channel] is the key word of SSH-CONNECT protocol

channel

channels can be used for a wide range of purposes, such as providing interactive login sessions, remote execution of commands, forwarded TCP/IP connections, and forwarded X11 connections.

All terminal sessions, forwarded connections, etc., are channels.
Either side may open a channel.
Multiple channels are multiplexed into a single connection. 所有channel底层都复用同一条链接隧道

channel type

Each channel has a type.
Usually, we will use “session” channels, but there are also “x11” channels, “forwarded-tcpip” channels, and “direct-tcpip” channels, etc.

Channel type                  Reference
         ------------                  ---------
         session                       [SSH-CONNECT, Section 6.1]
         x11                           [SSH-CONNECT, Section 6.3.2]
         forwarded-tcpip               [SSH-CONNECT, Section 7.2]
         direct-tcpip                  [SSH-CONNECT, Section 7.2]

direct-tcpip is used for “client-to-server forwarded connections” and forwarded-tcpip is used for “server-to-client forwarded connections”

Interactive Sessions

A session is a remote execution of a program, in which server and client communicate by “session” channels.
The program may be a shell, an application, a system command, or some built-in subsystem.
It may or may not have a tty, and may or may not involve X11 forwarding.
Multiple sessions can be active simultaneously.

Opening a Session

A session is started by sending the following message.

      byte      SSH_MSG_CHANNEL_OPEN
      string    "session"
      uint32    sender channel
      uint32    initial window size
      uint32    maximum packet size

(Client implementations SHOULD reject any session channel open requests to make it more difficult for a corrupt server to attack the client. Client应该拒绝任何session channel open requests,以防server可能的攻击)

as a client implementation example, golang.org/x/crypto/ssh use *ssh.Client.NewSession to open a "session" channel:

func (c *Client) NewSession() (*Session, error) {
    // "session" 指定了 channel 的类型
    ch, in, err := c.OpenChannel("session", nil)
    if err != nil {
        return nil, err
    }
    return newSession(ch, in)
}

Channel-Specific Requests

基于某个特定的channel,通信双方通过发送和接收Channel-Specific Requests完成信息交互

Many 'channel type' values have extensions that are specific to that particular 'channel type'. An example is requesting a pty (pseudo terminal) for an interactive session.不同类型的channel支持不同的request type

All channel-specific requests use the following format.

      byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    request type in US-ASCII characters only (such as "pty-req" for "session" type channel)
      boolean   want reply
      ....      type-specific data follows

例如,"session" channel 支持以下的request type

Request type                  Reference
         ------------                  ---------
         pty-req                       [SSH-CONNECT, Section 6.2]
         x11-req                       [SSH-CONNECT, Section 6.3.1]
         env                           [SSH-CONNECT, Section 6.4]
         shell                         [SSH-CONNECT, Section 6.5]
         exec                          [SSH-CONNECT, Section 6.5]
         subsystem                     [SSH-CONNECT, Section 6.5]
         window-change                 [SSH-CONNECT, Section 6.7]
         xon-xoff                      [SSH-CONNECT, Section 6.8]
         signal                        [SSH-CONNECT, Section 6.9]
         exit-status                   [SSH-CONNECT, Section 6.10]
         exit-signal                   [SSH-CONNECT, Section 6.10]
Starting a Shell or a Command

Once the session has been set up, a program is started at the remote
end. The program can be a shell, an application program, or a
subsystem with a host-independent name. Only one of these requests
can succeed per channel(三选一)
.

      byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "shell"
      boolean   want reply

This message will request that the user's default shell (typically
defined in /etc/passwd in UNIX systems) be started at the other end.

      byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "exec"
      boolean   want reply
      string    command

This message will request that the server start the execution of the
given command. The 'command' string may contain a path. Normal
precautions MUST be taken to prevent the execution of unauthorized
commands.

      byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "subsystem"
      boolean   want reply
      string    subsystem name

This last form executes a predefined subsystem. It is expected that
these will include a general file transfer mechanism, and possibly
other features.

global request

There are several kinds of requests that affect the state of the remote end globally, independent of any channels. A global request example is a request to start TCP/IP forwarding for a specific port.

global request name                  Reference
         ------------                  ---------
         tcpip-forward                 [SSH-CONNECT, Section 7.1]
         cancel-tcpip-forward          [SSH-CONNECT, Section 7.1]

Note that both the client and server MAY send global requests at any time, and the receiver MUST respond appropriately. All such requests use the following format.

      byte      SSH_MSG_GLOBAL_REQUEST
      string    request name in US-ASCII only
      boolean   want reply
      ....      request-specific data follows

The recipient will respond to this message with
SSH_MSG_REQUEST_SUCCESS or SSH_MSG_REQUEST_FAILURE if 'want reply' is
TRUE.

      byte      SSH_MSG_REQUEST_SUCCESS
      ....     response specific data

Usually, the 'response specific data' is non-existent.

Why am I not disconnected when I restart the sshd server on the server I'm connected to

See below we have sshd on process 14688 started by systemd (process 1). We also have one session, 14701 who was started by sshd:

[root@rh7test ~]# ps -aef | grep ssh
root    14688     1  0 04:46 ?        00:00:00 /usr/sbin/sshd -D
root     14701 14688  0 04:46 ?        00:00:00 sshd: root@pts/2

Then we retsart sshd and it gets a new PID (14805). Notice the old ssh process is still running on 14701 but it was disconnected from the (terminated) sshd and now the owner is process 1(这里是关键).

[root@rh7test ~]# systemctl restart sshd.service
[root@rh7test ~]# ps -aef | grep ssh
root     14701     1  0 04:46 ?        00:00:00 sshd: root@pts/2
root     14805     1  0 04:48 ?        00:00:00 /usr/sbin/sshd -D

Then we connect a new session and can see that the new one is owned by the restarted sshd (14805).

[root@rh7test ~]# ps -aef | grep ssh
root     14701     1  0 04:46 ?        00:00:00 sshd: root@pts/2
root     14805     1  0 04:48 ?        00:00:00 /usr/sbin/sshd -D
root     14811 14805  0 04:48 ?        00:00:00 sshd: root@pts/0

sshd listens for connections from clients. It forks a new daemon for each incoming connection.
The forked daemons handle key exchange, encryption, authentication, command execution, and data exchange.
This child process will not die if SSHD is restarted, but will be owned by process 1.
So your sshd listens for connections but then forks a new process when a user connects to the server.
When you restart sshd you are only restarting the process that listens for new connections. All existing connections stay intact.

https://unix.stackexchange.com/questions/615403/why-am-i-not-disconnected-when-i-restart-the-sshd-server-on-the-server-im-conne

而当network restart的时候,ssh当前的连接也不会断开。这是因为,network restart是非常快速的,restart从开始到结束这个时间段内,socket在server内存中对应的结构体仍然valid,也就是说ssh连接依赖的底层tcp连接未断开,tcp连接根本感知不到network restart。那么,上层的ssh连接仍然保持是非常自然的。注意区分network restart vs sshd restart时,ssh连接不断的原因

SSH authentication with SSH keys

Abstract:
The idea is to have a cryptographic key pair - public key and private key - and configure the public key on a server to authorize access and grant anyone who has a copy of the private key access to the server.
Host keys authenticate hosts. Authorized keys and identity keys authenticate users.

使用ssh key实现远程免密登录涉及以下步骤:

  • 公私钥生成与公钥部署ssh client 生成公私钥,生成公私钥使用ssh-keygen,例如ssh-keygen -t rsa -C "costumed_comment(such as your email address)"
    然后把公钥的内容追加到 ssh server指定用户的~/.ssh/authorized_keys。
    The public key can then be installed as an authorized key on a server using the ssh-copy-id
    如将公钥内容追加到root用户的/root/.ssh/authorized_keys,表示该公钥已被root用户认证可信
  • ssh client配置~/.ssh/config文件(可选,推荐)。配置~/.ssh/config可以针对不同的remote目标机器设置不同的登录配置,如配置hostname(真实地址)、IP、端口、指定登录方式(密码\ssh key\etc)和私钥文件等
    eg.配置如下config
# Read more about SSH config files: https://linux.die.net/man/5/ssh_config
Host devmachine
    HostName 9.9.9.9
    Port 99999
    User root
    PreferredAuthentications publickey  
    IdentityFile /Users/dev/.ssh/id_rsa_devmachine  # 指定私钥文件

配置完成后,ssh root@devmachine甚至直接ssh devmachine就能够登录上9.9.9.9。因为这个命令依据devmachine这个Host读取config内的管理配置,无需再声明IP、端口、私钥和登录用户等参数
ssh的config文件可以存在多个,vscode安装了remote-ssh插件后可以加载ssh的config文件,实现ssh到开发机做远程开发

如果不配置~/.ssh/config,也可以通过指定ssh参数的形式实现登录

指定IP、端口和私钥可以登录:
ssh -i /Users/dev/.ssh/id_rsa_devmachine root@9.9.9.9 -p 99999
通过输入密码登录:
ssh root@9.134.165.116 -p 36000

一点踩坑记录:
部分软件(如weterm、某些版本的vscode)的ssh功能在使用本地私钥时可能会报
Error: Error while signing data with privateKey: error:06000066:public key routines:OPENSSL_internal:DECODE_ERROR
解决方式-网上的办法是转格式
To fix this error, we can convert the private key file from OpenSSH private key format to PEM format.
ssh-keygen -p -m PEM -f target_id_rsa

配置github等代码托管平台使用ssh key免密登录,具体可以参考https://docs.github.com/en/authentication/connecting-to-github-with-ssh


ssh协议认证过程

参考:https://xdev.in/posts/understanding-of-ssh/
简单来说概括为以下两步:

  • ssh client验证 ssh server身份ssh client通过ssh命令发起连接请求, ssh server收到后返回自己的公钥指纹(RSA key fingerprint)。如果ssh clientssh server是首次建立连接,ssh client会收到提示如:
    The authenticity of host 'ssh-server.example.com (12.18.429.21)' can't be established.
    RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d.
    Are you sure you want to continue connecting (yes/no)?
    
    ssh client选择是否信任 ssh server,如果信任则会将 ssh server的公钥rsa-key写入本地的~/.ssh/known_hosts。后续ssh client再与 ssh server进行连接,则会对比连接时 ssh server发来的公钥与known_hosts中的是否一致。此时,服务端拥有客户端的公钥和本机的私钥,客户端拥有服务端的公钥和本机的私钥
  • ssh server验证 ssh client身份ssh server利用ssh client连接时指定的公钥加密一个随机数并协商会话密钥sessionkey,如果ssh client用私钥顺利解出随机数并发回ssh server,那么ssh server验证 ssh client身份完毕
  • 双向认证完毕,避免中间人(man-in-the-middle attack)攻击。双方使用sessionkey对data进行对称加密后传输

如果配置好公私钥后,用户仍然需要使用账号密码登录,检查

    1. 检查ssh server端的/etc/ssh/sshd_config是否允许公私钥登录

    RSAAuthentication yes
    PubkeyAuthentication yes
    AuthorizedKeysFile .ssh/authorized_keys

然后重启sshd服务
service sshd restart

    1. 检查ssh server端的用户家目录下~.ssh是否可读写

    ls -l ~/.ssh/
    total 20
    -rw-rw-r-- 1 xiu xiu 576 Oct 18 19:25 authorized_keys
    -rw------- 1 xiu xiu 90 Oct 18 18:07 config
    -rw------- 1 xiu xiu 1675 Oct 18 18:07 id_rsa
    -rw-r--r-- 1 xiu xiu 404 Oct 18 18:07 id_rsa.pub
    -rw-r--r-- 1 xiu xiu 409 Oct 18 19:15 known_hosts

    1. 开启sshd调试模式进行进一步调试可以参考:
      https://blog.51cto.com/zouqingyun/1874410
  • 关于ssh client的配置信息

    The ssh program on a host receives its configuration from either the command line or from configuration files ~/.ssh/config and /etc/ssh/ssh_config.

    Command-line options take precedence over configuration files. The user-specific configuration file ~/.ssh/config is used next. Finally, the global /etc/ssh/ssh_config file is used. The first obtained value for each configuration parameter will be used.

  • 忽略known_hosts file的公钥验证

You can use the following command to ignore known hosts when using ssh command line:

ssh -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" user@host

usage in scp is the same

scp -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" somefile.txt sarav@mwinventory.in :/var/tmp/

This command will skip host key checking by sending the key to a null known_hosts file.

The ssh known_hosts file is a file that stores the public key of all of the servers that you have connected using ssh.
利用option UserKnownHostsFile指定本次ssh连接要使用的 known_hosts file. 远端机器校验通过/指定无需校验远端 后,ssh会将远端机器的 public key 存入 known_hosts file. 如果 known_hosts file中 已有该远端机器的 public key,那什么也不做;如果发现该远端机器的 public key 和 known_hosts file 已有的不同,追加更新而不是覆盖更新known_hosts file

-o "StrictHostKeyChecking=no"表示不作远端机器的身份校验,"UserKnownHostsFile=/dev/null"表示将远端机器的public key写入本地/dev/null,也就是丢弃

  • SSH tunneling(also called SSH port forwarding,中文【端口转发】)

    包含3种端口转发,分别是Local Forwarding、Remote Forwarding和Dynamic Forwarding

    • Local Forwarding(本地端口转发到远端端口)

      In OpenSSH, local port forwarding is configured using the -L option:

      ssh -L local-port:target-host:target-port intermediate-host
      

      Eg. 设定localhost与gateway.example.com连通;而intra.example.com部署在gateway.example.com后的内网,与localhost不能互通

      ssh -L 80:intra.example.com:80 gateway.example.com
      

      This example opens a connection to the gateway.example.com jump server, and forwards any connection to port 80 on the local machine to the configured port 80 on intra.example.com.

      上面的命令让本地的ssh client监听本地80端口,任何发往本地80端口的请求都会redirect到ssh client,然后发往ssh server即gateway.example.com,随后server将直接deliver数据到intra.example.com的80端口,完成响应后resp流量按原路返回

      最终的效果是,请求http://localhost:80,实际上是请求了被gateway.example.com隔离的内网机器intra.example.com上部署的http://intra.example.com:80服务。这样一来,原本intra.example.com不能直接被localhost访问,但localhost通过ssh端口转发,把自己的80端口请求通过ssh连接转发到gateway.example.com:22然后再转发给intra.example.com,实现了外网对内网资源的访问

    tunnel http traffic via ssh local forward

    9.134.165.110:8080起了一个http服务,该http服务可以通过intermedia_server访问,而无法通过localhost访问
    使用ssh -N -L 8888:9.134.165.110:8080 intermedia_server -p 22 -l root做本地端口转发
    访问本地的8888端口就等于访问9.134.165.110的8080端口,http://localhost:8888 works

    tunnel https traffic via ssh local forward

    myip.ipip.net:443是我们的目标访问端口
    curl https://myip.ipip.net/会返回ip出口详情,如当前 IP:14.xx.xx.xx 来自于:中国 广东 广州 电信

    仿照tunnel http traffic,我们尝试ssh -N -L 8888:myip.ipip.net:443 intermedia_server -p 22 -l root,发现https://localhost:8888并不正常工作

    HTTPS connection can be redirected via SSH port forwarding - however the SSL/TLS certificate validation will fail in such cases as the host name does not match:
    we are connecting to https://localhost:8888 but the server certificate contains the name myip.ipip.net
    我们使用https协议访问localhost:8888,目标访问站点是localhost。流量被forward到绑定了myip.ipip.net的server。在SSL/TLS握手阶段,目标访问站点localhost与server namemyip.ipip.net并不匹配,于是SSL/TLS握手失败

    解决方案:
    server namemyip.ipip.net我们改不了,但我们可以改localhost name,把localhost name改成server name,SSL/TLS握手就能够正常: vim /etc/hosts -> 127.0.0.1 myip.ipip.net
    这时,访问https://myip.ipip.net:8888就是访问本地的8888端口,然后流量顺利通过intermedia_server被forward到真正的myip.ipip.net:443

    注意:https://myip.ipip.net:8888返回的IP信息,实际上是intermedia_server请求myip.ipip.net:443的返回,因为intermedia_server才是真正的请求方

    refer to https://superuser.com/questions/347415/is-it-possible-to-tunnel-https-traffic-via-ssh-tunnel-with-standard-ssh-programs

  • Remote Forwarding(远端端口转发到本地端口)

    In OpenSSH, remote SSH port forwardings are specified using the -R option. For example:

    ssh -R remote-port:target-host:target-port -N remotehost
    

    Eg.

    ssh -R 8080:localhost:80 public.example.com
    

    This allows anyone on the remote server to connect to TCP port 8080 on the remote server. The connection will then be tunneled back to the client host, and the client then makes a TCP connection to port 80 on localhost. Any other host name or IP address could be used instead of localhost to specify the host to connect to.

    与local forwarding相反,建立本地计算机到远程计算机的 SSH 隧道以后,本地转发是通过本地计算机访问远程计算机,而远程转发则是通过远程计算机访问本地计算机

    上面的命令建立了远端public.example.com到本地的ssh隧道,访问远程主机public.example.com:8080的请求,会被public.example.com经过ssh端口转发到本机,然后再转发到localhost:80

  • Dynamic Forwarding(动态端口转发)

    动态转发把本地端口绑定到 SSH 服务器就到此为止。至于 SSH 服务器要去访问哪一个网站,完全是动态的,取决于原始通信

    ssh -D local-port tunnel-host -N
    

    注意,这种转发采用了 SOCKS5 协议。访问外部网站时,需要把 HTTP 请求转成 SOCKS5 协议,才能把本地端口的请求转发出去

    SOCKS(SOCKet Secure),是一种会话层协议,主要用于客户端与外网服务器之间通讯的中间传递。当防火墙后的客户端要访问外部的服务器时,就跟SOCKS代理服务器连接。这个代理服务器控制客户端访问外网的资格,允许的话,就将客户端的请求发往外部的服务器

note: SSH端口转发完全基于基本的SSH连接,因此,凡是可以切断SSH连接的方式都可以终止端口转发,包括但不限于通过在远程终端上执行exit命令、暴力关闭本地终端窗口、远程主机关机、本地主机关机等

golang ssh client 实践

package cmdchannel

import (
    "bytes"
    "fmt"
    "strings"
    "testing"
    "time"

    "golang.org/x/crypto/ssh"
)

func TestSSH(t *testing.T) {
    cmd := "ls /"

    // ssh config, use password here
    addr := "xxx.xxx.xxx.xxx:22"
    sshCFG := &ssh.ClientConfig{
        Config: ssh.Config{},
        User:   "root",
        Auth: []ssh.AuthMethod{
            ssh.Password("passwd"),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        Timeout:         30 * time.Second,
    }

    // ssh client
    client, err := ssh.Dial("tcp", addr, sshCFG)
    if err != nil {
        err = fmt.Errorf("failed to get ssh client. %w", err)
        fmt.Println(err)
        return
    }

    // open a "session" channel, aka the session in rfc4254
    session, err := client.NewSession()
    if err != nil {
        fmt.Println(err)
        return
    }
    defer session.Close()

    var output bytes.Buffer
    session.Stdout = &output

    modes := ssh.TerminalModes{
        ssh.ECHO:          0,     // disable echoing
        ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
        ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
    }

    if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
        fmt.Println(err)
        return
    }

    err = session.Run(cmd)
    if err != nil {
        fmt.Println(err)
        return
    }
    result := output.String()
    fmt.Println("output is:", result)
}

func TestSSH1(t *testing.T) {
    cmd := "ls /; exit 0" // must exit

    // ssh config, use password here
    addr := "xxx.xxx.xxx.xxx:22"
    sshCFG := &ssh.ClientConfig{
        Config: ssh.Config{},
        User:   "root",
        Auth: []ssh.AuthMethod{
            ssh.Password("passwd"),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        Timeout:         30 * time.Second,
    }

    // ssh client
    client, err := ssh.Dial("tcp", addr, sshCFG)
    if err != nil {
        err = fmt.Errorf("failed to get ssh client. %w", err)
        fmt.Println(err)
        return
    }

    session, err := client.NewSession()
    if err != nil {
        fmt.Println(err)
        return
    }
    defer session.Close()

    cmdlist := strings.Split(cmd, ";")
    stdinBuf, err := session.StdinPipe()
    if err != nil {
        t.Error(err)
        fmt.Println(err)
        return
    }

    var outbt, errbt bytes.Buffer
    session.Stdout = &outbt

    session.Stderr = &errbt

    // get a pty for the session
    modes := ssh.TerminalModes{
        ssh.ECHO:          0,     // disable echoing
        ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
        ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
    }

    if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
        fmt.Println("pty err:", err)
        return
    }

    // run an interactive shell. 
    err = session.Shell()
    if err != nil {
        fmt.Println(err)
        return
    }

    start := time.Now()
    for _, c := range cmdlist {
        // 在shell里面,\n代表enter键换行,即执行当前命令c
        c = c + "\n"
        stdinBuf.Write([]byte(c))
    }
    session.Wait()

    fmt.Println("output is ", outbt.String())
    fmt.Println("err is ", errbt.String())
}

python ssh client 实践

Here is an example of how to use Python to connect to a remote server via an intermediate server using SSH:

import paramiko

# Define the intermediate server (jumper) details
jumper = paramiko.SSHClient()
jumper.set_missing_host_key_policy(paramiko.AutoAddPolicy())
jumper.connect('jumper.example.com', username='jumper_user', password='jumper_password')

# Define the remote server details
remote = paramiko.SSHClient()
remote.set_missing_host_key_policy(paramiko.AutoAddPolicy())
remote.connect('remote.example.com', username='remote_user', password='remote_password', sock=jumper.get_transport().open_channel('direct-tcpip', ('remote.example.com', 22), ('localhost', 10022)))

# Run a command on the remote server
stdin, stdout, stderr = remote.exec_command('ls -l')
print(stdout.read().decode())

This code uses the paramiko library to connect to the intermediate server (jumper) and then to the remote server. The sock parameter is used to specify the channel to the remote server via the intermediate server. The exec_command method is used to run a command on the remote server.


更多可以参考 https://github.com/wangdoc/ssh-tutorial

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

推荐阅读更多精彩内容