최후의 0day 탈옥: Tachy0n
(blog.siguza.net)- 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 분야가 얼마나 빠르게 변했는지를 보여줌
댓글과 토론
애플의 하드웨어가 훌륭하기는 하지만 소프트웨어는 사용자를 목줄 채우려는 의도로 가득하죠.
자신이 만들어 빌드한 앱을 자신의 기기에서만 작동시키려고 해도 100달러짜리 구독이 필요합니다.
중소규모 오픈 소스 앱을 사용하고 직접 빌드해 쓰는 개발자라면
애플의 기기에서 취약점을 이용해 탈옥해가며 사이드로드 하는 것 보단 그냥 안드로이드 쓰는 것이 편해요.
Hacker News 의견들
-
1조 달러 기업을 이긴 방식이 Apple이 특히 약한 단순하고 지루한 작업, 즉 회귀 테스트였다는 점이 인상적임
iOS 12에서 jailbreak에 쓰인 큰 취약점 중 하나였던 SockPuppet은 Project Zero의 Ned Williamson이 찾아 Apple에 보고했고, iOS 12.3에서 패치됐으며 이후 Project Zero 버그 트래커에서 공개됐음
그런데 iOS 12.4에서 마치 패치된 적이 없던 것처럼 다시 나타났고, 아마 Apple이 해당 버전용으로 XNU를 별도 브랜치로 포크하면서 패치를 적용하지 못한 듯함
이건 이런 종류의 취약점에 대한 회귀 테스트가 없다는 강한 신호였고, 알려진 1-day 몇 개만 자동화해도 Pwn이 바로 적중시킬 수 있었음
Linux, FreeBSD, OpenWRT, OpenSSH 같은 프로젝트의 새 버전에 과거 취약점을 계속 돌리는 CI 팜을 운영하는 곳이 얼마나 될지 궁금함- 회귀 테스트는 고친 버그가 다시 생기지 않는지 확인하는 표준 QA 절차임
20년 전 대학 시절 Mozilla에서 자원봉사 QA를 했는데, 렌더링/레이아웃이나 JavaScript 엔진 버그를 중심으로 계속 커지는 회귀 테스트 모음이 있었음
재현과 수정 확인을 위해 최소 테스트 케이스를 만들다 보니, 그걸 빌드 파이프라인에 넣기도 쉬웠음
버그는 피할 수 없지만, 시간과 돈을 들여 고친 버그가 되살아나는 건 최악의 시나리오임
품질을 중시하는 조직은 분명 회귀 테스트에 투자하지만, 많은 조직은 QA를 존중하지 않고 아예 안 하거나 최저가 외주로 넘김
Apple이 역사적으로 가장 주목도 높은 버그 부류인 jailbreak에 대한 회귀 테스트를 갖추지 않았다는 건 정말 이상함
요즘 Mozilla는 여러 비판을 받을 수 있지만, 2000년대 초반에도 Tinderbox와 Bugzilla 같은 도구로 꽤 탄탄한 QA와 CI/CD를 운영했음
DevOps가 유행하며 이런 방식을 대중화했을 때, 이미 다들 하고 있던 게 아니었나 싶었지만 그건 내 착각이었음 - 여기서 프로젝트가 정보기관까지 포함한다면, 적어도 G10 정보기관과 러시아, 중국, 북한, 그리고 수많은 민간 그룹은 이런 작업을 하고 있다고 봐도 안전함
- 근본 문제는 많은 조직이 보안 업무를 별도 흐름과 별도 버그 분류로 격리했다는 데 있어 보임
보안과 기능 개발의 분리가 만든 Conway의 법칙 같은 상황임
빌드/릴리스 절차와 성숙한 회귀 테스트 모음이 있더라도, 내부 조직 구조상 이런 보안 이슈는 그 안에 들어가지 않을 가능성이 큼 - 이름은 기억나지 않지만, 이슈마다 테스트 케이스 디렉터리가 있는 FOSS 프로젝트를 본 적 있음
수천 개는 쉽게 있었고, 아마 SQLite였던 것 같음
본받을 만한 방식임
수정 사항을 백포트하지 않는다면 테스트도 백포트하지 않을 가능성이 높아 보임
- 회귀 테스트는 고친 버그가 다시 생기지 않는지 확인하는 표준 QA 절차임
-
kheap separation, task port 완화, SSV, SPTM 같은 말을 보니, 외국어로 친구와 잘 대화하다가 갑자기 뇌수술이나 핵물리학 설명으로 넘어가 이해가 절벽에서 떨어지는 느낌이었음
용광로 개보수 대화를 통역하려 했던 때도 비슷했음
jailbreak가 이제 예전만큼 활발하지 않은 건 아쉬움
jailbreak한 iPad로 실용적인 걸 한 건 거의 없지만 재미는 있었고, 지금이라면 tethering 앱과 UTM, 그리고 JIT 해법을 설치하고 싶음
SideStore도 유망해 보였지만, 내 계정이 한때 유료 Apple Developer 계정이어서 만료되지 않는 앱 ID 10개가 남아 있고, 그래서 새 계정을 만들거나 다시 돈을 내지 않으면 UTM 같은 앱을 설치할 수 없음- 예전 iPhone 4를 jailbreak해서 썼고, 그게 iPhone을 주 기기로 쓸 수 있는 사실상 유일한 방법이었음
그걸 잃은 뒤에는 Android로 돌아갔고, 그때쯤 Android는 기본 기능 면에서 꽤 따라잡은 상태였음
- 예전 iPhone 4를 jailbreak해서 썼고, 그게 iPhone을 주 기기로 쓸 수 있는 사실상 유일한 방법이었음
-
Apple이 요즘 jailbreak에 100만 달러를 지급한다고 들었고, 그게 자유 시장 가격의 하한선일 것임
- 그 경계는 이미 2015년에 깨졌고, 거의 10년 전 일임: https://www.dailymail.co.uk/sciencetech/article-3301691/New-...
- 그게 시장 가격임: https://cyberscoop.com/zerodium-android-zero-days-bounty/
- jailbreak를 갖고 있다면 Apple에 연락해서 수백만 달러 보상을 신청하는 방법이 있는지 궁금함
중개업체를 거쳐야 하는 건지, 아니면 1차 고객지원으로 들어가 묻히지 않을 공개 이메일 같은 게 있는지 모르겠음
-
이게 사실이라면 Apple은 놀라운 전략을 쓴 셈임
기기에서 루트를 얻을 모든 길을 잠가두면, jailbreak 개발자들이 무료로 찾아낸 취약점을 Apple이 패치할 수 있음- 그런데 완전히 그런 건 아님
글에 따르면 비공개 커뮤니티는 여전히 익스플로잇을 갖고 있고 Apple은 그것들을 패치함
공개 커뮤니티나 이 개발자 쪽만 어떤 이유에서인지 더 이상 그렇지 않은 듯함
- 그런데 완전히 그런 건 아님
-
글 전체에서 가장 좋았던 문장은 “iOS 13.0에서 그 버그를 unpatch해준 누군가에게도 감사하고 싶다. 그것도 아주 멋진 행동이었다.”였음
-
“5년 뒤 우리가 어디에 있을지 상상도 못 하겠다”는데, 나는 상상됨
iMessage는 여전히 기기, 계정, 데이터 탈취를 가능하게 함 -
tethered인지 untethered인지 글에 나오지 않았음
- tachy0n은 로컬 권한 상승(LPE)이어서 그 분류가 별로 맞지 않음
이게 포함되어 배포된 jailbreak인 unc0ver는 아마 “semi-untethered”였던 것으로 알고 있음
- tachy0n은 로컬 권한 상승(LPE)이어서 그 분류가 별로 맞지 않음