Lobsters

Big Fish: Exploiting a 37-Year-Old Kernel Bug in OpenBSD

Big Fish: OpenBSD의 37년 된 커널 버그를 악용한 로컬 권한 상승

Calif 연구진은 OpenBSD에서 37년 된 공유 메모리 카운터 오버플로와 별도의 커널 범위 밖 쓰기를 결합해 로컬 root 권한을 얻었습니다. 익스플로잇은 KARL, SMAP, SMEP가 켜진 상태에서도 동작했으며, 글은 관련 취약점 수정과 커널 메모리 할당 및 자료구조를 겨냥한 추가 완화책을 설명합니다.

에디터 노트

이 익스플로잇의 성공은 오래된 카운터 오버플로 하나로 설명되지 않습니다. 공유 메모리 회계 오류가 use-after-free를 만들고 별도의 범위 밖 쓰기가 메모리를 덮으며, 예측 가능한 할당 위치와 재귀 PTE 매핑이 권한 상승 경로를 완성합니다. 각 취약점 수정과 공격 경로 전체를 겨냥한 완화책은 서로 다른 층위의 대응입니다. 원문은 두 취약점의 수정과 여러 후속 방안을 구분합니다. safe-unlinking, PTE 매핑 무작위화, 할당자 비결정성은 제안된 완화책이며 OpenBSD에 적용됐다고 밝히지 않습니다. 따라서 이 사례가 보여주는 결과는 특정 익스플로잇의 성공과 구체적인 방어 설계안까지입니다. 제안된 방안이 성능이나 호환성에 미치는 영향은 자료에서 다루지 않습니다.

AI 요약

Calif 연구진은 OpenBSD에서 로컬 사용자가 root 권한을 얻는 익스플로잇을 공개했습니다. 출발점은 약 37년 전 원본 BSD 코드에 들어간 System V 공유 메모리 첨부 횟수 카운터입니다. 카운터가 부호 있는 16비트 short로 정의되어 있어 최댓값인 32,767을 넘으면 음수로 돌아갑니다. 프로세스를 반복해 fork하면서 이 값을 -32,643까지 낮추면 경쟁 조건을 거쳐 use-after-free를 일으킬 수 있습니다. 연구진은 여기에 별도의 커널 범위 밖 쓰기 취약점을 결합했습니다. 두 취약점은 각각 errata 026과 018에서 수정됐습니다.

익스플로잇 경로

연구진은 KARL, SMAP, SMEP가 활성화된 OpenBSD에서 정보 노출 취약점 없이 익스플로잇을 구성했습니다. 먼저 SysV 공유 메모리 세그먼트 127개를 매핑한 뒤 munmap()으로 해제하고, 프로세스 259개를 fork해 첨부 횟수 카운터를 -32,643으로 만듭니다. 이어 sysctl(KERN_MALLOCSTATS)의 할당 통계를 살펴 힙 배치를 조정하고, IPC_RMID를 실행하는 프로세스와 크기가 혼동된 공유 메모리 세그먼트를 붙이는 보조 프로세스 사이에서 경쟁 조건을 유도합니다.

그다음 IP 소켓 할당 위치를 맞추고 IP_MULTICAST_TTL 값을 기본값이 아닌 2로 설정해, 16바이트 크기의 struct ip_moptions를 지속적으로 할당합니다. 구조체 마지막 단어에는 16비트 카운터 두 개가 들어갑니다. 조건부 -1 범위 밖 쓰기로 멀티캐스트 그룹 가입 개수와 최대 개수를 덮어씁니다. 소켓을 닫으면 그룹 멤버를 순회하면서 atomic decrement가 실행됩니다. 이 연산을 이용해 PTE의 재귀 매핑을 두 번 수정하고, 사용자 접근 가능 매핑을 supervisor 접근 가능 상태로 바꿉니다. 이후 예측 가능한 커널 포인터를 덮어 커널 코드 실행을 얻고, 공유 메모리 회계 값을 정리한 뒤 덮어쓴 포인터를 복구해 root 셸을 엽니다.

