1 puan yazan GN⁺ 4 시간 전 | 1 yorum | WhatsApp'ta paylaş
  • ymawky, yalnızca aarch64 assembly ile yazılmış, macOS için küçük bir statik HTTP sunucusudur ve libc sarmalayıcıları olmadan yalnızca Darwin ham sistem çağrılarını kullanır
  • GET, HEAD, PUT, OPTIONS, DELETE, bayt aralığı istekleri, dizin listeleme ve özelleştirilmiş hata sayfalarını destekler; ancak nginx yerine geçmek için değil, web sunucusunun nasıl çalıştığını anlamak amacıyla kolaylık katmanlarını kaldıran bir uygulamadır
  • İstek ayrıştırma, yüzde çözme, header denetimi, aralık değeri dönüştürme, hata işleme, dosya kapatma ve yanıt üretimine kadar her şeyin elle yazılması gerekir; Python'daki basit string bölme ya da int(string) karşılığı işler bile assembly'de onlarca ila yüzlerce satırlık doğrulama koduna dönüşür
  • Sunucu, her yeni bağlantı için fork() çağıran fork-on-request yapısındadır; bu nedenle uygulaması kolaydır ama eşzamanlı bağlantı kapasitesi düşüktür ve slowloris'e karşı savunmasız olabilir; bu yüzden header zaman aşımı ve Content-Length tabanlı gövde zaman aşımı uygular
  • PUT, önce .ymawky_tmp_<pid> geçici dosyasına yazar ve başarılı olursa yer değiştirir; ayrıca yol dolaşımı engelleme, O_NOFOLLOW_ANY, fstat64(), dizin listelemede URL kodlama ve HTML escape gibi dosya sistemi güvenliğini de doğrudan kendisi ele alır

ymawky genel bakış ve kısıtlar

  • ymawky, yalnızca aarch64 assembly ile yazılmış, macOS için küçük bir statik HTTP sunucusudur
  • libc sarmalayıcıları olmadan yalnızca Darwin ham sistem çağrılarını kullanır; harici kütüphaneler veya mevcut bir parser kullanmaz
  • Desteklenen özellikler GET, HEAD, PUT, OPTIONS, DELETE, bayt aralığı istekleri, dizin listeleme ve özelleştirilmiş hata sayfalarıdır
  • Projenin kısıtları şunlardır
    • yalnızca aarch64 assembly
    • macOS/Darwin hedefi
    • yalnızca ham syscalls, libc sarmalayıcıları yok
    • yalnızca statik dosyalar
    • önceden var olan parser yok
    • harici kütüphane yok
  • Amaç nginx'in yerini almak değil; bir web sunucusunun gerçekte nasıl çalıştığını anlamak için kolaylık katmanlarını kaldıran bir uygulamadır

Assembly ile web sunucusu yaparken gereken işler

  • Assembly, makine dili ile yüksek seviyeli diller arasındaki katmandır ve mov, add, ldr, str, cmp gibi komutlar çalıştırılabilir ikilinin baytlarıyla doğrudan eşleşir
  • svc #0x80, çalıştırılabilir ikilideki D4 00 10 01 baytlarının insan tarafından okunabilir biçimidir
  • String türü olmadığından string'ler bellekte ardışık bayt alanları olarak bulunur; ayrıca C'deki struct gibi dil özellikleri de olmadığından alan ofsetlerini ve toplam boyutu doğrudan bilmeniz gerekir
  • HTTP kütüphanesi, otomatik temizleme, exception veya object olmadığı için istek ayrıştırma, hata işleme, dosya kapatma ve yanıt üretimi gibi işlerin hepsini doğrudan yazmanız gerekir
  • Bir şey yanlış çalışsa bile CPU uyarı vermeden yürütmeye devam eder; yani sorun yazdığınız komutlarda ve bellek erişimindedir

