打开了之前看到的两个CREATOR_SSH_DEBUG调试
先看下state状态
// NOTE: When you add stuff here, don‘t forget to update m_packetHandlers.
enum SshStateInternal {
SocketUnconnected, // initial and after disconnect
SocketConnecting, // After connectToHost()
SocketConnected, // After socket‘s connected() signal
UserAuthServiceRequested,
UserAuthRequested,
ConnectionEstablished // After service has been started
// ...
};
2是SocketConnected,是前面3个包。
3是UserAuthServiceRequested,第四个包,说明第3个包处理之后就是认证服务请求了。
4是UserAuthRequested,第五个包。
5,服务已经启动了,说明五个包之后服务就起来了。
这个很重要,需要结合这个来分析抓到的包。差点忘了,这些是incoming接受到server的第几个包,差点惯性认为是交互的第几个包了。一开始写的时候就想错了,现在回过来说明下。这里的包状态说明使用(1)(2)(3)(4)(5)...来说明
登陆成功后显示信息
上面整个过程抓到的
第一个包,分析过了,连接成功后状态就是SocketConnected,然后发送ClientId
第二个包,状态为SocketConnected,第(1)个包,server返回ServerId
第三个包,状态为SocketConnected,之前就分析到第二个包,现在知道了收到ServerId后要发送Key Exchange Init,现在我依然没有去看SSH协议介绍,只说我的理解。这个应该是告诉Server,client这边key交换已经初始化好了
第四个包,状态为SocketConnected,第(2)个包,Server发送Key Exchange Init,同样应该是告诉client,server这边key交换已经初始化好了
第五个包,状态为SocketConnected,client发送Diffle-Hellman Key Exchange Init,具体是什么意思,后面看SSH协议时再去理解。这里分析完后,在本文文末还会拿点SSH协议内容来,来验证分析是否正确。
第六个包,状态为SocketConnected,第(3)个包,server发送了Diffle-Hellman Key Exchange reply,并有New Keys。既然是exchange,那么这里应该是server的new
keys信息吧,不是让client发送new keys的意思吧?看后面协议验证。
第七个包,状态可能为SocketConnected,也可能为UserAuthServiceRequested,待确定。client 发送自己的new keys。UserAuthServiceRequested之后还有个UserAuthRequested。
第七个包之后,都是加密数据包了。第(4)个包及之后。UserAuthRequested?
哎,那用户名和密码是在什么时候发送过去的?哦,我忽略了一个重要信息,就是字节数。
第八个包,len=52,第九个包,len=36。而第(4)个包len=52,第(5)个包len=36。那说明,这里才是用户名和密码的验证及后续信息传输的开始。也对,用户名和密码是登陆shell的,算shell信息,而不是ssh信息。
Diffie-Hellman:一种确保共享KEY安全穿越不安全网络的方法,它是OAKLEY的一个组成部分。Whitefield与Martin
Hellman在1976年提出了一个奇妙的密钥交换协议,称为Diffie-Hellman密钥交换协议/算法(Diffie-Hellman Key Exchange/Agreement Algorithm).这个机制的巧妙在于需要安全通信的双方可以用这个方法确定对称密钥。然后可以用这个密钥进行加密和解密。但是注意,这个密钥交换协议/算法只能用于密钥的交换,而不能进行消息的加密和解密。双方确定要用的密钥后,要使用其他对称密钥操作加密算法实际加密和解密消息。
http://blog.csdn.net/gsnumen/article/details/7293266《ssh详细登录过程》
认证原理:
有2种认证方式:
基于账号和口令的验证方式 和 基于公钥和私钥的验证方式
ssh的登录过程分为5个阶段
1、版本号协商阶段
2、密钥和算法协商阶段
3、认证阶段
4、会话请求阶段
5、会话交互阶段
1、版本号协商阶段
服务端打开端口22,等待客户连接。
客户端向服务端发起TCP连接,连接建立后,服务端向客户端发送第一个报文,包括版本标志字符串,格式为“协议版本号 次协议版本号 软件版本号”。
客户端收到报文后,解析协议版本号,如果服务端的协议版本号比自己的低,且客户端能支持服务端的低版本,就使用服务端的协议号,否则使用自己的协议版本号。
客户端回复服务端一个报文,包含了客户端决定使用的协议版本号。
服务端比较客户端发过来的版本号,决定是否能同客户端交互。
如果协商成功,就进入密钥和算法协商阶段。否则服务端断开TCP连接。
2、密钥和算法协商阶段
服务端和客户端分别发送算法协商报文给对方,报文中包含自己支持的公钥算法列表、加密算法列表、消息验证码算法列表、压缩算法列表等。
服务端和客户端根据对方和自己支持的算法得出最终使用的算法。
服务端和客户端利用DH交换算法、主机密钥对等参数,生成会话密钥和会话ID。
c公 客户端公钥
c密 客户端密钥
s公 服务端公钥
s密 服务端密钥
在版本号协商阶段完成后:
服务端将 s公 发送给客户端。
服务端生成会话ID ,设为 id ,发送给客户端。
客户端生成会话密钥,设为 key ,并计算 res = id 异或 key。
客户端将 res 用 s公 进行加密,将结果发送给服务端。
服务端用 s密 进行解密,得到 res。
服务器计算 res 异或 id,得到 key。
至此服务端和客户端都知道了会话密钥和会话ID,以后的数据传输都使用会话密钥进行加密和解密。
3、认证阶段
基于账号和口令的验证方式:
客户端使用密钥和算法协商阶段生成的会话密钥加密账号、认证方法、口令,将结果发送给服务器。
服务端使用获得的会话密钥解密报文,得到账号和口令。
服务端对这个账号和口令进行判断,如果失败,向客户端发送认证失败报文,其中包含了可以再次认证的方法列表。
客户端从认证方法列表中选择一种方法进行再次认证。
这个过程反复进行,直到认证成功或者认证次数达到上限,服务端关闭本次TCP连接。
基于公钥和私钥的验证方式:
使用ssh-keygen程序生成公钥 id_dsa.pub 和私钥 id_dsa,一般是在客户端上生成,然后把 id_dsa.pub 通过某种方式发送给服务端。
服务端放在将要远程登录过来的那个账号的目录的.ssh目录下面。
客户端使用密钥和算法协商阶段生成的会话密钥加密账号、认证方法、id_dsa.pub,将结果发送给服务端。
服务端使用会话密钥解密报文,得到账号、id_dsa.pub。 服务端在这个账号的目录的.ssh目录下找对应的公钥,如果没有找到,发送失败消息给客户端,如果找到,比较客户发送过来的这个公钥和找到的公钥,如果内容相同,服务端生成一个随机的字符串,简称“质询”,然后使用找到的公钥加密这个质询,然后使用会话密钥再次加密。
服务端把这个双重加密的数据发送给客户端。
客户端使用会话密钥解密报文,然后使用id_dsa再次解密数据,得到质询。
客户端使用会话密钥加密质询,发送给服务端。
服务端使用会话密钥解密报文,得到质询,判断是不是自己生成的那个质询,如果不相同,发送失败消息给客户端,如果相同,认证通过。
我配置sshd时使用密码登录,没有开启密钥认证。
“
客户端回复服务端一个报文,包含了客户端决定使用的协议版本号。
服务端比较客户端发过来的版本号,决定是否能同客户端交互。
如果协商成功,就进入密钥和算法协商阶段。否则服务端断开TCP连接。
”
这个似乎没有,第三个包是key exchange,可以理解为密钥协商。客户端没有再回复一个决定使用版本号的包啊?我是觉得没有回复。
就到这里吧,再去看下代码。
看了一下,发现之前看的
// RFC 4253, 4.2.
void SshConnectionPrivate::handleServerId()
{
#ifndef CREATOR_SSH_DEBUG
//#ifdef CREATOR_SSH_DEBUG
qDebug("%s: incoming data size = %d, incoming data = ‘%s‘",
Q_FUNC_INFO, m_incomingData.count(), m_incomingData.data());
#endif
const int newLinePos = m_incomingData.indexOf(‘\n‘);
if (newLinePos == -1)
return; // Not enough data yet.
// Lines not starting with "SSH-" are ignored.
if (!m_incomingData.startsWith("SSH-")) {
m_incomingData.remove(0, newLinePos + 1);
m_serverHasSentDataBeforeId = true;
return;
}
if (newLinePos > 255 - 1) {
throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR,
"Identification string too long.",
tr("Server identification string is %n characters long, but the maximum "
"allowed length is 255.", 0, newLinePos + 1));
}
const bool hasCarriageReturn = m_incomingData.at(newLinePos - 1) == ‘\r‘;
m_serverId = m_incomingData.left(newLinePos);
if (hasCarriageReturn)
m_serverId.chop(1);
m_incomingData.remove(0, newLinePos + 1);
if (m_serverId.contains(‘\0‘)) {
throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR,
"Identification string contains illegal NUL character.",
tr("Server identification string contains illegal NUL character."));
}
// "printable US-ASCII characters, with the exception of whitespace characters
// and the minus sign"
QString legalString = QLatin1String("[]!\"#$!&‘()*+,./0-9:;<=>[email protected][\\\\^_`a-z{|}~]+");
const QRegExp versionIdpattern(QString::fromLatin1("SSH-(%1)-%1(?: .+)?").arg(legalString));
if (!versionIdpattern.exactMatch(QString::fromLatin1(m_serverId))) {
throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR,
"Identification string is invalid.",
tr("Server Identification string \"%1\" is invalid.")
.arg(QString::fromLatin1(m_serverId)));
}
const QString serverProtoVersion = versionIdpattern.cap(1);
if (serverProtoVersion != QLatin1String("2.0") && serverProtoVersion != QLatin1String("1.99")) {
throw SshServerException(SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED,
"Invalid protocol version.",
tr("Server protocol version is \"%1\", but needs to be 2.0 or 1.99.")
.arg(serverProtoVersion));
}
if (m_connParams.options & SshEnableStrictConformanceChecks) {
if (serverProtoVersion == QLatin1String("2.0") && !hasCarriageReturn) {
throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR,
"Identification string is invalid.",
tr("Server identification string is invalid (missing carriage return)."));
}
if (serverProtoVersion == QLatin1String("1.99") && m_serverHasSentDataBeforeId) {
throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR,
"No extra data preceding identification string allowed for 1.99.",
tr("Server reports protocol version 1.99, but sends data "
"before the identification string, which is not allowed."));
}
}
m_keyExchange.reset(new SshKeyExchange(m_sendFacility));
m_keyExchange->sendKexInitPacket(m_serverId);
m_keyExchangeState = KexInitSent;
}
提示中就说了need ssh2.0或者ssh1.99,所以对于QSsh确实没有回复的。这些都处理好后,就直接sendKeyInitPacket()了,也就是Key Exchange Init。