CVE-2023-48409 Mali GPU overflow
2025년 7월 13일
ref #
- https://github.com/0x36/Pixel_GPU_Exploit
- https://starlabs.sg/blog/2025/06-solo-a-pixel-6-pro-story-when-one-bug-is-all-you-need/
이 취약점의 PoC는 pixel 7, pixel 8 의 특정 이미지에서 실행 가능한 코드를 담고있다. 글을 좀 더 찾아보니 pixel 6 pro에서도 악용한 글이 있었고 내가 가지고있는 pixel 6a 에서도 취약점이 존재할 수 있을 것 같아서 따라해볼 예정이다.
CVE-2023-48409 #
취약점 개요 #
Mali GPU는 저가 안드로이드 기기에서 자주 사용되는 ARM GPU 이다. 이 취약점은 Mali GPU 커널 드라이버 코드 중 Pixel 전용 커스터마이징 확장 코드(벤더용 커널 모듈)에서 추가된 ioctl에서 발견됐다.
gpu_pixel_handle_buffer_liveness_update_ioctl() 에서 64bit 정수 오버플로우가 발생하여 잘못 계산된 크기로 힙을 할당하고, 할당된 버퍼의 앞쪽에 OOB가 발생하여 인접한 커널 객체를 덮어쓰게 된다.
이로인해 페이지테이블이나 cred 구조체를 수정하여 SELinux를 꺼버리고, root 셸까지 획득할 수 있게된다.
이 패치커밋 에서 조치되었으며, pixel 6a 기준으로는 qpr1 버전의 이미지부터 커밋이 포함되어있다.
1PS C:\Users\kdh\Documents\GitHub\gpu> git branch -a --contains 5dec6c | grep bluejay
2 remotes/origin/android-gs-bluejay-5.10-android14-qpr1
3 remotes/origin/android-gs-bluejay-5.10-android14-qpr1-beta
4 remotes/origin/android-gs-bluejay-5.10-android14-qpr2
5 remotes/origin/android-gs-bluejay-5.10-android14-qpr2-beta
커밋이 포함된 기기들은 tegu, tangorpro, shusky, pantah, raviole, lynx, felix, comet, caimito, bluejay, akita 이런식인데, 두개의 기기에서 같이 사용하는 버전은 이름이 합쳐진 것을 알 수 있다.
ex) shusky = shiba (Pixel 8) + husky (Pixel 8 Pro)
UP1A = 최초릴리즈, UQ1A = qpr1, AP1A = qpr2, AP2A = qpr3 이기 때문에 UP1A 로 시작하는 이미지를 사용하면 취약점을 트리거할 수 있을것이다.
사전지식 #
ioctl #
커널과 유저영역 프로그램 사이에서 입출력 제어 명령을 전달하기 위한 시스템콜이다.
일반 파일을 대상으로 사용하는 read, write 로 처리하기 어려운 디바이스별 특수한 제어 작업을 수 행할때 사용한다.
1// 터미널 크기 조회
2struct winsize ws;
3ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
4
5// 파일 속성 조회
6int flags;
7ioctl(fd, FS_IOC_GETFLAGS, &flags);
8
9// Mali GPU의 liveness 업데이트 명령 (보통 vulkan, opengl 같은 API를 통해 사용됨)
10int fd = open("/dev/mali0", O_RDWR);
11struct update_data data = { ... };
12ioctl(fd, GPU_IOCTL_LIVENESS_UPDATE, &data);
CONFIG_HARDENED_USERCOPY #
취약 코드 #
1int gpu_pixel_handle_buffer_liveness_update_ioctl(struct kbase_context* kctx,
2 struct kbase_ioctl_buffer_liveness_update* update)
3{
4 int err = 0;
5 struct gpu_slc_liveness_update_info info;
6 u64* buff;
7
8 /* Compute the sizes of the user space arrays that we need to copy */
9 u64 const buffer_info_size = sizeof(u64) * update->buffer_count; // [1]
10 u64 const live_ranges_size =
11 sizeof(struct kbase_pixel_gpu_slc_liveness_mark) * update->live_ranges_count;
12 /* Nothing to do */
13 if (!buffer_info_size || !live_ranges_size)
14 goto done;
15
16 /* Guard against nullptr */
17 if (!update->live_ranges_address || !update->buffer_va_address || !update->buffer_sizes_address)
18 goto done;
19 /* Allocate the memory we require to copy from user space */
20 buff = kmalloc(buffer_info_size * 2 + live_ranges_size, GFP_KERNEL); // [2]
21
22 /* Set up the info struct by pointing into the allocation. All 8 byte aligned */
23 info = (struct gpu_slc_liveness_update_info){ // [3]
24 .buffer_va = buff,
25 .buffer_sizes = buff + update->buffer_count,
26 .live_ranges = (struct kbase_pixel_gpu_slc_liveness_mark*)(buff + update->buffer_count * 2),
27 .live_ranges_count = update->live_ranges_count,
28 };
29 /* Copy the data from user space */
30 err =
31 copy_from_user(info.live_ranges, u64_to_user_ptr(update->live_ranges_address), live_ranges_size);
32 if (err) {
33 dev_err(kctx->kbdev->dev, "pixel: failed to copy live ranges");
34 err = -EFAULT;
35 goto done; // [4]
36 }
37
38...
39
40done:
41 kfree(buff);
42 return err;
43}
패치 내역 #
<br>
## pixel 6a에서 트리거하기