Ham sistem çağrıları ve sunucu akışı

  • Darwin sistem çağrıları

    • ymawky, libc sarmalayıcıları yerine çekirdeği doğrudan çağırır
    • Darwin aarch64'te sistem çağrısı numarası x16 register'ına, Linux aarch64'te ise x8'e konur
    • open() sistem çağrısı numarası 5'tir; dosya adı ve mod gibi argümanlar doğrudan register'lara yerleştirildikten sonra svc #0x80 ile çekirdek çağrılır
    • open() başarısız olduğunda carry flag set edilir ve b.cs open_failed gibi carry flag kontrol edilerek hata işleme koduna dallanılır
  • Temel sunucu davranışı

    • Bir web sunucusunun temel akışı, isteği alıp işlemek ve durum kodu ile gerekli dosyaları döndürmektir
    • Soket kurulumu socket(AF_INET, SOCK_STREAM, 0), setsockopt(... SO_REUSEADDR ...), bind(sockfd, &addr, 16), listen(sockfd, 5), accept(sockfd, NULL, NULL) gibi adımlardan oluşur
    • ymawky, her yeni bağlantıda fork() çağıran fork-on-request bir sunucudur
    • Bu yaklaşım, istek işleme süreçleri arasında bellek paylaşmadığı için anlaşılması ve uygulanması kolaydır; ancak süreç başına bellek alanı nedeniyle yük artar ve nginx'in event tabanlı asenkron non-blocking modeline göre eşzamanlı bağlantı kapasitesi daha düşüktür
    • Eşzamanlı bağlantı sayısı arttıkça çekirdek, süreç içi yürütmeden çok süreçler arası geçişe zaman harcamaya başlar
  • İstek işleme sırasında gereken işler

    • İstek metodunun GET, HEAD, OPTIONS, PUT, DELETE hangisi olduğunu belirlemek
    • İstek yolunu çıkarmak ve %20 gibi yüzde kodlamalarını çözmek
    • Yol güvenlik denetimlerini yapmak ve istemcinin gönderdiği header alanlarını ayrıştırmak
    • İstenen dosyanın bilgisini almak ve bunun dizin mi yoksa normal dosya mı olduğunu ayırt etmek
    • PUT istek gövdesini geçici dosyaya yazmak ve yanıt header'ı ile gövdesini üretmek
    • Açık dosyaları kapatmak ve sunucunun çökmesini önleyecek şekilde hataları ele almak

HTTP parsing'i doğrudan uygulamak

  • İstek satırı ve header sonu

    • HTTP isteği, sunucunun yorumlaması gereken bir string'dir; örnek aşağıdaki gibidir
      GET /index.html HTTP/1.0\r\n
      Range: bytes=1-5\r\n\r\n
      
    • İlk satır GET isteğini, hedef dosya index.html'i ve HTTP sürümü HTTP/1.0'ı içerir
    • \r\n satır sonunu, \r\n\r\n ise header sonunu ifade eder
    • \r\n\r\n alınmazsa işlem 400 Bad Request ile sonlandırılmalıdır
  • Yol çıkarma

    • ymawky, desteklenen metodlarla ilk baytları karşılaştırarak istek türünü belirler ve ardından yolu çıkarır
    • Header'ı bayt bayt tarayarak / veya * arar; ancak HTTP/1.0 içindeki / karakterini yol sanmamak için / işaretinden önceki baytın boşluk olup olmadığını kontrol eder
    • Örneğin GET HTTP/1.0\r\n\r\n içinde HTTP/1.0 yer aldığı için / vardır; ama önceki bayt boşluk değilse 400 Bad Request döndürülür
    • Çoğu sistemde PATH_MAX 4096 bayt olduğundan, ymawky bir adet 4096 baytlık dosya adı buffer'ı ve null sonlandırıcı için 1 baytlık filename_buffer: .skip 4097 tanımlar
    • İstek yolu buffer'dan uzunsa rastgele belleğin üzerine yazmak yerine 414 URI Too Long döndürülmelidir
    • Python'daki text.split("GET /")[1].split(" ")[0] benzeri bir işlem, assembly'de HTTP geçerlilik denetimleri de eklendiğinde yaklaşık 200 satır olur
  • Yüzde çözme ve header alanı denetimi

    • Yolda % görüldüğünde sonraki iki baytın 0-9, a-f, A-F aralığında geçerli hexadecimal olup olmadığı kontrol edilir ve ilgili bayt değerine dönüştürülür
    • GET, Range: header'ına sahip olabilir; PUT için ise Content-Length: gerekir
    • Bu header'lar istek URL'si gibi sabit konumlarda olmadığından tüm header karakter karakter dolaşılmalıdır
    • \r sonrasında \n gelmezse ya da öncesinde \r olmadan \n gelirse header hatalı kabul edilir ve 400 Bad Request döndürülür
    • Yeni bir header satırı boşlukla başlıyorsa, header alanı boşlukla başlayamayacağı için 400 Bad Request döndürülür
  • String karşılaştırma ve sayı dönüştürme

    • Range: veya Content-Length: bulmak için iki string pointer'ı x0, x1 ve azami uzunluk x2 alan, karakter karakter karşılaştıran streqn fonksiyonu yazılır
    • Range: header'ı aşağıdaki gibi başlangıç veya bitiş kısmını atlayabilir, ama ikisinden biri mutlaka bulunmalıdır
      Range: bytes=10-
      Range: bytes=-10
      Range: bytes=5-10
      
    • Aralık değerleri string olduğu için ASCII rakamları tamsayıya çeviren atoi tarzı bir fonksiyona ihtiyaç vardır
    • 64 bit register taşmasını önlemek için sayı 19 basamak veya daha uzunsa hata olarak işlenir
    • Python'daki int(string) karşılığı işlem bile assembly'de sayı doğrulama, çarpma, toplama ve carry flag tabanlı başarı/başarısızlık sinyallerinin elle uygulanmasını gerektirir

