Bir tarayıcıda kod çalıştırmayı başarmak, işletim sisteminde serbestçe hareket edebilmek için her zaman yeterli olmaz. JavaScriptCore ele geçirildikten sonra bile saldırganın Safari'nin sandbox sınırlarını aşması gerekir. RET2 Systems, Pwn2Own 2018'de bu ikinci aşama için macOS'un masaüstü görüntüsünü yöneten WindowServer servisini hedefledi. O dönemde root yetkisiyle çalışan bu servisteki bellek bozulması hatasını, doğrudan servis sürecinin içinde çalıştırdıkları bir fuzzer ile bulmuşlardı.
Bu yazıda, RET2'nin Pwn2Own serisinin son bölümünde anlattığı çalışmayı Türkçe olarak ele alıyorum. CVE-2018-4193 olarak kaydedilen açık, macOS 10.13.3'te WindowServer üzerinden yetki yükseltmeye olanak tanıyordu. Hatanın kaynağı, _CGXRegisterForKey() adlı Mach mesaj işleyicisindeki işaretli ve işaretsiz tamsayı karşılaştırmasıydı. Görseller, RET2'nin özgün yazısından alınmıştır.
Ekibin hazırladığı bağımsız demoda, tarayıcıya ihtiyaç duymadan çalışan düşük yetkili bir süreç, yaklaşık 90 saniye içinde root yetkisine ulaşıyor.

Tek bir Mach mesajıyla başlayan hata
WindowServer, süreçler arası iletişim için Mach IPC üzerinden çeşitli özel arayüzler sunuyor. SkyLight çatısındaki SLPSRegisterForKeyOnConnection() fonksiyonu, bir kullanıcı sürecinden WindowServer'a mesaj göndermek için kullanılabiliyor. Bu mesaj, sunucu tarafında _XRegisterForKey() üzerinden _CGXRegisterForKey() işleyicisine ulaşıyor.
Buradaki hatalı karşılaştırma, saldırganın belirlediği indeksle dizi sınırlarının dışına erişilmesine izin veriyor. İlk sonuç, sınır dışı bir okuma (out-of-bounds read). Bellekteki belirli koşullar sağlandığında aynı işlem, sınır dışı yazmaya da dönüşebiliyor.
RET2'nin yayımladığı en küçük hata gösteriminde (PoC), WindowServer bağlantısı kurulduktan sonra açığı tetiklemek için özel olarak hazırlanmış tek bir parametre yeterli:
// Minimal tetikleyici; RET2 Systems'in CVE-2018-4193 PoC'sinden.
uint32_t BUG = 0x80000000 | 0x41414141;
result = SLPSRegisterForKeyOnConnection(cid, &psn, BUG, 1);PoC, düşük yetkili bir kullanıcı tarafından çalıştırıldığında WindowServer'ın hemen çökmesine neden oluyor. Böylece fuzzer'ın bulduğu sınır dışı erişim, küçük ve tekrar üretilebilir bir örnekle doğrulanıyor.
Yazma işleminin üç kısıtı
Bu açık, saldırgana istediği değeri istediği adrese yazma özgürlüğü vermiyor. Yazma işleminin üç kısıtı var:
- İndeks, her biri 24 bayt uzunluğundaki yapılardan oluşan bir diziye uygulanıyor. Bu yüzden hedef konum, dizinin başlangıcına göre
index * 24adımlarıyla belirleniyor. - Yazma işlemini yapan kodun çalışabilmesi için hedefin çevresindeki belleğin iki ön koşulu sağlaması gerekiyor.
- Saldırgan, yazılan değerleri serbestçe seçemiyor. Kod, normalde heap üzerindeki bir işaretçiyi ve hemen arkasına dört baytlık WindowServer bağlantı kimliğini (CID) yazıyor.


