개요
waitid는 fork()로 생성된 프로세스의 상태 변화에 따른 제어를 위한 함수이며 waitid system call 의
unsafe_put_user 함수 취약점과 have_canfork_callback을 이용해 root 권한을 획득할 수 있습니다.
취약한 버전
4.12 < Linux Kernel <= 4.14.0-rc+
취약점 설명 및 분석
waitid() 는 아래와 같이 정의되어 있습니다.
취약점이 발생하는 인자는 siginfo_t *infop 이며 siginfo_t 구조체는 아래와 같습니다.
waitid syscall call을 처리하는 코드를 보면 infop 인자에 대해 access_ok() 함수가 누락되어
있는것을 확인할 수 있습니다.
access_ok() 함수는 주소에 대해 유저 영역의 주소인지 커널 영역의 주소인지를 확인하는 함수 입니다.
해당 함수가 호출되면 SMAP 보호기법이 발동되며 유저 영역에서 커널 영역 주소를 사용할 수 없습니다.
즉, unsafe_put_user 함수를 통해 임의의 커널 주소에 특정 값을 덮어쓸 수 있습니다.
또한, infop이 참조할 수 있는 유효한 주소를 가리킨다면 EFAULT를 반환하지 않기 때문에 계속된
waitid system call 호출로 kernel의 base address를 알아낼 수 있으며 KASLR을 우회할 수 있습니다.
아래와 같은 코드를 작성해 kernel의 base address를 구할 수 있습니다.
kernel의 base address를 구하면 root 자격증명을 위한 prepare_kernel_cred, commit_creds
함수의 주소를 구할 수 있습니다.
하지만 아래 put_user의 코드와 같이 unsafe_put_user함수로 인해 Arbitrary write 는 발생하지만
덮어쓰는 값을 제어할 수는 없습니다.
입력하는 값을 제어할 수 없으므로 have_canfork_callback과 null pointer dereference를 이용합니다.
fork() 함수의 간략한 프로세스는 다음과 같습니다.
fork() -> do_fork() -> copy_process() -> cgroup_can_fork() -> do_each_subsys_mask()
-> for_each_set_bit() -> find_first_bit() -> ......
cgroup_can_fork 함수는 아래와 같고 do_each_subsys_mask함수의 인자로 have_canfork_callback
전역 변수를 사용하며 for_each_set_bit 함수는 have_canfork_callback 변수에 설정된 값을 검사합니다.
위의 프로세스를 asm언어로 확인해보면 아래와 같습니다. have_canfork_callback 변수를 인자로
find_first_bit함수를 호출하고 해당 함수의 결과값을 3과 비교하여 3보다 크면 0xffffffff810e021e로
jmp하게 됩니다.
unsafe_put_user 함수는 infop 주소의 첫번째 bit에 0x11을 설정합니다. 해당 값을 설정하면
find_first_bit 함수의 결과값은 0이 되어 call 0x0 을 만들어낼 수 있습니다.
mmap을 이용해 0x0 주소를 할당하고 해당 영역에 shellcode를 삽입한 후 call 0x0으로 root 권한을
획득할 수 있습니다.
해당 취약점은 access_ok() 함수의 누락이 문제가 되었기 때문에 access_ok() 함수를 추가하는
것으로 보안 패치가 되었습니다.
위협요소
공격자가 일반 사용자의 계정으로 서버의 root 권한을 획득할 수 있습니다. 서버의 root 권한을
탈취당하면 공격자가 서버를 완벽히 제어할 수 있으므로 빠른 보안패치 업데이트가 필요합니다.
대응방안
1) Kernel Version 업데이트