hlogin/hlogin.c

661 lines
16 KiB
C

/*
* hlogin(1)
*
* Copyleft (C) 2022 ~keith <keith@keithhacks.cyou>
*
* This file is part of hlogin.
*
* hlogin is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, version 3.
*
* hlogin is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* along with hlogin. If not, see <https://www.gnu.org/licenses/>.
*/
#define __USE_GNU
#define _GNU_SOURCE
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <locale.h>
#include "gettext.h"
#define VAR_(x) #x
#define VAR(x) VAR_(x)
#pragma message "LOCALEDIR=" VAR(LOCALEDIR)
#include <unistd.h>
#include <sys/types.h>
#include <security/_pam_types.h>
#include <security/pam_appl.h>
#include <security/pam_misc.h>
#include <syslog.h>
#include <pwd.h>
#include <grp.h>
#include <utmpx.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include "ui.h"
int pam_conv_handler(int msgc, const struct pam_message *msgv[], struct pam_response **resp, void *appdata_ptr)
{
if (msgc <= 0)
return PAM_CONV_ERR;
struct pam_response *result = calloc(msgc, sizeof(struct pam_response));
if (!result)
return PAM_CONV_ERR;
char *info_concat = NULL;
size_t info_len;
for (int i = 0; i < msgc; i++) {
int r;
char *text;
switch (msgv[i]->msg_style) {
case PAM_PROMPT_ECHO_OFF:
case PAM_PROMPT_ECHO_ON:
if (info_concat) {
ui_setup_message(info_concat);
ui_message_run();
free(info_concat);
info_concat = NULL;
}
ui_setup_dialog((char *) msgv[i]->msg, msgv[i]->msg_style == PAM_PROMPT_ECHO_OFF);
r = ui_run();
text = ui_get_text();
if (r != 0) {
_pam_overwrite(text);
free(text);
text = NULL;
}
result[i].resp_retcode = 0;
result[i].resp = text;
break;
case PAM_ERROR_MSG:
case PAM_TEXT_INFO:
if (!info_concat) {
info_len = strlen((char *) msgv[i]->msg);
info_concat = realloc(info_concat, sizeof(char) * (info_len + 1));
strcpy(info_concat, (char *) msgv[i]->msg);
info_concat[info_len] = '\0';
} else {
info_concat[info_len] = '\n';
size_t pos = info_len + 1;
info_len = pos + strlen((char *) msgv[i]->msg);
info_concat = realloc(info_concat, sizeof(char) * (info_len + 1));
strcpy(info_concat + pos, (char *) msgv[i]->msg);
info_concat[info_len] = '\0';
}
result[i].resp_retcode = 0;
result[i].resp = NULL;
break;
default:
if (info_concat) {
ui_setup_message(info_concat);
ui_message_run();
free(info_concat);
info_concat = NULL;
}
goto FAILURE;
}
}
if (info_concat) {
ui_setup_message(info_concat);
ui_message_run();
free(info_concat);
info_concat = NULL;
}
*resp = result;
return PAM_SUCCESS;
FAILURE:
if (result) {
for (int i = 0; i < msgc; i++) {
if (result[i].resp) {
_pam_overwrite(result[i].resp);
free(result[i].resp);
}
}
free(result);
result = NULL;
}
return PAM_CONV_ERR;
}
/** number of seconds to hang after a login failure or error */
unsigned int fail_delay = 3;
/** maximum number of login attempts before a failure */
unsigned int max_tries = 5;
char *origin = "test_origin";
char *tty_path = NULL;
char *tty_name = NULL;
char *tty_id = NULL;
char *remote_host = NULL;
pid_t login_pid;
void log_utmp(char *login_name)
{
struct utmpx ut;
memset(&ut, 0, sizeof(ut));
struct utmpx *ent;
setutxent();
while ((ent = getutxent()))
if (ent->ut_pid == login_pid && ent->ut_type >= INIT_PROCESS && ent->ut_type <= DEAD_PROCESS)
break;
// try by tty_name
if (ent == NULL && tty_name) {
setutxent();
ut.ut_type = LOGIN_PROCESS;
strncpy(ut.ut_line, tty_name, sizeof(ut.ut_line));
ent = getutxline(&ut);
}
// try by tty_id
if (ent == NULL && tty_id) {
setutxent();
ut.ut_type = DEAD_PROCESS;
strncpy(ut.ut_id, tty_id, sizeof(ut.ut_id));
ent = getutxid(&ut);
}
// copy or create
if (ent)
memcpy(&ut, ent, sizeof(ut));
else
memset(&ut, 0, sizeof(ut));
// update utmp entry
strncpy(ut.ut_user, login_name, sizeof(ut.ut_user));
ut.ut_type = USER_PROCESS;
ut.ut_pid = login_pid;
if (tty_id && ut.ut_id[0] == 0)
strncpy(ut.ut_id, tty_id, sizeof(ut.ut_id));
if (tty_name)
strncpy(ut.ut_line, tty_name, sizeof(ut.ut_line));
if (remote_host)
strncpy(ut.ut_host, remote_host, sizeof(ut.ut_host));
// TODO copy remote_addr
// set time
struct timeval tv;
gettimeofday(&tv, NULL);
ut.ut_tv.tv_sec = tv.tv_sec;
ut.ut_tv.tv_usec = tv.tv_usec;
pututxline(&ut);
endutxent();
updwtmpx(_PATH_WTMP, &ut);
}
const char *CANCEL_SENTINEL = "\0CANCEL\0";
/** try_login - prompt for login name & password and attempt to log in
* returns login_name on success or NULL on failure
*/
char *try_login(pam_handle_t *pam, unsigned int try_count)
{
int result;
// prompt for username
ui_setup_dialog(_("Login name:"), false);
result = ui_run();
char *login_name = ui_get_text();
if (result != 0)
return CANCEL_SENTINEL;
result = pam_set_item(pam, PAM_USER, login_name);
if (result != PAM_SUCCESS) {
syslog(LOG_NOTICE, _("Couldn't set PAM_USER: %s"), pam_strerror(pam, result));
ui_setup_message(_("Failed to begin authentication."));
ui_message_run();
return NULL;
}
result = pam_authenticate(pam, 0);
if (result != PAM_SUCCESS) {
// log to syslog
if (result == PAM_MAXTRIES || try_count >= max_tries)
syslog(LOG_NOTICE, _("TOO MANY LOGIN TRIES (%u) FROM '%s' FOR '%s', %s"),
try_count, origin, login_name, pam_strerror(pam, result));
else
syslog(LOG_NOTICE, _("FAILED LOGIN (%u) FROM '%s' FOR '%s', %s"),
try_count, origin, login_name, pam_strerror(pam, result));
// display error message
ui_setup_message(_("Authentication failed."));
ui_message_run();
return NULL;
}
return login_name;
}
int get_pam_login_name(pam_handle_t *pam, char **login_name)
{
const void *item = (const void *) *login_name;
int result = pam_get_item(pam, PAM_USER, &item);
*login_name = (char *) item;
return result;
}
void init_env(bool no_destroy, pam_handle_t *pam, struct passwd *pwd)
{
char *buf = getenv("TERM");
if (buf)
buf = strdup(buf);
if (!no_destroy)
environ = calloc(1, sizeof(char *));
setenv("HOME", pwd->pw_dir, 0);
setenv("USER", pwd->pw_name, 1);
setenv("LOGNAME", pwd->pw_name, 1);
setenv("SHELL", pwd->pw_shell, 1);
setenv("TERM", buf ? buf : "dumb", 1);
free(buf);
// TODO use ENV_PATH and ENV_SUPATH from logindefs
setenv("PATH", _PATH_DEFPATH, 0);
// apparently mailx is a bitch without this
asprintf(&buf, "%s/%s", _PATH_MAILDIR, pwd->pw_name);
setenv("MAIL", buf, 0);
free(buf);
char **pam_env = pam_getenvlist(pam);
for (int i = 0; pam_env && pam_env[i]; i++)
putenv(pam_env[i]);
}
static pid_t child_pid;
static volatile sig_atomic_t got_sig = 0;
static void handle_sig(int signal)
{
if (child_pid)
kill(-child_pid, signal);
else
got_sig = 1;
if (signal == SIGTERM)
kill(-child_pid, SIGHUP);
}
int main(int argc, char* argv[])
{
int result;
setlocale(LC_ALL, "");
bindtextdomain(PACKAGE, LOCALEDIR);
textdomain(PACKAGE);
openlog("hlogin", LOG_ODELAY, LOG_AUTHPRIV);
login_pid = getpid();
if (isatty(STDIN_FILENO))
tty_path = ttyname(STDIN_FILENO);
else if (isatty(STDOUT_FILENO))
tty_path = ttyname(STDOUT_FILENO);
if (tty_path) {
if (strncmp(tty_path, "/dev/", 5) == 0)
tty_name = tty_path + 5;
else
tty_name = tty_path;
for (char *x = tty_name; x && *x; x++) {
if ((*x) >= '0' && (*x) <= '9') {
tty_id = x;
break;
}
}
}
// TODO remote login stuff
if (remote_host)
origin = remote_host;
else if (tty_name)
origin = tty_name;
else
syslog(LOG_NOTICE, _("Couldn't determine origin: neither remote host nor TTY"));
pam_handle_t *pam = NULL;
struct pam_conv conv = { .conv = pam_conv_handler, .appdata_ptr = NULL };
result = pam_start(remote_host ? "remote" : "login", NULL, &conv, &pam);
if (result != PAM_SUCCESS) {
syslog(LOG_ERR, _("Couldn't initialize PAM: %s"), pam_strerror(pam, result));
fprintf(stderr, _("pam_start error: %s. Abort.\n"), pam_strerror(pam, result));
sleep(fail_delay);
return EXIT_FAILURE;
}
result = pam_set_item(pam, PAM_RHOST, remote_host);
if (result != PAM_SUCCESS) {
syslog(LOG_ERR, _("Couldn't set PAM_RHOST: %s"), pam_strerror(pam, result));
fprintf(stderr, _("pam_set_item error: %s. Abort.\n"), pam_strerror(pam, result));
sleep(fail_delay);
return EXIT_FAILURE;
}
if (tty_path) {
result = pam_set_item(pam, PAM_TTY, tty_path);
if (result != PAM_SUCCESS) {
syslog(LOG_ERR, _("Couldn't set PAM_TTY: %s"), pam_strerror(pam, result));
fprintf(stderr, _("pam_set_item error: %s. Abort.\n"), pam_strerror(pam, result));
sleep(fail_delay);
return EXIT_FAILURE;
}
}
ui_init();
char *login_name;
for (unsigned int try = 1; try <= max_tries; try++) {
login_name = try_login(pam, try);
if (login_name)
break;
}
if (login_name == CANCEL_SENTINEL) {
syslog(LOG_NOTICE, _("Login from '%s' aborted"), origin);
ui_end();
sleep(fail_delay);
return EXIT_SUCCESS;
}
if (!login_name) {
char *msg;
// TRANSLATORS: default value is 5
asprintf(&msg, _("Exceeded maximum login attempts (%u).\n"), max_tries);
ui_setup_message(msg);
ui_message_run();
sleep(fail_delay);
ui_end();
return EXIT_FAILURE;
}
// Is user account valid?
result = pam_acct_mgmt(pam, 0);
if (result == PAM_NEW_AUTHTOK_REQD)
result = pam_chauthtok(pam, PAM_CHANGE_EXPIRED_AUTHTOK);
if (result != PAM_SUCCESS) {
syslog(LOG_ERR, _("User validation failed for '%s': %s"), login_name, pam_strerror(pam, result));
switch (result) {
/*
case PAM_ACCT_EXPIRED:
asprintf(&msg, _("The user account '%s' has expired and cannot log in."), login_name);
break;
*/
case PAM_AUTHTOK_ERR:
case PAM_AUTHTOK_RECOVERY_ERR:
case PAM_AUTHTOK_LOCK_BUSY:
case PAM_AUTHTOK_DISABLE_AGING:
// asprintf(&msg, _("Failed to update login credentials for '%s'."), login_name);
ui_setup_message(_("Failed to update login credentials."));
break;
default:
// asprintf(&msg, _("Cannot log in as '%s'. Is the user account valid?"), login_name);
ui_setup_message(_("Cannot log in."));
}
ui_message_run();
sleep(fail_delay);
ui_end();
return EXIT_FAILURE;
}
// ensure we have the correct login_name
result = get_pam_login_name(pam, &login_name);
if (result != PAM_SUCCESS) {
syslog(LOG_ERR, _("Couldn't get PAM_USER: %s"), pam_strerror(pam, result));
ui_setup_message(_("Cannot log in. The user account data is missing or invalid."));
ui_message_run();
sleep(fail_delay);
ui_end();
return EXIT_FAILURE;
}
if ((!login_name) || login_name[0] == '\0') {
syslog(LOG_ERR, _("NULL user name. Abort."));
ui_setup_message(_("Cannot log in. The user account data is missing or invalid."));
ui_message_run();
sleep(fail_delay);
ui_end();
return EXIT_FAILURE;
}
struct passwd *pwd = getpwnam(login_name);
if (!pwd) {
syslog(LOG_ERR, _("No passwd entry for '%s'. Abort."), login_name);
ui_setup_message(_("Cannot log in. The user account data is missing or invalid."));
ui_message_run();
sleep(fail_delay);
ui_end();
return EXIT_FAILURE;
}
result = (pwd->pw_uid != 0)
? initgroups(login_name, pwd->pw_gid)
: setgroups(0, NULL);
if (result < 0) {
syslog(LOG_ERR, _("groups initialization failed: %m"));
ui_setup_message(_("Cannot log in. Unable to establish a session."));
ui_message_run();
sleep(fail_delay);
ui_end();
return EXIT_FAILURE;
}
// begin PAM session
result = pam_setcred(pam, PAM_ESTABLISH_CRED);
if (result != PAM_SUCCESS) {
syslog(LOG_ERR, _("Couldn't init PAM session: %s"), pam_strerror(pam, result));
ui_setup_message(_("Cannot log in. Unable to establish a session."));
ui_message_run();
sleep(fail_delay);
ui_end();
return EXIT_FAILURE;
}
result = pam_open_session(pam, 0);
if (result != PAM_SUCCESS) {
pam_setcred(pam, PAM_DELETE_CRED);
syslog(LOG_ERR, _("Couldn't init PAM session: %s"), pam_strerror(pam, result));
ui_setup_message(_("Cannot log in. Unable to establish a session."));
ui_message_run();
sleep(fail_delay);
ui_end();
return EXIT_FAILURE;
}
result = pam_setcred(pam, PAM_REINITIALIZE_CRED);
if (result != PAM_SUCCESS) {
pam_close_session(pam, 0);
syslog(LOG_ERR, _("Couldn't init PAM session: %s"), pam_strerror(pam, result));
ui_setup_message(_("Cannot log in. Unable to establish a session."));
ui_message_run();
sleep(fail_delay);
ui_end();
return EXIT_FAILURE;
}
endpwent();
ui_end();
log_utmp(login_name);
// TODO log audit and lastlog
syslog(LOG_INFO, _("LOGIN FROM '%s' BY '%s'"), origin, login_name);
// chown TTY
if (tty_path) {
// TODO set group as well
if (chown(tty_path, pwd->pw_uid, -1) != 0)
syslog(LOG_ERR, _("failed to chown TTY '%s': %m"), tty_path);
}
if (setgid(pwd->pw_gid) < 0 && pwd->pw_gid) {
syslog(LOG_ALERT, _("setgid() failed: %m"));
fprintf(stderr, _("error applying user GID: %m. Abort.\n"));
sleep(fail_delay);
return EXIT_FAILURE;
}
if ((!pwd->pw_shell) || pwd->pw_shell[0] == '\0')
pwd->pw_shell = _PATH_BSHELL;
init_env(false, pam, pwd);
// Get ready to fork!
// signal handlers - i think this is in case something comes in while we're forking?
struct sigaction sa, prev_hup, prev_term;
memset(&sa, 0, sizeof(sa));
signal(SIGALRM, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
signal(SIGTSTP, SIG_IGN);
sa.sa_handler = SIG_IGN;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGHUP, &sa, &prev_hup);
if (tty_path)
ioctl(0, TIOCNOTTY, NULL);
sa.sa_handler = handle_sig;
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGTERM, &sa, &prev_term);
closelog();
child_pid = fork();
if (child_pid < 0) {
fprintf(stderr, _("error forking: %m. Abort.\n"));
openlog("hlogin", LOG_ODELAY, LOG_AUTHPRIV);
syslog(LOG_ALERT, _("fork() failed: %m"));
pam_setcred(pam, PAM_DELETE_CRED);
pam_end(pam, pam_close_session(pam, 0));
sleep(fail_delay);
return EXIT_FAILURE;
}
if (child_pid) { // we're in the parent
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
sa.sa_handler = SIG_IGN;
sigaction(SIGQUIT, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
while (wait(NULL) == -1 && errno == EINTR);
openlog("hlogin", LOG_ODELAY, LOG_AUTHPRIV);
pam_setcred(pam, PAM_DELETE_CRED);
pam_end(pam, pam_close_session(pam, 0));
return EXIT_SUCCESS;
}
// child
// reset signal handlers
sigaction(SIGHUP, &prev_hup, NULL);
sigaction(SIGTERM, &prev_term, NULL);
if (got_sig)
return EXIT_FAILURE;
setsid();
// reopen tty
if (tty_path) {
int fd = open(tty_path, O_RDWR | O_NONBLOCK);
if (fd == -1) {
syslog(LOG_ERR, _("can't reopen tty: %m"));
return EXIT_FAILURE;
}
if (!isatty(fd)) {
close(fd);
syslog(LOG_ERR, _("%s is not a tty"), tty_path);
return EXIT_FAILURE;
}
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK);
// set tty as stdin, stdout, and stderr
for (int i = 0; i < fd; i++)
close(i);
for (int i = 0; i < 3; i++)
if (fd != i)
dup2(fd, i);
if (fd >= 3)
close(fd);
}
openlog("hlogin", LOG_ODELAY, LOG_AUTHPRIV);
if (ioctl(0, TIOCSCTTY, 1) != 0)
syslog(LOG_ERR, _("TIOCSCTTY failed: %m"));
signal(SIGINT, SIG_DFL);
// continue becoming the user
if (setuid(pwd->pw_uid) < 0 && pwd->pw_uid) {
syslog(LOG_ALERT, _("setuid() failed: %m"));
fprintf(stderr, _("error applying user UID: %m. Abort.\n"));
sleep(fail_delay);
return EXIT_FAILURE;
}
if (chdir(pwd->pw_dir) < 0) {
syslog(LOG_ALERT, _("failed to chdir to home (%s): %m"), pwd->pw_dir);
fprintf(stderr, _("error changing to home directory: %m. Abort.\n"));
sleep(fail_delay);
return EXIT_FAILURE;
}
pam_end(pam, PAM_SUCCESS | PAM_DATA_SILENT);
// execute login shell
char **shell_argv = calloc(2, sizeof(char *));
char *shell_base = strrchr(pwd->pw_shell, '/');
if (shell_base)
shell_base++;
else
shell_base = pwd->pw_shell;
shell_argv[0] = malloc(sizeof(char) * (strlen(shell_base) + 2));
shell_argv[0][0] = '-';
strcpy(shell_argv[0] + 1, shell_base);
execvp(pwd->pw_shell, shell_argv);
return EXIT_SUCCESS;
}