
비특권 개념 증명 및 Linux 커널 Binder use-after-free(CVE-2026-64468)에 대한 x86_64 로컬 권한 상승, KASAN 실습 환경 및 취약/수정 버전 차이 분석 포함.
binder_free_transaction() 프로세스 수명 use-after-free
이 저장소는 업스트림 커밋
f223d27a546c1e1f48d38fd67760e78f068fe8c4으로
수정된 Linux 커널 Binder use-after-free에 대해 다음을 포함합니다:
binder_chain_64468.c — 버그에 도달하여 커널이 이를 입증하도록 하는 권한 없는 개념 증명으로, KASAN 실험실과 취약/수정 차등 테스트(lab/, run.sh, verify.sh)를 포함합니다.exploit.c — x86_64용 독립형 로컬 권한 상승 코드입니다. gcc -O2 -pthread -o exploit exploit.c로 컴파일되며, 일반 사용자로 실행되어 루트 셸로 끝납니다.demo/ — 패치되지 않은 커널에서 실제 Debian 13 사용자 영역을 부팅하는 실험실로, 익스플로잇이 대상 시스템의 자체 gcc로 컴파일되고, 이후 장악하는 그 머신에서 실행될 수 있습니다.모든 것은 어떤 종류의 패치도 없는 스톡 업스트림 커널을 대상으로 일반 사용자(uid/gid 1000, capabilities 없음, namespaces 없음)로 실행됩니다.
경고
이 코드는 의도적으로 커널 객체 수명을 경쟁(race)시킨 후 커널 제어 흐름을 탈취합니다. 경쟁에서 패배하면 커널 힙 상태가 손상되어 머신이 패닉하거나 멈출 수 있습니다. 소유한 격리된 일회용 VM에서만 실행하십시오. 호스트에서는 실행하지 마십시오.
binder_free_transaction()은 t->lock 하에서 트랜잭션에서 대상 프로세스를 읽고, 해당 잠금을 해제한 다음, 대상의 내부 잠금을 획득합니다:```c
spin_lock(&t->lock);
target_proc = t->to_proc;
spin_unlock(&t->lock);
if (target_proc) {
binder_inner_proc_lock(target_proc); /* use after free */
Nothing keeps `target_proc` alive across that gap. A process that is being torn
down in parallel can reach `binder_proc_dec_tmpref() -> kfree()` in between, so
the lock is taken on freed memory. The upstream fix pins `t->to_thread` while
`t->lock` is still held, which keeps the owning process alive until the inner
lock has been used and released.
The vulnerability was reported by **Alice Ryhl** and fixed by **Carlos
Llamas**, both of Google. The
[original report](https://lore.kernel.org/all/[email protected]/)
carries the reference KASAN trace.
### 취약한 접근에 도달하기
*외부* `to_proc`에 도달할 수 있는 유일한 호출자는
`binder_send_failed_reply()`이며, `t->from`이 `NULL`일 때만
`t->from_parent`로 이동합니다:```c
target_thread = binder_get_txn_from_and_acq_inner(t);
if (target_thread) { ...; binder_free_transaction(t); return; }
next = t->from_parent;
binder_free_transaction(t);
t = next;
from_parent는 정확히 한 곳에서 할당되며, 그 할당은 몇 줄 앞서 binder의 잘못된 트랜잭션 스택 검사에 의해 보호됩니다. 즉, 스레드는 자신의 스택 최상단이 수신 중인 트랜잭션일 때만 동기 트랜잭션을 보낼 수 있습니다. 따라서 해당 체인의 모든 링크에 대해 자식의 송신자와 부모의 수신자는 동일한 스레드입니다.
이는 뚜렷한 결과를 낳습니다. binder_thread_release()는 proc->inner_lock을 보유한 채 종료되는 스레드의 스택을 순회하며, 다음 두 가지를 모두 기록합니다.```
iteration j : [holds child->lock ] child->from = NULL ; unlock child->lock
spin_lock(parent->lock)
iteration j+1 : [holds parent->lock] parent->to_proc = NULL
in **인접한 단일 워크의 반복**에서, 각각은 해당
`t->lock` 하에 있습니다. 워커는 워크가 `child->lock`을 해제한 후에만
`child->from == NULL`을 알게 되며, 자체 스냅샷을 위해 `parent->lock`이 필요합니다.
따라서 전체 기회는 해당 워크의 `spin_unlock(&child->lock)`과
`spin_lock(&parent->lock)` 사이의 간격 — 몇 개의 명령어에 불과합니다. 해제 워크는
스핀락을 보유하고 있어 그 지점에서 선점될 수 없습니다. 오직 인터럽트만이 이를 지연시킬 수 있습니다.
이것이 레이스가 좁은 이유이며, 개념 증명과 익스플로잇이 모두
확률적(probabilistic)인 이유입니다.
### 개념 증명이 구축하는 것
`binder_chain_64468.c`는 취약한 접근에 도달하는 가장 짧은 체인을 구성하므로,
워커가 그 간격 내에서 수행하는 작업은 binder가 허용하는 한 최소화됩니다 —
두 번의 `t->lock` 획득과 한 번의 `kfree()`, 외부 `inner_proc_lock` 없이,
`wake_up` 없이, 그리고 응답 전달 없이:```
B thread i --e2 (sync, code 0x4442414b)--> P thread Y_i
P thread Y_i --e1 (sync, code 0x54414c4c)--> B thread i (nested target)
B thread i stack: [ e2 outgoing , e1 incoming (top) ]
P thread Y_i stack: [ e2 incoming , e1 outgoing (top) ]
P가 바인더 fd를 닫으면 binder_deferred_release()가
Y_1..Y_K를 해제하고 마지막으로 binder_proc를 해제하는 동안, 모든 B 스레드가
동시에 BINDER_THREAD_EXIT를 실행합니다:```
binder_thread_release(B_i) nulls e1->to_proc (own proc) and e2->from
binder_send_failed_reply(e1) e1->from == NULL once Y_i was released
-> binder_free_transaction(e1) target_proc already NULL, kfree(e1)
-> binder_free_transaction(e2) target_proc == P <-- vulnerable access
세 번째 프로세스는 바인더 컨텍스트 매니저로, 다른 두 프로세스가 필요로 하는 핸들을 나눠주는 용도로만 사용된다. `exploit.c`는 바로 이 구성을 그대로 재사용한다.
### 두 개의 독립적인 조건
KASAN 리포트가 발생하려면 다음 두 가지가 모두 필요하다:
* **취약한 접근** — 워커가 위의 좁은 창 안에서 `parent->lock`을 획득하여 아직 살아있는 `to_proc`의 스냅샷을 찍어야 한다. 그리고
* **창 안에서의 해제(free) 도달** — 워커가 `t->lock`을 놓은 뒤 피해자의 내부 잠금을 획득하기 전에 CPU를 빼앗겨야 하며, 지연 해제(deferred release)가 완료되어 `binder_proc`가 해제될 때까지 CPU를 되찾지 못해야 한다.
첫 번째 조건에는 KASAN이 필요 없는 자체 오라클(oracle)이 있다. 바인더는 다음을 출력한다:```
binder: binder_free_proc: Unexpected outstanding_txns -1
whenever it happens, because the walker and binder_thread_release() then both
decrement the same counter for one transaction. Note this is not a
vulnerable/fixed differential by itself — the fix stops the free, not the second
decrement — so it appears on both kernels. It is used here only to show the
vulnerable code path being exercised.
The proof of concept stops at a 4-byte decrement of freed memory. Turning that into uid 0 needs four things, and none of them come from the bug itself: the bug leaks nothing.
struct binder_proc is 648 bytes and is allocated with a plain GFP_KERNEL
kzalloc — not __GFP_ACCOUNT. It therefore lands in kmalloc-1k,
together with every other unaccounted allocation of that size, and is not
isolated behind kmalloc-cg-*. That single fact is what makes the object
reclaimable at all.
The moment of the kfree() is not observable from userspace, and neither is the
moment the walker touches the object again, so there is nothing to time against.
The spray therefore runs as a pump: it allocates and frees kmalloc-1k
objects continuously, from the CPU that ran the deferred release, for as long as
the walkers are running.
System V messages are used for it. alloc_msg() is a plain unaccounted
kmalloc of a 48-byte header plus payload, so a 976-byte message is a 1024-byte
allocation; the quota is per queue rather than per uid; and msgrcv() frees
synchronously. Measured throughput: ~198,000 allocations per second, zero
failures.
add_key/user_key_payload was tried first and is a trap. Its payload is
charged against a per-uid byte quota (kernel.keys.maxbytes, 20000 by default)
which is released only when the key garbage collector destroys the key, so a
tight allocate/free loop exhausts it in milliseconds: measured 27,151
successful allocations against 2,121,009 failures — 98.7% of the spray silently
doing nothing, which looks exactly like a spray that never wins the slot.
KEYCTL_INVALIDATE made it worse (4,775 successes), because it queues GC work.
The walker touches four fields of the freed binder_proc (offsets measured with
pahole on the target build):
With outstanding_txns == 1 and is_frozen == 1, the decrement reaches zero and
the walker calls wake_up_interruptible_all(&proc->freeze_wait).
__wake_up_common then computes curr = head.next - 24 and calls
*(head.next - 8): a function pointer read from wherever head.next points,
which is a value the reclaimed object supplies.
That pointer has to reach memory the attacker controls, at a kernel address, and the bug leaks nothing. Both addresses come from prefetch timing instead — the same channel as KASLD (Brendan Coles, MIT), whose implementation this derives from:
커널 텍스트. A prefetch of a mapped kernel address resolves in the page
table walk and retires measurably faster than one of an unmapped address, even
though the access never becomes architecturally visible. Scanning the 2 MiB
slots of the text range shows the image as a run of fast slots; its first slot
is _text.
직접 매핑. With CONFIG_RANDOMIZE_MEMORY the direct map is
randomised in 1 GiB units, so it has to be located too. Unlike the text it
covers all of RAM, so it is the longest contiguous run of mapped slots. Two
refinements were needed to make this usable:
What the run reliably starts at is not page_offset_base itself but the first
slot the kernel could map with a 1 GiB page — the one covering physical 4 GiB.
Below that, the PCI hole and firmware reservations force 2 MiB pages whose
longer walk is not separable from unmapped here. That slot is exactly what the
spray needs, so it is what is used.
Then ~60% of physical memory is filled with copies of one crafted 4 KiB page, so a fixed offset from that anchor is backed by the crafted page whatever the layout turns out to be.
freeze_wait.head.next -> entry1 (in the sprayed page)
entry1.func = mov 0x28(%rdi),%rdi ; mov 0x18(%rdi),%rax ; jmp *0x58(%rax) RDI <- entry1+0x28 = &cred RAX <- cred+0x18 jmp *(RAX+0x58) = commit_creds -> commit_creds(cred)
entry2.func = mov $-1,%rax ; ret __wake_up_common breaks out on ret < 0
스택 피벗도 없고 `iretq`도 없다: 하이재킹된 스레드는 `ioctl()`에서 정상적으로 반환되어 단순히 루트 권한으로 사용자 공간으로 돌아온다.
### 위조된 cred와, 그것을 낭비했을 버그
디스패처 가젯은 `cred+0x18`에서 `RAX`를 가져오며, `cred+0x18`은 `euid`/`egid`이므로 체인 직후 `euid`는 커널 포인터의 하위 절반이 된다. 이는 표면적인 문제일 뿐이다. 표면적이지 않은 문제는 `prepare_creds()` — **이후의 모든 `fork()`와 `execve()`가 호출하는 함수** — 가 NULL 검사 없이 세 개의 필드를 역참조한다는 점이다:```c
get_group_info(new->group_info); /* refcount_inc(&gi->usage) */
get_uid(new->user); /* refcount_inc(&u->__count) */
new->ucounts = get_ucounts(new->ucounts);
위조된 자격 증명(cred)이 NULL로 남으면 uid 0이 되지만, 첫 번째 execve에서 커널이 패닉(panic)을 일으킨다 — 즉, 익스플로잇이 성공을 보고한 직후 시스템을 파괴하게 된다. 따라서 체인은 user, ucounts, group_info를 실제 커널 전역 변수인 root_user, init_ucounts, init_groups를 가리키도록 설정하며, 이들의 주소는 동일한 _text 베이스에서 얻어진다. 이 값들이 설정되면, 루트 권한을 획득한 스레드는 init_user_ns에 대해 CAP_SETUID를 보유하게 되므로 setresuid(0,0,0)을 호출하고, 커널은 위조된 cred 위에 깨끗하고 커널이 할당한 루트 cred를 설치한다. 그 이후에야 다른 작업이 수행된다.
권한 상승은 익스플로잇 바이너리의 setuid-root 복사본을 통해 부모 프로세스에 전달되며, 이 복사본은 셸을 exec하기 전에 스스로를 삭제(unlink)하므로 루트 셸은 깨끗한 터미널에서 메인 프로세스로 실행되고 setuid 파일은 남지 않는다.
uname -r만으로는 충분하지 않다. 벤더 커널은 해당 메인라인 버전을 변경하지 않고 binder 수정 사항을 백포트하는 경우가 일반적이다. 소스 또는 패키지 변경 로그를 확인해야 한다.
수정 사항은 Cc: stable로 태그되어 있으므로 stable 및 벤더 브랜치에 백포트가 적용된다.
이 둘은 서로 다른 질문이며, 두 번째 질문이 영향 범위를 결정한다.
취약한 코드는 CONFIG_ANDROID_BINDER_IPC가 설정된 곳이면 어디서든 컴파일된다. 여기에는 범용 배포판도 포함되지만, 조사된 모든 배포판에서 드라이버는 기본적으로 로드되지 않는 모듈이며, 로드되더라도 init_binder_device()는 miscdev.mode를 설정하지 않고 misc 디바이스를 등록하므로 devtmpfs는 /dev/binder를 0600 root:root로 생성한다. Android에서는 ueventd가 이를 0666으로 열어주는데, 이것이 바로 이 버그가 Android에서 중요하고 여기서는 대부분 그렇지 않은 이유다.
배포판 자체가 제공하는 커널 패키지에서 읽은 구성:
따라서 데스크톱 배포판에서의 실제 노출은 간접적이다: binder를 로드하고 이를 열어주는 모든 것 — Waydroid, Anbox, Android 에뮬레이터 또는 컨테이너 런타임** — 은 커널에 여전히 버그가 있는 시스템에서 Android의 도달 가능성을 정확히 다시 도입한다.
이들은 취약점에는 영향을 미치지 않는다. 이 익스플로잇에 영향을 미친다.
두 커널 모두 표준 업스트림 트리이다. 아무것도 패치되지 않았다.
첫 번째 실험실의 CONFIG_KASAN_GENERIC은 활성화 도구가 아니라 탐지기이다: 레이스(race)는 이 옵션 없이도 동일하다. CONFIG_PREEMPT는 실제 전제 조건이며 Android가 제공하는 것이다.
아키텍처는 버그에는 영향을 미치지 않는다 — 이는 아키텍처 독립적인 C 코드의 수명(lifetime) 오류이다. 그러나 익스플로잇에는 매우 큰 영향을 미친다: 가젯, prefetch 채널, 직접 매핑(direct-map) 레이아웃은 모두 x86_64 전용이다.
요구 사항: clang, lld, make, cpio, gzip, qemu-system-x86_64, docker (Debian rootfs 조립에만 필요), Linux git 트리의 로컬 클론, 그리고 gcc.
gcc -O2 -pthread -o exploit exploit.c ./exploit
커널이 `demo/`에 있는 것과 다른 경우, 먼저 해당 오프셋을 추출하세요:```sh
./mkoffsets.sh /path/to/vmlinux > offsets.h
gcc -O2 -pthread -DEXPLOIT_OFFSETS='"offsets.h"' -o exploit exploit.c
조정 가능한 변수들(모두 선택 사항이며, 모두 환경에서 읽어옵니다):
CVE64468_SECONDS, CVE64468_THREADS, CVE64468_SPRAY_PERCENT,
CVE64468_CALL_OFFSET_MB, CVE64468_KASLR_ATTEMPTS, CVE64468_DELAY_MAX_US,
CVE64468_DELAY_STEP_US, CVE64468_STAGGER_US, CVE64468_VERBOSE,
CVE64468_SHELL.
LINUX_GIT=/path/to/linux ./lab/build.sh # x86_64 LINUX_GIT=/path/to/linux TARGET_ARCH=arm64 ./lab/build.sh # arm64 ./run.sh vulnerable ./verify.sh 600 16
The script creates two detached worktrees at the commits above, refuses to run
if either worktree is dirty, verifies that the vulnerable tree lacks the fix and
the fixed tree carries it, builds both kernels, and packs the proof of concept
into an initramfs.
### The exploitation laboratory```sh
./demo/build-kernel.sh # vulnerable kernel, KASAN off, hardening on
./demo/build-rootfs.sh # Debian 13 userland + gcc, as an initramfs
./demo/run-demo.sh # boot it; this is what the recording shows
./demo/verify.sh logs/ # unattended reliability run, one guest per trial
demo/run-demo.sh는 게스트를 부팅하고 콘솔을 uid 1000에 넘겨주며, 게스트 자체의 gcc로 exploit.c를 컴파일하고 실행합니다.
커널 이미지, 작업 트리, rootfs 트리 및 initramfs는 실험실 산출물이므로 버전 관리 대상이 아닙니다.
docs/example-output.txt는 실제 취약 커널 실행 기록으로 KASAN 보고서를 포함하며, docs/patched-negative-output.txt는 수정된 커널의 대조군이고, docs/e2e-results.json은 기계가 읽을 수 있는 결과입니다.
보고된 호출 체인은 상위 보고서와 정확히 일치합니다:``` BUG: KASAN: slab-use-after-free in queued_spin_lock_slowpath+0x62/0x6a0 Read of size 4 at addr ffff888100282270 by task cve64468-B/91 CPU: 6 UID: 1000 PID: 91 Comm: cve64468-B Not tainted 7.2.0-rc1+ #2 PREEMPT _raw_spin_lock+0x55/0x60 binder_free_transaction+0x80/0x1f0 binder_send_failed_reply+0x98/0x340 binder_thread_release+0x528/0x5e0 binder_ioctl+0x1ca/0xeb0 __x64_sys_ioctl+0x8c9/0xcf0
Allocated by task 93: binder_open+0xb5/0x7b0
Freed by task 9: kfree+0x113/0x310 binder_deferred_func+0x104b/0x1180 process_scheduled_works+0x69f/0xbb0
`UID: 1000`은 개념 증명 자체의 권한 없는 사용자 ID이며, 피해자
`binder_proc`는 `binder_open()`에 의해 할당되고, binder 지연
워크큐에 의해 해제되며, 해제 이후 워커에 의해 읽힙니다.
기록된 실행, 2026-08-16, `./verify.sh 1200 16 3` — 변형당 동시 QEMU/KVM
게스트 3개, 각각 10 vCPU, 16 스레드, 변형당 1200초, **양쪽 모두 커널
패치 없음**:
| | 취약한 `114a116aaa5f` | 패치된 `f223d27a546c` |
| --- | --- | --- |
| 시도 횟수 | 158,384 | 158,471 |
| 워크 | 2,534,144 | 2,535,536 |
| 설정 실패 | 0 | 0 |
| 취약한 접근 (`Unexpected outstanding_txns -1`) | 1,127 | 934 |
| **KASAN `slab-use-after-free`** | **8** | **0** |
이것이 차이입니다: 동일한 워크로드, 0.06% 이내의 동일한 시도 횟수,
그리고 use-after-free는 패치되지 않은 취약한 커널에서만 발생합니다.
취약한 접근은 *두* 커널 모두에서 나타나며, 이는 예상된 결과입니다 — 패치는
프로세스가 윈도우 내부에서 해제되는 것을 막을 뿐, `outstanding_txns`의 두
번째 감소를 막지는 않습니다. 그래서 해당 줄은 값싼 오라클로만 사용되며
차이 지표로는 절대 사용되지 않습니다.
### 권한 상승

`docs/lpe-output.txt`는 데모 게스트의 실제 트랜스크립트이며,
`docs/lpe-demo.cast`는 위 애니메이션이 렌더링된 전체, 편집되지 않은
Asciinema 녹화입니다 (`asciinema play docs/lpe-demo.cast`로 전체를
재생할 수 있습니다). 애니메이션은 해당 녹화의 긴 레이싱 중간 부분을
생략합니다 — 게스트의 시리얼 콘솔이 약 24분간의 레이스 전체에 걸쳐 binder
디버그를 스트리밍하며, 이는 수십 메가바이트의 스크롤 로그로 렌더링될
것입니다 — Debian 서문과 root 셸은 유지하고, exploit 자체의
`hit after 26661 attempts ... in 1437s` 줄이 정확히 무엇이 생략되었는지
명시합니다. 둘 다 단일 실행입니다: 예산 내에서 승리하지 못한 게스트는
전원이 꺼지고, 녹화는 승리로 편집되는 대신 단순히 반복됩니다.
측정된 성공률은 아래 *신뢰성*을 참조하십시오.
## 신뢰성
레이스는 두 측면 모두 확률적이므로, 실패한 실행은 때때로 예상되는
동작이지, 손상된 exploit이 아닙니다.
### 메모리 안전성
위 실행에서 도출된 취약한 커널의 비율:
| 수량 | 값 |
| --- | --- |
| 시도 비율 | 게스트당 ~44회/초, 3개에 걸쳐 ~132회/초 |
| 취약한 접근 | 시도당 7.1e-3 |
| 접근이 주어졌을 때 윈도우 내부에 해제가 안착할 확률 | 7.1e-3 |
| KASAN 보고 | 약 20,000회 시도당 1회, 즉 이 비율에서 약 2.5분마다 1회 |
### 권한 상승
각 게스트는 독립적인 시행입니다: 자체 KASLR, 자체 direct-map
무작위화를 가지며, 승리한 게스트는 레이싱을 중단합니다. 따라서 이 수치는
시도당이 아닌 **부팅당 성공률**입니다.
기록된 캠페인, 2026-08-16 23:32 UTC, `GUESTS=6 CPUS=8 MEMORY_MB=9216 ./demo/verify.sh`
— 6개의 동시 QEMU/KVM 게스트, 패치되지 않은 `114a116aaa5f`, Debian 13
유저랜드, 각각 60분 예산, 게스트 내부에서 컴파일된 exploit, uid 1000으로 시작:
| | |
| --- | --- |
| uid 0에 도달한 게스트 | **6개 중 2개** |
| root까지의 시간 | 623초 및 1,344초 |
| 승리한 레이스에서의 시도 횟수 | 13,839 및 31,125 |
| 승리하지 못한 각 게스트의 시도 횟수 | 전체 3,600초 동안 약 95,000회 |
| 캠페인 전체 시도 횟수 | 427,676 (6.8M 스택 워크) |
| 관찰된 취약한 접근 (`Unexpected outstanding_txns -1`) | 256 |
| **커널 크래시, oops 또는 패닉** | **0**, 9 게스트-시간 동안 |
그 표에서 읽어낼 가치가 있는 두 가지가 있습니다.
**본질적으로 승리한 모든 레이스가 root가 되었습니다.** KASAN 실험실은
취약한 접근이 주어졌을 때 해제가 윈도우 내부에 안착할 확률을 7.1e-3으로
측정합니다. 여기서 관찰된 256개의 접근에 적용하면 캠페인 전체에서 약 1.8개의
use-after-free가 예측됩니다 — 그리고 2개의 root가 얻어졌습니다. 재확보,
주소 발견 및 체인은 병목이 아닙니다. 레이스가 병목입니다.
**아무것도 크래시되지 않았습니다.** 9 게스트-시간 동안 어떤 게스트도 oops를
발생시키지 않았으며, 승리하지 못한 4개를 포함합니다. 체인은 올바르게
위치하고 스프레이된 페이지에 대해 발화하거나, 전혀 발화하지 않습니다 —
이것이 2단계의 다수결 거부가 존재하는 이유입니다.
그 거부는 실제로 발화합니다. 이미 부하가 걸린 호스트에서 한 번에 6개의
게스트를 부팅했을 때, 2단계에서 포기한 게스트가 하나 발생했으며 다음과
같은 메시지가 나왔습니다.```
[*] direct map not found; refusing to fire at an unverified address
및 경쟁 없이 종료되었습니다. 이것이 의도된 동작입니다: 타이밍 채널이 합의에 도달할 수 없을 때 낭비된 부팅이 올바른 결과이며, 확인된 적 없는 주소에서 체인을 발사하는 대안보다 훨씬 낫습니다.
docs/lpe-results.json은 사용된 정확한 커널과 initramfs의 SHA-256을 포함한 기계 판독 가능 형식을 담고 있습니다.
게스트를 동시에 실행하는 것은 단지 병렬 처리만을 위한 것이 아닙니다. 부하가 많은 호스트에서 KVM은 게스트 vCPU를 디스케줄링하며, 이것이 바로 두 번째 조건이 필요로 하는 지연입니다: 워커는 t->lock을 해제한 후 피해자의 내부 잠금을 획득하기 사이에 CPU를 잃어야 합니다. 유휴 호스트의 단일 게스트는 세 개의 동시 게스트의 취약 접근률의 약 20분의 1로 측정되었습니다.
하지만 상한선이 있습니다. 32스레드 호스트에서 각각 8 vCPU를 가진 8개 게스트, 게스트당 약 5GiB의 직접 매핑 스프레이로 호스트가 스왑에 들어갔고 8개 게스트 중 3개는 전혀 진행하지 못했습니다. 6개가 기본 제공 값입니다.
선택적 게스트 내 선점 헬퍼 스레드는 적중률을 개선하지 않으면서 시도율을 약 3배 증가시키는 것으로 측정되어 사용되지 않습니다.
한 가지 환경 요인이 예상보다 더 중요하게 작용했습니다: binder 자체의 디버그 출력. binder.debug_mask가 기본값일 때 드라이버는 경쟁 중에 대량의 속도 제한 pr_info 트래픽을 방출하며, 이로 인해 발생하는 printk 및 콘솔 잠금 압력은 두 번째 조건이 필요로 하는 정확한 선점 창을 늘립니다. 녹화를 위해 콘솔을 깔끔하게 하려는 명백한 방법인 binder.debug_mask=0으로 이를 끄면 테스트에서 적중률이 눈에 띄게 낮아졌습니다: 게스트는 적중 없이 두 승자의 시도 횟수를 훨씬 넘어 실행되었습니다. 따라서 demo/run-demo.sh는 binder 디버그를 기본값으로 두며, 조용한 콘솔은 명시적 옵트인입니다. 이것은 실험실의 속성일 뿐 익스플로잇의 속성이 아닙니다 — 하지만 이 경쟁이 익스플로잇 자체가 제어하는 어떤 것보다 시스템 전반의 타이밍 지터에 얼마나 의존하는지를 잘 보여줍니다.
/dev/binder를 열고 binder 트랜잭션을 보내고 binder 스레드를 종료할 뿐입니다. 아무것도 설치하지 않으며 아무것도 남기지 않습니다.panic=1 oops=panic으로 부팅되어 손상된 상태에서 계속 진행하는 대신 실행이 종료됩니다. 항상 깨끗한 부팅에서 다시 시작하십시오.<[email protected]> (트위터:
@aramosf)2026-08-16에 SearchSploit(로컬 Exploit-DB 사본) 및 웹 검색으로
CVE-2026-64468, binder_free_transaction 및
f223d27a546c를 확인했습니다. 이 CVE에 대한 공개 익스플로잇이나 개념 증명은 발견되지 않았습니다.
SearchSploit은 관련 없는 오래된 Android binder 항목만 반환합니다. 이는
특정 시점의 확인일 뿐 영구적인 보장이 아닙니다.
| 상태 | 주장 |
|---|
| 확인됨 | 취약점은 실제이며, 권한 없는 프로세스에서 도달 가능하고, 업스트림 수정이 이를 제거합니다. |
| 입증됨 | 소멸 중인 binder_proc의 취약한 역참조가 패치되지 않은 커널에서 자연스럽고 반복적으로 발생하며, 패치된 커널에서는 절대 발생하지 않습니다. |
| 입증됨 | KASAN이 보고한 전체 use-after-free가 패치되지 않은 커널에서 발생합니다. |
| 입증됨 | 공격자가 제어하는 바이트로 해제된 binder_proc을 재확보하고, 커널 제어 흐름을 탈취하며, x86_64에서 권한 없는 사용자로부터 uid 0으로의 권한 상승이 발생합니다. |
| 주장하지 않음 | 특정 벤더 또는 Android 기기에 대한 어떤 결과도 없습니다. 아래 나열된 업스트림 커널만 x86_64에서 테스트되었습니다. |
| 주장하지 않음 | 제공된 익스플로잇이 수정 없이 배포판 커널에서 동작한다는 것. 영향을 받는 시스템을 참조하십시오: 커널별 오프셋이 필요하며, 조사된 모든 범용 배포판에서 binder 장치는 애초에 권한 없는 사용자가 접근할 수 없습니다. |
| Offset | Field | What the walker does |
|---|
| 108 | int outstanding_txns | decrements it |
| 113 | bool is_frozen | reads it |
| 120 | wait_queue_head_t freeze_wait | walks it if outstanding_txns == 0 && is_frozen |
| 624 | spinlock_t inner_lock | takes and releases it |
| 상태 | 커밋 | 비고 |
|---|
| 취약점 도입 계열 | a370003cc301 | 업스트림 Fixes: 태그로 명명됨 |
| 취약점 검증됨 | 114a116aaa5f | 수정 커밋의 직접 부모; 인접한 CVE-2026-64469 수정을 포함하므로 이 쌍은 CVE-2026-64468만 단독으로 분리함 |
| 수정된 메인라인 | f223d27a546c | 테스트 대상 수정 커밋 |
| 배포판 | 커널 | ANDROID_BINDER_IPC | 디바이스 | SLAB_BUCKETS | RANDOM_KMALLOC_CACHES | 비특권 사용자 도달 가능? |
|---|
| Debian 13 (trixie) | 6.12.101 | m | ANDROID_BINDER_DEVICES="binder", BINDERFS 꺼짐 | y | 꺼짐 | 아니요 — 모듈이 로드되지 않음; /dev/binder는 0600 |
| Debian 12 (bookworm) | 6.1.0 | m | ANDROID_BINDER_DEVICES="binder", BINDERFS 꺼짐 | 해당 없음 (6.11 이전) | 해당 없음 (6.6 이전) | 아니요 — 동일 |
| Ubuntu 24.04 LTS | 6.8.0 | m | BINDERFS=m, ANDROID_BINDER_DEVICES="" | 해당 없음 (6.11 이전) | y | 아니요 — mount -t binder에 root 필요 |
| Ubuntu 22.04 LTS | 5.15.0 | m | BINDERFS=m, ANDROID_BINDER_DEVICES="" | 해당 없음 | 해당 없음 | 아니요 — 동일 |
| Android (AOSP / 벤더) | 6.1, 6.6, 6.12 GKI | y | /dev/binder, /dev/hwbinder, /dev/vndbinder | — | — | 예 — 드라이버가 플랫폼 IPC이며 전 세계적으로 접근 가능 |
| 옵션 | 여기서의 효과 |
|---|
CONFIG_SLAB_BUCKETS (6.11+) | 이 reclaim에 치명적. msg_msg를 자체 kmalloc 버킷으로 격리하므로 펌프(pump)는 binder_proc의 슬롯에 절대 도달할 수 없다. Debian 13이 이를 설정한다. 다른 미계상 1 KiB 할당을 찾아야 한다. 관심 대상 Android 기기가 실행하는 커널 6.6은 이 기능이 전혀 존재하지 않는다. |
CONFIG_RANDOM_KMALLOC_CACHES (6.6+) | 호출 사이트별로 kmalloc-1k를 여러 캐시로 분할하므로 펌프는 동일한 캐시를 맞춰야 한다. reclaim에 1/16의 세금이지 벽이 아니다. Ubuntu는 설정하고 Debian은 설정하지 않는다. |
페이지 테이블 격리 (nopti 미사용) | 주소 탐색에 치명적. PTI가 활성화되면 prefetch는 커널 텍스트를 볼 수 없으며, 익스플로잇은 이를 감지하고 중지한다. PTI는 위의 모든 배포판에 컴파일되어 있지만 CPU가 활성 여부를 결정한다: Meltdown의 영향을 받지 않는 하드웨어에서는 꺼져 있으며, 이 결과가 측정된 환경이 바로 그렇다. |
CONFIG_SLAB_FREELIST_RANDOM, ..._HARDENED | 배포판이 제공하는 대로 실험실에서 활성화됨. 측정 가능한 효과 없음: 펌프는 freelist 순서를 예측하지 않고 단순히 많은 객체를 할당한다. |
KASLR (RANDOMIZE_BASE, RANDOMIZE_MEMORY) | 활성화됨. prefetch 단계로 무력화됨; nokaslr 없음. |
| 커널별 오프셋 | 체인은 _text로부터의 오프셋으로 commit_creds, cred 관련 전역 변수 3개, 가젯 2개가 필요하다. mkoffsets.sh는 대상 vmlinux에서 이를 추출한다. 이 값이 없으면 익스플로잇은 잘못된 주소에서 실행된다. 이는 방어가 아니라 모든 커널 익스플로잇의 빌드별 속성이다. |
메모리 안전성 실험실 (lab/) | 익스플로잇 실험실 (demo/) |
|---|
| 커널 버전 | 7.2.0-rc1+ | 7.2.0-rc1+ |
| 취약한 커밋 | 114a116aaa5f0295376cdf12da743c5bce3b20ce | 동일 |
| 수정된 커밋 | f223d27a546c1e1f48d38fd67760e78f068fe8c4 | — (익스플로잇은 취약한 커널에서만 측정됨) |
| 아키텍처 | x86_64 (KVM) 및 arm64 (TCG) | x86_64 (KVM) |
| 컴파일러 | Ubuntu clang 21.1.8 / LLD 21.1.8 | 동일 |
| KASAN | 켜짐 — 탐지기 역할 | 꺼짐 — slab 레이아웃을 변경하여 모든 reclaim을 비대표적으로 만들 수 있음 |
| Slab 하드닝 | — | SLAB_FREELIST_RANDOM, SLAB_FREELIST_HARDENED 켜짐; SLAB_BUCKETS, RANDOM_KMALLOC_CACHES 꺼짐 |
| KASLR | — | RANDOMIZE_BASE, RANDOMIZE_MEMORY 켜짐 |
| 사용자 영역 | 최소 initramfs | Debian GNU/Linux 13 (trixie), 배포판 자체 gcc 사용 |
| 시작 신원 | uid 1000, gid 1000, capabilities 없음, namespaces 없음 | 동일 |
| 부팅 커맨드 라인 | console=ttyS0 rdinit=/init panic=1 oops=panic kasan_multi_shot=1 | console=ttyS0 loglevel=4 rdinit=/init — nopti, nokaslr, mitigations=off 없음 |