[Kernel Exploit Tech] Cross-Cache Attack
Buddy Allocator, Slub Allocator에 대한 이해가 필요합니다.
Buddy Allocator에 대한 내용은 이 글, Slub Allocator에 대한 내용은 이 글을 참고하세요.
슬럽 할당자가 버디 할당자로부터 할당받은 페이지를 일정 크기로 니눠 관리한다는 점을 고려하면, 다음과 같은 공격을 생각해볼 수 있다.

- 민감한 구조체와 동일한 크기를 가지거나 같은
kmalloc
캐시에 들어가는 구조체를 할당받음 - 이 구조체를
free()
해 dangling pointer를 얻음 - 민감한 구조체를 다수 할당받음
- 이제 위에서 만들어 둔 dangling pointer를 사용해 민감한 구조체에 읽기와 쓰기가 가능해짐.
이 방법을 사용하면 커널의 base 주소를 유출하는 등 여러 가지 공격이 가능해진다. 그러나 리눅스 커널 5.14버전에서 kmalloc-cg
가 도입되며 이런 공격 방식이 힘들어졌다.
1. Dedicated / Generic Cache
슬럽 할당자는 일반적으로 2종류1의 cache를 갖는다.
- Generic Cache - 우리가
kmalloc-N
으로 알고 있는 그 캐시다. - Dedicated Cache - 특정한 종류의 구조체만 담는 캐시다.
Generic cache는 어떤 종류의 object든, 크기 조건만 맞으면 같은 캐시에 들어간다. 예를 들어 64바이트짜리 A
와 B
구조체가 있다고 하자.
두 구조체는 둘 다 kmalloc-64
에 들어간다. 그러나 A
를 위한 dedicated cache가 존재한다면 이 캐시에는 A
밖에 들어가지 못한다. B
가 절대로 들어갈 수 없다는 것이다.
Dedicated cache의 가장 대표적인 예시로 cred_jar
이 있는데, 여기에는 cred
구조체만 들어갈 수 있다.
그러나 리눅스 커널 5.14버전에서 accounted cache가 도입되며 generic cache가 kmalloc-N
과 kmalloc-cg-N
으로 나뉘게 되었다.
account cache는 aliasing참고이 일어나지 않도록 설정된 청크인데, 결과적으로 공격에 주로 사용되는 구조체들이 accounted cache로 이동하게 되면서 위에서 말한 3번째 단계가 불가능하게 되었다.
민감한 구조체를 아무리 할당받아도 kmalloc-cg-N
에 할당되고, dangling pointer가 존재하는 kmalloc-N
영역에는 할당되지 않게 된 것이다. 이를 우회하기 위해 cross-cache attack이 등장했다.

2. Cross-Cache Attack
위에서 살펴본 상황을 우회하기 위해서 공격자는 원래 generic cache였던 slab을 generic accounted cache로 옮겨야 한다. 이를 통해 원래는 generic cache 어딘가를 가리키고 있던 dangling pointer가 generic accounted cache를 가리키게 되면서 3번째 단계가 가능해진다. 이를 위해 공격자는 다음과 같은 방법을 사용한다.

- Dangling pointer이 존재하는 slab에 있는 object를 전부 해제시켜 slub 할당자가 이 slab을 buddy 할당자에게 넘겨 recycle하도록 함
kmalloc-cg
에 속하는 구조체를 다수 할당받아 기존의kmalloc-cg-N
이 고갈되도록 함- 여기서 더 할당하면 결국 slub 할당자는 buddy 할당자로부터 새로운 page, 즉 slab을 할당받음 (reclaim)
- 이때 버디 할당자의
free_area[]
는 기본적으로 LIFO 구조를 따르기 때문에 새로 할당받은 slab은 방금 recycle된, 즉 만들어 둔 dangling pointer가 내부에 존재하는 page일 확률이 큼 - 따라서 dangling pointer로 공격자가 원하던
kmalloc-cg
에 존재하는 민감한 object에 대한 읽기/쓰기가 가능해짐.
3. 한계점
3-1. Recycle / Reclaim 과정의 불확실성
우선 recycle / reclaim 과정이 굉장히 어렵다. 우선 첫 번째 단계에서 얼마나 많은 object들을 할당해야 할지 모른다.
단편적으로 그림의 1단계에서 짙은 회색으로 표시된 object들이 공격자가 할당한 object이다.
공격자는 최소한 한 슬랩에 대해 자신이 해제가능한 object로 채워야 한다 (recycle을 위해).
이를 위해 기존에 존재하는 slab 0
를 꽉 채워 새로운 슬랩 할당을 유도해야 하는데,
slab 0
에 남은 할당가능한 object가 얼마나 존재하는지 전혀 알 방법이 없기 때문에 무작정 많이 할당을 시도하는 방법밖에 없다.
3-2. Noise 영향
또한 generic cache는 공격자뿐만 아니라 다른 커널 스레드들이 굉장히 자주 사용하는 cache이기 때문에, 이들에 의한 noise가 발생할 수 있다.
즉 recycle을 위해 “무작정 많이 할당”하고 “무작정 많이 해제”하는 동안 미리 recycle된 슬랩들이 다른 커널 스레드에 의해 reclaim되어
다시 kmalloc-N
에 속한 slab이 될 수 있고, 이 경우에 공격자는 결국 kmalloc-cg
에 접근하지 못하게 되므로 공격이 실패할 수밖에 없다.
3-3. 효용이 떨어짐
마지막으로 민감한 object에 쓰기/읽기가 가능하다고 하더라도 그 범위는 수 바이트에 불과하다. 이를 통해 커널의 base주소 등의 유출은 가능하겠지만 광범위한 AAR/AAW가 불가능하기 때문에 공격의 난이도에 비해 효용이 떨어진다.
이런 문제들을 해결하기 위해 SLUBStick이 발표되었는데, 이 논문은 다음 글에서 다뤄볼 예정이다.
* 참고
aliasing이란 새로 만들어진 kmem_cache
가 기존의 kmem_cache
를 공유하도록 하는 과정이다. 예를 들어, foo_cache
라는 이름의 64바이트짜리 kmem_cache
를 새로 만든다고 해 보자.
이때 다음과 같이 kmem_cache_create()
를 호출해 새로운 kmem_cache
를 만들 수 있다.
kmem_cache_create("foo_cache", 64, 0,
SLAB_HWCACHE_ALIGN, NULL)
이때 커널은 foo_cache
라는 이름을 가진 kmem_cache
를 만들기는 하지만, 이 kmem_cache
를 위해 새로운 slab을 할당하지는 않는다. 대신, foo_cache
는
kmalloc-64
와 이름만 다를 뿐 정확히 똑같기 때문에 foo_cache
를 kalloc-64
의 alias처럼 사용한다. 즉 foo_cache
는 kmalloc-64
의 slab을 공유하는 것이다.

그러나 다음과 같이 accounted cache를 만들면, kmalloc-64
대신 kmalloc-cg-64
의 slab을 쓰게 된다.
kmem_cache_create("foo_cache", 64, 0,
SLAB_HWCACHE_ALIGN | SLAB_ACCOUNT, NULL);

만약 다음과 같이 dedicated cache를 만들면, 완전히 새로운 slab을 할당받아 사용한다.
kmem_cache_create("foo_cache", 64, 0,
SLAB_HWCACHE_ALIGN | SLAB_NO_MERGE, NULL)

-
개수가 아니다! ↩
Leave a comment