[Paper Reading] Cross-Cache Attacks for the Linux Kernel via PCP Massaging
이번 2026 NDSS symposium에 발표된 새로운 커널 힙 공격기법 논문에 대해 정리해 보았다.
1. Background
SLUB Allocator
커널에서 사용되는 기본 메모리 할당자로, kmem_cache_alloc(), kmalloc()을 통해 할당, kfree()를 통해 해제가 이루어진다.
SLAB
각 object를 포함하고 있는 구조체. freelist를 통해 새로운 object를 할당 / 해제한다.
slab 자체는 page 단위로 할당되며, page frame allocator에 의해서 관리된다.
kmem_cache
전체적인 SLUB allocator를 관장하는 구조체이다. 특정한 object만 할당할 수 있는 dedicated cache(e.g. task_struct), 일반적인 object를 할당할 수 있는 generic cache(e.g. kmalloc-xx)로 구분된다.
내부적으로 kmem_cache_cpu, kmem_cache_node를 통해 slab을 관리한다.
kmem_cache_cpu
per-cpu 객체에 해당한다. main slab은 따로 관리되며, 나머지 slab들은 list 형태로 partial에 저장되어 있다.
kmem_cache_node
per-NUMA 객체에 해당한다. slab들은 partial list에 저장되어 있다.
세부 동작
할당 받을 slab을 선택하는 과정은 크게 4단계로, 각 단계에서 실패 시 아래 단계로 내려가며, 아래로 갈수록 slow-path에 해당한다.
1. kmem_cache_cpu->slab을 선택.
2. kmem_cache_cpu->partial에서 slab을 선택.
3. kmem_cache_node->partial에서 slab을 선택.
4. page frame allocator에서 새로운 slab을 할당, 해당 slab을 선택.
이후 선택된 slab에서 object를 할당받게 된다.
Page Frame Allocator
Buddy Allocator라고도 불리며, physical memory를 관리하는 할당자이다.
메모리는 page 단위로 할당이 이루어지며, order의 형태로 크기가 결정된다. order는 0부터 시작하며, order-k의 경우, 2^k * page_size(4096 bytes) 의 크기를 가지게 된다.
각 order 별로 free_area라는 freelist를 통해 관리된다. 만약 order-k page의 할당 요청이 들어왔는데 해당 freelist가 비어있는 경우, 상위 order(k + 1)의 freelist에서 선택한 후, 반으로 쪼개서 앞의 절반을 할당받는 형태로 동작한다. 이 때, 앞의 절반을 left-buddy, 뒤의 절반을 right-buddy라고 명명한다.
Zone, Migration type에 대해서는 설명을 생략한다.
Per-CPU-Pageset (PCP) lists
Buddy Allocator의 경우에도 마찬가지로 성능 향상을 위해 per-cpu 객체를 통해 관리가 이루어지는데, 그것이 바로 pcp list이다. pcp list가 추가된 형태의 Buddy Allocator의 동작은 다음과 같다.
order-k page의 할당 요청이 들어온 경우,
1. order-k pcp list 확인, 있다면 할당 (rmqueue_pcplist()) 후 리턴.
2. list가 비어있다면, Buddy Allocator를 통해 여러 개의 batch order-k pages를 할당받아(rmqueue_bulk()) order-k pcp list에 추가함.
3. order-k pcp list로 부터 page를 할당받고 리턴.
정리하자면, 위 3계층은 아래와 같이 연결된다.
1. SLUB allocator 단에서 할당.
2. 만약 실패할 경우, pcp list 단에서 할당.
3. 만약 실패할 경우, Buddy Allocator에서 batch를 가져와 pcp list를 채워넣음 → 이후 pcp list에서 할당.
2. Approach
Main Idea
A. Timing side-channel을 이용해 rmqueue_pcplist() (slower path)와 rmqueue_bulk() (slowest path) 중 어느 것이 호출되는지를 구분할 수 있을 것이다.
B. 만약 구분할 수 있다면, 이를 활용하여 서로 다른 cache 간의 physical memory address를 contiguous하게 만들어 OOB cross-cache attack을 reliable하게 만들 수 있을 것이다.
A. Timing side-channel
위 그림은 timing-object의 SLUB allocation(fast allocation), rmqueue_pcplist()(slower allocation), rmqueue_bulk()(slowest allocation)가 각각 성공한 경우의 걸린 시간을 나타낸 것이다.
그림에서 볼 수 있듯이, rmqueue_pcplist()와 rmqueue_bulk() 간의 구분 가능한 시간 차이가 존재한다. 이를 활용하면 높은 확률로 heap massaging을 성공시킬 수 있다.
Timing side-channel을 활용할 때에는 timing object와 persistent object를 사용한다. timing object는 할당 이후 바로 해제되는 과정을 거치는 object로, 이 특성을 활용하면 SLUB state를 추론할 수 있다. 추론된 정보를 기반으로 persistent object를 영구적으로 할당하여 원하는 새로운 state를 만들 수 있다. 이 일련의 과정을 probe and drain 이라고 정의할 것이다.
B. Cross-cache Massaging(PCPLost)
3가지 케이스로 나누어 설명한다.
용어 정의
n_v: vulnerable object가 속한 slab의 order
n_t: target object가 속한 slab의 order
N: objects per slab
i) n_v == n_t
예시로 n_v == 0, n_t == 0인 경우로 설명한다.
1-1. vulnerable cache를 대상으로 probe and drain을 진행, 내부에 존재하는 main slab, partial slab을 drain 한다. 계속 할당하다 보면 pcp list 또한 drain 되고, 결과적으로 rmqueue_bulk()를 호출하게 된다.
1-2. 이후 order-1에서의 page가 쪼개져서 pcp list에 batch로 저장되고 (그림에서의 (1), (2), (3) pages), rmqueue_bulk()에 의해 (1) page가 리턴된다. 여기서 주목할 점은 (1), (2), (3)은 physically contiguous한 page라는 점이다.
2-1. vulnerable cache를 대상으로 probe and drain을 진행, 새로운 pcp list 할당((2) page)이 이루어질 때를 탐지. N - 1개를 할당, 이후 1 vulnerable object를 할당하게 되면 (2) page의 마지막에 vulnerable object가 할당된다.
2-2. target cache를 대상으로 probe and drain을 진행, 새로운 pcp list 할당((3) page)이 이루어질 때를 탐지. 이후 해당 (3) page를 target object로 가득 채운다. (2) page와 (3) page는 인접한 메모리였으므로, 성공적으로 target object를 vulnerable object의 OOB 대상으로 삼을 수 있다.
3. OOB exploit이 가능한 상태가 된다.
유의점
512라는 base값을 설정 (기준은 base >> page_order) 하여 probe and drain 이전에 pcp list를 drain해서 높은 order의 split이 이루어지도록 하였다. 이는 “Buddy Allocator의 free_area에 order-0 page가 batch size만큼 존재하는 경우”를 사전적으로 차단하는 역할을 하여, 높은 order의 split이 더 잘 일어날 수 있게 하는 역할을 한다.
물론, order가 높아질수록 batch size도 적어지므로, 이를 고려하여 drain하는 page의 양 또한 적은 값으로 설정하였다.
ii) n_v > n_t
예시로 n_v == 1, n_t == 0인 경우로 설명한다.
1. vulnerable cache를 대상으로 probe and drain을 진행, order-1 pcp list를 drain한다.
2. rmqueue_bulk() 호출을 탐지. (1), (2), (3) page가 order-1 pcp list로 들어가고, (1) page가 리턴된다. 이 때, (3) page는 order-2 page가 split 되면서 생긴 left-buddy에 해당한다. right-buddy는 order-1 free_area로 옮겨지게 된다.
3. 이후 N * batch - 1 만큼의 spray + 1 vulnerable object allocation을 통해 (3) page의 마지막에 vulnerable object를 위치시킨다.
4. target cache를 대상으로 probe and drain을 진행, order-0 pcp list를 drain 한다.
5. rmqueue_bulk() 호출을 탐지. (1) - (5) page가 order-0 pcp list로 들어가고, (1) page가 리턴된다. 이후 2번에서 split 되었던 right-buddy ((2) page)가 order-0 pcp list에 들어가게 된다.
6. 이후 추가적인 target object allocation을 진행하게 되면, target object가 (2) page에 들어가게 되고, 이는 vulnerable object에 인접한 메모리에 해당한다.
7. OOB exploit이 가능한 상태가 된다.
| n_v - n_t | > 1인 경우?
target object의 rmqueue_bulk() 호출을 여러 번 detect하는 것을 통해 동일한 방식으로 massaging이 가능하다.
iii) n_v < n_t
예시로 n_v == 0, n_t == 1인 경우로 설명한다.
1. vulnerable cache를 대상으로 probe and drain 과정을 수행하여 pcp list를 drain한다. 참고로 n_t까지의 pcp list를 모두 drain시켜야 한다. 그래야 이후에 rmqueue_bulk() 과정에서 n_t보다 큰 order page의 split이 일어나서 앞에서와 동일한 방법을 적용할 수 있게 된다.
2. rmqueue_bulk() 호출을 탐지, order-2 page의 split이 일어나게 된다. (1) - (3) page가 order-0 pcp list로 들어가게 되고, (1) page가 리턴된다. 여기서 (3) page가 order-2 기준 left-buddy에 해당한다.
3. N * batch - 1 spray + 1 vulnerable object allocation을 통해 order-0 pcp list를 drain하고, (3) page의 마지막에 vulnerable object가 위치하게 한다.
4. target cache를 대상으로 probe and drain을 진행, order-1 pcp list를 drain한다.
5. rmqueue_bulk() 호출을 탐지, 여기서 할당된 (1) page는 2번에서의 right-buddy에 해당한다. 즉, 2번에서 할당된 (3) page와 현재 (1) page는 서로 인접해 있는 상태.
6. OOB exploit이 가능한 상태가 된다.
| n_t - n_v | > 1인 경우
ii)에서와 같이, 마찬가지로 일반화 될 것으로 보인다. 하지만, attacker 입장에서는 rmqueue_bulk() 호출을 탐지할 수는 있지만, 이게 몇 번까지 이루어져야 하는지에 대한 정보가 없다. 이 상태에서 vulnerable object를 지속적으로 할당하는 것은 의도하지 않은 crash를 유발하여 공격을 실패하게 만들 가능성이 있다.
하지만 vulnerable object와 target object는 선택하기 나름이므로, attacker는 다른 object를 선택함으로써 이 케이스를 회피할 수 있다.
C. Minimizing false positives
단일 timing 샘플이 특정 범위에 들어왔다는 것만으로 이벤트 발생을 단정하지 않는다. 대신, 해당 이벤트가 실제로 일어났다면 채워졌을 영역을 다시 소진할 만큼 할당을 수행한 뒤, 후속 timing 샘플이 예상과 일치하는지 확인한다. 일치한다면 해당 할당 이벤트 (rmqueue_pcplist() / rmqueue_bulk())가 실제로 발생했다고 높은 확률로 판정한다.
이 방식은 false positive를 줄이는 대신 유효한 hit도 일부 버리게 되지만, 추가 할당으로 인한 약간의 메모리 overhead외에 massaging 관점에서 큰 손해는 아니다.
D. Pivoting temporal vulnerabilities
UAF, DF같은 경우, pivoting을 통해 OOB로 바꾸어 exploit이 가능하다.
i) UAF
다음과 같은 순서를 통해 UAF → OOB로의 pivoting이 가능하다.
1. UAF-vulnerable page와 target cache page를 인접하게 만든다.
2. vulnerable object를 할당한 직후 바로 해제, dangling pointer를 만든다.
3. 동일한 위치에 pivot object를 할당. 여기서 pivot type에 따라 in-cache 혹은 cross-cache primitive가 사용될 수 있다.
4. pivot object의 length field를 corrupt, OOB write primitive를 획득한다.
5. 만약 pivot object가 slab의 마지막에 위치해 있었다면, target cache를 공격할 수 있다.
ii) DF
UAF와 유사하다. 한 가지 step이 추가되어 DF → UAF → OOB의 형태로 pivoting 할 수 있다.
3. Evaluation
RQ1. PCPLost의 reliability는 어떻게 되는가?
Adjacency, Adjacency + object alignment, CPU pinning 여부, 커널 내부가 Idle한지의 여부에 따라 나누어 결과를 정리하였다.

