Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
Pixel_GPU_Exploit — Pixel7/8 Pro용 Android 14 커널 익스플로잇 | Kitploit
도구/GitHubGitHub/0x36/pixel_gpu_exploit
Android SecurityPrivilege EscalationMemory ForensicsVulnerability AnalysisExploitationLearning & EducationBinary Exploitation
GitHub0x36/pixel_gpu_exploit

Pixel_GPU_Exploit

Pixel7/8 Pro용 Android 14 커널 익스플로잇

저장소 보기
5578842년 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

Mali GPU 커널 LPE

이 글은 구글에 독립적으로 식별하여 보고한, 기본 애플리케이션 샌드박스에서 접근 가능한 Mali GPU 내의 두 가지 커널 취약점에 대한 심층 분석을 제공합니다. 임의의 커널 읽기/쓰기 기능을 달성하는 커널 익스플로잇을 포함합니다. 결과적으로, SELinux를 비활성화하고 다음 Android 14 버전을 실행하는 Google Pixel 7 및 8 Pro 모델에서 root 권한을 상승시킵니다:

  • Pixel 8 Pro: google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys
  • Pixel 7 Pro: google/cheetah/cheetah:14/UP1A.231105.003/11010452:user/release-keys
  • Pixel 7 Pro: google/cheetah/cheetah:14/UP1A.231005.007/10754064:user/release-keys
  • Pixel 7: google/panther/panther:14/UP1A.231105.003/11010452:user/release-keys (by m4b4 (Marcel))

취약점

이 익스플로잇은 두 가지 취약점을 활용합니다: gpu_pixel_handle_buffer_liveness_update_ioctl ioctl 명령에서 불완전한 패치로 인한 정수 오버플로우와 타임라인 스트림 메시지 버퍼 내의 정보 유출입니다.

잘못된 정수 오버플로우 수정으로 인한 gpu_pixel_handle_buffer_liveness_update_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

  • 직렬화된 메시지 버퍼로, 특정 내용은 메시지 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); }

root@kitploit:~
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 배열을 할당하고, 이들이 서로 인접해야 합니다.

pipe_buffer와 buff 객체를 인접하게 배치하기

커널 메모리에 많은 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)==로 선택했습니다.

struct page 주소 얻기

pipe_buffer를 올바르게 사용하려면 페이지 주소가 필요합니다. 의도적으로 생성하고 파괴할 수 있는 kbase_kcpu_command_queue 객체의 커널 주소를 식별할 수 있다면, 이 객체를 사용하기 좋은 후보로 삼을 수 있으며, virt_to_page를 사용하여 해당하는 struct page를 찾을 수 있습니다.

pipe_buffer에 쓸 내용

그러므로 pipe_buffer 객체는 다음과 같습니다:```c struct pipe_buffer { struct page *page; unsigned int offset, len; const struct pipe_buf_operations *ops; unsigned int flags; unsigned long private; };

root@kitploit:~
앞서 언급했듯이, `page` 필드는 유효한 페이지 주소를 포함해야 합니다. `offset`과 `len` 필드는 `PAGE_SIZE`를 초과해서는 안 됩니다. 그렇지 않으면 파이프가 head/tail 카운터를 증가시켜 새로운 `pipe_buffer` 객체가 사용되고, 가짜 파이프 버퍼에 대한 제어를 잃게 됩니다.
또한, `flags`는 `PIPE_BUF_FLAG_CAN_MERGE`여야 합니다. 그래야 이후의 `pipe_write` 호출이 head 카운터를 무조건 증가시키고 다음 파이프 버퍼를 사용하는 대신, 먼저 현재 `pipe_buffer`에 쓰기 요청을 수용할 공간이 있는지 확인하고, 공간이 있으면 `len` 필드에 저장된 값부터 시작하여 동일한 파이프 버퍼에 데이터를 추가하기만 하기 때문입니다.
`pipe_write`와 `pipe_read`에 의해 호출되는 `pipe_buf_confirm`에서 장치가 충돌하는 것을 방지하려면, `ops` 포인터도 유효한 커널 주소여야 하며 `ops->confirm` 필드는 _NULL_로 설정되어 있어야 합니다. 유출된 `kbase_kcpu_command_queue` 객체 내에서 NULL이며 어떤 상황에서도 변하지 않는 오프셋을 간단히 사용할 수 있습니다.

### Choosing the Optimal Offset Value for Underflow
`buff`, `kbase_kcpu_command_queue` 및 `pipe_buffer`의 할당 크기는 ~0x4000~ 바이트이지만, 저는 **0x8000** 바이트로 버퍼를 언더플로우(underflow)하기로 선택했습니다. 이유는 무엇일까요?

읽기 및 쓰기 작업 중에 `pipe_buffers`가 어떻게 업데이트되는지 간략히 살펴보겠습니다. `pipe_buffer`를 다음과 같이 조작할 수 있다고 가정해 봅시다.```c
struct pipe_buffer {
	.page = virt_to_page(addr),
	.offset =  0,
	.len = 0x40,
	.ops = kcpu_addr + 0x50,
	.flags = PIPE_BUF_FLAG_CAN_MERGE,
	unsigned long private = 0
};

