본문 바로가기

linux kernel

2020년 5월 11일자 lwn.net 기사 : O_MAYEXEC — explicitly opening files for execution

Mickaël Salaün의 openat2 시스템콜에 O_MAYEXEC 플래그를 추가하는 패치를 소개하고 있다. 패치는 아직 메인라인 커널에 merge되지는 않았고 2018년말에 첫 패치가 나온 이후 5번째 리비젼을 거듭하는 중이다.

유닉스 계열 시스템에서 파일을 실행하려면 첫째, 해당 파일이 실행권한 비트가 설정되어 있어야 한다.

$ ls -l some_exec
-rwxr-xr-x 1 owner owner 160 5 14 18:23 some_exec

둘째, 파일이 위치한 파일시스템이 noexec 마운트 옵션으로 마운트되지 않은 곳이어야 한다.

이 두가지 조건으로 파일이 실행되거나 안되거나 할 수 있는데 인터프리터나 링커를 사용할 경우 이 조건을 간단히 무시하게된다.

 

예를 들어  perl -e 를 사용하여 실행권한이 없는 펄스크립트 파일을 실행시킬 수가 있다.

$ ls -l rw_script.pl
-rw-rw-rw- 1 owner owner 160 5 14 18:23 rw_script.pl
$ perl -e rw_script.pl



O_MAYEXEC에 대해

커널에서 메모리 페이지에 대해 VM_READ, VM_WRITE, VM_EXEC 로 현재 권한을 나타내고 VM_MAYREAD, VM_MAYWRITE, VM_MAYEXEC로 메모리 페이지가 변환가능한 미래 권한?을 표현한다.

mmap 시스템콜을 호출하여 vmflag를 설정해 줄 수 있는데 이때 do_mmap내부적으로는 디폴트로 VM_MAYREAD|VM_MAYWRITE|VM_MAYEXEC를 설정해준다. 추후에 mprotect 시스템콜 호출로 메모리 권한을 바꿀 수 있다는 뜻.

#define VM_READ 0x00000001 /* currently active flags */
#define VM_WRITE 0x00000002
#define VM_EXEC 0x00000004
#define VM_SHARED 0x00000008
/* mprotect() hardcodes VM_MAYREAD >> 4 == VM_READ, and so for r/w/x bits. */
#define VM_MAYREAD 0x00000010 /* limits for mprotect() etc */
#define VM_MAYWRITE 0x00000020
#define VM_MAYEXEC 0x00000040
#define VM_MAYSHARE 0x00000080

Grsecurity Pax mprotect

grsecurity의 CONFIG_PAX_MPROTECT를 설정하면 VM_WRITE로 오픈한 파일을 mprotect로 VM_EXEC로 바꾸지 못하게 막아버린다. do_mmap에서 VM_MAYEXEC를 제거해 버리는 방식으로 W^X 를 달성하여 시스템을 좀더 안전하게 만든다.

 

Code from grsecurity_3.1-4.9.17

        /* Do simple checking here so the lower-level routines won't have
         * to. we assume access permissions have been handled by the open
         * of the memory object, so we don't do any here.
         */
        vm_flags |= calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) |
                        mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; // 메인라인 코드는 이렇게 기본적으로 (may)rwx를 허용한다.
#ifdef CONFIG_PAX_MPROTECT
        if (mm->pax_flags & MF_PAX_MPROTECT) {
#ifdef CONFIG_GRKERNSEC_RWXMAP_LOG
                if (file && !pgoff && (vm_flags & VM_EXEC) && mm->binfmt &&
                    mm->binfmt->handle_mmap)
                        mm->binfmt->handle_mmap(file);
#endif
#ifndef CONFIG_PAX_MPROTECT_COMPAT
                if ((vm_flags & (VM_WRITE | VM_EXEC)) == (VM_WRITE | VM_EXEC)) {
                        gr_log_rwxmmap(file);
#ifdef CONFIG_PAX_EMUPLT
                        vm_flags &= ~VM_EXEC;
#else
                        return -EPERM;
#endif
                }
                if (!(vm_flags & VM_EXEC))
                        vm_flags &= ~VM_MAYEXEC;
#else
                if ((vm_flags & (VM_WRITE | VM_EXEC)) != VM_EXEC)
                        vm_flags &= ~(VM_EXEC | VM_MAYEXEC);
#endif
                else
                        vm_flags &= ~VM_MAYWRITE;
        }
#endif// pax mprotect는 이렇게 W^X를 달성한다.

하지만 pax mprotect가 활성화 되어 있더라도 writable 메모리를 executable로 못바꿀 뿐 여전히 인터프리터를 통한 실행은 가능하다.

 

패치 구현 방식

open 시스템콜의 인자로 invalid flag를 전달하게 되면 코드가 아예 무시를 하도록 되어 있다.

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
|- ksys_open(filename, flags, mode);
    |- do_sys_open(AT_FDCWD, filename, flags, mode);
         |- struct open_how how = build_open_how(flags, mode);
             |- 
