Operating Systems 2026F: Tutorial 4

From Soma-notes
Revision as of 19:47, 4 October 2026 by Soma (talk | contribs) (→‎Setup)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

This tutorial is not yet finalized.

In this tutorial you will learn about permissions, accounts, how logging in works, and ssh.

Background

In UNIX and UNIX-like systems like Linux, kernel code runs in supervisor mode (ring 0 on x86-64) and user code runs in user mode (ring 3 on x86-64). When the CPU is in supervisor mode, all system resources can be accessed; when the CPU is in user mode, only a limited set of resources can be used, namely those that have been allocated to the current process.

A process is an abstraction over the limited access of the CPU's user mode. All processes run in user mode. While kernel code can be doing work on behalf of a specific process, properly speaking no kernel code is in a process. Instead, the kernel implements the process abstraction.

The kernel, however, does not treat all processes the same. The kernel allows some processes to do more than others depending upon the user and group of the process. We can see what access a process has by looking at the permissions associated with files.

Most users have limited access. One user, root (with user ID of 0), has as much access as is possible from a process. The kernel will even allow the root process to modify how the kernel operates. Processes running as root are still just processes, though, and any access that the kernel gives to such processes is at the discretion of the kernel. (In fact, there are setups which limit the privileges of the root user.)

File Permissions

A process' permission to access files in UNIX is determined by the user and group associated with that process. When a person logs in, their initial process is given the user and group associated with their account. All child processes of that initial login process inherit the same user and group.

A user can belong to multiple groups but a process only belongs to one group at a time. (There are ways to change the group associated with a process.) A file is always owned by someone and is always associated with a group. All files on the UNIX file system (including directories and other special files) have three different sets of permissions:

  • owner permissions
  • group permissions
  • other permissions

Each of these have read, write, and/or execute permissions along with some other special permissions.

One special permission is the setuid bit. When an executable has this bit set, the process will run as the user who owns that executable (rather than as the user associated with the process that did the execve). The setuid bit is particularly important when a file is owned by root, as it gives us a way for regular users to escape the bounds of normal permissions. We will see the setuid root bit in action in this tutorial.

The ls command with the -l option can be used to show both the permissions of a file as well as the owner and group associated with the file. Permissions are listed first, followed by the owner and the group.

Logging in to a UNIX system

In order to log in to a UNIX system (Linux or otherwise), the following steps must occur (potentially not in this order).

  1. The user must authenticate themselves, proving their identity and that they are allowed to access the system. By default this is done through a username and password.
  2. A new process, U, should be created for the authenticated user.
  3. The login program must establish communication with the user via some communications channel. Normally this channel will be a device. Standard in, out, and error for U should be connected to this device.
  4. U changes uid and gid to that of the new user.
  5. U sets up other aspects of the user's context (mainly setting key environment variables).
  6. U does an execve of the user's chosen shell.

3000userlogin is a basic implementation of steps 4-6. (Steps 2 and 3 are accomplished by running 3000userlogin in a shell, as running an external command means the shell first creates a new process, as we have seen. We are skipping 1.)

SSH

Secure Shell, or ssh, is the standard method for accessing UNIX and UNIX-like systems remotely. It is a more secure replacement for older technologies such as rsh and telnet.

The standard implementation of ssh is openssh, a version of ssh that was originally created for OpenBSD but has been ported to other systems. OpenBSD has a reputation for being the most secure UNIX-like system around. It tends to be a bit harder to configure, have less support for current hardware, and have lower overall performance than Linux-based systems; however, it can be very competitive for some workloads.

While ssh support password-based authentication, it is meant to be used with more secure forms of authentication. For example, ssh supports public key authentication, a type of authentication that makes use of public key cryptography. Instead of a shared key (such as a password) that is known to both parties, with public key cryptography, keys come in two parts: a public part and a private part. The public part should be widely distributed, while the private part needs to be carefully secured.

Public key cryptography can be used to send secret messages to anyone you can get the public key for. With digital signatures, public key cryptography can also be used to verify the authenticity and integrity of messages, so long as we know the public key of the originating party.

With public key authentication, we use a key pair to authenticate rather than sending a password.

A cool feature of public key authentication is it is possible to automatically log into a system once the appropriate private key is made available, thus giving us the convenience of passwordless logins with better security than passwords.

Setup

3000userlogin

When 3000userlogin is properly compiled and set up, you can run

 ./3000userlogin someuser

and you'll be logged in as "someuser", assuming someuser exists.

You can add a user with the adduser command. For example, to create the user "someuser":

 sudo adduser someuser

Note you'll have to answer several questions.

If you just compile 3000userlogin normally, you won't be able to log in as anyone except the current user. To compile and set up 3000userlogin, do the following:

 gcc -O -Wall 3000userlogin.c -o 3000userlogin
 sudo chown root:root 3000userlogin
 sudo chmod u+s 3000userlogin

The chown command makes the binary owned by root, and the chmod command makes it setuid. Thus, when the program is execve'd it will have an effective user ID of root (euid=0).

Alternately, you can download the code and use the associated makefile to build by doing the following:

 wget https://homeostasis.scs.carleton.ca/~soma/os-2020w/code/tut4.tar.gz
 tar xzf tut4.tar.gz
 cd tut4
 sudo apt install make    # make isn't installed on the class VMs oops
 make
 make setuid

SSH