While the bug gives the ability to arbitrary control this content of this object, it only does so once because the underflowed object is freed immediately after the ioctl call finishes. This actually poses a problem because I need to manually update the pipe_buffer object to make it useable again since each pipe read/write operation:

  • The .page field is not updated; it remains the same, and when the buffer is empty, it is released, which I do not want to happen because the .ops field is not correctly set.
  • Because the pipe_buffer updates the .offset field on a read operation, therefore, I cannot read the same memory region again.
  • The data written to the pipe_buffer will be appended to the buffer starting from the .len value (assuming that PIPE_BUF_FLAG_CAN_MERGE flag is set) and the .len is updated accordingly. That is, we can't write data into the exact address twice.

As a result, unless I properly update the pipe_buffer after each read or write operation, I cannot read and write from/to the same pipe at the same time. That's why underflowing with 0x8000 bytes is much more practical, because instead of overwriting a single pipe_buffer, I'll overwrite two distinct pipe_buffer instances of two distinct pipes objects: one for will be considered for read and the other for write operations.```c #define PIPE_BUF_FLAG_CAN_MERGE 0x10 /* can merge buffers */

pipe_read = (struct pipe_buffer *)( ptr); pipe_read->page = virt_to_page(ta->kcpu_kaddr); pipe_read->offset = 0; pipe_read->len = 0xfff; pipe_read->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_read->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_read->private = 0;

pipe_write = (struct pipe_buffer )( ptr + 0x4000); pipe_write->page = virt_to_page(ta->kcpu_kaddr); pipe_write->offset = 0; pipe_write->len = 0; / This is the starting position of the pipe_write */ pipe_write->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_write->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_write->private = 0;

root@kitploit:~
`pipe_read`는 대상 페이지에서 `.offset = 0`부터 `0xfff` 바이트까지 데이터를 읽는 데 사용될 가짜 파이프 버퍼이며, `pipe_write`는 `.len = 0`부터 `0xfff` 바이트까지 데이터를 쓰는 데 사용될 가짜 `pipe_buffer`입니다.
다시 한 번 강조하자면, `PAGE_SIZE`보다 더 많은 바이트를 쓰면 파이프가 헤드 카운터를 증가시키므로 새로 할당된 `pipe_buffer`를 사용하게 되어 가짜 `pipe_write`에 대한 제어를 잃게 됩니다. 반면에 `fake_read` 버퍼를 비우면(0xfff 데이터를 읽으면) 커널이 `ops→release`를 호출하여 실제 페이지를 해제하게 되고, 아직 커널 텍스트 주소를 확보하지 못했기 때문에 커널이 충돌합니다.
파이프 읽기와 쓰기 작업을 분리하여 하나의 파이프 끝에서 쓰기가 다른 파이프 버퍼에 간섭하지 않도록 하고 그 반대도 마찬가지로 했지만, 핵심 문제는 아직 해결하지 못했습니다: 파이프 버퍼를 안정적으로 업데이트하는 방법은 무엇일까? 떠오른 명백한 답은 파이프 읽기 또는 쓰기 호출이 있을 때마다 스프레이 과정을 반복하는 것이었습니다. 그러나 이것은 익스플로잇 신뢰성에 심각한 영향을 미치기 때문에 말이 되지 않습니다. 다음 섹션에서는 목표를 두 가지 하위 목표로 나누겠습니다: 먼저 `.page` 필드에만 집중하고, 그 다음에 `.len/.offset` 필드를 처리하겠습니다.

### `pipe_buffer→page` 필드 수정하기

놀랍게도 `.page`를 전혀 업데이트할 필요가 없고 실제로도 업데이트하지 않습니다. 그 이유는 `pipe_buffer→page`를 덮어써서 유출된 `kbase_kcpu_command_queue`의 페이지 주소를 가리키도록 할 수 있기 때문입니다. 따라서, ** `kbase_kcpu_command_queue` 객체를 해제하고 새로운 `pipe_buffer` 객체와 겹치게 하면 됩니다. 맞아요! 이제 합법적인 `pipe_buffer` 객체를 가리키는 `pipe_buffer→page`를 갖게 되었습니다!
`kbase_kcpu_command_queue`를 `pipe_buffer`로 교체하면 정기적으로 `.page` 필드를 업데이트할 필요 없이 합법적인 파이프 버퍼를 조작할 수 있습니다. 그러나 여전히 `.len` 및 `.offset` 필드를 처리해야 합니다.

### `pipe_buffer→len/offset` 필드 수정하기

앞서 언급했듯이, 파이프 읽기/쓰기를 수행하면 `.len` 및 `.offset` 필드가 업데이트되어 동일한 페이지에 대한 후속 읽기/쓰기 작업이 두 개의 개별 파이프에서 수행되더라도 사용할 수 없게 됩니다. 여기에 또 다른 트릭이 있습니다: ** `.len/.offset` 필드를 전혀 건드리지 않고 데이터를 읽고/쓸 수 있는 기술이 있습니다! ** 이는 `pipe_read/write`에서 `copy_page_from_iter` 및 `copy_page_to_iter` 호출에 폴트를 발생시켜 달성할 수 있습니다! 네, `copy_to/from_user`와 마찬가지로 `copy_page_to/from_iter`는 `iov_iter` 구조체를 통해 전달되는 사용자 공간에서/로 데이터를 복사하며, 폴트를 발생시킬 수 있습니다.

이전 예제를 계속 진행하면, 주소에 8바이트의 데이터를 쓰려면 제공된 사용자 공간 버퍼 크기가 8이어야 하고, 그 뒤에 매핑되지 않거나 읽을 수 없는 메모리 영역이 와야 하며, `write` 시스템 호출에 크기 인수로 `9`를 전달하여 쓰려는 데이터의 양을 나타냅니다. 이 작업은 8바이트를 쓰고 아홉 번째 바이트에서 실패합니다. 그 이유는 매핑되지 않거나 읽을 수 없는 메모리 위치를 만나기 때문입니다. 결과적으로 데이터는 대상 커널 버퍼에 효과적으로 기록되었으며 `.len` 필드는 수정되지 않았습니다. `pipe_write` 커널 함수는 `buf->len` 필드를 업데이트하지 않고 그냥 반환됩니다.```c
		if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
		    offset + chars <= PAGE_SIZE) {
			ret = pipe_buf_confirm(pipe, buf);
			if (ret)
				goto out;

			ret = copy_page_from_iter(buf->page, offset, chars, from);
			if (unlikely(ret < chars)) {
				ret = -EFAULT;
				goto out;
			}

			buf->len += ret;
			if (!iov_iter_count(from))
				goto out;
		}

