berkesrc
  • Projeler
  • Deneyim
  • İletişim
  • Yazılar
entr
© 2026 Berke SerçeBaşa dön ↑
← Tüm yazılar

25 Eki 20269 dk okuma

Safari sandbox'ından root yetkisine: WindowServer açığı

RET2 Systems'in Pwn2Own 2018'de kullandığı CVE-2018-4193 açığı, Safari sandbox'ından çıkıp root yetkisine ulaşan bir exploit zincirine nasıl dönüştü?

  • Güvenlik
  • macOS
  • Pwn2Own
  • Exploit

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.

macOS 10.13.3'te WindowServer üzerinden root yetkisine ulaşan exploit'in animasyonlu gösterimi
Tarayıcıdan bağımsız çalışan WindowServer exploit'i, yaklaşık 90 saniyede root yetkisine ulaşıyor. Kaynak: RET2 Systems.

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:

  1. İ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 * 24 adımlarıyla belirleniyor.
  2. Yazma işlemini yapan kodun çalışabilmesi için hedefin çevresindeki belleğin iki ön koşulu sağlaması gerekiyor.
  3. 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.
Sınır dışı yazmanın gerçekleşmesi için gereken iki ön koşulun sözde kodla gösterimi
Yazma işleminin gerçekleşmesi için bellekteki iki ön koşulun sağlanması gerekiyor. Kaynak: RET2 Systems.
r15 ve ecx değerlerinin, saldırganın belirlediği indeks üzerinden sınır dışına yazılması
Saldırgan indeksi belirleyebiliyor; yazılan r15 ve ecx değerleri üzerindeki kontrolü ise sınırlı. Kaynak: RET2 Systems.

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.

Alt baytları CID ile değiştirilen Objective-C işaretçisinin tagged pointer olarak yorumlanması
CID ile yapılan kısmi üzerine yazma, Objective-C işaretçisinin yorumlanma biçimini değiştiriyor. Kaynak: RET2 Systems.

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.

WindowServer'daki HotKey nesnelerini sorgulayan ve değiştiren API'lerden bazıları
HotKey nesneleri üzerinde işlem yapan API'lerden bazıları. Kaynak: RET2 Systems.

Ekip, exploit'i beş aşama üzerine kurdu:

  1. Açığı kullanarak WindowServer'ın MALLOC_TINY heap bölgesinin sonuna yakın bir adresi sızdırmak.
  2. 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.
  3. Bozulmuş işaretçinin gösterdiği bellek alanına, içeriği saldırgan tarafından belirlenen bir CFMutableArray yerleştirmek.
  4. Dizideki bir CFStringRef işaretçisinin tek bir bitini değiştirerek onu sahte nesne verisine yönlendirmek ve kod çalıştırmak.
  5. 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:

Heap spraying ve sınır dışı yazma kullanılarak bir heap işaretçisinin sızdırılması
Yazma işlemi, önceden hazırlanmış string'in içine bir heap işaretçisi ve CID bırakıyor. Kaynak: RET2 Systems.

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_TINY bö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:

İkinci aşamada, ayrılmış blokların bir kısmı serbest bırakılarak HotKey nesneleri için boşluk açılması
Yan yana duran bloklar belirleniyor; seçilen bloklar serbest bırakılarak HotKey nesnelerine yer açılıyor. Kaynak: RET2 Systems.

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:

İkinci aşamanın sonunda, HotKey işaretçisinin saldırganın kontrolündeki belleğe yöneldiği heap yerleşimi
HotKey liste işaretçisi, saldırganın kontrolündeki bir bellek bloğuna yönlendirilmiş durumda. Kaynak: RET2 Systems.

Üçü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:

Sahte HotKey ve CFMutableArray yapılarının aynı bellek alanında bayt düzeyinde örtüşmesi
Aynı bellek alanı, HotKey API'leri ve CoreFoundation tarafından farklı yapılar olarak yorumlanıyor. Kaynak: RET2 Systems.

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.

Bellek doldurma işlemlerini, örtüşen nesneleri ve tek bitlik değişikliği bir arada gösteren exploit yerleşimi
Birden fazla bellek doldurma işlemi ve zincirlenen bozulmaların oluşturduğu son yerleşim. Kaynak: RET2 Systems.

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.

WindowServer'ın ana Mach mesaj işleme döngüsü
Shellcode, işlemlerini tamamladıktan sonra kontrolü ana mesaj döngüsüne geri veriyor. Kaynak: RET2 Systems.

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.
Exploit zincirini çalıştırıp sonuçlarını kaydeden otomatik test altyapısı
Exploit zincirinin başarı oranını ölçmek için yapılan otomatik testler. Kaynak: RET2 Systems.

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.