
Pixel7/8 Pro용 Android 14 커널 익스플로잇
이 글은 구글에 독립적으로 식별하여 보고한, 기본 애플리케이션 샌드박스에서 접근 가능한 Mali GPU 내의 두 가지 커널 취약점에 대한 심층 분석을 제공합니다. 임의의 커널 읽기/쓰기 기능을 달성하는 커널 익스플로잇을 포함합니다. 결과적으로, SELinux를 비활성화하고 다음 Android 14 버전을 실행하는 Google Pixel 7 및 8 Pro 모델에서 root 권한을 상승시킵니다:
google/husky/husky:14/UD1A.231105.004/11010374:user/release-keysgoogle/cheetah/cheetah:14/UP1A.231105.003/11010452:user/release-keysgoogle/cheetah/cheetah:14/UP1A.231005.007/10754064:user/release-keysgoogle/panther/panther:14/UP1A.231105.003/11010452:user/release-keys (by m4b4 (Marcel))이 익스플로잇은 두 가지 취약점을 활용합니다: gpu_pixel_handle_buffer_liveness_update_ioctl ioctl 명령에서 불완전한 패치로 인한 정수 오버플로우와 타임라인 스트림 메시지 버퍼 내의 정보 유출입니다.
Google은 이 커밋에서 gpu_pixel_handle_buffer_liveness_update_ioctl ioctl 명령의 정수 오버플로우를 해결했습니다. 처음에 이 문제를 보고했을 때, 버그가 앞서 설명한 패치의 문제로 인해 발생했다고 생각했습니다. 보고서를 검토한 후, 취약점 분석이 부정확했음을 깨달았습니다. 패치가 불완전하다는 첫 번째 가정에도 불구하고, 패치는 계산에서 언더플로우를 효과적으로 해결하고 방지합니다. 이로 인해 변경 사항이 프로덕션 빌드에 적용되지 않았다고 의심하게 되었습니다. 그러나 계산에서 언더플로우를 일으킬 수는 있지만 오버플로우를 일으키는 것은 불가능합니다. 이는 ioctl 명령이 부분적으로 수정되었음을 시사하지만, 위에 표시된 패치가 아닌 다른 방식입니다. IDA를 살펴보면 프로덕션 릴리스에 또 다른 불완전한 패치가 포함되어 있으며, 이 패치는 Mali GPU 커널 모듈의 어떤 git 브랜치에도 존재하지 않는다는 것이 밝혀졌습니다.
이 취약점은 Android 최신 버전에서 처음 발견되어 2023년 11월 19일에 보고되었습니다. 이후 Google은 내부적으로 이미 이 문제를 식별했으며 12월 Android 보안 게시판에서 CVE-2023-48409로 할당하여 중복 이슈로 표시했다고 알려왔습니다. 보고서 제출 전(커밋 날짜 기준 약 8월 30일)에 이미 내부적으로 버그가 식별되었음을 확인할 수 있었지만, 여전히 혼란스러운 점이 있습니다. 구체적으로, 최신 기기의 10월 및 11월 보안 패치 레벨(SPL)이 여전히 이 취약점에 영향을 받았다는 점이 이상합니다. —이전 버전은 조사하지 않았습니다. 따라서 이것이 진정으로 중복 이슈였고 적절한 패치가 내 제출 이전에 12월에 예정되어 있었는지, 아니면 이 취약점을 해결하는 과정에서 누락이 있었는지 최종적으로 판단할 수 없습니다.
어쨌든, 이 버그를 강력하게 만드는 요소는 다음과 같습니다:
info.live_ranges 버퍼는 완전히 사용자 제어 가능합니다.info.live_ranges 포인터가 buff 커널 주소의 시작 이전 임의의 오프셋에 위치하도록 계산을 오버플로우할 수 있습니다.이 취약점은 2022년 iOS 15 커널에서 발견하고 익스플로잇한 DeCxt::RasterizeScaleBiasData() 버퍼 언더플로우 취약점과 유사점이 있습니다.
Mali GPU는 정보를 수집하고 직렬화한 다음 특정 형식으로 링 버퍼에 쓰기 위해 설계된 사용자 지정 타임라인 스트림을 구현합니다. 사용자는 kbase_api_tlstream_acquire ioctl 명령을 호출하여 파일 디스크립터를 얻고, 이 링 버퍼에서 읽을 수 있습니다. 메시지의 형식은 다음과 같습니다:
직렬화된 메시지 버퍼로, 특정 내용은 메시지 ID에 따라 달라집니다.
예를 들어, __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait 함수는 kbase_kcpu_command_queue 및 dma_fence 커널 포인터를 메시지 버퍼로 직렬화하여 결과적으로 사용자 공간 프로세스에 커널 포인터를 유출시킵니다.```c
void __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait(
struct kbase_tlstream *stream,
const void *kcpu_queue,
const void *fence
)
{
const u32 msg_id = KBASE_TL_KBASE_KCPUQUEUE_ENQUEUE_FENCE_WAIT;
const size_t msg_size = sizeof(msg_id) + sizeof(u64)
+ sizeof(kcpu_queue)
+ sizeof(fence)
;
char *buffer;
unsigned long acq_flags;
size_t pos = 0;
buffer = kbase_tlstream_msgbuf_acquire(stream, msg_size, &acq_flags);
pos = kbasep_serialize_bytes(buffer, pos, &msg_id, sizeof(msg_id)); pos = kbasep_serialize_timestamp(buffer, pos); pos = kbasep_serialize_bytes(buffer, pos, &kcpu_queue, sizeof(kcpu_queue)); pos = kbasep_serialize_bytes(buffer, pos, &fence, sizeof(fence));
kbase_tlstream_msgbuf_release(stream, acq_flags); }
PoC(Proof of Concept) 익스플로잇은 새로운 kcpu 큐 객체가 할당될 때마다 `kbasep_kcpu_queue_new` 함수에서 전달되는 메시지 ID `KBASE_TL_KBASE_NEW_KCPUQUEUE`를 모니터링하여 `kbase_kcpu_command_queue` 객체 주소를 유출합니다.
Google은 이 취약점이 2023년 3월에 보고되었으며 보안 게시판에서 [CVE-2023-26083](https://source.android.com/docs/security/bulletin/2023-07-01)로 지정되었다고 알려주었습니다. 그럼에도 불구하고, 저는 10월과 11월 보안 패치 수준(SPL)이 적용된 최신 Pixel 기기에서 해당 문제를 재현할 수 있었으며, 이는 수정 사항이 올바르게 적용되지 않았거나 전혀 적용되지 않았음을 나타냅니다. 이후 Google은 크레딧 없이 12월 보안 업데이트 게시판에서 해당 문제를 신속하게 해결했으며, 이후 이 문제가 중복 보고로 간주된다고 알려왔습니다. 그러나 이 문제를 중복으로 분류한 근거는 여전히 의문스럽습니다.
## 익스플로잇
---
저는 흥미로운 두 가지 취약점을 가지고 있습니다. 첫 번째 취약점은 할당된 `buff` 주소보다 앞에 있는 모든 16바이트 정렬 커널 주소의 내용을 수정할 수 있는 강력한 기능을 제공합니다. 두 번째 취약점은 커널 메모리 내 객체의 잠재적 위치에 대한 힌트를 제공합니다.
### buffer_count 및 live_ranges_count 값에 대한 참고 사항
`buffer_count`와 `live_ranges_count` 필드를 완전히 제어할 수 있으므로, 대상 slab과 쓰려는 정확한 오프셋을 유연하게 선택할 수 있습니다. 그러나 `buffer_count`와 `live_ranges_count` 값을 선택할 때는 몇 가지 제약 조건과 요소를 신중히 고려해야 합니다.
- 두 값은 서로 관련되어 있으며, 새로 도입된 모든 검사를 우회한 경우에만 오버플로우가 발생합니다.
- 음수 오프셋이 16바이트 정렬되어야 한다는 요구 사항은 임의 위치에 쓰는 기능을 제한합니다. 그러나 일반적으로 이것은 큰 장애가 되지 않습니다.
- 더 큰 오프셋을 선택하면 의도하지 않은 메모리 영역에 많은 양의 데이터가 쓰여질 수 있습니다. 예를 들어, 할당 크기가 `0x3004`로 오버플로우되면 `live_ranges` 포인터는 `buff` 객체의 할당된 공간으로부터 `-0x4000` 바이트 지점으로 설정됩니다. 그러면 `copy_from_user` 함수는 `update->live_ranges_count` 곱하기 4 계산에 따라 `0x7004` 바이트를 쓰게 됩니다. 결과적으로 이 작업은 `live_ranges` 포인터와 `buff` 할당 사이의 메모리 영역을 사용자 제어 데이터로 덮어씁니다. 따라서 해당 범위 내에 중요한 시스템 객체가 실수로 덮어쓰여지지 않도록 주의해야 합니다. 이 작업은 `copy_from_user` 호출을 포함하므로, 사용자 소스 버퍼 뒤에 원하지 않는 메모리 영역을 의도적으로 매핑 해제하여 `EFAULT`를 트리거함으로써 민감한 위치에 데이터가 쓰여지는 것을 방지할 수 있습니다. 그러나 이 접근 방식은 효과적이지 않습니다. 그 이유는 `raw_copy_from_user` 함수가 실패하면 대상 커널 버퍼의 나머지 바이트를 0으로 채우기 때문입니다. 이 동작은 오류로 인해 부분 복사가 발생한 경우 커널 버퍼의 나머지 부분에 초기화되지 않은 데이터가 포함되지 않도록 구현되었습니다.```c
static inline __must_check unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
unsigned long res = n;
might_fault();
if (!should_fail_usercopy() && likely(access_ok(from, n))) {
instrument_copy_from_user(to, from, n);
res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
memset(to + (n - res), 0, res);
return res;
}
이를 고려하여, 덮어쓸 객체와 쓸 데이터를 신중히 선택해야 합니다.
이 불운한 검사에 막혀 있기 때문에, 제 전략은 널(null) 처리해도 원치 않는 결과가 발생하지 않는 객체를 식별하는 것입니다. 하지만 그 전에 해결해야 할 또 다른 문제가 있습니다. 지난 부분에서 할당 크기를 마음대로 선택할 수 있으므로 할당 버퍼를 서비스하기 위해 어떤 범용 slab 캐시 할당자든 사용할 수 있다고 말한 것을 기억하시나요? 그것은 올바르지 않습니다. 그 이유는 다시 copy_from_user 때문입니다! 이는 CONFIG_HARDENED_USERCOPY 완화 조치 때문입니다. 이 조치는 커널 대상 버퍼(이 경우 힙 객체)에 해당하는 slab 캐시 크기와 일치하지 않는 크기 지정을 금지합니다. 버퍼의 페이지가 slab 페이지인지 확인하고, 그렇다면 일치하는 kmem_cache->size를 검색하여 사용자가 제공한 크기가 이를 초과하지 않는지 확인합니다. 그렇지 않으면 크기 불일치로 인해 커널이 충돌합니다. 즉, 다시 말해, 범용 할당자에 속하는 객체는 대상으로 삼을 수 없지만, 큰 크기를 가진 객체(즉, 페이지 할당자가 직접 서비스하는 객체)는 여전히 대상으로 삼을 수 있습니다.
가장 먼저 떠오른 생각은 pipe_buffer 기술을 사용하는 것이었습니다. 이는 임의 읽기/쓰기 프리미티브를 얻는 매우 우아한 기술입니다. 이 기술에 대해 자세히 설명하지는 않겠지만, 독자들은 Interrupt Labs의 훌륭한 블로그를 읽어보시기 바랍니다. 파이프 객체를 구성할 때, pipe_buffer 객체는 처음에 16개 요소의 배열로 생성됩니다. 그러나 배열 크기는 fcntl(F_SETPIPE_SZ)를 사용하여 조정할 수 있습니다. 따라서 pipe_buffer 배열 할당을 페이지 할당자가 서비스할 수 있도록 조정할 수 있어 공격에 완벽한 대상 객체가 됩니다.
pipe_buffer 객체를 대상 후보로 선택한 후, 커널 읽기/쓰기를 달성하기 위한 다음 단계는 언더플로우 취약점을 사용하여 그 내용을 덮어쓰는 것입니다. 이를 통해 pipe_buffer->page 필드를 덮어쓰는 페이지가 있는 모든 메모리 위치에서 읽기/쓰기를 할 수 있습니다. 취약점 덕분에 임의 데이터를 쓸 수 있으므로 pipe_buffer의 page 필드를 포함한 전체 내용을 제어할 수 있습니다. 그러기 위해서는 취약한 kbuff 객체보다 먼저 pipe_buffer 배열을 할당하고, 이들이 서로 인접해야 합니다.
커널 메모리에 많은 kbase_kcpu_command_queue 객체를 스프레이한 후, 이어서 여러 개의 pipe_buffer 배열을 스프레이했습니다. pipe_max_size의 제한으로 인해 pipe_buffer 배열만을 주 스프레이 소스로 사용할 수 없었습니다. 따라서 먼저 kbase_kcpu_command_queue 객체로 스프레이를 시작하기로 결정했습니다. kbase_kcpu_command_queue 객체를 선택한 이유는 두 가지입니다. 할당 크기가 0x38C8이므로 페이지 할당자가 처리하며, 정보 커널 누출 버그를 사용하여 결정론적으로 커널 주소를 얻을 수 있어 스프레이에 좋은 객체이자 대상으로도 좋은 객체이기 때문입니다(다음 섹션에서 살펴보겠습니다).
앞서 언급했듯이, fcntl(F_SETPIPE_SZ)를 사용하여 pipe_buffer 배열 할당 크기를 늘려 페이지 할당자가 서비스할 수 있도록 했습니다. 더 구체적으로, kbase_kcpu_command_queue 할당과 일관성을 유지하기 위해 할당 크기를 ==0x4000 바이트 (4 * PAGE_SIZE)==로 선택했습니다.