Install the openssh-server package (note on openstack it will already be installed, but on a default Ubuntu desktop install it won't be):

sudo apt-get install openssh-server

Create a second user in the virtual machine named "other" (or any other name you wish to use):

sudo adduser other

(Answer the subsequent prompts however you wish, just remember the password.)

At this point you should be able to log in to the "other" account using ssh:

ssh other@localhost

You'll have to enter your password.

If you don't get a password prompt, password authentication has probably been disabled. (Password authentication has been disabled on the openstack VMs.) To enable it, do the following

 sudo nano /etc/ssh/sshd_config    (or vi, or emacs)

In the editor change the line "PasswordAuthentication no" to "PasswordAuthentication yes". Then, to restart sshd:

 sudo service sshd restart

Be sure to change it back after you've set up public key authentication!

Public key authentication

Create a public key file for your account (as user student, ubuntu, or your personal account):

ssh-keygen

(Accept the default filename and choose at least a simple passphrase.)

You just created a certificate! (A certificate is just a public key with metadata.)

Copy the key to the other account:

cat ~/.ssh/id_rsa.pub >> authorized_keys
scp authorized_keys other@localhost:.
rm authorized_keys
ssh other@localhost
(as user other)
mkdir ~/.ssh (if it doesn't exist already)
chmod 700 ~/.ssh  (make it private)
mv ~/authorized_keys ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Now you can log in to user other by typing in the passphrase you used to lock the key you generated.

To avoid entering this passphrase every time, you can give it to the authentication agent (generally, ssh-agent) that was started when you logged in:

 ssh-add

Tasks

  1. Compile and setup 3000userlogin as described above. Create a new user account. Verify that you can use 3000userlogin to login as the new user without typing the password of the new user.
  2. What is returned as the user's password by getpwnam()? Is this what you expected?
  3. Compare the uid, gid, euid, egid when running 3000userlogin as a regular user, running it setuid root, and running it as root (i.e., running it from a root shell without the setuid bit set). Also check to see what happens when you set the setgid bit (with different group IDs on the file).
  4. Why does 3000userlogin change its gid before changing its uid? What happens if you switch the order of these operations?
  5. Make 3000userlogin use the shell that is specified in the user's password entry. Check by making a new user and setting its shell to a new shell and then see if that new shell runs when you run 3000userlogin. You should change a user's shell with the chsh command, run as that user.
  6. Can you set 3000shell to be a user's default shell? What changes do you have to make for chsh to accept 3000shell? Does anything obvious break when running 3000shell this way, and how can you change 3000userlogin to fix it?
  7. Does a user's default shell have to be a regular shell? Could it instead be an arbitrary program? How do you know?
  8. How important is each of the environment variables that is set by 3000userlogin? Are these the only environment variables that are set after you successfully login?
  9. Note that 3000userlogin uses environ not envp (as an argument to main) to access environment variables. Why not use envp? (Try changing the code to use envp and see what happens.)
  10. Setup password-less login to your local system between the student and other account (or any two other accounts), following the above instructions. Note that the "local" account is the one you are running the ssh command on.
  11. Setup password-less login to access.scs.carleton.ca, following the instructions above.
  12. Secure shell can be used to directly run a command on another program rather than the default shell. For example, try "ssh student@localhost bc -l" and see that bc is run. When you do this, does ssh directly run the specified command or does it first run a shell and have the shell run the command? Use opensnoop.bt or execsnoop.bt to verify.
  13. What files in /etc does bash access when you login via 3000userlogin? How does this compare to what bash opens when you log in via ssh?
  14. (Optional): Prompt for the user's password before logging the user in. To do this, you'll need to get the right password hash and then figure out how to hash the entered password to check if it matches. See online guides about how passwords are stored on Linux for help.

Code

Download code for this tutorial (3000userlogin.c and Makefile)

3000userlogin.c

Download 3000userlogin.c

/* 3000userlogin.c */
/* version 0.1 */

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <pwd.h>

int main(int argc, char *argv[])
{
        int result;
        char *shell_argv[3];
        char *username;
        extern char **environ;
        struct passwd *pw_entry;
                
        if (argc < 2) {
                fprintf(stderr, "Usage: %s <username>\n", argv[0]);
                exit(-1);
        }

        printf("uid=%d, euid=%d, gid=%d, egid=%d\n",
               getuid(), geteuid(), getgid(), getegid());

        username = argv[1];
        
        pw_entry = getpwnam(username);
        if (pw_entry == NULL) {
                fprintf(stderr, "Could not find user %s.\n", username);
                exit(-2);
        }
                       
        result = setgid(pw_entry->pw_gid);
        if (result != 0) {
                fprintf(stderr, "Failed to change to gid %d\n",
                        pw_entry->pw_gid);
                exit(-3);
        }

        result = setuid(pw_entry->pw_uid);
        if (result != 0) {
                fprintf(stderr, "Failed to change to uid %d\n",
                        pw_entry->pw_uid);
                exit(-4);
        }

        result = chdir(pw_entry->pw_dir);
        if (result != 0) {
                fprintf(stderr, "Failed to change to home dir %s\n",
                        pw_entry->pw_dir);
                exit(-5);
        }

        shell_argv[0] = "bash";
        shell_argv[1] = "--login";
        shell_argv[2] = NULL;

        clearenv();
        setenv("USERNAME", pw_entry->pw_name, 1);
        setenv("PATH", "/usr/bin:/bin", 1);
        setenv("SHELL", "/bin/bash", 1);
        setenv("HOME", pw_entry->pw_dir, 1);
        setenv("COMP3000", "yes", 1);
        
        execve("/bin/bash", shell_argv, environ);
     
        fprintf(stderr, "Failed to exec bash\n");
        return -6;
}

Makefile

.PHONY: setuid

3000userlogin: 3000userlogin.c
	gcc -Wall -O 3000userlogin.c -o 3000userlogin

setuid: 3000userlogin
	@echo "The following changes ownership to root, group to root, and sets the setuid bit on 3000userlogin:"
	sudo chown root:root 3000userlogin && sudo chmod u+s 3000userlogin
	@echo "3000userlogin is now setuid root!"