Ekibin, bu ön koşulları sağlayan bir bellek yerleşimi oluşturması ve işaretçi ile CID çiftinin üzerine yazılmasından yararlanabileceği bir hedef bellek bloğu bulması gerekiyor.
Komşu bellek bloklarında hedef aramak
Çalışmada incelenen macOS kullanıcı modu heap ayırıcısı, Hoard benzeri bir yapıya sahip. Ayrılan bellek blokları arasında yönetim verisi bulunmadığı için nesneler bellekte yan yana yerleşebiliyor. Bu durumda bir bloğun sınırını aşan yazma, doğrudan komşu bloğun ilk baytlarını bozabiliyor.
RET2, böyle bir bozulmanın hangi nesneler üzerinde işe yarayacağını görmek için mevcut fuzzer'larının bazı parçalarını Frida ile yeniden kullandı. malloc, realloc ve free çağrılarını izleyerek ayrılmış bellek bloklarını, boyutlarını ve çağrı yığınlarını kaydettiler. Ardından yaklaşık her 10 saniyede bir, bu blokların rastgele seçilmiş bir bölümünün ilk dört baytını belirli bir değerle değiştirdiler. Böylece açığın oluşturabileceği bellek bozulmasını taklit edip ortaya çıkan çökmeleri gruplandırabildiler.
Özellikle ilk alanı işaretçi olan nesneler ilgi çekiyordu. Bu işaretçinin bir kısmı değiştirilerek saldırganın kontrolündeki veriye yönlendirilebilirse, nesnenin sonraki kullanımları da etkilenebilirdi. Objective-C nesnelerinde, böyle bir durumdan objc_msgSend() aracılığıyla kod çalıştırmaya giden bilinen teknikler vardı.
Ancak bağlantı kimliğinin bit düzeni, bu tekniğin doğrudan kullanılmasını engelliyordu. Araştırmacılar, macOS'taki Mach port numaralarının en düşük iki bitinin daima 1 olduğunu gördü. Bunun, XNU'nun port oluştururken yaptığı bir maskeleme işleminden kaynaklandığını düşündüler.
İncelenen Objective-C ve CoreFoundation uygulamasında ise en düşük biti 1 olan bir değer, sıradan bir bellek adresi yerine tagged pointer olarak yorumlanıyordu. Bu tür değerler, nesne verisini doğrudan kendi bitleri içinde taşıyor. İşaretçinin alt kısmı CID ile değiştirildiğinde bu bit de 1 oluyordu. Çalışma zamanı artık değeri normal bir nesne işaretçisi gibi kullanmadığından, ekip bu yolla doğrudan objc_msgSend() üzerinden kod çalıştıramıyordu.

Bu yüzden ekip, doğrudan CoreFoundation nesnelerini bozmak yerine WindowServer'ın kendi yönettiği yapılara yöneldi.
HotKey listesi
Bu arayışta HotKey nesneleri uygun bir hedef olarak öne çıktı. WindowServer, uygulamaların kaydettiği klavye kısayollarını bağlı liste içinde tutuyor. Bir HotKey nesnesinin ilk alanında, daha önce oluşturulmuş kısayol nesnesine giden hotkey->next işaretçisi bulunuyor.
SkyLight, bu nesneleri oluşturmak, sorgulamak ve değiştirmek için çeşitli API'ler sunuyor. Liste işaretçisi saldırganın kontrolündeki belleğe yönlendirildiğinde, aynı API'lerle sahte bir HotKey yapısının alanlarına erişmek de mümkün hâle geliyor.

Ekip, exploit'i beş aşama üzerine kurdu:
- Açığı kullanarak WindowServer'ın
MALLOC_TINYheap bölgesinin sonuna yakın bir adresi sızdırmak. - Bellek yerleşimini düzenleyip bir HotKey nesnesini saldırganın kontrolündeki bloğun hemen arkasına yerleştirmek ve liste işaretçisini kısmen değiştirmek.
- Bozulmuş işaretçinin gösterdiği bellek alanına, içeriği saldırgan tarafından belirlenen bir
CFMutableArrayyerleştirmek. - Dizideki bir
CFStringRefişaretçisinin tek bir bitini değiştirerek onu sahte nesne verisine yönlendirmek ve kod çalıştırmak. - Bellekte oluşan bozulmaları giderip WindowServer'ın çalışmaya devam etmesini sağlamak.
Birinci aşama: Heap yerleşimini öğrenmek
RET2, CGSSetConnectionProperty() aracılığıyla MALLOC_TINY bölgesine yaklaşık 500 bin CFString nesnesi yerleştiriyor. Çok sayıda benzer nesneyle belleği dolduran bu tekniğe heap spraying deniyor. Her string'in içeriği neredeyse tamamen sıfırlardan oluşuyor; ortasında dört baytlık bir 0x400 değeri bulunuyor. Bu değer ve çevresindeki sıfırlar, yazma işleminin iki ön koşulunu karşılayacak şekilde düzenleniyor.
Sınır dışı erişimde kullanılan indeks, 0xFFEDFFC0 gibi bir başlangıç değerinden artırılarak deneniyor. Denemelerden biri hazırlanan bellek düzenine denk geldiğinde, string'lerden birinin içeriği değişiyor. İşlem, WindowServer çökmeden gerçekleşiyor:

