ROP같은 code control로 권한 상승을 유도하는 것이 아닌, Data-Oriented Programming으로 커널 데이터 구조를 직접 조작해서 권한 상승을 이루는 기법이다.
Linux의 프로세스마다 존재하는 task_struct 구조체에는 자격 정보를 담는 cred 구조체 포인터가 존재한다. cred 내부에는 uid, gid 등 ID 값들이 있는데, 이 중 uid.val을 0으로 바꾸면 해당 프로세스는 root privilege (UID 0)을 얻게 된다.
즉 DOP는 code flow 조작 없이 존재하는 커널 함수나 연산을 조합하여 uid.val=0 대입 연산을 수행하는 것이 목표이다.
DOP 기반 권한 상승에는 세 가지 단계가 필요하다.

task_struct, current나 cred객체의 주소같은 민감한 포인터 주소를 알아내야 한다. 
cred->uid 위치를 읽을 수 있다.
cred->uid가 저장된 정확한 커널 주소에 0을 써 넣어야 프로세스가 루트 권한을 획득할 수 있게 된다.DOP 기법은 커널 내부의 데이터 구조만을 조작해 최근에는 많이 막힌 ROP를 대체하는 기법이다. ROP는 offset 및 gadget에 많이 의존하기 때문에 패치가 들어올 때마다 가젯이 변경 및 삭제되고, 오프셋이 랜덤화되면서 매번 ROP chain을 구현하기 힘들다. 그러나 DOP는 heap에 올라가는 객체 간의 상대적 배치나 필드 오프셋이 크게 바뀌지 않는 한 (커널 버전 업데이트 등) 패치 전후에 똑같이 동작한다. (Patch-agnostic) 다시 말하면 mitigation이 적용되기 어렵다. function이 추가로 들어와도 결국 구조체 구조가 변하지 않는 이상 공격 방법은 똑같다.
macOS wireless stack에서 발생하는 취약점.
wireless stack은 크게 유저 공간과 커널 공간으로 나눌 수 있다.
유저 공간에서는 SSID 검색, 연결 요청이나 인증(802.1X), 암호화 설정, 채널 관리(=주파수 설정) 등 "어떤 네트워크에 어떻게 접속할지"를 결정하는 고수준의 제어를 할 수 있다.
커널 공간에서는 하드웨어를 제어하고, 802.11 MAC header를 생성 및 파싱하고, 재전송 및 ack 처리, 프레임 복호화 등 "실제로 무선 칩셋과 주고받는 낮은 레벨의 프레임 처리" 수행한다.
ssid는 Service Set Identifier의 약자로, WiFi 네트워크의 이름 문자열을 말한다.
802.1X는 유무선 네트워크 접근 제어의 표준으로, '내가 이 네트워크에 접속해도 되는 디바이스인지'를 확인받기 위한 client<->RADIUS 간의 인증 과정을 말한다.
RADIUS는 네트워크 접근 제어를 위해 쓰이는 인증 서버 프로토콜로, Authentication(인증)-Authorization(권한 부여)-Accounting(사용 기록)을 수행한다.
kext는 macOS의 kernel인 XNU에 새로운 기능을 동적으로 추가할 수 있게 해주는 모듈이다.
macOS에서 boradcom 드라이버는 IO80211Family.kext 위에서 동작하며, 무선 하드웨어 제어를 담당한다.
IO80211Family.kext는 macOS에 내장된, 802.11 wireless stack을 담당하는 커널 확장 모듈
해당 CVE는 kext에서 유저 공간과 커널 공간 사이에서 발생하는 데이터 이동을 attack surface로 삼는다.
공격 대상은 IO80211Family.kext 에 정의된 setIE()와 getIE() 함수로,
IO80211Family가 User‑space ↔ Kernel‑spcae 경계에서 제공하는 두 가지 핵심 인터페이스이다.
두 함수 모두 ioctl()(=IOConnectCallMethod()) 형태로 유저 영역에서 호출 가능하며,
802.11 프레임에 포함되는 IE 블록을 유저 메모리 ↔ 커널 메모리 사이에 복사(copyin/copyout)할 수 있다.
int AirPort_BrcmNIC::setIE(a1, a2, apple80211_ie_data *input)
{
uint8_t *ptr = osl_mallocz(*(a1 + 2528), 10000);
...
strncpy_chk(ptr, "add", 4, 4);
ptr[12] = input->data->id;
memcpy(ptr+14, &input->data->len, input->ie_len-1);
/* Point 0. this value is the key point of triggering overflow */
ptr[13] = BYTE(input->ie_len-1);
// store the buffer to "vndr_ie" variable
err = wlIovarOp(a1, "vndr_ie", 0, 0, ptr, v18 + 14);
}
int getIE(a1, a2, a3, a4, input)
{
struct apple80211_ie_data data;
vndr_ie *ptr;
copyIn(*(input + 32), &data, 0x20uLL);
...
/* Point 1. allocate with size that user input */
ptr = IOMalloc(data.ie_len);
data.ie_data = ptr;
...
// this function calls AirPort_BrcmNIC::getIE() internally.
apple80211RequestIoctl(this, 0xC03069C9, 85, a2, &data);
...
err = copyOut(&data, *(input + 32), 32); //meta data
if(!err)
copyOut(data.ie_data, user_ptr, data.ie_len);
}
IE (Information Element)는 IEEE 802.11 무선 프레임 안에서 부가 정보를 주고받기 위해 쓰이는 자료 구조이다. (TLV 자료구조, type-length-value를 담음) IE가 사용되는 프레임으로는 beacon, probe request/response 등 프레임에 자주 쓰인다.
setIE에서 len 필드를 마음대로 설정해 두면 getIE에서 그 len값을 기준으로 유저 버퍼를 할당하고 복사하기 때문에, getIE를 호출할 때 크기를 len보다 작게 지정하면 heap을 덮어쓰는 overflow가 발생한다. 즉 버퍼 크기 할당 및 복사 제어가 사용자한테 열려 있어 heap overflow를 유발할 수 있다.
즉, getIE가 복사해주는 데이터 크기를 내부에서 제대로 검증하지 않아 커널 heap의 버퍼 크기를 넘어서는 과도한 크기 복사가 일어나면 heap overflow가 발생하는 것이다.
기본 heap OOB만으로는 권한 상승을 유발할 수는 없다.
macOS는 copyOut 등으로 유저 메모리로 데이터를 복사할 때 객체 크기를 검사하는 추가 방어 기법이 존재한다. 할당된 객체 크기보다 더 큰 크기를 복사하려고 하면 kernel panic을 일으켜 과도한 메모리 읽기를 방지하는 Hardened Copy 기법이 존재한다.
getIE()에서는 apple80211RequestIoctl(this, 0xC03069C9, 85, a2, &data);에서 heap overflow를 발생시켜도, 뒤에 메타데이터를 한번 복사할 때와, 유저 영역으로 해당 정보를 가져올 때 copyOut을 호출하게 되고, 여기서 overflow를 감지하고 panic을 띄운다.
그런데 코드 구조상 이를 우회할 수 있다. exploit 과정에서는, getIE의 첫 번째 copyOut()을 고의로 실패시켜 첫 if 조건문을 건너뛰게 하면, 두 번째 copyOut()을 호출하지 않게 한다. 메타 데이터 복사 자체는 panic을 일으키지도 않고 그냥 에러 띄우면 실행도 안되기 때문이다.
첫 copyOut()을 실패시키는 방법은 읽기만 가능한 페이지에 매핑되지 않은 페이지나 NULL 페이지를 복사하도록 유도하면, 커널은 유저 공간 쓰기 불가 상황을 감지해서 err !=0 을 반환하게 된다.
heap overflow 단계에서 버퍼 크기 할당 및 복사 제어를 공격자 마음대로 할 수 있다는 점을 통해, overflow로 오염시킬 수 있는 영역을 정할 수 있다. macOS 커널은 크기별로 kalloc zone을 나누어 관리하는데, 32바이트 이하 chunk는 kalloc.32에, 192바이트 이하 chunk는 kalloc.192에 저장하는 식이다.
사전에 Heap Spray를 통해 (IODataQueue 등 사용) kalloc.192 zone에 가능한 많이 동일 크기 chunk를 채우고, 일부를 해제하여 빈 공간을 만들면 후에 새로 할당된 chunk의 위치를 고정시킬 수 있다. 이후 getIE가 할당할 크기만큼 공간을 비워두면, heap overflow를 발생시킬 수 있는 조건이 갖추어진다.
struct ip6_pktopts {
//...
struct in6_pktinfo *ip6po_pktinfo; // <-- 포인터 필드
int ip6po_minmtu; // <-- 정수 필드
// ...
};
타깃 obj로 kalloc.192에 존재하는 struct ip6_pktopts를 잡았는데, 해당 구조체는 내부에 kalloc.32 내부의 chunk를 가리키는 포인터가 존재한다. 즉 kalloc.192 안에 들어있는 포인터를 조작하여 작은 chunk로 재진입할 수 있는 구조를 가지고 있다. 즉 임의 포인터를 커널 메모리 내부에 쓸 수 있게 되기 때문에, ip6po_pktinfo 포인터 값을 유저가 조작한 주소를 가리키도록 유도하고, copyOut이 해당 조작된 포인터를 따라가 해당 주소에 있는 메모리 값을 그대로 유저 공간에 가져오는 Information Leak이 가능하다.
kalloc.32 영역에 mach_task_self() 값을 담은 chunk를 스프레이 해두고, overflow를 통해 kalloc.192에 덮어쓴 포인터가 다른 mach_task_self() chunk중 하나의 시작 주소를 가리키므로 mach_task_self()의 포인터 주소를 유저 모드에 누출할 수 있다.
mach_task_self() 포인터 내부에는 ipc_port 구조체 포인터가 있고, ipc_port 구조체에는 task_struct 구조체 포인터가 있기 때문에 해당 주소를 읽을 수 있다. ip6po_pktinfo 포인터 값을 반복적으로 덮어써서 해당 과정을 반복적으로 읽으면 커널 전체 어디든 한 단계씩 따라 들어가서 원하는 구조체의 멤버를 읽어낼 수 있다. (AAR)
struct proc 포인터까지 얻었으면, cred 영역까지 찾아갈 수 있고, 해당 필드를 0으로 덮어쓰면 프로세스를 root 권한으로 상승시킬 수 있게 되는 것이다.
DOP는 exploit 전반에 걸쳐 어떤 object의 어떤 data를 덮어서 데이터 흐름을 탈취할 것인지 결정하는 것이 중요하다. 어떤 객체의 포인터 값을 바꾸면 Information leak, AAR, AAW로 이어질 수 있을지 분석하는 것이 주가 된다.
CVE-2021-31077의 경우에는 struct ip6_pktopts 내부의 struct in6_pktinfo *ip6po_pktinfo;를 선택했는데, getIE 버퍼가 kalloc.192 zone에 속하고 ip6_pktopts 역시 동일 zone에 속하기 때문에 OOB 취약점을 활용할 수 있다는 점과, ip6po_pktinfo 내부에 kalloc.32를 가리키는 포인터가 있었다는 점이 재귀적으로 info leak을 발생시킬 수 있었다는 이유 때문이다.