[Kernel Exploit Tech] Cross-Cache Attack

Buddy Allocator, Slub Allocator에 대한 이해가 필요합니다.
Buddy Allocator에 대한 내용은 이 글, Slub Allocator에 대한 내용은 이 글을 참고하세요.

슬럽 할당자가 버디 할당자로부터 할당받은 페이지를 일정 크기로 니눠 관리한다는 점을 고려하면, 다음과 같은 공격을 생각해볼 수 있다.

kernel_uaf.jpg
  1. 민감한 구조체와 동일한 크기를 가지거나 같은 kmalloc 캐시에 들어가는 구조체를 할당받음
  2. 이 구조체를 free()해 dangling pointer를 얻음
  3. 민감한 구조체를 다수 할당받음
  4. 이제 위에서 만들어 둔 dangling pointer를 사용해 민감한 구조체에 읽기와 쓰기가 가능해짐.

이 방법을 사용하면 커널의 base 주소를 유출하는 등 여러 가지 공격이 가능해진다. 그러나 리눅스 커널 5.14버전에서 kmalloc-cg가 도입되며 이런 공격 방식이 힘들어졌다.

1. Dedicated / Generic Cache

슬럽 할당자는 일반적으로 2종류1의 cache를 갖는다.

  1. Generic Cache - 우리가 kmalloc-N으로 알고 있는 그 캐시다.
  2. Dedicated Cache - 특정한 종류의 구조체만 담는 캐시다.

Generic cache는 어떤 종류의 object든, 크기 조건만 맞으면 같은 캐시에 들어간다. 예를 들어 64바이트짜리 AB 구조체가 있다고 하자. 두 구조체는 둘 다 kmalloc-64에 들어간다. 그러나 A를 위한 dedicated cache가 존재한다면 이 캐시에는 A밖에 들어가지 못한다. B가 절대로 들어갈 수 없다는 것이다. Dedicated cache의 가장 대표적인 예시로 cred_jar이 있는데, 여기에는 cred구조체만 들어갈 수 있다.

그러나 리눅스 커널 5.14버전에서 accounted cache가 도입되며 generic cache가 kmalloc-Nkmalloc-cg-N으로 나뉘게 되었다. account cache는 aliasing참고이 일어나지 않도록 설정된 청크인데, 결과적으로 공격에 주로 사용되는 구조체들이 accounted cache로 이동하게 되면서 위에서 말한 3번째 단계가 불가능하게 되었다. 민감한 구조체를 아무리 할당받아도 kmalloc-cg-N에 할당되고, dangling pointer가 존재하는 kmalloc-N 영역에는 할당되지 않게 된 것이다. 이를 우회하기 위해 cross-cache attack이 등장했다.

kernel_uaf_fail_by_kmalloc_cg.jpg

2. Cross-Cache Attack

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

cross_cache.jpg
  1. Dangling pointer이 존재하는 slab에 있는 object를 전부 해제시켜 slub 할당자가 이 slab을 buddy 할당자에게 넘겨 recycle하도록 함
  2. kmalloc-cg에 속하는 구조체를 다수 할당받아 기존의 kmalloc-cg-N이 고갈되도록 함
  3. 여기서 더 할당하면 결국 slub 할당자는 buddy 할당자로부터 새로운 page, 즉 slab을 할당받음 (reclaim)
  4. 이때 버디 할당자의 free_area[]는 기본적으로 LIFO 구조를 따르기 때문에 새로 할당받은 slab은 방금 recycle된, 즉 만들어 둔 dangling pointer가 내부에 존재하는 page일 확률이 큼
  5. 따라서 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_cachekmalloc-64와 이름만 다를 뿐 정확히 똑같기 때문에 foo_cachekalloc-64의 alias처럼 사용한다. 즉 foo_cachekmalloc-64의 slab을 공유하는 것이다.

generic_cache.jpg

그러나 다음과 같이 accounted cache를 만들면, kmalloc-64 대신 kmalloc-cg-64의 slab을 쓰게 된다.

kmem_cache_create("foo_cache", 64, 0, 
                  SLAB_HWCACHE_ALIGN | SLAB_ACCOUNT, NULL);
account_generic_cache.jpg

만약 다음과 같이 dedicated cache를 만들면, 완전히 새로운 slab을 할당받아 사용한다.

kmem_cache_create("foo_cache", 64, 0, 
                  SLAB_HWCACHE_ALIGN | SLAB_NO_MERGE, NULL)
dedicated_cache.jpg
  1. 개수가 아니다! 

Leave a comment