Hayatıma anlam (eksikliği) katmak için aarch64 assembly ile bir web sunucusu yapmak
(imtomt.github.io)- 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ı veContent-Lengthtabanlı 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,cmpgibi komutlar çalıştırılabilir ikilinin baytlarıyla doğrudan eşleşir svc #0x80, çalıştırılabilir ikilidekiD4 00 10 01baytları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
structgibi 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ı
x16register'ına, Linux aarch64'te isex8'e konur open()sistem çağrısı numarası5'tir; dosya adı ve mod gibi argümanlar doğrudan register'lara yerleştirildikten sonrasvc #0x80ile çekirdek çağrılıropen()başarısız olduğunda carry flag set edilir veb.cs open_failedgibi 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,DELETEhangisi olduğunu belirlemek - İstek yolunu çıkarmak ve
%20gibi 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
PUTistek 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
- İstek metodunun
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
GETisteğini, hedef dosyaindex.html'i ve HTTP sürümüHTTP/1.0'ı içerir \r\nsatır sonunu,\r\n\r\nise header sonunu ifade eder\r\n\r\nalınmazsa işlem400 Bad Requestile sonlandırılmalıdır
- HTTP isteği, sunucunun yorumlaması gereken bir string'dir; örnek aşağıdaki gibidir
-
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; ancakHTTP/1.0iç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\niçindeHTTP/1.0yer aldığı için/vardır; ama önceki bayt boşluk değilse400 Bad Requestdöndürülür - Çoğu sistemde
PATH_MAX4096 bayt olduğundan, ymawky bir adet 4096 baytlık dosya adı buffer'ı ve null sonlandırıcı için 1 baytlıkfilename_buffer: .skip 4097tanımlar - İstek yolu buffer'dan uzunsa rastgele belleğin üzerine yazmak yerine
414 URI Too Longdö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ın0-9,a-f,A-Faralığı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;PUTiçin iseContent-Length:gerekir- Bu header'lar istek URL'si gibi sabit konumlarda olmadığından tüm header karakter karakter dolaşılmalıdır
\rsonrasında\ngelmezse ya da öncesinde\rolmadan\ngelirse header hatalı kabul edilir ve400 Bad Requestdöndürülür- Yeni bir header satırı boşlukla başlıyorsa, header alanı boşlukla başlayamayacağı için
400 Bad Requestdöndürülür
- Yolda
-
String karşılaştırma ve sayı dönüştürme
Range:veyaContent-Length:bulmak için iki string pointer'ıx0,x1ve azami uzunlukx2alan, karakter karakter karşılaştıranstreqnfonksiyonu yazılırRange:header'ı aşağıdaki gibi başlangıç veya bitiş kısmını atlayabilir, ama ikisinden biri mutlaka bulunmalıdırRange: bytes=10- Range: bytes=-10 Range: bytes=5-10- Aralık değerleri string olduğu için ASCII rakamları tamsayıya çeviren
atoitarzı 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 metotturPUT /file.txt,file.txtdosyasını oluşturur ya da mevcut dosyanın tamamını üzerine yazar;1234iki kez gönderilse bile dosya içeriği12341234değil1234olur- Herkese açık bir
PUTtehlikeli olabilir ve işleme sırasında şu sorunlar düşünülmelidir- İstek işlenirken sürecin çökmesi
- İstemcinin
Content-Lengthdeğerini 2KB deyip yalnızca 100 bayt göndermesi - İstemcinin
Content-Lengtholarak 50GB gibi çok büyük bir değer göndermesi
config.SiçindekiMAX_BODY_SIZEvarsayılan olarak 1GB'tır;Content-Lengthbunu aşarsa413 Content Too Largedö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ı20ile 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ı10veyaunlinkat()sistem çağrısı472ile 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ğindeconfig.SiçindekiALLOW_DIR_LISTINGseçeneğinin açık olup olmadığı kontrol edilir- Dizin listeleme devre dışıysa
403 Forbiddendöndürülür - Etkinse
getdirentries64()sistem çağrısı344ile 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ı
&.-~><fooise href%26.-~%3E%3Cfoo, görüntülenen metin ise&.-~><fooolur ve nihai çıktı şöyledir<a href="%26.-~%3E%3Cfoo">&.-~><foo</a> - Böylece
<script>something evil</script>gibi gövde tarafında XSS oluşturabilecek adlar ya da"><script>something dastardly</script>gibihref="..."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.SiçindekiHEADER_REQ_TIMEOUT_SECSsüresi içinde alınmazsa408 Request Timeoutgönderilir ve bağlantı kapatılır - İstek gövdesi alınırken istemci uzun süre veri göndermezse
config.SiçindekiRECV_TIMEOUTuyarı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: 1073741823gö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
- Kötü niyetli bir istemci
- Bunu azaltmak için ymawky,
Content-Lengthve saniye başına asgari bayt hızına göre zaman aşımı hesaplartimeout = grace_period + content_length / min_bps grace_period, tüm gövdeler için verilen asgari süredir;min_bpsise sunucunun kabul ettiği en düşük aktarım hızıdır- Varsayılan
min_bps16KB/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ı
GETveHEADisteklerinde, ymawky önce istek yolunu açar, ardından dosya descriptor'ı üzerindefstat64()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.SiçindekiDEFAULT_DIRdeğeri olanwww/'dir /etc/shadowisteğiwww/etc/shadowolur; dolayısıylawww/etc/shadowgerçekten yoksa sonuç 404'tür- Ancak
/../../../../etc/shadow,www/../../../../etc/shadowbiç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_NOFOLLOWflag'i, son yol bileşeni sembolik bağlantıysaopen()ç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
- POSIX'teki
Apple'a özgü davranış
-
Zaman aşımı işleme ve
sigaction()- İstek zaman aşımını uygulamak için
setitimer()sistem çağrısı83ile belirli bir sürenin sonundaSIGALRMgönderilmelidir - Varsayılan olarak
SIGALRM, child süreci sonlandırır; ancak ymawky önce408 Request Timeoutgöndermelidir - Bunun için
sigaction()sistem çağrısı46kullanılır - Darwin'in ham
sigactionyapısısa_trampalanını görünür kılar - Normalde libc,
sa_trampalanını ayarlayarak stack ve register'ları kaydeder,sigreturnhazırlığını yapar ve ardından handler'a dallanır - ymawky'nin zaman aşımı handler'ı
408 Request Timeoutgö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_handlerilesigreturnatlanır
- İstek zaman aşımını uygulamak için
-
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ı336vardır - Bu çağrı genellikle
ps,lsof,topgibi 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_PROCSdeğerini aşarsa yeni bağlantılar503 Service Unavailableile reddedilir
- Apple'da çalışan süreçleri ve alt süreç bilgilerini alabilen, çok iyi belgelenmemiş bir
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
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ş
“İntihar” gibi ifadeler ele alınırken lütfen içerik uyarısı eklensin. Hatta daha iyisi, hiç anılmaması olur
Bu yorumu görünce tekrar aradım ama yine bulamadım; bir şeyi mi kaçırdım?
Her şeyin “tamamen assembly ile yazıldığı” söylenince aklıma Therac-25 soruşturma raporu geliyor