Ardından bağlantıya eklenen özellikler, CGSCopyConnectionProperty() ile geri okunuyor. Ekip, içeriklerinde yazma işleminin bıraktığı [8 baytlık işaretçi][CID] çiftini arıyor:
- Heap işaretçisi, yeni başlatılmış bir WindowServer sürecinde
MALLOC_TINYbölgesinin sonuna yakın bir adresi gösteriyor. - CID, bulunan verinin gerçekten sınır dışı yazma işlemiyle oluştuğunu doğruluyor.
Sızan adres, sonraki bellek düzenlemeleri için bir referans noktası sağlıyor. İncelenen heap uygulamasında MALLOC_TINY, yaklaşık 256 MB'lık bölgelerle daha düşük adreslere doğru öngörülebilir biçimde büyüdüğü için ekip, sonraki yerleşimleri bu adres üzerinden hesaplayabiliyor.
İkinci aşama: HotKey işaretçisini yönlendirmek
Heap yerleşimi hakkında bilgi edinildikten sonra exploit, bağlantı özelliklerini kullanarak 1 ile 4 GB arasında yeni bir bellek alanını dolduruyor. Amaç, bir HotKey nesnesini saldırganın kontrolündeki string'in hemen arkasına yerleştirmek. Nesneleri istenen sıraya getirmek için yapılan bu düzenlemeye heap grooming deniyor.
Bu işlem iki bölüme ayrılıyor. İlk bölümde bellekte yan yana duran value blokları belirleniyor. Her çiftin ikinci bloğu, CGSSetConnectionProperty() fonksiyonuna NULL value verilerek serbest bırakılıyor. Böylece heap içinde boşluklar açılıyor. Ardından art arda yapılan CGSSetHotKey() çağrıları, bu boşlukları HotKey nesneleriyle dolduruyor. Hedeflenen yerleşimde bir HotKey nesnesi, saldırganın kontrolündeki string'in hemen arkasında bulunuyor:

Hedeflenen sınırda açık tetiklendiğinde, yazma işlemi string bloğunun sonunu aşıp HotKey nesnesinin ilk dört baytına CID değerini yazıyor. Örneğin 0x00007fd15cfa8730 olan hotkey->next işaretçisi, 0x00007fd100005413 gibi bir değere dönüşüyor. İşaretçi artık geçerli bir HotKey nesnesi yerine, hesaplanan başka bir bellek alanını gösteriyor. Yazıda bu bozulmuş yapı dangling HotKey olarak adlandırılıyor. Bozulma gerçekleşmeden tamamlanan ikinci bölümdeki bellek doldurma işlemi, işaretçinin yöneldiği alanın da ayrılmış olmasını sağlıyor.
Sonraki adım, bozulmuş işaretçinin hangi string'in içine denk geldiğini bulmak. Ekip, başlangıçta kimliği 0 olan sahte HotKey üzerinde CGSSetHotKeyEnabled() çağrısını kullanıyor. Çağrı, belleğe yerleştirilen string'lerden birinin tek bir bitini değiştiriyor. String'ler yeniden okunup karşılaştırıldığında değişen blok bulunuyor. Böylece sahte HotKey yapısının hangi bellek bloğuyla örtüştüğü belirleniyor:

Üçüncü aşama: Aynı alana bir dizi yerleştirmek
Bu aşamada, sahte HotKey'in gösterdiği alana başka bir nesne yerleştirmek gerekiyor. Ancak yeni nesnenin bellek düzeni, HotKey API'lerinin beklentileriyle uyumlu olmalı. CID'nin alt bitleri 1 olduğu için sahte HotKey tek sayılı bir adreste bulunuyor. next alanının NULL ya da geçerli bir işaretçi olarak kalması gerekiyor. Ayrıca CGSSetHotKeyEnabled() çağrısının değiştirdiği bit, sekiz baytlık hizalama esas alındığında alttaki değerin 24. bitine denk geliyor.
Üyeleri CFStringRef olan ve uygun konuma yerleştirilmiş bir CFMutableArray, bu koşulları karşılıyor. Dizi başlığındaki iki işaretçi alanı sayesinde sahte HotKey'in next alanı NULL kalabiliyor. Dizideki string işaretçilerinden biri de API'nin değiştirebildiği bitin tam üzerine denk geliyor:

