grub2中的密码设置及grub中的回调机制
1 grub.cfg 和 /etc/grub.d/xxx
使用UEFI作为固件引导的操作系统,在系统下往往存在以下文件:
/etc/grub.d/01_users,当然了,我们先来说明一下:
在/etc/grub.d 目录下存在众多的脚本文件,01_users只是其中之一。/etc/grub.d 目录下众多的脚本文件都有他们特定的功能。
那么/etc/grub.d/01_users这个脚本文件有什么作用呢?
/etc/grub.d/01_users 这个脚本通常与设置grub菜单的安全选项有关,比如启用密码保护以防止未经授权的操作,编辑启动选项或
进入单用户模式等,01_users这个脚本可以决定哪些用户能够访问grub菜单,并且包含了为这些用户设置的密码哈希值!
/etc/grub.d/xxx 与grub.cfg文件的关系:
比如 当我们执行 grub2-mkconfig -o /boot/efi/EFI/vendor/grub.cfg 时,/etc/grub.d 目录下的众多的脚本文件
和/etc/default/grub 这些文件就会被grub2-mkconfig 用作生成一个新的grub.cfg的配置文件。
grub.cfg 这个文件大家都很熟悉了,其实grub.cfg 文件中全是grub可以执行的命令罢了!
2 /etc/grub.d/xxx 中指定的环境变量
对于用户权限来说,我们来看以下的设置:
### BEGIN /etc/grub.d/01_users ###
if [ -f ${prefix}/user.cfg ]; then
source ${prefix}/user.cfg
if [ -n "${GRUB2_PASSWORD}" ]; then
set superusers="root"
export superusers
password_pbkdf2 root ${GRUB2_PASSWORD}
fi
fi
### END /etc/grub.d/01_users ###
上述脚本有两个核心的功能:
只有以 root 用户身份登录的人才能访问 GRUB 的某些高级功能或进行更改。
定义超级用户的密码:password_pbkdf2 指示 GRUB 使用 PBKDF2 算法对密码进行了哈希处理。
3 修改grub2中的密码
上面我们已经说了,password_pbkdf2 这个命令指示了grub2中使用了pbkdf2 算法对密码进行了哈希处理。
那么加入我们想要修改grub2中的密码,如何修改:
a. 先生成要设定的密码哈希值
grub中提供了生成pbkdf2密码哈希的工具:grub2-mkpasswd-pbkdf2
在自己的系统下执行grub2-mkpasswd-pbkdf2 命令即可生成一个对应的密码哈希值, pbkdf2生成的密码是124位.124位的,
后面我们用biglongstring来代替。
b. 将生成的哈希值存放到脚本中:
util/grub.d/00_header.in
cat <<EOF
set superusers=root
password_pbkdf2 root grub.pbkdf2.sha512.10000.biglongstring
EOF
c. 重新编译grub2并安装即可得到加密后的密码哈希值,并存放在grub.cfg中
如此以来,我们便有了root用户密码
4 grub2 中如何校验密码?
当我们设置完grub2的密码之后,在grub 的menu entry界面,用户按下c或者e进入menu entry时,
grub便会提示用户输入用户名及密码,进行校验。
Enter username:
Enter password:
grub是如何进行密码的校验的:
a. 首先在include/grub/command.h中声明了一个注册命令的函数:
static inline grub_command_t
grub_register_command (const char *name,
grub_command_func_t func,
const char *summary,
const char *description)
{
return grub_register_command_prio (name, func, summary, description, 0);
}
这个注册命令的函数会在几乎所有的grub的模块中都会被调用的到
b. grub-core/normal/auth.c 中对应的注册了authenticate这个命令, authenticate 在grub2中是一种用户校验的机制,
专门用来校验用户输入的用户名和密码。
void
grub_normal_auth_init (void)
{
cmd = grub_register_command ("authenticate",//命令名称
grub_cmd_authenticate,//命令对应的执行函数
N_("[USERLIST]"), //命令摘要
N_("Check whether user is in USERLIST.")); //命令描述
}
void
grub_normal_auth_fini (void)
{
grub_unregister_command (cmd);
}
c. grub_cmd_authenticate函数详解:
static grub_err_t
grub_cmd_authenticate (struct grub_command *cmd __attribute__ ((unused)),
int argc, char **args)
{
return grub_auth_check_authentication ((argc >= 1) ? args[0] : "");
}
d. 在执行grub_auth_check_authenticate函数之前,我们先来了解一下另外一个函数
在进入grub的menu entry之前,首先grub会通过读取grub.cfg中的所有已注册的用户,为每一个用户都注册一个回调函数
grub_err_t
grub_auth_register_authentication (const char *user,
grub_auth_callback_t callback,
void *arg)
{
struct grub_auth_user *cur;
cur = grub_named_list_find (GRUB_AS_NAMED_LIST (users), user); //从用户列表中找到user用户
if (!cur)
cur = grub_zalloc (sizeof (*cur));
if (!cur)
return grub_errno;
cur->callback = callback;
cur->arg = arg;
if (! cur->name)
{
cur->name = grub_strdup (user);
if (!cur->name)
{
grub_free (cur);
return grub_errno;
}
//将初始化好的用户节点信息保存到对应的队列中
grub_list_push (GRUB_AS_LIST_P (&users), GRUB_AS_LIST (cur));
}
return GRUB_ERR_NONE;
}
e. 上面既然已经为每一个用户都注册好了一个回调函数,当我们进入menu entry的时候就会比较用户名和密码进行校验:
grub_err_t
grub_auth_check_authentication (const char *userlist)
{
char login[1024];
struct grub_auth_user *cur = NULL;
char entered[GRUB_AUTH_MAX_PASSLEN];
//user是定义的grub_auth_user的结构体指针
struct grub_auth_user *user;
//定义1024个字节的内存并清空
grub_memset (login, 0, sizeof (login));
//调用is_authenticated, 参数为userlist的地址, 目前的代码逻辑是如果没有设置superusers,
//则is_authenticated直接返回1,直接不在进行校验,也就是目前在grub中普通用户如果在没有设置superusers的情况下是不进行校验的
if (is_authenticated (userlist))
{
punishment_delay = 1;
return GRUB_ERR_NONE;
}
//提示用户输入用户名
grub_puts_ (N_("Enter username: "));
//获取用户输入的用户名,用户输入的用户名会保存到login中
if (!grub_username_get (login, sizeof (login) - 1))
goto access_denied;
//提示用户输入密码,
grub_puts_ (N_("Enter password: "));
//将用户输入的密码保存到entered数组中
if (!grub_password_get (entered, GRUB_AUTH_MAX_PASSLEN))
goto access_denied;
//#define FOR_LIST_ELEMENTS(var, list) for ((var) = (list); (var); (var) = (var)->next)
FOR_LIST_ELEMENTS (user, users)
{
if (grub_strcmp (login, user->name) == 0)
//将用户输入的密码与user->name进行匹配,如果匹配成功则将user赋给cur
cur = user;
}
if (!cur || ! cur->callback)
//如果cur为空,或者cur->callback为空则直接退出
goto access_denied;
//调用cur的回调函数, 传入用户输入的用户名和密码进行匹配
//回调函数在grub_auth_register_authentication函数中已经进行注册了,那么具体会调用哪个函数呢?
//会调用grub-core/commands/password_pbkdf2.c 文件中的grub_cmd_password 函数中调用grub_auth_register_authentication (args[0], check_password, pass);
//将此处调用的callback设置为 checkpassword函数的地址,
//所以此处调用callback函数进行用户名和密码的校验时, 实际上调用的是check_password函数!
//上面这一段也是本篇的重中之重!
cur->callback (login, entered, cur->arg);
......
//所以最终是cur调用callback函数完成用户名和密码的校验!
}
接下来,感兴趣的读者可以看一下grub中是如何获取用户名及密码的:(不感兴趣的可以直接阅读f)
//获取用户名
static int
grub_username_get (char buf[], unsigned buf_size)
{
unsigned cur_len = 0; //当前输入的字符数量
int key;
//存储用户按下的键值
while (1)
{
key = grub_getkey ();
//获取用户输入的按键
//如果输入的是回车或者换行则直接退出
if (key == '\n' || key == '\r')
break;
//如果按下ESC键则清空当前输入并退出
if (key == GRUB_TERM_ESC)
{
cur_len = 0;
break;
}
//如果是退格键则删除上一个字符
if (key == GRUB_TERM_BACKSPACE)
{
if (cur_len)
{
cur_len--;
grub_printf ("\b \b");
}
continue;
//跳过本次循环继续下一次循环
}
if (!grub_isprint (key))
//如果不是可打印的字符则直接忽略
continue;
if (cur_len + 2 < buf_size)
//确保还有空间存储新字符和字符串结束符
{
buf[cur_len++] = key;
//将字符输入存入缓冲区, 并增加缓冲区长度
grub_printf ("%c", key);
//回显输入的字符
}
}
grub_memset (buf + cur_len, 0, buf_size - cur_len);
//使用0填充剩余的缓冲区, 确保字符串正常终止
grub_xputs ("\n");
//打印换行符,光标移动到下一行
grub_refresh ();
return (key != GRUB_TERM_ESC);
}
获取密码:
//与用户名不同的是,密码不会回显到屏幕上
int
grub_password_get (char buf[], unsigned buf_size)
{
unsigned cur_len = 0;
int key;
while (1)
{
key = grub_getkey ();
if (key == '\n' || key == '\r')
break;
if (key == GRUB_TERM_ESC)
{
cur_len = 0;
break;
}
if (key == '\b')
{
if (cur_len)
cur_len--;
continue;
}
if (!grub_isprint (key))
continue;
if (cur_len + 2 < buf_size)
buf[cur_len++] = key;
}
grub_memset (buf + cur_len, 0, buf_size - cur_len);
grub_xputs ("\n");
grub_refresh ();
return (key != GRUB_TERM_ESC);
}
f 用户名和密码的校验
上面的分析出了, 最终的用户名和密码的校验的功能是在 grub-core/commands/password_pbkdf2.c 文件
中的grub_cmd_password 函数中调用grub_auth_register_authentication (args[0], check_password, pass);
那么我们来具体的分析一下, 注册的回调函数check_password函数。
注册的回调函数check_password函数,首先,我们在grub.cfg 中指定了set superusers=root,andy,Andy 相当于指定了三个超级用户。
设置password_pbkdf2 root grub.pbkdf2.sha512.10000.biglongstring
password_pbkdf2 andy grub.pbkdf2.sha512.10000.binglongstring
password_pbkdf2 Andy grub.pbkdf2.sha512.10000.binglongstring
相当于,进入引导界面的时候, 要执行password_pbkdf2这个命令, 这个命令后面跟的root是用户名,
grub.pbkdf2.sha512.10000.biglongstring是格式化的字符串。
当我们执行password_pbkdf2命令的时候, 要执行的函数就是grub_cmd_password这个函数, 而执行这个函数传递参数两个,
分别是:root、grub.pbkdf2.sha512.10000.biglongstring 这两个参数。
static grub_err_t
grub_cmd_password (grub_command_t cmd __attribute__ ((unused)),
int argc, char **args)
{
grub_err_t err;
const char *ptr, *ptr2;
grub_uint8_t *ptro;
struct pbkdf2_password *pass;
//命令行参数必须是2个, 就是上面说的, 一个是用户名, 一个是加密后的密码的散列值
if (argc != 2)
return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("two arguments expected"));
//检查第二个参数是否是以"grub.pbkdf2.sha512." 开头的, 必须是以它开头的才是合法的参数
if (grub_memcmp (args[1], "grub.pbkdf2.sha512.",
sizeof ("grub.pbkdf2.sha512.") - 1) != 0)
return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("invalid PBKDF2 password"));
//解析出来迭代次数, 和分隔符
ptr = args[1] + sizeof ("grub.pbkdf2.sha512.") - 1;
pass = grub_malloc (sizeof (*pass));
pass->c = grub_strtoul (ptr, &ptr, 0);
if (*ptr != '.')
ptr++;
//分离出盐值和期望的哈希值
ptr2 = grub_strchr (ptr, '.');
if (!ptr2 || ((ptr2 - ptr) & 1) || grub_strlen (ptr2 + 1) & 1)
{
grub_free (pass);
return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("invalid PBKDF2 password"));
}
pass->saltlen = (ptr2 - ptr) >> 1;
pass->buflen = grub_strlen (ptr2 + 1) >> 1;
//解码盐值
ptro = pass->salt = grub_malloc (pass->saltlen);
if (!ptro)
{
grub_free (pass);
return grub_errno;
}
while (ptr < ptr2)
{
int hex1, hex2;
hex1 = hex2val (*ptr);
ptr++;
hex2 = hex2val (*ptr);
ptr++;
if (hex1 < 0 || hex2 < 0)
{
grub_free (pass->salt);
grub_free (pass);
return grub_error (GRUB_ERR_BAD_ARGUMENT,
/* TRANSLATORS: it means that the string which
was supposed to be a password hash doesn't
have a correct format, not to password
mismatch. */
N_("invalid PBKDF2 password"));
}
*ptro = (hex1 << 4) | hex2;
ptro++;
}
//解码期望的哈希值
ptro = pass->expected = grub_malloc (pass->buflen);
if (!ptro)
{
grub_free (pass->salt);
grub_free (pass);
return grub_errno;
}
ptr = ptr2 + 1;
ptr2 += grub_strlen (ptr2);
while (ptr < ptr2)
{
int hex1, hex2;
hex1 = hex2val (*ptr);
ptr++;
hex2 = hex2val (*ptr);
ptr++;
if (hex1 < 0 || hex2 < 0)
{
grub_free (pass->expected);
grub_free (pass->salt);
grub_free (pass);
return grub_error (GRUB_ERR_BAD_ARGUMENT,
N_("invalid PBKDF2 password"));
}
*ptro = (hex1 << 4) | hex2;
ptro++;
}
//check_passeord 函数就是上面在auth.c中注册的回调函数, 传递给check_password函数的参数为:
//cur->callback (login, entered, cur->arg);
//login 是用户输入的用户名, entered是用户输入的密码
err = grub_auth_register_authentication (args[0], check_password, pass);
if (err)
{
grub_free (pass);
return err;
}
grub_dl_ref (my_mod);
return GRUB_ERR_NONE;
}
g 执行check_password之前,已经从grub.cfg将对应的
超级用户的密码的盐值, 盐值长度, 哈希值等信息已经解析出来了,并且已经保存到了pass中了。
static grub_err_t
check_password (const char *user, const char *entered, void *pin)
{
grub_uint8_t *buf;
struct pbkdf2_password *pass = pin;
gcry_err_code_t err;
grub_err_t ret;
buf = grub_malloc (pass->buflen);
if (!buf)
return grub_crypto_gcry_error (GPG_ERR_OUT_OF_MEMORY);
//实现了pbkdf2算法, 用于从密码中派生出一个固定长度密钥出来。
//需要传入的参数为指向散列算法描述符的指针, 输入的密码, 输入的密码的长度, 解析出来的盐值, 盐值的长度, 迭代次数,
//buf保存的是派生出来的密钥, 和期望派生的密码的长度。
err = grub_crypto_pbkdf2 (GRUB_MD_SHA512, (grub_uint8_t *) entered,
grub_strlen (entered),
pass->salt, pass->saltlen, pass->c,
buf, pass->buflen);
if (err)
ret = grub_crypto_gcry_error (err);
//比较派生出来的密钥与期望的密钥的是否相同
else if (grub_crypto_memcmp (buf, pass->expected, pass->buflen) != 0)
ret = GRUB_ACCESS_DENIED;
else
{
grub_auth_authenticate (user);
ret = GRUB_ERR_NONE;
}
grub_free (buf);
return ret;
}
通过比较派生出来的用户输入的密码明文字符串grub.cfg中解析出来的明码明文是否相同实现了对密码的校验!