2

I've built an application that emulates a HID device via /dev/uhid on linux. My application is broken into two programs. First, a very simple setuid root binary that opens /dev/uhid and emulates just the one device, passing messages back and forth to the program that invoked it. Second, an application that actually contains all of the device logic, and uses the other binary just to encapsulate uhid_event messages and talk to the kernel.

Anyone with console access can plug in a hardware USB device anyway, but for security, I would like the setuid program to refuse to run on behalf of non-console users.

My question: What's the simplest, most robust way for a setuid root application to check if it was invoked by a console user and bail if not, or to restrict execution of the program to console users in the first place?

Z0OM
  • 3,149
user3188445
  • 5,257

3 Answers3

1

By console do you mean specifically a hardware console, the system console, or any VT? If you mean latter a real simple solution is to test if STD*_FILENO if are associated with a tty with a isatty() call to each and see if any stream is associated with a VT. If one is then check if the PID is equal to the Process Group ID. If one of the standard stream is associated with a tty and the current process is the process group leader then it is likely that the program was run by a user on a VT of some kind.

EDIT 1: With further clarification that the original question was about local / remote logins and not virtual terminal vs everything else the above answer is moot.

To my knowledge the user accounting database UTMP/UTMPX API is the only one that mentions remote logins so that might be the best solution. Just search the user accounting database ut_host field to see if a valid ip address is associated with a users login.

  • What I care about is whether the user is likely to have access to physical USB ports. The application might get launched by systemd. Anyway, the setuid program standard file descriptors will be hooked to the parent process and log, rather than a terminal. That said, I could compare getuid() to the owner of /dev/ttyN, but this won't work if people are using a display manager. – user3188445 Jun 10 '23 at 00:12
  • Oh. For clarification you are concerned about checking if the user logged in locally and not over something like ssh more so than if a user has logged into a virtual terminal? – Zeno of Elea Jun 10 '23 at 01:11
  • Basically I want to achieve something similar to the access control you get when adding TAG+="seat" to a udev rule, except there's no device node involved, just a program to execute. – user3188445 Jun 10 '23 at 02:50
  • Yes, exactly. My program should refuse to run for users logged in over ssh, and work for local users whether logged in on a vt or through xdm. – user3188445 Jun 11 '23 at 16:37
  • The best solution I can think of programmatically is using UTMP to search login records but this has some caveats. I am modifying my original answer to flesh out this point. – Zeno of Elea Jun 13 '23 at 06:53
  • I mean rather than utmp, since it's not clear which ptys are associated with the console, I could do something like loginctl | grep seat0. But what's the programmatic way of accessing the systemd loginctl database? – user3188445 Jun 13 '23 at 20:14
1

So I don't know if this is secure, but it superficially appears to give me what I want. Has to be linked with -lsystemd. Would love for someone to comment on the security or post a better answer...

#include <cstring>
#include <iostream>

#include <stdlib.h> #include <unistd.h> #include <systemd/sd-login.h>

int is_remote() { char *s = NULL; int n = sd_pid_get_session(getpid(), &s); if (n < 0) { std::cerr << "sd_pid_get_session: " << std::strerror(-n) << std::endl; return -1; } n = sd_session_is_remote(s); free(s); if (n < 0) { std::cerr << "sd_pid_get_session: " << std::strerror(-n) << std::endl; return -1; } return n; }

int main(int argc, char **argv) { if (is_remote()) { std::cerr << "remote access not allowed" << std::endl; return 1; } // Do actual program ... return 0; }

user3188445
  • 5,257
1

If you're using C or C++, and don't want the systemd dependency, you can use the POSIX standard ttyname() or ttyname_r() to get your process's controlling terminal:

SYNOPSIS

#include <unistd.h>

char ttyname(int fildes); int ttyname_r(int fildes, char name, size_t namesize);

DESCRIPTION

The ttyname() function shall return a pointer to a string containing a null-terminated pathname of the terminal associated with file descriptor fildes. The application shall not modify the string returned. The returned pointer might be invalidated or the string content might be overwritten by a subsequent call to ttyname(). The returned pointer and the string content might also be invalidated if the calling thread is terminated.

The ttyname() function need not be thread-safe.

The ttyname_r() function shall store the null-terminated pathname of the terminal associated with the file descriptor fildes in the character array referenced by name. The array is namesize characters long and should have space for the name and the terminating null character. The maximum length of the terminal name shall be {TTY_NAME_MAX}.

Just pass 0 (or STDIN_FILENO) to get the name of your process's controlling terminal in a portable manner.

On every instance of Linux I've checked, users logged on to a text console have a /dev/ttyN tty.

That seems to be definitive for text console logins.

Graphical logins are a bit harder. On Linux you'll get a psuedoterminal name like /dev/pts/N. Which means your DISPLAY environment variable is your controlling terminal. As a first approximation, if that's :0 or :0.0, the process is almost certainly being run by someone logged in to the physical console. That may be enough for you, although you could make an XOpenDisplay() call with the value from the DISPLAY environment variable to be sure.

That will misidentify anyone using something like XVNC or other remote-desktop protocols to access a system that's configured to give those users the :0 display instead of starting with the :10 display as being logged in to the physical console. That is something I've never seen, but it's theoretically possible.

If you don't have to deal with that, you've now identified that your process is running on a Linux physical console.

If you do have to handle that situation, you have to determine if the X server you're connecting to is running on the physical console. Offhand, I don't know any easy way to do that, but if it's a local display you should be able to get the PID from lsof /tmp/.X11-unix/XN (although parsing the output of fuser is probably easier), where N is the display number from the DISPLAY environment variable. Once you get the PID, you can read /proc/PID/fd/0 to get the controlling terminal of the X server, and if that's /dev/ttyN, that again indicates the user is logged on to the physical console, with physical access to the system.

Andrew Henle
  • 3,780
  • Thanks, I upvoted because it's useful. However, there's a possibility that my program doesn't have a controlling terminal because it's launched by user systemd or something, so I really want to know if any terminal belonging to the invoking uid corresponds to a console terminal. So I'm still going to leave the question open in the hopes that there's some way of hooking into whatever udev does for console devices like audio, or maybe of leveraging consolekit or polkit or whatever the non-deprecated systemd equivalent is. – user3188445 Jun 14 '23 at 05:04