연구진은 공격 원리를 write-what-where로 요약합니다. TAILQ_REMOVE가 메모리 쓰기를 일으키고, 쓰기 대상은 SMAP과 SMEP를 우회하도록 supervisor 접근 권한을 얻은 사용자 메모리 매핑이며, 실제 덮어쓰는 위치는 예측 가능한 커널 포인터입니다.

완화 방안

KARL은 커널 바이너리를 재배치해 함수와 데이터 주소를 무작위화하지만, 연구진은 커널 메모리 할당자가 결정적인 점을 지적합니다. 부팅 시 할당자와 best-fit 할당자 모두 기본 주소를 무작위화하지 않습니다. 같은 하드웨어에서는 부팅 초기 구조체가 매번 예측 가능한 주소에 놓일 수 있고, 그 안의 함수 포인터가 공격 대상이 됩니다. 익스플로잇은 lo0와 enc0의 ioctl 처리기 포인터를 활용해 한쪽으로 제어 흐름을 얻고 다른 쪽으로 덮어쓴 포인터를 복구합니다.

제안된 완화책 중 하나는 safe-unlinking입니다. 이중 연결 리스트에서 노드를 제거하기 전 링크 관계가 유효한지 검사하고, 불일치하면 abort()나 panic()으로 메모리 손상을 막습니다. 글은 FreeBSD와 NetBSD의 sys/queue.h에 비슷한 기능이 있다고 설명합니다. 또 다른 방안은 재귀 PTE 매핑 주소를 부팅 때 무작위로 정하는 것입니다. 무작위 바이트로 PML4 슬롯을 선택하면 PTE_BASE가 일정한 주소에 고정되지 않습니다. 무작위화 대신 PTE를 읽기 전용으로 두고 수정 시 CR0.WP를 바꾸는 방법도 언급합니다.

커널 할당자에는 부팅 시 할당 시작 주소를 무작위화하거나 런타임 할당 순서를 섞는 방안을 제시합니다. 주소 무작위화는 생기는 간격을 나중에 재사용할 수 있어 추가 비용이 사실상 없다고 설명합니다. 런타임 순서 무작위화는 할당 과정에 오버헤드를 더합니다. 이 두 방식 중 하나만으로도 별도의 취약점 없이 예측 가능한 포인터 위치를 깨뜨릴 수 있다는 것이 연구진의 설명입니다.

글은 컴파일러 기반 완화책도 다룹니다. Undefined Behaviour sanitizer의 일부인 array bounds sanitizer는 고정 크기 배열의 범위를 벗어난 접근을 막을 수 있습니다. Calif는 FreeBSD에 이 검사를 릴리스 빌드의 익스플로잇 완화책으로 사용할 수 있다고 제안했고, 이 검사로 잡혔을 취약점 21건을 비공개로 제보했다고 밝혔습니다. 일부에는 로컬 권한 상승 영향이 있었다고 합니다. 앞으로 나올 Alloc Tokens는 포인터를 담은 할당과 그렇지 않은 할당을 분리해, 비포인터 영역의 오버플로가 포인터를 건드리기 어렵게 하고 use-after-free에서 다른 종류의 할당을 대체 메모리로 이용하는 경로를 줄이는 기능으로 소개됩니다.

정리

연구진은 OpenBSD가 코드 감사, 안전하지 않은 API 교체, 암호화, 권한 분리, 샌드박싱, ASLR과 W^X 같은 방어 기법을 꾸준히 도입해 왔다고 설명합니다. 이 사례에서는 취약점 두 개의 결합뿐 아니라 결정적인 할당 위치와 재귀 PTE 매핑이 공격 경로에 쓰였습니다. 글은 취약점 수정 외에도 특정 익스플로잇 기법의 기반을 없애거나 공격 비용을 높이는 방안을 제안합니다.

원문: Calif / 번역·요약: Trawling