Yeni yerleşim, HotKey kimliğinin bulunduğu baytları da değiştiriyor. Exploit, kimliğin bilinmeyen 12 bitini CGSGetHotKey() çağrılarıyla deneyerek doğru değeri buluyor. Aynı çağrı, sahte yapının bazı alanlarını geri döndürdüğü için ek bir bilgi sızıntısı da sağlıyor. leak1 ve leak3 değerleri birleştirilerek MALLOC_LARGE bölgesindeki bir CFStringRef işaretçisinin tam değeri elde ediliyor.
Bu adres üzerinden, hedef işaretçinin ilgili bitinin 1 olup olmadığı belirleniyor. Bitin sıfırlanması gerekiyorsa CGSSetHotKeyEnabled() çağrısıyla kısayol devre dışı bırakılıyor.
Böylece dizideki bir CFStringRef işaretçisi, MALLOC_LARGE içinde saldırganın kontrol ettiği bir alana yönlendiriliyor. Bu alan, büyük string'ler aracılığıyla belleğe yerleştirilen sahte Objective-C nesnelerini ve ROP zincirini içeriyor.
Kod çalıştırmak ve servisi çalışır durumda bırakmak
Bozulmuş CFStringRef, artık bellekteki sahte string nesnesini gösteriyor. Dizinin serbest bırakılması, içindeki nesnelerin de bırakılmasını tetikliyor. Bu sırada saldırganın hazırladığı nesne üzerinden objc_msgSend() tekniği devreye giriyor ve programın kontrol akışı değiştiriliyor.
RET2, hazırladığı bir COP gadget'ının ardından ROP ve JOP zincirlerini kullanıyor. Zincir; okuma, yazma ve çalıştırma izinlerine sahip bir bellek sayfası oluşturup kontrolü shellcode'a aktarıyor. Kod artık root yetkisiyle çalışan WindowServer sürecinde yürütüldüğünden, Safari sandbox'ının dışına çıkılmış oluyor.

WindowServer'ın çökmesi masaüstünü de etkileyeceği için, kod çalıştırdıktan sonra servisin normal işleyişine dönmesi gerekiyor. Shellcode, bağlantının özellik sözlüğündeki ve HotKey listesindeki bozuk referansları temizliyor. Ardından kontrolü SLXServer içindeki ana Mach mesaj döngüsüne geri veriyor. Böylece servis, masaüstünü çalışır durumda tutarak görevine devam edebiliyor.

Geliştirme süresi ve başarı oranı
- CVE-2018-4193, 16 Şubat 2018'de bulundu. Açığın çalışan ve güvenilir bir exploit'e dönüştürülmesi yaklaşık 100 kişi-saat sürdü. Ekip, kendi JavaScriptCore exploit'i için de benzer bir süre harcadığını belirtiyor.
- Her denemeden önce yeniden başlatılan bir 2017 MacBook Pro üzerinde, 1.000'den fazla çalıştırmada yaklaşık %85 başarı oranı elde edildi.
- Exploit'in çalışması ortalama 90 saniyeyi aşıyordu. Sürenin büyük kısmı WindowServer'ın heap yerleşimini hazırlamakla geçtiği için ekip, kaçışa Eternal Groom adını verdi.

RET2, WindowServer'ı ve macOS heap yapısını daha yakından tanıyan bir araştırmacının daha hızlı ve güvenilir bir exploit geliştirebileceğini düşünüyordu. Bu nedenle bir yarışma açıp koşulları karşılayan ilk araştırmacıya ticari Binary Ninja lisansı teklif etti. Çözümün, macOS 10.13.3 üzerinde yalnızca CVE-2018-4193'ü kullanması, 10 saniyeden kısa sürede çalışması, WindowServer'ı çökertmemesi ve en az %90 başarı oranına ulaşması gerekiyordu. Araştırmacının yöntemi yayımlaması da şartlar arasındaydı.
Lisansı kazanan @elvanderb, çözümünü OffensiveCon '19'da sundu. RET2'nin yarışmada kullandığı exploit zincirinin tamamına da GitHub üzerinden erişilebiliyor.