동일한 내용이 읽기 작업에도 적용됩니다. 8바이트를 읽으려면 버퍼의 9번째 바이트를 읽을 수 없게 만든 다음 9바이트를 읽겠다고 요청하면 .offset 필드를 변경하지 않고 데이터가 사용자 버퍼로 복사됩니다. 결과적으로, 스프레이 과정을 반복적으로 거치지 않고도 모든 커널 메모리 주소에 대해 무제한 읽기/쓰기 작업을 수행할 수 있습니다.

루트 권한 획득

이제 강력한 임의 읽기/쓰기 기본 요소를 확보했으므로, Interrupt Labs 블로그 게시물에 설명된 기법을 사용하여 VMEMMAP_START 배열의 모든 struct page를 살펴보고 커널 텍스트 시작 주소를 확인했습니다. 그런 다음 _Android 11월 보안 업데이트_에서 init_task가 null 처리되었음을 알게 되어 대신 kthreadd_task를 사용했습니다. kthreadd_task 커널 주소를 확보하여 task->tasks 목록을 탐색하고 자신의 current 태스크 커널 주소를 얻은 다음 cred 구조체를 0으로 설정하여 루트 권한을 획득할 수 있었습니다.

나중에, pipe_buffer 객체에서 이미 anon_pipe_buf_ops 커널 텍스트 주소를 가지고 있었기 때문에 모든 페이지 주소를 스캔할 필요가 없다는 것을 깨달았습니다. 이 정보를 통해 커널 텍스트 기준 주소를 추론하여 KASLR을 효과적으로 우회할 수 있었습니다.

