SSH登陆失败

背景

最近在用rock-chips的芯片做项目,在移植openssh后,无法正常使用密码登录ssh,记录解决思路和解决方法。

shadow

/etc/shadow 文件是 Linux 系统中用于存储用户密码和账户过期信息的文件。它包含了每个用户的账户信息,尤其是与身份验证和密码相关的内容。每一行包含一个用户的信息,字段之间由冒号 : 分隔。

具体格式为:

1
用户名:加密后的密码:最后更改密码的天数:最小天数:最大天数:警告期:不活动期:到期时间:保留字段

下面是每个字段的详细说明:

  1. 用户名 (用户名):
    • 存储用户的用户名。
    • 例如:rootjohn
  2. 加密后的密码 (加密后的密码):
    • 存储用户的密码,通常是经过加密处理的密码。
    • 如果该字段包含一个特殊字符 !*,表示该账户已被锁定,不能登录。
    • 例如:$6$WqWvMyD6$zGs... 表示加密后的密码(此处为 SHA-512 加密)。
  3. 最后更改密码的天数 (最后更改密码的天数):
    • 记录自 1970年1月1日以来的天数,表示最后一次更改密码的日期。
    • 例如:18000 表示密码在 18000 天更改。
  4. 最小天数 (最小天数):
    • 密码更改的最小天数。设置为 0 时,用户可以立即更改密码;如果大于 0,则必须等待至少这个天数才能更改密码。
    • 例如:0 表示不限制密码更改的最小时间。
  5. 最大天数 (最大天数):
    • 密码的最大有效天数,超过此天数后必须更改密码。
    • 例如:90 表示密码在 90 天后到期,用户必须更改密码。
  6. 警告期 (警告期):
    • 在密码到期前的多少天开始警告用户,提醒其更改密码。
    • 例如:7 表示密码到期前 7 天开始警告用户。
  7. 不活动期 (不活动期):
    • 密码过期后,用户账户在不活动多少天后会被禁用。如果设置为 0,表示密码过期后立即禁用账户。
    • 例如:30 表示密码到期后的 30 天内账户可以保持不活动状态。
  8. 到期时间 (到期时间):
    • 账户的到期日期,表示账户从该日期开始不再可用。值为 0 时表示没有设置到期日期。
    • 例如:0 表示账户没有设置到期日期。
  9. 保留字段 (保留字段):
    • 目前此字段保留,通常为空。

密码加密类型

/etc/shadow 文件中,密码的加密类型通常通过密码字段的前缀来标识。每种加密算法在加密后的密码字符串中都有特定的标识符。这些标识符帮助系统和程序知道如何解密和验证密码。

1
2
3
4
root:$6$KbVQqYnd$B7JCO4sZj3a9fEv1gN66yIZw1sVgStVoCgPj.Som1JjszRrlQZ8xoAneLwUNlU4p4uK3Frlwz9h.Mt7NkWo8y.:18000:0:90:7:30:0:
johndoe:$6$7Zqg7YhZ$YsYjX3VLo7WLBmBQpijA3e7/FaKZ19n3nFs.L4Bd32XYkXqkc7fG5z.k2d5bD6fnmPInFoDdrYIoFD2e7ab4L.:18200:5:60:5:10:0:
alice:$1$4HnHuygI$uMNFPmygPqJtE5T6EYFiOT9zq0tM8UmQIkcGz1yb7s.:18500:7:120:10:14:0:
guest:!!:19000:0:0:0:0:0:
  • DES$1$
  • MD5$1$(但与 DES 区别通常是密码长度)
  • SHA-256$5$
  • SHA-512$6$
  • Blowfishbcrypt$2a$$2y$
  • 未加密的密码:如果密码字段为空(例如:root::),则表示用户密码为空,或未加密(即无保护)。

密码过期

登陆失败后查看system log引入眼帘的第一个错误是密码过期:

1
auth.info sshd[162]: User root password has expired (root forced)

重设密码后仍然显示过期,重设密码指令:

1
2
3
4
5
6
# passwd
Changing password for root
New password:
Bad password: too short
Retype password:
passwd: password for root changed by root