PUT işleme ve geçici dosya stratejisi

  • PUT, aynı istek birden çok kez gönderildiğinde son sunucu durumunun aynı kaldığı idempotent bir metottur
  • PUT /file.txt, file.txt dosyasını oluşturur ya da mevcut dosyanın tamamını üzerine yazar; 1234 iki kez gönderilse bile dosya içeriği 12341234 değil 1234 olur
  • Herkese açık bir PUT tehlikeli olabilir ve işleme sırasında şu sorunlar düşünülmelidir
    • İstek işlenirken sürecin çökmesi
    • İstemcinin Content-Length değerini 2KB deyip yalnızca 100 bayt göndermesi
    • İstemcinin Content-Length olarak 50GB gibi çok büyük bir değer göndermesi
  • config.S içindeki MAX_BODY_SIZE varsayılan olarak 1GB'tır; Content-Length bunu aşarsa 413 Content Too Large döndürülür
  • Mevcut dosyayı doğrudan açıp yazmak, hata durumunda yarım yazılmış dosya bırakabilir; bu yüzden ymawky önce .ymawky_tmp_<pid> biçiminde bir geçici dosyaya yazar
  • getpid() sistem çağrısı numarası 20 ile pid alınır ve özel yazılmış itoa() ile string'e çevrilirken buffer taşması denetlenir
  • İstemci gövdesi geçici dosyaya tamamen ve başarılı şekilde yazıldıktan sonra geçici dosyanın adı hedef dosya adıyla değiştirilir ve dosya sunucuda yerini alır
  • İstemci bağlantıyı beklenmedik şekilde keserse, zaman aşımı olursa ya da hatalı gövde gönderirse geçici dosya unlink() sistem çağrısı 10 veya unlinkat() sistem çağrısı 472 ile silinir
  • Mevcut dosya yalnızca eksiksiz bir isteğin başarıyla aktarılmasından sonra üzerine yazılır

Dizin listeleme ve escape işlemleri

  • GET /somedir/ isteği geldiğinde config.S içindeki ALLOW_DIR_LISTING seçeneğinin açık olup olmadığı kontrol edilir
  • Dizin listeleme devre dışıysa 403 Forbidden döndürülür
  • Etkinse getdirentries64() sistem çağrısı 344 ile istenen dizinin dosya bilgileri bir buffer'a doldurulur
  • Buffer, her dosyanın adını ve dosya adı uzunluğunu içerir; ymawky bunları tıklanabilir HTML üretmek için kullanır
  • Her dosya için istemciye gönderilen temel biçim şöyledir
    <a href="filename">filename</a>
    
  • href="..." içindeki dosya adı, URL yol segmenti olarak yüzde kodlanmalı; ekranda görünen metin ise HTML escape işleminden geçirilmelidir
  • Dosya adı &.-~><foo ise href %26.-~%3E%3Cfoo, görüntülenen metin ise &amp;.-~&gt;&lt;foo olur ve nihai çıktı şöyledir
    <a href="%26.-~%3E%3Cfoo">&amp;.-~&gt;&lt;foo</a>
    
  • Böylece <script>something evil</script> gibi gövde tarafında XSS oluşturabilecek adlar ya da "><script>something dastardly</script> gibi href="..." alanında XSS oluşturabilecek adlar çalıştırılamaz

