@@ 0,0 1,105 @@
+# Chad Thundercode just won't stop polluting the global namespace!
+from os import fstat, geteuid, getegid
+from stat import *
+from pwd import getpwuid
+from grp import getgrgid
+from syslog import *
+from hashlib import sha256
+import struct
+
+# In glibc, the ident string ("pam_biospw") pointer will be retained internally by the syslog routines, even after we return and get unloaded
+# So if a poorly-written PAM-aware program uses syslog() without openlog()ing after pam_authenticate(), a use-after-free will occur
+# At best; this will lead to confusion and misdirected emails, since our ident string could appear instead of the program's in the logs
+# https://web.archive.org/web/20220223143922/https://www.gnu.org/software/libc/manual/html_node/openlog.html
+# https://elixir.bootlin.com/glibc/glibc-2.35/source/misc/syslog.c#L316
+# musl, on the other hand, clones the string internally, so at least a UAF won't happen
+# https://git.musl-libc.org/cgit/musl/tree/src/misc/syslog.c?h=v1.2.3#n65
+# To avoid all that, we use a decorator (overkill, but looks nice) to ensure that we always closelog() after we return from the auth function
+def cleanup(pam_sm_func):
+ def pam_sm_func_wrap(pamh, flags, argv):
+ status = pam_sm_func(pamh, flags, argv)
+ closelog()
+ return status
+
+ return pam_sm_func_wrap
+
+
+@cleanup
+def pam_sm_authenticate(pamh, flags, argv):
+ # It's worth noting that pam_unix does logging a bit differently
+ # For example, it uses LOG_AUTHPRIV and LOG_NOTICE (for auth fails)
+ # One might argue that we're not following the established logging conventions, and that's ok :)
+ openlog("pam_biospw", LOG_CONS | LOG_PID, LOG_AUTH)
+
+ # Reading an efivar is generally slow, so you might want to cache it somewhere (e.g. using an init script) and pass the cached file as an arg
+ # Also, Linux does have an efivars read rate-limit for non-root users (100 per second, which is pretty generous)
+ # https://lore.kernel.org/lkml/CAKv+Gu86gohjooxurSr1KTfbUKPP-wf+XHdN88OrF=Pta9Ug3A@mail.gmail.com/t/
+ # https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=bef3efbeb897b56867e271cdbc5f8adaacaeb9cd
+ var_path = "/sys/firmware/efi/efivars/LenovoRuntimePassword-0997fb62-c849-4d91-9266-05333bc55ce7"
+ if len(argv) >= 2:
+ var_path = argv[1]
+
+ try:
+ var_file = open(var_path, "rb")
+ except IOError as e:
+ syslog(LOG_ERR, "Could not open {}: {}".format(var_path, e.strerror))
+ return pamh.PAM_SYSTEM_ERR
+
+ var_file_stat = fstat(var_file.fileno())
+
+ if var_file_stat.st_mode & S_IWOTH:
+ syslog(LOG_ERR, "{} is world-writable".format(var_path))
+ return pamh.PAM_SYSTEM_ERR
+
+ # This is the default on most Linux distros
+ if var_file_stat.st_mode & S_IROTH:
+ syslog(LOG_WARNING, "{} is world-readable".format(var_path))
+
+ # Ideally, the efivar would be only readable by us to prevent unwanted snooping
+ our_euid = geteuid()
+ our_egid = getegid()
+ if var_file_stat.st_uid != our_euid or var_file_stat.st_gid != our_egid:
+ syslog(LOG_WARNING, "{} isn't owned by {}:{}".format(var_path, getpwuid(our_euid).pw_name, getgrgid(our_egid).gr_name))
+
+ EXPECTED_VAR_SIZE = 0x72
+ var_size = var_file_stat.st_size
+ if var_size != EXPECTED_VAR_SIZE:
+ syslog(LOG_ERR, "Incorrect variable size: Expected {} bytes instead of {}".format(EXPECTED_VAR_SIZE, var_size))
+ return pamh.PAM_SYSTEM_ERR
+
+ # Python 2's bytes type is just an alias for str, so we use a bytearray instead
+ var_bytes = bytearray(var_file.read())
+ var_file.close()
+
+ # The first 4 bytes are ignored as they're the attributes of the variable in Linux
+ # https://www.kernel.org/doc/html/v5.17/filesystems/efivarfs.html
+ calculated_checksum = sum(var_bytes[0x4:0x6E])
+
+ # Python 2 doesn't have int.from_bytes(), sigh...
+ # < = little endian, I = unsigned 32-bit int
+ stored_checksum = struct.unpack("<I", var_bytes[0x6E:])[0]
+ if calculated_checksum != stored_checksum:
+ syslog(LOG_ERR, "Checksum mismatch: Expected {} instead of {}".format(calculated_checksum, stored_checksum))
+ return pamh.PAM_SYSTEM_ERR
+
+ # We could simply check 0x2A directly, but there's no harm in going the extra km
+ # H = unsigned 16-bit short (integer)
+ supervisor_pw_flag = struct.unpack("<H", var_bytes[0x2A:0x2C])[0]
+ stored_pw_hash = var_bytes[0x2C:0x3C]
+ if supervisor_pw_flag != 1 or stored_pw_hash == bytearray(0x10):
+ syslog(LOG_ERR, "No password set")
+ return pamh.PAM_SYSTEM_ERR
+
+ pam_message = pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "Supervisor BIOS Password: ")
+ input_pw = pamh.conversation(pam_message).resp.encode("UTF-16LE")
+ input_pw_hash = sha256(input_pw).digest()[:0x10]
+
+ if input_pw_hash != stored_pw_hash:
+ syslog(LOG_ERR, "Incorrect password")
+ return pamh.PAM_AUTH_ERR
+
+ return pamh.PAM_SUCCESS
+
+
+def pam_sm_setcred(pamh, flags, args):
+ return pamh.PAM_SUCCESS