查看/etc/shadow发现第三个字段为0,手动改成1800,不会再出现密码过期的错误。

改密码仍然出错的原因是系统时间不对导致,发现系统时间是19700101

1
2
# date
Thu Jan 1 00:27:11 UTC 1970

使用date -s修改系统事件后,再重设密码也可解决此问题。

密码错误

解决第一个问题后仍然登录失败,错误是密码错误:

1
Failed password for root from 192.168.1.111 port 62048 ssh2

可以确定的是我密码一定是一样的,因为设置的是1111,首先清除密码在做尝试

1
2
3
4
5
# passwd -d root
passwd: password for root changed by root

# cat /etc/shadow
root::1800:0:99999:7:::

这样是可以不用密码登录成功的,我立马想到了之前项目遇到的ssh不支持sha512的问题,觉得可能是加密方式的不支持导致的,于是更换des:

1
2
3
4
5
6
7
8
# passwd -a des
Changing password for root
New password:
Bad password: too short
Retype password:
passwd: password for root changed by root
# cat /etc/shadow
root:J85gt9z3v.MB6:18000:0:99999:7:::

仍然是不用密码就登陆进来了,可能是passwd的des没有$1$导致的,但是手动加上后仍然会密码错误。

源码分析

这种情况下只有在openssh的源码中跟踪,来查看密码为什么会被判断为错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int
sys_auth_passwd(struct ssh *ssh, const char *password)
{
    Authctxt *authctxt = ssh->authctxt;
    struct passwd *pw = authctxt->pw;
    char *encrypted_password, *salt = NULL;

    /* Just use the supplied fake password if authctxt is invalid */
    char *pw_password = authctxt->valid ? shadow_pw(pw) : pw->pw_passwd;

    if (pw_password == NULL)
        return 0;

    /* Check for users with no password. */
    if (strcmp(pw_password, "") == 0 && strcmp(password, "") == 0)
        return (1);
     
    /*
     * Encrypt the candidate password using the proper salt, or pass a
     * NULL and let xcrypt pick one.
     */
    if (authctxt->valid && pw_password[0] && pw_password[1])
        salt = pw_password;
    encrypted_password = xcrypt(password, salt);

    /*
     * Authentication is accepted if the encrypted passwords
     * are identical.
     */
    return encrypted_password != NULL &&
        strcmp(encrypted_password, pw_password) == 0;
}

以上是密码判断的代码,通过添加debug信息,我发现encrypted_password为空,导致永远返回密码错误。问题应该就出现在xcrypt()中,离真相越来越近了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
char *
xcrypt(const char *password, const char *salt)
{
    char *crypted;

    /*
     * If we don't have a salt we are encrypting a fake password for
     * for timing purposes.  Pick an appropriate salt.
     */
    if (salt == NULL)
        salt = pick_salt();

#if defined(__hpux) && !defined(HAVE_SECUREWARE)
    if (iscomsec())
        crypted = bigcrypt(password, salt);
    else
        crypted = crypt(password, salt);
# elif defined(HAVE_SECUREWARE)
    crypted = bigcrypt(password, salt);
# else
    crypted = crypt(password, salt);
#endif

    return crypted;
}

继续添加debug信息,最终调用的是crypt()函数,在服务器上写test调用crypt发现可以正常加密,所以应该是编译的时候链接出了问题。

编译分析

我计划使用交叉编译工具编译和服务器相同的test,在rock-chips的板子上运行,但是我竟然没在lib目录里找到crypt的库,只有crypto的库,于是我立马到板子上查看也没有crypt的库,所以在openssh的编译的链接中,没有链接到正确的库。

最终我在一个叫runtime_lib的目录中找到了crypt的库,使用这个库编译test在板子上验证通过,可以正常加密。

解决问题

在openssh的编译中链接上crypt库,最终解决问题。


SSH登陆失败
https://carl-5535.github.io/2024/11/05/工作总结/SSH登陆失败/
作者
Carl Chen
发布于
2024年11月5日
许可协议