SELinux: kernel: The steps for security checking a program before it runs and the corresponding security rules


(0 comments)

All programs are checked for security before running. In more detail, whenever the execve() function (or its front-ends functions) is called to execute a program, the kernel conducts a check of permissions. It does that by using hook functions installed with execve().
The execve() function is defined in the fs/exec.c file of the kernel source code. When preparing for the imminent process, it calls the internal function do_open_execat() to retrieve a file structure pointer. The file pointer is later taken to a linux_binprm structure (defined in the include/linux/binfmts.h header file) with its pointer is bprm. The linux_binprm structure is responsible for keeping the arguments that are used when loading binary. There are three stages of security checking: checking program file access, checking when preparing the linux_binprm structure, and checking when loading binary.

1) Checking the program file access
When called, do_open_execat() calls do_filp_open() (defined in fs/namei.c), with the flag argument is an open_flags structure. The open_flags structure is initialized with its component open_flag = O_LARGEFILE | O_RDONLY | __FMODE_EXEC, and access mode component acc_mode = MAY_EXEC.
The __FMODE_EXEC flag indicates that a program is about to be run, and the MAY_EXEC access mode indicates that the executable binary is expected. The flag structure is used later to direct the appropriate control. The do_filp_open() function then calls path_openat() with the flag structure passed. At this point, the file structure is initialized with the alloc_empty_file() function (defined in the file fs/file_table.c) in which there is an argument to be the open_flag flag component. There, the f_flags component of the file structure is assigned with the open_flag flag component, and the f_mode component of the file structure is assigned as follows, where f is the pointer of the file structure and open_flag becomes the flags argument:

f->f_mode = OPEN_FMODE(flags);

Where the OPEN_FMODE macro is defined in the include/linux/fs.h header file as follows

#define OPEN_FMODE(flag) ((__force fmode_t)(((flag + 1) & O_ACCMODE) | \
					    (flag & __FMODE_NONOTIFY)))

O_ACCMODE is defined in the include/linux/fs.h header file with a value of 3, flag plus 1, so (flag + 1) & O_ACCMODE as well as OPEN_FMODE(flag) contain at least bit of 1. Meanwhile FMODE_READ is defined in the include/linux/fs.h header file as 1. Hence the f_mode component of the file structure includes the FMODE_READ bit.

Because of the flag information of the file structure, in the next stage of the path_openat(), function do_last() is called, still carries the flag structure. If all goes well, that is, the program file was found successfully, the do_last() function will call the may_open() function. There is a call to the function inode_permission() there to check the permissions.
First, the check is performed for file access permissions like a regular Linux system (ie the read, write, execute permissions of a file that we work with chmod command). Because the mode is MAY_EXEC, the program file must be an executable binary. Next, the control is passed to the security_inode_permission() function to check security (this function is defined in security/security.c).
Now is the beginning of a security check hook. The security_inode_permission() function calls the selinux_inode_permission() hook function (defined in the file security/selinux/hooks.c). Inside this hook function, the file_mask_to_av() function converts accesses to security permissions. The full source code for the file_mask_to_av() function is as follows:

/* Convert a Linux mode and permission mask to an access vector. */
static inline u32 file_mask_to_av(int mode, int mask)
{
	u32 av = 0;

	if (!S_ISDIR(mode)) {
		if (mask & MAY_EXEC)
			av |= FILE__EXECUTE;
		if (mask & MAY_READ)
			av |= FILE__READ;

		if (mask & MAY_APPEND)
			av |= FILE__APPEND;
		else if (mask & MAY_WRITE)
			av |= FILE__WRITE;

	} else {
		if (mask & MAY_EXEC)
			av |= DIR__SEARCH;
		if (mask & MAY_WRITE)
			av |= DIR__WRITE;
		if (mask & MAY_READ)
			av |= DIR__READ;
	}

	return av;
}

The mask argument is the access mode to be checked, which is MAY_EXEC. The mode argument is used to check whether the object is a file or a directory. Because the program file is being checked, the function returns FILE__EXECUTE. This permission is defined in the header file <your_build_dir>/security/selinux/av_permissions.h (generated automatically when compiling the kernel) with a value of 0x00004000U, and corresponds to the execute permission in the source code of security policy.
Next, use the avc_has_perm_noaudit() function to check if the permission is granted or not (this function is defined in security/selinux/avc.c). The avc_has_perm_noaudit() function checks but does not audit because auditing or not is dependent on the policy, and that is determined by the next function avc_audit_required() (defined in the security/selinux/include/avc.h header file). If the permission is denied and belongs to an explicit dontaudit rule in the policy, the audit is not performed, and the inspection ends with result of disallowing. Otherwise, the control is passed down to the audit_inode_permission() function (defined in the security/selinux/hooks.c) for further audit.

