一、SSH协议简介
我们经常会使用ssh username@hostIp
命令登陆我们的linux服务器,如下图所示:
我们也明白这是使用了SSH协议进行登陆,但我们想知道的是,为什么可以使用SSH协议进行登陆,而且为什么使用SSH就是安全的,其背后的原理是什么?下面我们就一起来探讨下这几个话题。
当然啦!如果现在你手头上有相关公网可访问的云主机,那么请你登陆你的云主机,然后执行grep sshd.*Failed /var/log/secure
命令看看,或许你会惊讶的发现有很多输出日志,你就会明白到底有多少人想尝试登陆你的主机了。
简单地说,SSH协议是建立在不安全的网络之上的进行远程安全登陆的协议。它是一个协议族,其中有三个子协议,分别是:
- 1、传输层协议
[SSH-TRANS]
:提供服务器验证、完整性和保密性功能,建立在传统的TCP/IP协议之上。 - 2、验证协议
[SSH-USERAUTH]
:向服务器验证客户端用户,有基于用户名密码和公钥两种验证方式,建立在传输层协议[SSH-TRANS]
之上。 - 3、连接协议
[SSH-CONNECT]
:将加密隧道复用为若干逻辑信道。它建立在验证协议之上。
这里不对SSH协议做更加详细的介绍,我们可以自行百度一下。下面的写作思路将先通过wireshire抓包分析SSH协议上述三个流程,最后将提出整个学习过程中小编遇到的问题进行探讨。
二、SSH协议握手过程分析
在接着下面的内容之前,这里有几个概念非常重要!
- 1、
会话密钥 key
:key是通过客户端和服务器之间通过诸如D-H算法协商出来的。 - 2、
公钥 pub key
:pub key成为服务器主机密钥server_host_key
,用于SSH-TRANS
传输协议进行服务器验证,说白了就是客户端去验证服务器用的
SSH协议握手过程大致流程如下图所示:
下面是小编通过wireshire工具抓的数据包,让我们分别一步步进行分析:
- 1、TCP三次握手建立连接
我们都知道,TCP协议有个叫三次握手的过程,从上面图中可以看出序号(647-649)即是TCP连接建立过程。
NO. | 描述 | seq | Win | ACK | 解释 |
---|---|---|---|---|---|
647 | 第一次握手 | 0 | 8192 | 无 | seq = 0表示客户端当前的TCP包序列号 |
648 | 第二次握手 | 0 | 14600 | 1 | seq = 0,表示服务器端当前的TCP包序列号 ack = 1(客户端seq + 1),表示对客户端第 seq = 0 的TCP包进行应答 |
649 | 第三次握手 | 1 | 65536 | 1 | seq = 1,表示客户端端当前的TCP包序列号 ack = 1(服务器seq + 1),表示对服务器端第 seq = 0 的TCP包进行应答 |
- 2、SSH版本协议交换
上图序号(647-649)即是SSH版本协议交换过程。
NO. | 描述 | 解释 |
---|---|---|
650 | 协议版本协商 | 服务器将自己的SSH协议版本发送到客户端,格式为:SSH-protoversion(版本号)-softwareversion(自定义) SP(空格一个,可选) comments(注释,可选) CR(回车) LF(换行) |
651 | 协议版本协商 | 客户端将自己的SSH协议版本发送到服务器,格式为:SSH-protoversion(版本号)-softwareversion(自定义) SP(空格一个,可选) comments(注释,可选) CR(回车符) LF(换行符) |
这一步其实没什么高大上的内容,就是发送一个格式为SSH-protoversion-softwareversion SP comments CR LF
的字节流而已。
- 3、密钥协商key
上图序号(652-677)即是SSH版本协议交换过程。
密钥协商过程从客户端和服务器相互发出Key Exchange Init
请求开始,主要是告诉对方自己支持的相关加密算法列表、MAC算法列表等。
最后协商成功之后,将会生成一个对称加密会话密钥key
以及一个会话ID
,在这里要特别强调,这个是对称加密密钥key,不要和公钥相混淆了,公钥和密钥在上面开头已经着重强调两者的区别了,公钥是给客户端去验证服务器用的。
在这一步中,公钥会从服务器传送到客户端:
而会话密钥是通过D-H算法计算出来的,不会在网络上传输,其破解的难度取决于离散对数的破解难度,一般不会被破解的,有兴趣的可以自行了解该算法原理。
下面我将贴出Key Exchange Init
发送的请求包数据分析
NO. | 描述 | 解释 |
---|---|---|
1 | kex_algorithms | 密钥交换算法,里边即包含我们使用的D-H算法,用于生成会话密钥 |
2 | server_host_key_algorithms | 服务器主机密钥算法,可以采用 ssh-rsa,ssh-dss,ecdsa-sha2-nistp256 ,有公钥和私钥的说法,公钥即我们上面讲到的pub key,对于公钥私钥的概念,可以参见understanding public key private key concepts |
3 | encryption_algorithms_client_to_server | 对称加密算法,常用的有aes128-cbc,3des-cbc |
4 | mac_algorithms_client_to_server | MAC算法,主要用于保证数据完整性 |
5 | compression_algorithms_client_to_server | 压缩算法 |
- 4、认证阶段
上图序号(678-680)即是SSH版本认证阶段。
1、基于账号和口令的验证方式
客户端将自己的
用户名 + 密码
用上面生成的会话密钥key
进行加密之后传送到服务器端进行验证,服务器端验证通过,则响应成功,否则在进行有限次(推荐是20次)重新认证。至于服务器是怎么验证的,是否结合了会话ID小编也不清楚,网上众说纷纭。
注意,用户名和密码是采用上面密钥协商阶段生成的会话密钥key进行加密的,包括后面的连接会话阶段所传送的数据都是,不要认为是采用服务器的pub key加密的2、基于公钥和私钥的验证方式
这种方式也称为免密登陆。简单地说,就是客户端自己生成公钥私钥(通常采用ssh-keygen程序生成),然后将公钥以某种方式(通常是手动添加)保存到服务器
~/.ssh/authorized_keys
文件中,以后服务器都会接受客户端传过来的经过会话密钥加密过的公钥,然后解密得到公钥之后和本地authorized_keys
配置的公钥是否相等,如果是,则允许登陆。
如果你配过github或者gitlab的公钥,其实第二种方式认证方式很好理解,因为github它们也是采用SSH协议进行代码克隆的,没有配置公钥好像是不允许克隆的。
三、相关问题
- 1、密钥协商阶段安全吗?有没有中间人攻击的情况!
就我的理解,第一次总是不安全的。为什么呢?上面说到协商过程中,服务器会将自己的公钥server_host_key_algorithms
发送给客户端,但是客户端无法保证它拿到的公钥就是目标服务器所发出来的,很可能有个中间人拦截了你的请求,然后中间人发了另外一个公钥给到你客户端,这就不安全了!这也很好解释了为什么我们第一次登陆的时候,Shell终端总是会出现这个提示的原因:
这也是将确认权留给客户端自己去判断的一种策略。相反,如果想要更加安全,那么我们可以采用第二种认证方式进行登陆。由于提示的是经过MD5之后的公钥,那么我们怎么判断这个值是有效的呢?请看下面第3个疑问。
- 2、传输协议协商出来的会话密钥和会话ID到底有什么作用?
会话密钥:对称加密算法的密钥,用于对通信数据进行加解密,会话ID有点像WEB中的Session,就是用来表示每一个会话的,同时在认证阶段也起判断是否同一会话有效的作用。
- 3、既然密码验证登陆,那么客户端第一次登陆的时候如何验证服务器公钥的正确性?
请你告诉小编吧!小编苦于找不到答案!
四、其他
下面是相关资料文档:
- 1、SSH RFC中文文档地址
- 2、The Secure Shell (SSH) Protocol Architecture
- 3、The Secure Shell (SSH) Transport Layer Protocol
我们可能会问了,既然有了SSH协议文档,那么假如我们想要照着文档写一个实现出来,那么应该怎么去入手呢?其实SSH中的传输协议[SSH-TARNS]
是基于TCP协议的,因此我们可以从创建一个最基本Java Socket套接字开始,逐步实现SSH的传输协议、认证协议以及连接协议。如果你对实现过程感兴趣,可以研究下ganymed-ssh2-build209.jar
中的源码,结合SSH协议RFC文档,你就会发现,其实SSH协议中的协商和认证过程,其实都是通过Socket输入流、输出流的形式实现的。小编自己撸到协商阶段果断放弃,原因是不懂得算法太多了!