curl’de bir heap overflow’un nasıl ortaya çıktığı
(daniel.haxx.se)- curl 8.4.0’ın yayımlanmasıyla ortaya çıkan CVE-2023-38545, SOCKS5 proxy işlemleri sırasında oluşan bir heap buffer overflow’du ve curl güvenlik sorunları arasında nadir görülen HIGH ciddiyetinde bir açıktı
- Sorun, 2020’de SOCKS5 bağlantı kodunun bloklayan çağrılardan non-blocking state machine yapısına dönüştürülmesi sırasında eklendi ve curl 7.69.0’dan itibaren etkili oldu
- Uzak ad çözümleme modunda 255 baytı aşan host adlarını yerel çözümlemeye çeviren mevcut mantık, state machine’in yeniden çağrılmasıyla birleşince çok uzun host adının küçük bir buffer’a kopyalanmasına yol açabiliyordu
- Saldırının mümkün olması için libcurl istemcisinin SOCKS5 proxy-resolver-mode ve otomatik redirect kullanması, saldırganın kontrolündeki HTTPS sunucusunun da 16 KB’den büyük ve 64 KB’den küçük/eşit host adı içeren HTTP 30x
Location:değeri döndürmesi gerekiyordu - curl 8.4.0’dan itibaren çok uzun host adlarında uzak çözümlemeden yerel çözümlemeye geçmek yerine hata döndürülüyor ve aynı senaryoyu engelleyen bir test eklendi
CVE-2023-38545 ve curl 8.4.0
- curl 8.4.0 sürümüyle birlikte CVE-2023-38545 güvenlik duyurusu ve ayrıntıları yayımlandı
- Bu sorun, curl’de uzun süredir görülen en ciddi güvenlik sorunlarından biri olarak değerlendirildi ve ciddiyet seviyesi HIGH olarak belirlendi
- Temel kusur, SOCKS5 proxy bağlantısı işlenirken belirli koşullarda ortaya çıkan bir heap buffer overflow idi
SOCKS5 ve ad çözümleme yöntemi
- curl, Ağustos 2002’den beri SOCKS5 destekliyor
- SOCKS5, ağ iletişimini aradaki bir sunucu üzerinden kuran bir proxy protokolüdür
- Tor üzerinden iletişim kurmak için kullanılabilir
- Kurum veya şirket içinden internete erişirken de kullanılabilir
- SOCKS5’te host adı çözümleme için iki yöntem vardır
- İstemci host adını yerel olarak çözümler ve çözümlenen adresi proxy’ye iletir
- İstemci tam host adını proxy’ye gönderir ve proxy bunu uzaktan çözümler
Açığın eklendiği 2020 değişikliği
- 2020’nin başında SOCKS5 proxy’ye bağlanan fonksiyon, bloklayan çağrıdan non-blocking state machine yapısına dönüştürüldü
- Bu değişiklik 14 Şubat 2020’de master’a girdi ve curl 7.69.0 içine dahil edildi
- curl 7.69.0, bu iyileştirmenin yer aldığı ilk sürüm ve sonuç olarak CVE-2023-38545’e karşı açık olan ilk sürüm oldu
- Değişikliğin amacı, çok sayıda paralel aktarımın hepsi SOCKS5 üzerinden geçtiğinde daha belirgin bir iyileşme sağlamaktı
State machine’de bozulan çözümleme modu
- State machine fonksiyonu, bağlantı kurulana kadar ağdan daha fazla veri geldikçe tekrar tekrar çağrılır
- Fonksiyonun başındaki
socks5_resolve_localyerel değişkeni, curl’ün host adını kendisinin mi çözümleyeceğini yoksa adı proxy’ye mi ileteceğini gösterir - Bu değişken, state machine her çalıştığında fonksiyon çağrısının başında proxy moduna göre yeniden ayarlanır
- INIT durumundaki koşul sorunu yarattı
- SOCKS5’in host adı alanı en fazla 255 bayta izin verir
- Host adı 255 baytı aşarsa SOCKS5 proxy bunu çözümleyemez
- Mevcut curl kodu, uzak çözümleme modunda çok uzun bir host adıyla karşılaşınca
socks5_resolve_localdeğeriniTRUEyaparak yerel çözümleme moduna geçiyordu
- Kullanıcı uzak çözümleme istemişse curl’ün modu değiştirmek yerine başarısız olması gerekiyordu; ancak uzun zaman önce eklenen bu geçiş davranışı aynen devam etmişti
Heap overflow’un oluştuğu akış
- SOCKS5 sunucusu yeterince hızlı değilse ve state machine’in ilerlemesi için daha fazla ağ verisi alınamazsa fonksiyon döner
- Daha sonra veri oluştuğunda aynı state machine fonksiyonu yeniden çağrılır
- Yeniden çağrıldığında
socks5_resolve_local, fonksiyonun başında proxy moduna göre yeniden ayarlanır- Önceki çağrıda çok uzun host adı nedeniyle
TRUEolan değer korunmaz - Değer yeniden proxy’nin adı uzaktan çözümlemesi gereken duruma döner
- Önceki çağrıda çok uzun host adı nedeniyle
- curl, proxy’ye gönderilecek protokol frame’ini bellek buffer’ında oluşturur ve hedef bilgilerini kopyalar
- Hatalı durum değeri nedeniyle çok uzun host adı olduğu gibi iletilmeye çalışılırsa, ayrılan hedef buffer’ın ötesine geçip bitişikteki heap belleğin üzerine yazılabilir
Buffer boyutu ve host adı kısıtları
- curl, proxy’ye gönderilecek protokol frame’ini oluştururken normal indirme buffer’ını yeniden kullanır
- İndirme buffer’ı boyutu koşulları şöyledir
- Varsayılan değer 16 KB’dir
- curl aracı buffer boyutunu 100 KB olarak ayarlar
- Uygulama isteğine göre farklı boyutlar kullanılabilir
- İzin verilen minimum boyut 1024 bayttır
- Buffer boyutu 65541 bayttan küçükse bu overflow mümkündür
- Buffer ne kadar küçükse mümkün olan overflow boyutu o kadar büyür
- URL’deki host adı için pratikte bir boyut sınırı yoktur, ancak libcurl URL parser’ı 65535 baytı aşan adları reddeder
- DNS, en fazla 253 bayta kadar host adlarına izin verir
- 253 baytı aşan yasal adlar nadirdir; 1024 baytı aşan gerçek adları görmekse fiilen çok zordur, bu nedenle saldırı için kasıtlı olarak çok uzun bir host adı gerekir
- URL’nin host adı alanına yalnızca bazı oktetler girebilir; geçersiz bayt değerleri URL parser tarafından reddedilir
- libcurl, IDN kütüphanesi kullanacak şekilde derlenmişse bu kütüphane de geçersiz host adlarını reddedebilir
Saldırının gerçekleşmesi için gereken koşullar
- Saldırgan, libcurl kullanan istemcinin SOCKS5 proxy’ye proxy-resolver-mode ile eriştiği bir HTTPS sunucusunu kontrol etmelidir
- Saldırı sunucusu, HTTP 30x yanıtıyla manipüle edilmiş bir redirect döndürebilir
- Redirect’in
Location:header’ında şu biçimde çok uzun bir host adı yer alabilirLocation: https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/- Host adı uzunluğu 16 KB’den büyük ve 64 KB’den küçük/eşit olabilir
- libcurl istemcisinde otomatik redirect takibi açıksa ve SOCKS5 proxy bu yerel değişken sorununu tetikleyecek kadar “yeterince yavaşsa”, manipüle edilmiş host adı çok küçük bir buffer’a kopyalanır
- Bu koşullar sağlandığında bitişikteki heap belleğe kadar yazma gerçekleşir ve heap buffer overflow oluşur
Düzeltme ve bug bounty
- curl 8.4.0’dan itibaren çok uzun host adı nedeniyle uzak çözümlemeden yerel çözümlemeye geçilmiyor; bunun yerine hata döndürülüyor
- Aynı senaryo için özel bir test case de eklendi
- Bu sorun Jay Satiro tarafından raporlandı, analiz edildi ve yamalandı
- Bu açık için curl’de bugüne kadar ödenen en büyük bug bounty verildi
- Raporlayan kişiye 4.660 dolar
- IBB policy uyarınca curl projesine 1.165 dolar
C ve bellek güvenli diller
- curl C yerine bellek güvenli bir dilde yazılmış olsaydı bu tür kusurlar ortaya çıkmazdı
- Ancak curl’ü başka bir dile port etmek şu anda gündemde değil
- Gerçekçi yaklaşım iki seçeneğe indirgeniyor
- Bellek güvenli dillerle yazılmış bağımlılıklara daha fazla izin vermek, onları kullanmak ve desteklemek
- hyper entegrasyonunda olduğu gibi curl’ün bazı bölümlerini kademeli olarak değiştirmek
- Bu tür geliştirme şu anda çok yavaş ilerliyor ve ilgili zorluklar da açıkça görülüyor
- curl yakın gelecekte de C ile yazılmış halde kalacak
- curl 8.4.0’da bildirilen en son 2 CVE de dahil edildiğinde, bugüne kadar curl’de bulunan güvenlik açıklarının %41’i bellek güvenli bir dil kullanılsaydı muhtemelen ortaya çıkmayabilirdi
- Ancak C ile ilgili sorunların ilk yaklaşık %80’inin eklendiği dönemde Rust bu amaç için pratik bir seçenek değildi
1315 gün sonra bulunan kusur
- Bu kusur kodda 1315 gün boyunca kaldı
- Daha iyi bir test seti olsaydı bulunabilirdi
- curl birçok statik kod analiz aracını düzenli olarak çalıştırıyor, ancak bu fonksiyondaki sorunu hiçbir analiz aracı bulamadı
- 20 milyardan fazla kurulum ortamına giren kodda heap overflow dağıtmış olmak tavsiye edilecek bir deneyim değildi
- Yayın öncesi raporlama ve işleme süreci HackerOne report üzerinden görülebilir
1 yorum
Hacker News yorumları
Bu kadar çok cihazın fiilen büyük kısmı tek bir kişi tarafından yazılmış bir kütüphaneye bağımlı olması hâlâ şaşırtıcı. Üzerindeki yük muazzam olmalı; aşağıdaki cümlede de bu görülüyor
“Şimdi kodu okuyunca hatayı görmemek imkânsız. Bu hatayı fark etmemiş olmamı ve kusurun 1315 gün boyunca kodun içinde keşfedilmeden kalmış olmasını kabullenmek gerçekten acı veriyor. Özür dilerim. Ben sadece insanım.”
Daniel bunu görürse, bence yoğun emeği için teşekkürler; özür dilemesine hiç gerek yok. Sonuçta kaynak kod herkesin okuyup inceleyebilmesi için açıktı
https://www.buzzfeed.com/chrisstokelwalker/the-internet-is-b...
“Bu rapor tamamen doğru görünüyor ve içimi derinden acıtıyor.”
Blogdaki “2 milyardan fazla kurulum ortamına giren koda heap overflow dağıtma deneyimini tavsiye etmek istemem” ifadesi de öyle; bu kadar büyük bir sorumluluğu bu kadar az karşılıkla taşımak acımasızca
Kişisel olarak, ne kadar odaklanmaya çalışsam da ne kadar çok şeyi kaçırdığımı sık sık düşünmüşümdür. Kod incelemesi gibi etkinlikler bu yönü, yani dikkat eğitimi ya da bütünsel/bağlamsal farkındalığı geliştirmek için iyi bir yöntem gibi görünüyor. Örneğin yıllardır sıkça yanından geçtiğim bir yerde birinin iyi baktığı bir çiçek tarhı vardı; ilk fark ettikten sonra yaklaşık bir yıl boyunca durup bakardım, ama birkaç ay önce hemen birkaç fit yanında eş gibi duran başka bir çiçek tarhı olduğunu ilk kez fark ettim. O tarhın muhtemelen başından beri orada olma ihtimali yüksek, ama varlığından haberdar olmadığım için kesin olarak bilemiyorum
Bunu insanlığın toplam bilgisine genişleterek düşününce, bazen tek bir kişinin bir şeyi fark ettiği ve o gözlemin davranışı değiştirerek hepimize yayılmaya başladığı hissine kapılıyorum. Bazen birçok deneme ve uzun zaman gerekiyor. Henüz kimsenin fark etmediği alçakta asılı meyvelerin ne kadar çok olabileceğini de merak ediyorum; ayrıca hâlihazırda dikkatini vermiş insanların ortaya koyduğu basit ve temel iyi uygulamaları bütünleştirmenin bile herkesin taban seviyesini yükseltebileceğini düşünüyorum
Julia Evans’ın https://jvns.ca/blog/2023/10/06/new-talk--making-hard-things... yazısı ve Dan Luu’nun https://danluu.com/p95-skill/ yazısı da bununla bağlantılı. Sonuç olarak Stenberg’e, curl’e ve her gün doğal kabul ederek kullandığımız internet altyapısının birçok parçasını inşa eden insanlara büyük minnettarlık duyuyorum
Bazen “bozuk değilse tamir etme” ilkesi “bozulana kadar destekleme” diye yorumlanıyor ve oldukça tatsız sürprizlere yol açıyor
Sürdürülebilirlik sorunu olabilir, ama bu tür projeler gelecekte de var olmaya devam edecek gibi görünüyor; Daniel gibi yalnızca kendisi için değil topluluk için çalışan herkesi saygıyla selamlıyorum
Bellek güvenliği ve bellek güvenli diller hakkındaki sonuç son derece makul. Özeti şu
curl C yerine bellek güvenli bir dille yazılmış olsaydı bu sınıftaki kusurlar mümkün olmazdı
Bu yönde uygulanabilir ve makul görünen yaklaşım, bellek güvenli dillerle yazılmış bağımlılıklara daha fazla izin vermek/kullanmak/desteklemek ve hyper’ın devreye alınması gibi curl’ün bazı kısımlarını kademeli olarak parça parça değiştirmek
Ancak bu geliştirme şu anda neredeyse buzul gibi yavaş ilerliyor ve ilgili zorlukları acı verecek kadar net gösteriyor. curl öngörülebilir gelecekte de C olarak kalacak. Bundan hoşlanmayanlar kolları sıvayıp bizzat çalışabilir
curl 8.4.0’da bildirilen en yeni iki CVE de dahil edildiğinde, bugüne kadar curl’de bulunan güvenlik açıklarının kümülatif toplamının %41’i, bellek güvenli bir dil kullanılsaydı muhtemelen ortaya çıkmazdı. Ancak Rust, C ile ilgili sorunların ilk yaklaşık %80’inin eklendiği dönemde bu kullanım için pratik bir seçenek değildi
Çeşitli statik kod analiz araçlarını tekrar tekrar çalıştırdılar, ancak bu fonksiyonda hiçbir sorun bulamadılar
Harika bir analiz yazısı. Ancak CVE’yi okuduktan sonra bile hangi durumlarda etkilendiği hâlâ belirsiz. Anladığım kadarıyla aşağıdaki koşullar sağlandığında etkileniliyor:
SOCKS5 proxy kullanmak, ana makine adını bu proxy üzerinden çözümlemek, tampon boyutunun varsayılan 100 KB’den 65541 baytın altına değiştirilmiş olması ve SOCKS5 proxy’nin isteği hemen işleyemeyecek kadar yavaş olması.
Düzeltme: Tamponla ilgili açıklama doğru değil. libcurl indirme tamponunu yeniden kullanıyor ve varsayılan değer 16 KB; ancak curl’ün kendisi,
--limit-ratekullanılmadığı sürece bunu elle 100 KB’ye ayarlıyormuş. Ayrıca gecikme koşulu da yanlış. CVE’ye göre sıradan bir sunucu gecikmesi bile bu hatayı tetiklemeye yetecek kadar “yavaş” olabilir; saldırganın hizmet engelleme yapması ya da SOCKS sunucusunu kontrol etmesi gerekmezmiş.Bu yüzden saldırı yolu çok dar görünüyor; kaçırdığım bir şey var mı merak ediyorum.
root@1aac5e228e16:/build/curl-7.74.0# curl -vvv -x socks5h://host.docker.internal:9050 $(python3 -c "print(('A'10000), end='')")Trying 192.168.65.254:9050...* SOCKS5: server resolving disabled for hostnames of length > 255 [actual len=10000]* SOCKS5 connect to AAAAA...* Send failure: Bad file descriptor* Failed to send SOCKS5 connect request.Segmentation faulthttps://gist.github.com/xen0bit/0dccb11605abbeb6021963e2b1a8...
docker-proxyadında bir shell script paketi yaptım; openconnect VPN çalıştıran bir Docker konteyneri üzerinden SOCKS5 tüneli oluşturuyor. Birden fazla müşterinin farklı VPN’leriyle uğraşırken, her VPN makinedeki tüm trafiği kendi tarafına göndermeyi dayattığında işe yarıyor.https://github.com/carlosonunez/docker-proxy
DNS çözümlemesi uzakta gerçekleşecek şekilde tasarlandığı için bu CVE burada doğrudan geçerli.
Şimdiye kadar okuduğum CVE analiz yazıları arasında en iyisi denebilir. Çağın temel yazılımlarından birini yapan kişi olmasına rağmen yazı boyunca alçakgönüllü bir tutum sergilemesini de takdir etmemek elde değil. Gerçekten saygı uyandırıcı.
if(!socks5_resolve_local && hostname_len > 255) {socks5_resolve_local = TRUE;}Bu gerçekten kötü bir fikir. Sansür aşma araçlarıyla gizliliğini koruyan kişiler için DNS üzerinden kimlikleri sızabilir.
Yazar da aynı görüşte.
https://hackerone.com/reports/2187833
“curl C yerine bellek güvenli bir dille yazılsaydı bu tür kusurlar imkânsız olurdu” deniyor; ama en fazla imkânsız olmuş olabilirdi denebilir. Dilin sanal makine bariyerinin dışına çıkmanın bir yolu olup olmadığını her zaman bilemeyiz. Ancak yazar, RFC1123’te belirtilen DNS ana makine adı sınırını açıkça göz ardı etmiş ve bu sınır Java kütüphanelerinde de sabit kodlanmış durumda.
https://www.rfc-editor.org/rfc/rfc1123
“URL’deki ana makine adı için pratikte gerçek bir boyut sınırı yoktur; ancak libcurl’ün URL ayrıştırıcısı 65535 bayttan uzun adları reddeder. DNS en fazla 253 baytlık ana makine adlarına izin verir. Bu nedenle 253 bayttan uzun yasal adlar nadirdir. 1024 bayttan uzun gerçek adlar ise fiilen duyulmuş şeyler değildir.”
DNS bugün vakaların %99,9’unda kullanılıyor olabilir, ancak ana makine adını adrese çözümleyen tek mekanizma değil.
RFC’nin ana makine adı boyutu için üst sınırı nerede belirlediğini bulamadım.
Derleyicinin belleğe sınırların dışından erişilmediğini, açık sahiplik olmadan erişilmediğini ve yalnızca temel nesnenin ömrü içinde erişildiğini kanıtlamaya çalışması anlamına da gelebilir. Örneğin Rust bunu yapar.
Elbette derleyici kritik noktalara iç hata kontrolleri ve korumalar da ekleyebilir. Rust bunu yapmıyor, ama register içindeki bit flip gibi şeylerden endişe etmeniz gereken sistemlerde olması iyi olabilir. Örneğin X-ray tarayıcıları gibi, ECC belleğin yakalayamadığı yüksek radyasyonlu ortamlarda.
Sömürülebilmesi için çok spesifik koşullar gerektiren bir meseleye göre ortada epey abartı ve dram var
Dün yayımlanan Windows 10 güvenlik güncellemesi yeni curl sürümünü içermiyordu. Windows’ta yer alan curl hâlâ 8.0.1
Uzun ve ilginç bir yazı ama “Sonuç olarak sistem kütüphaneleri güvenli dillerle yazılmalı ya da doğruluğu kanıtlanmış olmalı” diye özetlenebilir
Çağımızın en iyi, en nazik ve en şeffaf C programcılarından biri böyle bir yazı yazıyorsa buna dikkat etmek gerekir