n_v == n_t인 경우, 전부 다 성공률이 90% 이상인 것을 확인할 수 있다.
n_v > n_t인 경우, CPU pinning을 하지 않은 경우만 성공률이 40% - 50%대로 떨어졌다. 이는 attack 프로세스가 sleeping인 동안 다른 프로세스가 free_area에 접근하여 massaging 과정을 방해했다는 것을 의미한다.

n_v < n_t인 경우, 성공률이 매우 낮게 나온 것을 확인할 수 있다. 이는 앞서 언급했던 대로, vulnerable object의 입장에서 얼마나 rmqueue_bulk()가 이루어져야 하는지에 대한 사전 정보가 없기 때문인 것이 주 이유이다.
정리하자면, CPU pinning의 활용 여부, 그리고 n_v < n_t의 여부는 공격자의 역량에 따라 달라지므로, 숙련된 공격자를 가정한다면 PCPLost는 90% 이상의 성공률을 지닌 굉장히 reliable한 공격 기법이라고 볼 수 있다.
RQ2. UAF, DF를 성공적으로 pivot할 수 있는가?
UAF 취약점이 존재하는 custom kernel module을 작성하여 테스트하였다.
i) UAF → OOB
pivot object로는 kmalloc-cg-* cache 안에 존재하는 struct msg_msg를, target object로는 kmalloc-32에 존재하는 struct shm_file_data를 선택하였다.
pivoting 과정은 다음과 같이 진행된다.
1. struct msg_msg->m_ts field를 조작, 메시지 크기를 늘린다.
2. 늘어난 크기를 바탕으로 이후에 memcpy()가 호출되면 OOB가 발생하게 된다.
예시로는 msg_msg를 들었지만, length 조작 가능 + 이후 memcpy()를 통한 데이터 복사 primitive가 가능하다면 pivoting object로 사용 가능하다. 즉 pivoting의 성공 여부는 전적으로 vulnerability 종류에 따라 달라지게 된다.
ii) DF → OOB
앞서 언급했듯이, DF → UAF로의 추가적인 pivoting 과정을 거치게 되면 i)에서의 경우와 마찬가지로 성공적인 exploit이 가능하다.
RQ3. Cross-cache defenses를 우회할 수 있는가?
SLAB_VIRTUAL, SLAB_FREELIST_RANDOM 두 가지 보호기법에 대해 설명한다.
i) SLAB_VIRTUAL
slab을 physmap 영역이 아닌 virtual address에 별도 매핑하고, 해당 virtual address는 다른 kmem_cache가 사용할 수 없게 하여 cross-cache attack을 막는 보호기법이다.
a) Spatial vulnerabilities
표를 보면 Same-order와 Cross-order 둘 다 90% 이상의 높은 성공률을 보이는 것을 확인할 수 있다.
SLAB_VIRTUAL의 경우, cross-cache slab 재사용은 막지만, 서로 다른 kmem_cache에서 할당되는 주소는 virtually contiguous하다는 단점이 있다. 이는 오히려 인접한 주소 예측을 쉽게 하여 OOB 취약점을 성공적으로 방어할 수 없다.
참고
저자들이 SLAB_VIRTUAL 저자들과 논의한 결과, guard pages를 도입한 버전 (SLAB_VIRTUAL_GP)가 있다는 것을 확인하였다. 해당 버전의 경우, linear OOB(연속적인 write)는 성공적으로 막을 수 있었지만, non-linear OOB(offset 기반의 write)의 경우에는 막지 못한다는 단점이 존재한다.
b) Temporal vulnerabilities
a)에 의해 cross-cache 간의 virtually contiguous 할당이 가능하기 때문에, 앞서 설명한 UAF / DF → OOB로의 pivoting은 동일하게 진행할 수 있다. 다만, SLAB_VIRTUAL의 특성 상 vulnerable object의 cache와 pivot object의 cache는 동일해야 한다는 추가적인 제약 조건이 생기게 된다.
ii) SLAB_FREELIST_RANDOM
SLAB_FREELIST_RANDOM은 slab 내부적으로 freelist의 순서를 랜덤하게 결정하는 보호기법으로, vulnerable object가 할당되는 경우 slab 내부 어느 위치에 할당되는지 알 수 없다. 앞서 말한 N - 1개의 spray object 할당 → vulnerable object 할당 을 통해 slab의 마지막 위치에 vulnerable object가 온다는 것을 보장할 수 없는 것이다.
OOB read의 경우 문제가 되지 않는다. 단순히 성공할 때 까지 반복하면 된다. 하지만 OOB write의 경우, 실패할 경우 crash가 발생할 수 있기 때문에 다른 우회방안이 필요하다.
위 그림과 같이, vulnerable object가 slab 내 어느 위치에 있든, 그 object의 끝에서 SLAB_SIZE만큼 떨어진 지점에는 반드시 target object가 존재한다. target slab의 크기가 SLAB_SIZE보다 크기만 하면 되며, 이는 n_v == n_t인 경우와 n_t > n_v인 두 가지 경우에 보장된다. 즉 vulnerable object의 위치를 몰라도 되므로 SLAB_FREELIST_RANDOM이 무력화된다.
페이로드는 corruption pattern을 반복하는 형식으로 구성한다. 이를 통해 어떤 target object가 덮이든 유효한 트리거가 되도록 만들 수 있다.
결국 vulnerability object는 단 한 번만 트리거하면 되기에, vulnerable object의 잘못된 corruption으로 인한 예상치 못한 crash는 방지할 수 있다.
n_t < n_v인 경우
동일한 방법을 적용할 수 있으나, write가 target slab의 경계를 넘어갈 수 있다는 단점이 존재, 추가적인 crash 가능성이 생기게 된다.
이는 write 크기를 target slab 크기로 줄이는 방법을 통해 해결할 수 있다. 하지만, target object가 하나도 덮이지 않는 경우가 생기게 되는데, 이 경우에 한해서 동일한 공격을 반복 수행하는 방식으로 해결 가능하다.
write 가능한 바이트 수가 제한적인 경우
일반적으로 공격자가 쓸 수 있는 바이트 수가 적을 수 있다. 이때는 제한된 크기의 overwrite와 위 방법을 묶어 PCPLost 자체를 반복 수행한다. PCPLost는 안정적으로 cross-cache layout을 만들어 주므로, 익스플로잇 시간만 좀 더 걸릴 뿐, crash가 없는 안정적인 익스플로잇이 가능하게 된다.
0을 쓸 수 있는 취약점의 이점
일부 target object에 대해서는 취약점이 0을 쓸 수 있는지가 중요한 요건이 된다. 포인터를 NULL로 만들어 두면 crash를 피하면서 object를 안정적으로 corrupt 시킬 확률이 증가한다.
RQ4. Real-world 취약점에도 적용 가능한가?
6가지 real-world 취약점에 대해 평가를 진행하였다.
앞서 설명한 바와 같이, OOB 취약점에 대한 익스플로잇이 가능하였고, UAF 취약점의 경우에도 pivoting을 활용하여 익스플로잇을 성공적으로 수행할 수 있었다.
SLAB_FREELIST_RANDOM, SLAB_VIRTUAL, SLAB_FREELIST_HARDENED의 보호기법도 성공적으로 우회할 수 있었다. 물론, 각 CVE 별로 취약점의 특성이 다르기 때문에, 우회한 보호기법의 종류는 서로 다른 것을 확인 가능하다.
4. Summary
세 가지 관점에서 논문이 주장하는 공격기법의 가치를 정리할 수 있을 것 같다.
Novelty
이전 related works(PSPRAY, SLUBstick, …)에서 주목하지 않았던 PCP list에서의 timing side-channel(slower path / slowest path 구분)을 이용하여 side-channel → contiguous memory allocation → OOB exploit primitive로 연계할 수 있었다는 점.
Reliability
PCPLost 기법 자체의 reliability(90% 이상의 성공률), 그리고 이후에 언급된 crash를 유발하지 않는 다양한 기법들(특히 SLAB_FREELIST_RANDOM 파트)과의 연계성을 통해 실제 real-world에서의 높은 reliability를 설명해 주었다는 점.
Pivoting
단순 OOB exploit primitive가 아닌, UAF / DF → OOB로의 추가적인 pivoting을 통해 공격기법의 coverage를 넓혔고, 이를 활용하여 UAF 버그임에도 불구하고 SLAB_VIRTUAL 보호기법을 무력화할 수 있었다는 점.
Reference
원문: https://www.ndss-symposium.org/wp-content/uploads/2026-f862-paper.pdf