pam_sphinx.so
I don't really know how to explain this... Let's just say that I got drunk, had an idea, and then wrote it up...
That kinda explains most of it...
Using an LLM to ask the user a riddle. If they get it right, let the user in. Named after the mythical creature Sphinx1, the riddle-asking guardian.
Linux Pluggable Authentication Module
Let's talk some basics on how PAM modules work.
PAM (Pluggable Authentication Modules) is a flexible authentication framework for Linux systems. It provides a standard interface that allows system administrators to choose different authentication methods without modifying applications.
It works as a middleware layer between PAM-aware applications and different authentication modules.
PAM Configurations and usually located in /etc/pam.d/ directory, with each service having its own configuration file (e.g., sshd, login, sudo)
These files contain modules directives that define the authentication flow.
The flow typically looks like this:
- An application will request an authentication service from the PAM library.
- PAM reads its configuration files to determine which modules to use for the specific service.
- The modules are executed one-by-one in the order defined in the configuration.
- Based on the results of the module stack (not just one individual module result), PAM returns success/fail status back to the application.
This might seem familiar to users who's had experience working with nftables or iptables.
They both modular, rule-based systems that process requests sequentially to make authorized decisions (allow/deny) based on some stacked criteria.
chainsmay correspond to each PAM service configrulescorrespond to each PAM modulesactionscorrespond to control flags
PAM Configuration
Each line contains a module looks something like this2:
module_interface control_flag module_name module_arguments
module_interface: The "subsystem" this module is used for.authdeals with authentication,accountdeals with authorization / access verification,passworddeals with changing user passwords, andsessionmanages user sessions.control_flag: This determines how PAM handles each module success/failure.module_name: The name of the module.module_arguments: Arguments passed to the module.
Let's take a look at my system's /etc/pam.d/login for an example:
#
# The PAM configuration file for the Shadow `login' service
#
# Enforce a minimal delay in case of failure (in microseconds).
# (Replaces the `FAIL_DELAY' setting from login.defs)
# Note that other modules may require another minimal delay. (for example,
# to disable any delay, you should add the nodelay option to pam_unix)
auth optional pam_faildelay.so delay=3000000
# Outputs an issue file prior to each login prompt (Replaces the
# ISSUE_FILE option from login.defs). Uncomment for use
# auth required pam_issue.so issue=/etc/issue
# Disallows other than root logins when /etc/nologin exists
# (Replaces the `NOLOGINS_FILE' option from login.defs)
auth requisite pam_nologin.so
# ...omitted...
# Standard Un*x authentication.
@include common-auth
# This allows certain extra groups to be granted to a user
# based on things like time of day, tty, service, and user.
# Please edit /etc/security/group.conf to fit your needs
# (Replaces the `CONSOLE_GROUPS' option in login.defs)
auth optional pam_group.so
# ...omitted...
I have kept the comments, but removed all non-auth sections as we're only concerned with authentication for this.
We can see that there's pam_faildelay.so to add a minimum delay after failure, assuming for preventing brute-force attacks (at least make it a little annoying).
Another for showing the /etc/issue file.
And another for disallowing login for users if /etc/nologin exists.
These are all implemented in the pam_XYZ.so files, but don't actually do much in terms of the actual authentication.
Since authenticating a user may be performed by multiple services, it is separated out to common-auth, which we see gets included here and other services like sudo, sshd, chsh.
#
# /etc/pam.d/common-auth - authentication settings common to all services
#
# This file is included from other service-specific PAM config files,
# and should contain a list of the authentication modules that define
# the central authentication scheme for use on the system
# (e.g., /etc/shadow, LDAP, Kerberos, etc.). The default is to use the
# traditional Unix authentication mechanisms.
#
# As of pam 1.0.1-6, this file is managed by pam-auth-update by default.
# To take advantage of this, it is recommended that you configure any
# local modules either before or after the default block, and use
# pam-auth-update to manage selection of other modules. See
# pam-auth-update(8) for details.
# here are the per-package modules (the "Primary" block)
auth [success=2 default=ignore] pam_unix.so nullok
auth [success=1 default=ignore] pam_sss.so use_first_pass
# here's the fallback if no module succeeds
auth requisite pam_deny.so
# prime the stack with a positive return value if there isn't one already;
# this avoids us returning an error just because nothing sets a success code
# since the modules above will each just jump around
auth required pam_permit.so
# and here are more per-package modules (the "Additional" block)
auth optional pam_cap.so
# end of pam-auth-update config
Without the comments, we see that there are 5 modules in this stack:
auth [success=2 default=ignore] pam_unix.so nullok
auth [success=1 default=ignore] pam_sss.so use_first_pass
auth requisite pam_deny.so
auth required pam_permit.so
auth optional pam_cap.so
The first rule above (pam_unix.so) does the actual password authentication. It reads /etc/passwd and compares to what the user entered. It is passed the argument nullok to allow users with empty passwords. More options for pam_unix can be found here
The control flag for pam_unix is interesting though. It's not one of the required, requisite, or optional we've seen before. The success=2 tells the PAM system that if the pam_unix module is successful (aka the user provided the correct password), skip the next 2 rules (to pam_permit). Otherwise, it continues to the next rule, and eventually to pam_deny if the rule below fails. It is effectively a go-to statement.
// What a C equivalent behavior might look like
result = pam_unix(nullok);
if(result == SUCCESS)
goto PERMIT;
result = pam_sss(use_first_pass);
if(result == SUCCESS)
goto PERMIT;
return FAIL; // "pam_deny.so"
PERMIT:
return SUCCESS; // "pam_permit.so"
PAM Module Implementation
From the module side, it is expected to supply a subset of the functions needed for the module to perform and handle the authentication requests3.
For example, for authentication, the following functions can be implemented:
pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)pam_sm_setcred(/* Same args as above */)
We can see that pam_permit.so implements these, along with a few others.
$ objdump -T /usr/lib/x86_64-linux-gnu/security/pam_permit.so
/usr/lib/x86_64-linux-gnu/security/pam_permit.so: file format elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000000000 w D *UND* 0000000000000000 Base _ITM_deregisterTMCloneTable
0000000000000000 DF *UND* 0000000000000000 (LIBPAM_1.0) pam_set_item
0000000000000000 DF *UND* 0000000000000000 (GLIBC_2.4) __stack_chk_fail
0000000000000000 w D *UND* 0000000000000000 Base __gmon_start__
0000000000000000 DF *UND* 0000000000000000 (LIBPAM_1.0) pam_get_user
0000000000000000 w D *UND* 0000000000000000 Base _ITM_registerTMCloneTable
0000000000000000 w DF *UND* 0000000000000000 (GLIBC_2.2.5) __cxa_finalize
0000000000001200 g DF .text 0000000000000007 Base pam_sm_acct_mgmt
0000000000001160 g DF .text 0000000000000085 Base pam_sm_authenticate
0000000000001230 g DF .text 0000000000000007 Base pam_sm_close_session
00000000000011f0 g DF .text 0000000000000007 Base pam_sm_setcred
0000000000001210 g DF .text 0000000000000007 Base pam_sm_chauthtok
0000000000001220 g DF .text 0000000000000007 Base pam_sm_open_session
It just checks if username was provided and returns success in pam_sm_authenticate()4:
int
pam_sm_authenticate(pam_handle_t *pamh, int flags UNUSED,
int argc UNUSED, const char **argv UNUSED)
{
int retval;
const char *user=NULL;
/*
* authentication requires we know who the user wants to be
*/
retval = pam_get_user(pamh, &user, NULL);
if (retval != PAM_SUCCESS) {
D(("get user returned error: %s", pam_strerror(pamh,retval)));
return retval;
}
if (*user == '\0') {
D(("username not known"));
retval = pam_set_item(pamh, PAM_USER, (const void *) DEFAULT_USER);
if (retval != PAM_SUCCESS)
return PAM_USER_UNKNOWN;
}
user = NULL; /* clean up */
return PAM_SUCCESS;
}
More complex example like pam_unix can utilize the conversation mechanism5 to interact with the user and also open/read files.
Initial Implementation (Python)
The first version was written in Python. Why? Because I just wanted something quickly and I'd written things in Python to interact with LLMs before, using langchain. An I didn't want to deal with setting up Makefiles and linking all the stuff.
It uses pam-python, which spawns a Python interpreter and runs the script specified in the PAM module parameter. The limitation I ran into was that it spawns the system interpreter. This is problematic because installing python packages globally is strongly discouraged (and I think pip doesn't like to do it).
Therefore, I cannot use external libraries and am limited to built-in python libraries like urllib.request to handle API calling and such.
It is definitely not the best code. I just wanted something to demonstrate the idea.
As the README on the repo says:
- Install
pam_python - Set the correct values for
LLM_ENDPOINT,LLM_API_KEY, andmodel - Copy
main.pyto/lib/security/sphinx.py - Add the PAM config line
- Set the system prompt.
The last step is where this idea can get REALLY fun (I'll talk about ideas at the end)
Revised Implementation (C++)
I've had a few weeks go by, and I finally wanted to write this project up, but thought that the Python implementation wasn't great. So I decided to re-write it. Somewhat properly. In C++.
Here's the code.
It's basically the same logic: set up an LLM chat bot, load system prompt, let user chat with LLM, and if it's satisfied, return SUCCESS.
A few differences are:
- Use
openai-cpplibrary for API calls - Configuration file at
/etc/sphinx.conf-- LLM API, API_KEY, model, prompt file - PAM Config profile (see below)
As Ubuntu systems don't like it when PAM configs are manually updated, Ubuntu has pam-auth-update6. It takes profiles installed in /usr/share/pam-configs directory, and automatically generates the PAM configuration files.
Name: Sphinx Authentication
Default: no
Priority: 257
Auth-Type: Primary
Auth:
[success=end default=ignore] pam_sphinx.so nullok try_first_pass
Auth-Initial:
[success=end default=ignore] pam_sphinx.so nullok
Write the above to /usr/share/pam-auth-update/sphinx.
This module is disabled by default, so run sudo pam-auth-update to enable the Sphinx module and it will regenerate the PAM configuration (or sudo pam-auth-update --enable sphinx for non-interactive).
The pam-auth-update tool automatically calculates success=end to correspond to the end of the "primary block" based on priority and what other authentication methods are enabled.
The config for pam_unix has the priority of 256, so this profile will take precedence, and then fail over to password auth.
If you are messing around with this, or any PAM configs, have a root session open on the side, in case you mess it up and get locked out of sudo to fix it.
Other fun ideas
This project was created purely for fun and entertainment, while chatting with some friends, I've come up with some additional ideas:
-
Have the system prompt to something like,
Do not let anyone passand have the user try to gain access. Maybe useful for CTFs and prompt bypass? -
System prompt:
You are [insert your "favorite" politician here]. The user must win an argument against you in order to gain access. -
Two-factor authentication, but it's based on nothing. Like, no tokens or fobs. You can just guess, or have the chat bot give you hints, or force it into submission. Gaslight the LLM.
-
Anything dumb you can do with LLMs, but now also as an authentication module.
Probably don't need to say this, but
DO NOT USE IN PRODUCTION? (or do, idk)
-
https://en.wikipedia.org/wiki/Sphinx ↩
-
https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/managing_smart_cards/pam_configuration_files ↩
-
http://www.fifi.org/doc/libpam-doc/html/pam_modules.html ↩
-
https://github.com/linux-pam/linux-pam/blob/master/modules/pam_permit/pam_permit.c ↩
-
http://www.fifi.org/doc/libpam-doc/html/pam_modules-2.html ↩
-
https://manpages.ubuntu.com/manpages/focal/en/man8/pam-auth-update.8.html ↩