Ağ güvenliği ve zaman aşımı

  • slowloris, çok sayıda bağlantıyı açık tutup isteği tamamlamayarak sunucu kaynaklarını kilitleyen bir hizmet engelleme saldırısıdır
  • ymawky, fork-on-request yapısı nedeniyle slowloris'e karşı savunmasız olabilir
  • Tüm header, config.S içindeki HEADER_REQ_TIMEOUT_SECS süresi içinde alınmazsa 408 Request Timeout gönderilir ve bağlantı kapatılır
  • İstek gövdesi alınırken istemci uzun süre veri göndermezse config.S içindeki RECV_TIMEOUT uyarınca aynı işlem uygulanır
  • Ancak her okuma için basit zaman aşımı yeterli değildir
    • Kötü niyetli bir istemci Content-Length: 1073741823 gönderip her 9 saniyede 1 bayt yollarsa, içerik uzunluğu üst sınırdan yalnızca 1 bayt küçük olduğu için kabul edilir ve 10 saniyelik zaman aşımıyla 300 yıldan uzun süre beklenebilir
  • Bunu azaltmak için ymawky, Content-Length ve saniye başına asgari bayt hızına göre zaman aşımı hesaplar
    timeout = grace_period + content_length / min_bps
    
  • grace_period, tüm gövdeler için verilen asgari süredir; min_bps ise sunucunun kabul ettiği en düşük aktarım hızıdır
  • Varsayılan min_bps 16KB/s'dir; cömerttir ama sonsuz değildir
  • Bu yaklaşım hizmet engelleme saldırılarını tamamen durdurmaz, ancak belirli saldırıların kaynağı ne kadar süre meşgul edebileceğini sınırlar

Dosya sistemi güvenliği

  • Dosya bilgisi kontrol sırası

    • GET ve HEAD isteklerinde, ymawky önce istek yolunu açar, ardından dosya descriptor'ı üzerinde fstat64() sistem çağrısı 339 çalıştırarak dosya türü ve boyutu gibi bilgileri alır
    • Önce yol üzerinde stat64() sistem çağrısı 338 çalıştırıp sonra dosyayı açmak, kontrol zamanı ile kullanım zamanı arasında dosyanın değişmesine yol açabilecek bir TOCTOU race condition oluşturabilir
  • docroot ve yol dolaşımı engelleme

    • Tüm istek yollarının başına docroot eklenir
    • Varsayılan docroot, config.S içindeki DEFAULT_DIR değeri olan www/'dir
    • /etc/shadow isteği www/etc/shadow olur; dolayısıyla www/etc/shadow gerçekten yoksa sonuç 404'tür
    • Ancak /../../../../etc/shadow, www/../../../../etc/shadow biçimine dönüşebilir ve docroot dışına çözümlenebilir; bu nedenle ek savunma gerekir
    • ymawky, string içinde .. geçen tüm yolları körü körüne reddetmez; yalnızca yol segmenti tam olarak .. ise reddeder
    • %2E%2E, çözme işleminden sonra .. olur; bu nedenle bu kontrol yüzde çözmeden sonra yapılmalıdır
  • Sembolik bağlantı işleme

    • POSIX'teki O_NOFOLLOW flag'i, son yol bileşeni sembolik bağlantıysa open() çağrısının başarısız olmasını sağlar
    • Darwin'deki O_NOFOLLOW_ANY, yolun herhangi bir bileşeni sembolik bağlantıysa çağrıyı başarısız yapar
    • Docroot içine belirli bir sembolik bağlantı yerleştirilebiliyorsa zaten başka sorunlar da vardır; yine de bu flag ek bir savunma sağlar