From this we draw a thing that the more dontaudit rules, the faster the system because of the fewer checks. In addition, dontaudit rules help keep the system stable when the policy is well established, avoiding changes that could pose a security threat or disrupt the policy when adding allow rules suggesting by audit2allow. For example, the rule below is always good, it implies that all user domains should not be allowed to access the password file. All thus denied access no information will be passed to audit2allow

dontaudit userdomain shadow_t:file manage_file_perms;

The function may_open() ends and at this point we already have a permission required in the security policy to run a program, i.e execute. The purpose of our thorough analysis is to develop the right policy: sufficient to implement and not redundant to pose vulnerabilities.

We return to do_last() function, after may_open() is vfs_open() (defined in fs/open.c). This function calls do_dentry_open() function. Inside the do_dentry_open() function, there is a call to the security_file_open() function to check security (this function is defined in security/security.c). The security_file_open() function then calls the hook function selinux_file_open() (defined in the file security/selinux/hooks.c). The selinux_file_open() hook function performs a permission check by calling file_path_has_perm(). The file_path_has_perm() function has the following signature

static inline int file_path_has_perm(const struct cred *cred,
                                    struct file *file,
                                    u32 av);

Where the argument av is the required permissions (or access vector) argument, computed in the hook function by the open_file_to_av() function. This function again thanks to the file_to_av() function defined as follows

static inline u32 file_to_av(struct file *file)
{
	u32 av = 0;

	if (file->f_mode & FMODE_READ)
		av |= FILE__READ;
	if (file->f_mode & FMODE_WRITE) {
		if (file->f_flags & O_APPEND)
			av |= FILE__APPEND;
		else
			av |= FILE__WRITE;
	}
	if (!av) {
		/*
		 * Special file opened with flags 3 for ioctl-only use.
		 */
		av = FILE__IOCTL;
	}

	return av;
}

The file_to_av() function converts a file into an access vector. Because file-> f_mode contains the FMODE_READ bit, we get av = FILE__READ. After the file_to_av() function ends and returns av, the open_file_to_av() function adds the FILE__OPEN permission. So we have av = FILE__READ | FILE__OPEN.
Once av has been set, the file_path_has_perm() function checks the permissions using the inode_has_perm() function, and at the end of the function, the result is returned by the avc_has_perm() function - this is the function that checks directly, including the check and audit (defined in file security/selinux/avc.c).
Up to this point the file structure is completed if the permissions are granted, and passed back to the bprm pointer inside the execve() function (in fact, the __do_execve_file() function via a calling chain execve() → do_execve() → do_execveat_common () → __do_execve_file()).

Also at this point, we need three permissions, which are FILE__EXECUTE, FILE__READ and FILE__OPEN, where FILE__EXECUTE is checked first, then the next two are checked at the same time.
Corresponding to the security policy, we need to allow the current domain to execute, read and open the program file so that it becomes executable. For example

allow staff_t myapp_exec_t:file { read open execute };

The above rule states that the staff_t domain is allowed to execute a binary file of type myapp_exec_t.

However, that is just the rule for program file. In order to fully execute the binary we need also to add domain related rules. So we continue to analyze the execve() function.

Below is the summary diagram

2) Checking when preparing linux_binprm structure
Once the file structure is in place, execve() performs the preparation to load the program in which the preparation of the linux_binprm structure is most essential. It calls prepare_binprm(). The prepare_binprm() function fills the linux_binprm structure and calls the selinux_bprm_set_creds() hook function (defined in the security/selinux/hooks.c file) through the security_bprm_set_creds() function (defined in the security/security.c file).
At the beginning, the function checks to see if the current process has set security context for the imminent process (using the setexeccon(), setexeccon_raw() or setexecfilecon() functions). If the security context is already set, it simply assigns the security identifier (sid) set to the imminent process (which is essentially the current process being arranged to execute the new program), then resets the exec security identifier (exec sid) of the imminent process to zero. This indicates that after every execve() call, the exec sid is always reset and the programmer doesn't need to erase it in the program. In this case, the domain transition is done explicitly by the program without using the automatic domain transition feature of kernel. To do so, the requirement is that the current domain must have setexec permission. For example

allow staff_t self:process setexec;

The checking for setexec permission is in the selinux_setprocattr() hook function. However, that is done outside execve() and details about it are beyond the scope of this article.

If exec sid is not set, the security_transition_sid() function (defined in security/selinux/ss/services.c) will compute sid for the imminent process.

If sid does not change, ie no transition, then the avc_has_perm() function will check the FILE__EXECUTE_NO_TRANS (non-transition execution) permission of the current domain for the program file, corresponding to the execute_no_trans permission in the security policy. Thus, if the staff_t domain needs to execute a program file of type myapp_exec_t in place, in addition to the above required permissions, an additional required permission is execute_no_trans

allow staff_t myapp_exec_t:file { read open execute execute_no_trans };

