
CVE-2018-4185: Apple의 Meltdown 완화로 인해 도입된 iOS 11.2-11.2.6 커널 포인터 노출.
iOS 11.2는 kASLR 슬라이드를 알아내는 데 사용할 수 있는 커널 정보 유출을 도입했습니다. 이 문제는 새로 추가된 기능인 __ARM_KERNEL_PROTECT__로 인해 발생했으며, 이 기능은 실수로 thread_get_state를 사용하여 스레드의 레지스터 값을 가져올 때 커널 함수 Lel0_synchronous_vector_64_long의 주소가 레지스터 x18에 나타나게 했습니다. 이 문제는 iOS 애플리케이션 충돌 로그에서 커널 포인터가 나타나기 시작하면서 발견되었습니다.
iOS 11.2에서 Apple은 arm64에 __ARM_KERNEL_PROTECT__라는 기능을 도입했습니다. osfmk/arm64/proc_reg.h의 주석에 따르면:
`__ARM_KERNEL_PROTECT__`는 코어가 EL0 모드에서 EL1 전용 매핑을 읽거나 접근할 수 있는 잠재적인 아키텍처 또는 마이크로아키텍처 취약점을 방어하기 위한 기능입니다. 이는 코어가 EL1 모드에서 EL0 모드로 전환될 때 가능한 한 많은 매핑을 제거하고, 코어가 EL0 모드에서 EL1 모드로 전환될 때 해당 매핑을 복원함으로써 달성됩니다.
즉, EL1(커널 모드)에서 EL0(사용자 모드)로 전환될 때 가능한 많은 커널 매핑이 제거됩니다. 이는 Spectre 또는 Meltdown과 같은 마이크로아키텍처 취약점을 악용할 때 커널 메모리 매핑에 대한 공격 표면을 제한해야 합니다.
XNU 버전 4570.20.62와 4570.31.3 사이의 차이점을 살펴보면, __ARM_KERNEL_PROTECT__와 관련하여 파일 osfmk/arm64/locore.s에 레지스터 x18에 대한 새로운 참조가 여러 개 나타납니다. 특히, 시스템 호출(명령어 svc #0)에서 호출되는 예외 벡터 Lel0_synchronous_vector_64가 이제 다음과 같이 보입니다:
.text
.align 7
Lel0_synchronous_vector_64:
MAP_KERNEL
BRANCH_TO_KVA_VECTOR Lel0_synchronous_vector_64_long, 8
매크로 BRANCH_TO_KVA_VECTOR는 다음과 같이 정의됩니다:
.macro BRANCH_TO_KVA_VECTOR
#if __ARM_KERNEL_PROTECT__
/*
* Find the kernelcache table for the exception vectors by accessing
* the per-CPU data.
*/
mrs x18, TPIDR_EL1
ldr x18, [x18, ACT_CPUDATAP]
ldr x18, [x18, CPU_EXC_VECTORS]
/*
* Get the handler for this exception and jump to it.
*/
ldr x18, [x18, #($1 << 3)]
br x18
#else
b $0
#endif /* __ARM_KERNEL_PROTECT__ */
.endmacro
이 매크로는 해당 함수에 대한 포인터를 레지스터 x18에 로드하여 실제 예외 벡터 구현인 Lel0_synchronous_vector_64_long으로 간접 분기를 수행합니다. 그러나 이 x18의 덮어쓰기는 사용자 공간 레지스터가 Lel0_synchronous_vector_64_long에 의해 호출되는 함수 fleh_dispatch64에 의해 저장되기 전에 발생합니다. 즉, 사용자 레지스터가 저장될 때 x18은 실제로 사용자 공간의 원래 값이 아니라 Lel0_synchronous_vector_64_long에 대한 포인터가 됩니다.
x18은 예외 반환 시 지워지지만, 사용자 레지스터 상태에 커널 포인터를 저장하는 것은 thread_get_state를 사용하여 저장된 사용자 레지스터 상태(레지스터 x18의 값 포함)를 사용자 공간에 복사할 수 있기 때문에 문제가 됩니다. 스레드가 Lel0_synchronous_vector_64_long 함수의 주소를 얻기 위해 해야 할 일은 자기 자신에 대해 thread_get_state를 호출하고 보고된 x18 값을 확인하는 것뿐입니다. 이렇게 얻은 x18 값에서 Lel0_synchronous_vector_64_long의 정적 주소를 빼면 kASLR 슬라이드를 쉽게 알아낼 수 있습니다.
위에서 언급했듯이 익스플로잇은 간단합니다. thread_get_state 함수를 호출하고 레지스터 x18의 값을 확인한 다음, 커널 함수 Lel0_synchronous_vector_64_long의 정적 주소를 빼기만 하면 됩니다.
저는 2018년 2월 26일, iOS 애플리케이션 충돌 로그에서 레지스터 x18에 커널 포인터가 있는 것을 발견하고 이 문제를 알게 되었습니다. 빠른 확인 결과 동일한 값이 기기의 모든 충돌 로그에서 레지스터 x18에 나타나는 것으로 보아 심각한 정보 유출임을 알 수 있었습니다.
다음으로 실험을 통해 레지스터 x18에서 정확히 무슨 일이 일어나고 있는지 확인하려고 했습니다. 빈 iOS 앱에 중단점을 설정하고 lldb를 사용하여 레지스터 x18의 값을 읽었으며, 유출이 충돌하는 애플리케이션에만 국한되지 않음을 확인했습니다. 그 다음 인라인 어셈블리를 사용하여 x18의 값을 읽으려고 시도했고, 얻은 값이 reg read x18과 같은 명령어를 사용할 때 디버거가 표시하는 값과 일치하지 않는다는 것을 발견했습니다. 이는 아마도 유출이 실제로 thread_get_state에 있으며, CPU가 사용자 공간에서 실행되는 동안 레지스터 x18에 실제로 커널 포인터가 포함되어 있지 않음을 시사했습니다. thread_get_state를 사용하여 x18 값을 읽는 빠른 개념 증명을 통해 이 함수가 실제로 유출의 원인임을 확인했습니다.
저는 문제를 발견한 당일인 2018년 2월 26일에 Apple에 보고했습니다.
작성자: Brandon Azad