Apple'a özgü davranış

  • Zaman aşımı işleme ve sigaction()

    • İstek zaman aşımını uygulamak için setitimer() sistem çağrısı 83 ile belirli bir sürenin sonunda SIGALRM gönderilmelidir
    • Varsayılan olarak SIGALRM, child süreci sonlandırır; ancak ymawky önce 408 Request Timeout göndermelidir
    • Bunun için sigaction() sistem çağrısı 46 kullanılır
    • Darwin'in ham sigaction yapısı sa_tramp alanını görünür kılar
    • Normalde libc, sa_tramp alanını ayarlayarak stack ve register'ları kaydeder, sigreturn hazırlığını yapar ve ardından handler'a dallanır
    • ymawky'nin zaman aşımı handler'ı 408 Request Timeout gönderip gerekli kaynakları kapatır ve child süreci sonlandırır; dolayısıyla geri dönmesine gerek yoktur
    • Bu yüzden trampoline yuvası, zaman aşımı yanıtını doğrudan gerçekleştiren kodu gösterecek şekilde kullanılır ve sa_handler ile sigreturn atlanır
  • proc_info() ve child process sayısını sınırlama

    • Apple'da çalışan süreçleri ve alt süreç bilgilerini alabilen, çok iyi belgelenmemiş bir proc_info() sistem çağrısı 336 vardır
    • Bu çağrı genellikle ps, lsof, top gibi araçlarda kullanılır
    • ymawky, etkin child process sayısını saymak için proc_info() kullanır
    • Azami bağlantı sayısı yapılandırılabildiği için yaşayan child sayısını bilmek gerekir
    • proc_info(), child process bilgisini bir buffer'a yazar; her öğenin boyutu bilindiğinden kaydedilen bayt sayısından child sayısı hesaplanabilir
    • Child sayısı MAX_PROCS değerini aşarsa yeni bağlantılar 503 Service Unavailable ile reddedilir

Sonuç ve proje bilgisi

  • Statik bir web sunucusunda zor olan kısım, soket açıp listen etmekten çok istek ayrıştırma ve tüm sınır durumlarını ele almaktı
  • İstekler, yollar ve yanıtlar aslında baytlardan ibarettir; aralık istekleri hassas olmalı, dosya adları da bulundukları yere göre farklı şekilde escape edilmelidir
  • Assembly, istek ayrıştırma, bellek yönetimi, hata işleme, string dönüştürme, zaman aşımı ve dosya güvenliği gibi her şeyi doğrudan yazmayı zorunlu kılar
  • ymawky, imtomt tarafından sürdürülmektedir

1 yorum

 
GN⁺ 4 시간 전
Lobste.rs görüşleri
  • Müthiş. Geçmişte akıllı cihazlar üreten küçük bir şirketle entegrasyon işi yapmıştım; o şirketin tek mühendisi assembly dili dışında hiçbir şey bilmiyordu
    Donanım kontrol kodundan sunucu işletim sistemine, bizim kullandığımız JSON web API'ye kadar her şey doğrudan assembly ile yazılmıştı
    Bir keresinde web API'nin alakasız bir cihazın verisini döndürdüğü bir hatayla karşılaştık; meğer işletim sisteminin zamanlama sisteminde off-by-one hatası varmış ve “veritabanı” web servisine yanlış satırı döndürüyormuş

    • Acaba o kişinin adı Mel olabilir miydi?
  • “İntihar” gibi ifadeler ele alınırken lütfen içerik uyarısı eklensin. Hatta daha iyisi, hiç anılmaması olur

    • Ne? Yazının bir kısmını hızlıca okudum ama ilk okuyuşumda intiharla ilgili bir ifade görmedim
      Bu yorumu görünce tekrar aradım ama yine bulamadım; bir şeyi mi kaçırdım?
    • Hiç mizah anlayışına sahip olmamak, hem kişinin kendi sağlığı hem de toplumun geneli için çok daha tehlikeli
  • Her şeyin “tamamen assembly ile yazıldığı” söylenince aklıma Therac-25 soruşturma raporu geliyor