If there is a domain transition (either automatically with the type_transition statement in the policy or manual domain transition using the setexeccon() function in the program), the avc_has_perm() function will check for the relevant permissions, in which minimum   permissions are the current domain must be allowed to transition to the new domain and the new domain must be allowed to have an entry point for the program file. Those are PROCESS__TRANSITION and FILE__ENTRYPOINT permissions corresponding to transition and entrypoint permissions in the security policy. Assuming the staff_t domain transitions to the myapp_t domain when executing the binary of type myapp_exec_t, the set of necessary (might not sufficient) rules would be

allow staff_t myapp_exec_t:file { read open execute };
allow staff_t myapp_t:process transition;
allow myapp_t myapp_exec_t:file entrypoint;

Below is the summary diagram

3) Checking when loading the binary
We need one more permission, that is, the map permission for mapping the program file into memory. However, execve() has encountered a bug notifying the lack of this permission. As follows:
When exec_binprm() is called, it calls the search_binary_handler() function defined as below

int search_binary_handler(struct linux_binprm *bprm)
{
	bool need_retry = IS_ENABLED(CONFIG_MODULES);
	struct linux_binfmt *fmt;
	int retval;

	/* This allows 4 levels of binfmt rewrites before failing hard. */
	if (bprm->recursion_depth > 5)
		return -ELOOP;

	retval = security_bprm_check(bprm);
	if (retval)
		return retval;
    
	retval = -ENOENT;
 retry:
	read_lock(&binfmt_lock);
	list_for_each_entry(fmt, &formats, lh) {
		if (!try_module_get(fmt->module))
			continue;
		read_unlock(&binfmt_lock);

		bprm->recursion_depth++;
		retval = fmt->load_binary(bprm);
		bprm->recursion_depth--;

		read_lock(&binfmt_lock);
		put_binfmt(fmt);
		if (retval < 0 && !bprm->mm) {
			/* we got to flush_old_exec() and failed after it */
			read_unlock(&binfmt_lock);
			force_sigsegv(SIGSEGV);
			return retval;
		}
		
		if (retval != -ENOEXEC || !bprm->file) {
			read_unlock(&binfmt_lock);
			return retval;
		}
	}
	read_unlock(&binfmt_lock);

	if (need_retry) {
		if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&
		    printable(bprm->buf[2]) && printable(bprm->buf[3]))
			return retval;
		if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)
			return retval;
		need_retry = false;
		goto retry;
	}

	return retval;
}

The search_binary_handler() function calls the load_binary function pointer (which is a component of the linux_binfmt structure) to load the binary. The load_binary function pointer typically points to the load_elf_binary() function (defined in the file fs/binfmt_elf.c). The load_elf_binary() function has been causing problem since it calls flush_old_exec() to flush traces of the currently running executable to setup the new program later. This is the point of no return, then the error codes including the lack of map permission cannot return to the execve() caller.
So the fix is to check the permission before this point, the most convenient is before calling the binary load function. We see before loading the binary, there is a security check function, ie security_bprm_check(). However, it lacks an essential hook function. The security_bprm_check() function is defined in security/security.c as follows:

int security_bprm_check(struct linux_binprm *bprm)
{
	int ret;

	ret = call_int_hook(bprm_check_security, 0, bprm);
	if (ret)
		return ret;
	return ima_bprm_check(bprm);
}

It calls bprm_check_security hook but no hook found and call_int_hook() always returns success, which means bypassing the check.
To fix, we add the bprm_check_security hook to the file security/selinux/hooks.c

LSM_HOOK_INIT(bprm_check_security, selinux_bprm_check_security),

Then define the hook function as follows:

static int selinux_bprm_check_security(struct linux_binprm *bprm) {

    struct file *file = bprm->file;
    struct common_audit_data ad;
    ad.type = LSM_AUDIT_DATA_FILE;
    ad.u.file = file;
    return inode_has_perm(current_cred(), file_inode(file),
                          FILE__MAP, &ad);
}

Complete information is in the patch file linux-5.6.7-selinux_hooks-1.patch.

Thus we added a hook to check the FILE__MAP permission before loading the binary, which corresponds to the map permission in the security policy. When the staff_t domain executes a binary file of type myapp_exec_t without domain transition, the minimum rule would be

allow staff_t myapp_exec_t:file { read open execute execute_no_trans map };

With domain transition, the minimum set of rules for the staff_t domain to transition to the myapp_t domain when executing the binary of type myapp_exec_t would be

allow staff_t myapp_exec_t:file { read open execute map };
allow staff_t myapp_t:process transition;
allow myapp_t myapp_exec_t:file entrypoint;

If it is automatic domain transition, the type_transition statement must be present in the policy

type_transition staff_t myapp_exec_t:process myapp_t;

If it is manual domain transition using setexeccon(), the current domain must have setexec permission

allow staff_t self:process setexec;

Below is the summary diagram

We have reviewed in detail the steps for security checking a program before running at the source code level. It is the most accurate information that forms the basis for an effective security policy.

Currently unrated

Comments

There are currently no comments

New Comment

required

required (not published)

optional

required


What is 3 + 2?

required