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
체크포인트
- openat2 시스템콜은 커널 v5.6 부터 포함되었다.
- 여기에 사용되 커널 소스코드는 v5.7-rc3 이다.
- 패치는 아직 merge전이며 Mickaël Salaün의 패치셋 v5를 참고했다.