- tachy0n은 iOS 13.0~13.5에서 동작한 커널 권한 상승 exploit로, 2020년 5월 23일 unc0ver v5.0.0에 0day로 포함되어 당시 최신 iOS를 직접 겨냥함
- 핵심은
lio_listio의 Lightspeed 경쟁 조건으로,kalloc.16객체를 두 번 해제해 서로 다른 객체가 같은 메모리를 가리키게 만들 수 있었음 - 같은 계열의 버그는 이전 iOS 11 대상 Spice jailbreak/untether에서도 쓰였지만, 앱 샌드박스와
racoon환경은 권한·샌드박스·스프레이 기법에서 제약이 크게 달랐음 - unc0ver용 exploit은
OSData겹침을 반복적으로 확보하고,IOBufferMemoryDescriptor와IOAcceleratorFamily2를 이용해 fake task와 fake mach port를 구성함 - iOS 14 이후 Apple은 allocator 분리, sequestering, PAC, 객체별 하드닝으로 exploit 전략 자체를 차단하는 쪽으로 이동했고, 공개 iOS 커널 exploit 지식은 iOS 13 시절보다 크게 뒤처진 상태임
tachy0n 공개가 이례적이었던 이유
- tachy0n은 iOS 13.0부터 13.5까지 영향을 준 오래된 exploit이며, 2020년 5월 23일 unc0ver v5.0.0에 포함되어 공개됨
- 당시 기준으로는 표준적인 커널 로컬 권한 상승(kernel LPE)이었지만, 최신 iOS 버전에 영향을 주는 0day jailbreak로 공개됐다는 점이 드물었음
- Apple은 exploit 공개 약 1주일 뒤 해당 버그만을 위한 패치를 배포함
- 이 버그는 iOS 13.5에서 0day였지만, 이전에는 1day 형태로 이미 활용된 적이 있었음
- Pwn20wnd는 앱 샌드박스에서 도달 가능한 0day를 찾았고, 출발점은 알려진 1day들에 대한 회귀 테스트였음
- iOS 12의 SockPuppet은 iOS 12.3에서 패치됐지만 iOS 12.4에서 다시 나타남
- 이 사례는 해당 종류의 버그에 대한 Apple의 회귀 테스트 부재를 드러냈고, Pwn20wnd는 몇몇 알려진 1day 회귀 테스트를 구현해 hit를 얻음
Lightspeed: lio_listio 경쟁 조건
- tachy0n의 핵심 버그는 Synacktiv의 Lightspeed 버그이며, CVE-2020-9859와 CVE-2018-4344 가능성과 관련됨
- 취약점은 비동기 또는 배치 파일 I/O를 수행하는
lio_listiosyscall에 있음 - 커널은 제출된 I/O 작업을 추적하기 위해
aio_lio_context구조체를 할당함- 구조체에는
io_waiter,io_issued,io_completed필드가 있음
- 구조체에는
- 실제 작업은 별도 스레드에서 수행되고, 모든 I/O가 완료되면
do_aio_completion이 해당 컨텍스트를 해제함 - 아무 작업도 스케줄되지 않은 경우에는
lio_listio의 현재 스레드가 컨텍스트를 해제해야 함 - 문제는 이 검사에 경쟁 조건이 있다는 점임
- 다른 스레드에 작업이 제출됐지만, 검사 시점 전에 이미 완료되어 컨텍스트가 해제될 수 있음
- 이때
lio_listio는 dangling pointer가 된lio_context를 다시 검사함
double free가 exploit으로 이어지는 흐름
- exploit에 필요한 순서는 다음과 같음
lio_listio가lio_context를 할당함- 작업이 완료되고
do_aio_completion이lio_context를 해제함 - 해제된 메모리를 공격자가 제어하는 객체로 재할당하고
lio_context->io_issued == 0처럼 보이게 만듦 lio_listio가 이를 보고 공격자 객체를 다시 해제함- 같은 메모리를 또 다른 객체로 재할당해 서로 다른 두 할당이 같은 메모리를 가리키게 만듦
- 64비트 기기에서
lio_context는 가장 작은 zone인kalloc.16에 들어감 - iOS 14 이전에는 객체 종류와 무관하게 크기 단위로 같은 allocation site를 공유했음
- C++ 객체, 포인터 배열, 사용자 제공 데이터 버퍼가 같은 크기 bucket 안에서 서로의 메모리를 재사용할 수 있었음
- 이 double free는 중간 재할당이 없으면 쉽게 치명적 상태로 가는 일반적인 double free와 다르게 동작함
lio_context->io_issued는 할당 중에는 0이 되지 않음- 해제 후 allocator가 첫 8바이트를 canary 값과 freelist pointer 또는 객체 주소 XOR 값으로 덮음
- 따라서 두 번째 free는 중간에 재할당이 있고 4~7바이트가 0인 경우에만 발생함
- 실제 exploit은 이 경쟁을 여러 번 재시도할 수 있었고, 시스템의 다른 객체가 우연히 필요한 바이트를 0으로 만들어 double free를 유발하는 경우는 실전에서 드물었음
Spice에서의 이전 활용
- 같은 버그는 iOS 11.x를 대상으로 한 Spice jailbreak/untether에서도 사용됨
- Spice는 Sparkey, littlelailo와 함께 Jake Blair 팀에서 작업됐으며, 당시 최신 버전은 iOS 13.x였음
- 목표는 앱 환경과
racoon환경 모두에서 mach port forgery를 만드는 것이었음- iOS 14 이전 커널 exploit에서는 사용자 제공 값을 mach port 포인터로 해석하게 만들면 이후 단계가 결정적으로 쉬워졌음
- Lightspeed로 mach port forgery를 만드는 기본 흐름은 다음과 같았음
lio_context의 첫 번째 free를 유발함- 크기 1 또는 2의 OOL mach ports descriptor를 가진 mach message를 spray함
- 첫 entry를
MACH_PORT_NULL로 두어kalloc.16에 들어가고io_issued가 0처럼 보이게 함 - 두 번째 free로 OOL mach ports 배열을 해제함
kalloc.16에 제어 데이터를 spray해 mach ports 배열을 fake pointer로 대체함
앱 샌드박스와 racoon의 제약 차이
- A7~A9(X)에서는 PAN이 없어
mmap과mlock만으로 userland 주소를 커널 포인터처럼 역참조할 수 있었음 - A10과 A11도 지원하려 했지만, 앱 샌드박스에서는 적절한 커널 주소 누수와 제어 데이터 배치 대상을 찾지 못해 완성되지 않았음
- 활용하려던 1day에는 Ian Beer의 커널 스택 정보 누수와 backboardd 샌드박스 탈출이 있었음
- 계획은 공유 메모리 포인터를 leak하거나 커널
__DATA세그먼트에 데이터를 배치하는 방식이었음 - 적절한 target을 찾지 못해 앱 경로의 A10/A11 지원은 이뤄지지 않음
- 계획은 공유 메모리 포인터를 leak하거나 커널
racoon경로는 조건이 달랐음- root로 실행되지만 일반 앱보다 더 빡빡한 샌드박스를 가짐
- IOSurface 접근이 없어 일반적인
OSUnserializeXMLviaIOSurface::setValue스프레이를 쓸 수 없었음 - 대신
RootDomainUserClient::secureSleepSystemOptions안의OSUnserializeXML호출을 이용해 일부 객체를 leak 형태로 spray할 수 있었음
racoon은 모든 sysctl 읽기와 쓰기를 허용하는 샌드박스 프로파일을 가졌고 root 권한도 있었음- 커널 slide를 알면 커널
__DATA에 있는 sysctl global을 알려진 주소의 데이터 저장소처럼 쓸 수 있었음 - Spice에서는
vm.swapfileprefix를 선택함
- 커널 slide를 알면 커널
- 커널 slide 확보에는 panicall의 CVE-2018-4413을 사용함
sysctl_procargsx의 정보 누수로kernel_map의 초기화되지 않은 커널 메모리 거의 한 페이지를 leak할 수 있었음- 이로써 커널 코드와 heap pointer를 얻고 A7~A11을 처리할 수 있었음
- 앱 샌드박스에서는
sysctl_procargsx가 차단되어 같은 방식이 불가능했음
unc0ver용 tachy0n exploit 구조
- unc0ver 대상은 A8~A13이었기 때문에 A10+를 무시하거나 userland 역참조에 의존할 수 없었음
- exploit은 실패 가능한 메모리 corruption 단계를 감안해 두 계층 구조로 설계됨
- 하위 계층은
lio_listio를 호출하는 freerer 스레드와 IOSurface를 통해OSData를 unserialize하는 racer 스레드를 실행함 - 기본값은 freerer 4개, racer 16개이며 조정 가능함
- 하위 계층은
- IOSurface를 통해 unserialize되는 데이터는 여러
OSDataentry를 가진OSDictionary였음io_issued에 해당하는 위치는 0이어야 함0x41414141,0x69696969같은 magic value와 key 값k가 overlap 감지에 사용됨
- 경쟁 후 모든
OSData값을 검사함- magic value가 바뀐 객체는 시스템의 다른 객체가 가져간 것으로 보고 나중에 cleanup 대상으로 표시함
- key와 buffer 내부의
k값이 다르면 다른OSData객체가 같은 backing buffer를 가리키는 overlap이 발생한 것으로 판단함
- 코드의
maybe_reyoink와overlap함수는 이러한 겹침 정보를 구성해 상위 계층에 넘김 - 상위 계층은 겹친
OSData객체를 이용해 fake mach port를 구성함- 하나의
OSData를 free함 - OOL port descriptor를 가진 mach message를 spray함
- 다른
OSData를 free함 - fake task port 포인터를 담은 새
OSData로 재할당함
- 하나의
알려진 커널 주소에 제어 데이터 배치하기
- exploit은 OOL ports descriptor 배열로 재할당된 내용을
OSData로 읽어 mach port의 raw kernel pointer를 leak할 수 있었음 - 이후 단계에서는 task port와
IOSurfaceRootservice port 주소를 leak하는 데 이를 사용하지만, 핵심 문제는 제어 가능한 buffer의 커널 주소를 안정적으로 얻는 것이었음 - XNU 소스에서 찾은 후보는
IOMemoryDescriptor였음_ranges필드는IOVirtualRange배열이며, 단일IOVirtualRange는kalloc.16에 정확히 들어감- 다만 일반
IOMemoryDescriptor는 range가 하나뿐이면 heap allocation 대신_singleRange를 사용함
IOBufferMemoryDescriptor는 예외적으로 range 하나에 대해IONew(IOAddressRange, 1)를 호출해 heap allocation을 수행함- 이를 임의로 할당하고 사용자 주소 공간에 매핑할 수 있는 편리한 위치가
IOAcceleratorFamily2의 AGX 인터페이스였음IOGraphicsAccelerator2에서 type 0 userclient를 열면IOAccelContext2를 얻음::clientMemoryForType()으로 세 가지 memory descriptor를 map할 수 있음- type 0은 0x8000 bytes라 victim descriptor 식별에 사용됨
- exploit은 다음 루프를 사용함
IOAccelContext2를 열고 겹친OSData두 개를 가져옴- 하나의
OSData를 free함 - 미리 연
IOAccelSharedUserClient2를IOConnectAddClient()로 연결함 - 남은
OSData를 읽어 첫 8바이트가 page-aligned kernel pointer이고 다음 8바이트가0x8000인지 확인함 - 조건이 맞지 않으면
IOAccelContext2를 닫고 반복함
pageable memory, fake port, zone_require
- memory descriptor를 process에 map하고 커널 주소를 알아낸 뒤에도 메모리가
kIOMemoryPageable로 만들어진다는 문제가 남았음 - fake mach port와 fake task 객체는 preemption이 꺼진 상황에서 접근될 수 있으므로, 커널 쪽에서 해당 page를 fault-in해야 했음
IOAccelContext2::processSidebandBuffer를 간접 호출하는IOAccelContext2::submit_data_buffersexternal method 2를 두 번 호출해 이를 처리함- 공유 메모리 시작점에서 0x10바이트 뒤의 구조체를 읽음
- 첫 구조체는
tok == 0x100이고 전체 page를 덮도록 만들어 두 번째 page까지 진행하게 함 - 두 번째 page는 이후 fake object 데이터를 둘 수 있음
- 이후 단계는 fake task, fake port, OOL descriptor switcheroo, 임의 읽기 primitive 구성으로 이어짐
zone_require우회도 필요했음- 당시
zone_require는zone_map밖 page를 허용하며 page의 첫0x20바이트를 metadata처럼 해석했음 - 올바른 zone index를 넣으면 원하는 zone에 대한 통과권처럼 사용할 수 있었음
- 이 때문에 task용 page와 mach port용 page, 총 두 page가 필요했음
- 당시
- 이 exploit은 현재 GitHub에 공개되어 있음
공개 이후 분석과 패치
- 최신 서명 버전에 대한 완전한 0day exploit 공개는 iOS jailbreak scene의 주목을 받음
- 당시 Project Zero에서 일하던 Brandon Azad는 exploit 공개 후 4시간 안에 취약점을 파악하고 Apple에 알림
- 해당 분석은 How to unc0ver a 0-day in 4 hours or less에 정리됨
- exploit 공개 6일 뒤 Synacktiv는 새 글을 통해 iOS 12의 원래 fix가 memory leak을 만들었고, 그 memory leak을 고치려는 시도가 원래 버그를 되살렸을 가능성을 다룸
- exploit 공개 9일 뒤 Apple은 패치를 배포함
- 이후 XNU에는 해당 버그에 대한 회귀 테스트가 추가됨
- 공개 54일 뒤 reverse-engineered 버전인 “tardy0n”이 Odyssey jailbreak에 포함됐고, 대상은 역시 iOS 13.0~13.5였음
iOS 14 이후 달라진 exploit 환경
- iOS 14는 Apple의 커널 보안 전략 변화를 보여줌
- iOS 14 이전에는 초기 primitive가 heap overflow, C++ 객체 over-release, type confusion 등 무엇이든 다음 target은 대체로 mach port였음
- iOS 14에서 가장 큰 변화 중 하나는 allocator인
kalloc과zalloc이었음- zone map을 여러 “kheap” 범위로 나눔
- 사용자 제어 데이터와 커널 객체가 서로 다른 heap에 들어가도록 분리함
- 커널 객체에는 sequestering을 적용해 특정 zone에 할당된 virtual address page가 재부팅 전까지 다른 zone에 재사용되지 않게 함
- physical memory는 해제될 수 있지만 virtual memory range는 다른 객체에 재사용되지 않아 커널 객체 type confusion을 사실상 차단함
- guard page, 부팅마다 달라지는 zone allocation 시작 위치, 이후의 정교화가 더해져 cross-zone 공격의 신뢰성이 크게 낮아짐
- Apple은 단순히 개별 버그를 막는 데서 exploit 전략을 막는 방식으로 바뀜
- exploit이 kmsg struct를 corruption target으로 쓰면 해당 구조체가 서명됨
- pipe buffer를 안정적인 커널 읽기/쓰기 인터페이스로 쓰면 관련 포인터가 PAC 적용 대상이 됨
- 관련 없는 객체를 victim으로 쓰는 방식이 나오면 해당 객체 타입이 하드닝됨
- 그 결과 exploit 개발에서 초기 memory corruption 0day보다 exploit strategy가 더 가치 있어지는 상황이 생김
공개 지식의 단절
- iOS 14 이전에는 공개 iOS 보안 연구 지식이 비공개 지식과 거의 비슷한 수준이었다고 평가됨
- iOS 14 이후에는 예외를 제외하면 정보 공유가 사실상 멈춘 상태가 됨
- iOS 19 beta가 몇 주 남은 시점에도 iOS 18 또는 iOS 17에 대한 공개 커널 exploit은 없는 상태라고 정리됨
- Apple 보안 노트에는 야생에서 악용된 취약점이 때때로 올라오지만, 공개 정보는 비공개 연구를 따라가지 못하고 있음
- tachy0n이 공개된 지 5년밖에 되지 않았다는 점은 iOS 커널 exploit 분야가 얼마나 빠르게 변했는지를 보여줌