...
inline struct open_how build_open_how(int flags, umode_t mode)
{
        struct open_how how = {
                .flags = flags & VALID_OPEN_FLAGS,   // 보다시피 invalid flag는 그냥 덮어써서 없애버린다.
                .mode = mode & S_IALLUGO,
        };
...
         |- return do_sys_openat2(dfd, filename, &how);
//openat 도 ksys_open 말고 do_sys_open를 바로 호출한다는 점 말고는 크게 다른점이 없다.

openat2의 경우 모르는 flag가 넘어오면 fail이 되도록 구현되어 있으므로 openat2에 O_MAYEXEC를 추가하였다. 아래는 openat2시스템콜 메인라인의 코드 부분.

SYSCALL_DEFINE4(openat2, int, dfd, const char __user *, filename,
                struct open_how __user *, how, size_t, usize)
  |- return do_sys_openat2(dfd, filename, &tmp);
      |- int fd = build_open_flags(how, &op);
...
inline int build_open_flags(const struct open_how *how, struct open_flags *op)
{
        int flags = how->flags;
        int lookup_flags = 0;
        int acc_mode = ACC_MODE(flags);
        /* Must never be set by userspace */
        flags &= ~(FMODE_NONOTIFY | O_CLOEXEC);
        /*
         * Older syscalls implicitly clear all of the invalid flags or argument
         * values before calling build_open_flags(), but openat2(2) checks all
         * of its arguments.
         */
        if (flags & ~VALID_OPEN_FLAGS)    // invalid flag는 -EINVAL을 리턴한다. open과 다르지? 그러췌!
                return -EINVAL;
...

패치에서는 아래와 같이 O_MAYEXEC가 지원되도록 추가했다.

@@ -1029,6 +1031,12 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op)
if (flags & __O_SYNC)
flags |= O_DSYNC;

+ /* Checks execution permissions on open. */
+ if (flags & O_MAYEXEC) {
+ acc_mode |= MAY_OPENEXEC;
+ flags |= __FMODE_EXEC;
+ }
+
op->open_flag = flags;

/* O_TRUNC implies we need access checks for write permissions */

 

패치는 새로운 sysctl 컨트롤로 fs.open_mayexec_enforce를 추가한다. 디폴트 값 0는 현재와 동일하게 동작하도록 해서 아무도 문제가 없도록 유지. bit 0이 set 되면 noexec로 마운트된 파일시스템에서 O_MAYEXEC 로 openat2를 호출하면 fail 이 된다. bit 1이 set 되면 파일이 실행권한이 없으면 fail이 된다.

 

Integrity measurement가 이 openat2 O_MAYEXEC추가로 덕을 보는 또다른 서브시스템이다. Integrity measurement도 integrity criteria에 만족하지 않는 파일들의 실행을 막도록 설정할 수 있는데 예제처럼 인터프리터를 써버리면 이 기능이 무용지물이 된다. 이 openat2 O_MAYEXEC패치는 hook을 추가해서 openat2가 실패를 하도록 만든다.

--- a/security/integrity/ima/ima_main.c +++ b/security/integrity/ima/ima_main.c
@@ -438,7 +438,8 @@ int ima_file_check(struct file *file, int mask)

security_task_getsecid(current, &secid);
return process_measurement(file, current_cred(), secid, NULL, 0,
- mask & (MAY_READ | MAY_WRITE | MAY_EXEC |
+ mask & (MAY_READ | MAY_WRITE |
+ MAY_EXEC | MAY_OPENEXEC |
MAY_APPEND), FILE_CHECK);
}
EXPORT_SYMBOL_GPL(ima_file_check);
// ima policy로 MAY_OPENEXEC를 추가했음을 알수있다.

아마도 이 아이디어로 SELinux나 Smack같은 시큐리티 모듈들이 인터프리터를 통해 실행하는 것을 막도록 label을 추가한다던지 할 수도 있겠다.

 

인터프리터 업데이트

이 패치가 메인라인에 merge가 되면 많은 인터프리터들이 openat2에 O_MAYEXEC를 사용하여 올바른 권한으로 스크립트를 실행하게 변경이 될것이다. 파이썬 프로젝트의 경우 적어도 2017년부터 OS에 audit information을 제공하도록 작업중에 있다. 이 작업은 파이썬 3.9 릴리즈를 위해 2019년 5월에 승인된 PEP 578 ("Python runtime audit hooks")로 공식발표되었다.

 

링크

기사 원문: https://lwn.net/Articles/820000/

패치: https://lwn.net/ml/linux-kernel/20200505153156.925111-1-mic@digikod.net/

PEP 578: https://lwn.net/Archives/PythonIndex/#Python_Enhancement_Proposals_PEP-PEP_578

Grsecurity pax mprotect: https://pax.grsecurity.net/docs/mprotect.txt

 

체크포인트

  1. openat2 시스템콜은 커널 v5.6 부터 포함되었다.
  2. 여기에 사용되 커널 소스코드는 v5.7-rc3 이다.
  3. 패치는 아직 merge전이며 Mickaël Salaün의 패치셋 v5를 참고했다.