SELinux 비활성화

익스플로잇은 SELinux도 비활성화합니다. 커널 텍스트 기준 주소를 사용하여 selinux_state 전역 구조체 위치를 찾은 다음 .enforcing 값을 0으로 설정하기만 하면 됩니다.

개념 증명

보고서에 첨부된 개념 증명은 10월 및 11월 ASB가 적용된 Android 14를 실행하는 Pixel 7 및 8 Pro 기기에서 테스트되었으며, 거의 100%의 성공률을 달성했습니다. 또한 일부 하드코딩된 오프셋을 사용하기 때문에 익스플로잇이 다른 기기에서 즉시 작동하지 않는다는 점도 중요합니다. 새 기기를 지원하려면 다음 정보를 제공해야 합니다.

  • 커널 기준 주소로부터의 kthreadd_task 오프셋
  • 커널 기준 주소로부터의 selinux_state 오프셋
  • task_struct->cred, task_struct->pid, task_struct->tasks 구조체 오프셋
  • 커널 기준 주소로부터의 anon_pipe_buf_ops 오프셋

컴파일

익스플로잇을 독립 실행형 바이너리로 컴파일하려면 다음 명령어를 사용한 후 adb shell로 실행하세요.```sh $ aarch64-linux-androidXX-clang++ -static-libstdc++ -w -Wno-c++11-narrowing -DUSE_STANDALONE -o poc poc.cpp -llog $ adb push poc /data/local/tmp/ $ adb shell /data/local/tmp/poc

root@kitploit:~
이 디렉토리를 Android Studio 앱에 포함시켜 익스플로잇을 실행할 수도 있으며, cmake 파일에 `-w -Wno-c++11-narrowing`을 추가하여 쓸모없는 C++ 경고를 비활성화해야 합니다.

### Demo```shell
$ adb logcat  |grep -i EXPLOIT
11-28 16:04:12.500  7989  7989 E EXPLOIT : [+] Target device: 'google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys' 0xa9027bfdd10203ff 0xa90467faa9036ffc
11-28 16:04:15.563  7989  7989 E EXPLOIT : [+] Got the kcpu_id (0) kernel address = 0xffffff8901390000  from context (0x0)
11-28 16:04:18.441  7989  7989 E EXPLOIT : [+] Got the kcpu_id (255) kernel address = 0xffffff89b0bf8000  from context (0xff)
11-28 16:04:18.442  7989  7989 E EXPLOIT : [+] Found corrupted pipe with size 0xfff
11-28 16:04:18.442  7989  7989 E EXPLOIT : [+] SUCCESS! we have a fake pipe_buffer (0)!
11-28 16:04:18.444  7989  7989 E EXPLOIT : 10 00 39 01 89 FF FF FF  10 00 39 01 89 FF FF FF  | ..9.......9.....
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 B0 CD 12 C0 FF FF FF  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.445  7989  7989 E EXPLOIT : [+] Freeing kcpu_id = 0 (0xffffff8901390000)
11-28 16:04:18.446  7989  7989 E EXPLOIT : [+] Allocating 61 pipes with 256 slots
11-28 16:04:18.462  7989  7989 E EXPLOIT : [+] Successfully overlapped the kcpuqueue object with a pipe buffer
11-28 16:04:18.463  7989  7989 E EXPLOIT : 40 AB BA 26 FE FF FF FF  00 00 00 00 30 00 00 00  | @..&........0...
11-28 16:04:18.463  7989  7989 E EXPLOIT : 70 37 8D F1 DA FF FF FF  10 00 00 00 00 00 00 00  | p7..............
11-28 16:04:18.463  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00                           | ........
11-28 16:04:18.463  7989  7989 E EXPLOIT : [+] pipe_buffer {.page = 0xfffffffe26baab40, .offset = 0x0, .len = 0x30, ops = 0xffffffdaf18d3770}
11-28 16:04:18.463  7989  7989 E EXPLOIT : [+] kernel base = 0xffffffdaf0010000, kthreadd_task = 0xffffff8002da3780 selinux_state = 0xffffffdaf28a3168
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Found our own task struct 0xffffff88416c5c80
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Successfully got root: getuid() = 0 getgid() = 0
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Successfully disabled SELinux
11-28 16:04:20.102  7989  7989 E EXPLOIT : [+] Cleanup  ... OK
도구 다운로드