Blok zinciri Uygulamaları ve Güvenlik Sorunları

Bu yazı ilk defa Arka Kapı dergisinin 3'üncü sayısında yayınlanmıştır.

Blok zinciri alanında, sonunda, gerçek uygulamaları görmeye ve kullanmaya başlıyoruz. Hala daha emekleme adımlarında olduğunu düşündüğüm bu teknolojinin en büyük getirilerinden bir tanesi ‘Decentralised Applications’ (DApp) yani merkezi olmayan uygulamaların geliştirilmesine olanak tanıması. Bu tarz uygulamaların yaygınlaşmasında en çok etkisi olan teknoloji de şüphesiz ki Ethereum blok zinciridir. Daha önceleri birçok arkadaşım bu konunun detaylarından bahsettiği için detaylarına girmeyeceğim. Ancak bu yazıda ele almak istediğim konuyla da ilgili olduğu için ‘Ethereum Virtual Machine’ (EVM) yani Ethereum Sanal Makinesinden biraz daha detaylı bahsedeceğim ve farklı saldırı vektörlerini anlatmaya çalışacağım.

Image for post
Image for post
Resim globalhalo.com websitesindeki bir makaleden alintidir.

Ethereum Virtual Machine

Ethereum’un desteklediği ve akıllı kontrat hazırlamamıza olanak sağlayan yazılım geliştirme dilleri derlenerek sanal makine seviyesinde anlaşılabilecek 16’lık düzende ifade edilebilen talimatlara dönüştürülür. Bu şekilde sanal makine, Just-In-Time (JIT) derleme yaparak üzerinde çalıştığı işletim sistemine göre optimizasyonları gerçekleştirerek akıllı kontratların çalışmasına olanak tanır. Tabi ki bu işlemler izole bir alanda şekilde çalıştırılıyor ve dışarıdan müdahale edilerek manipülasyon yapılması engelleniyor. Yani özetlemek gerekirse Solidity diliyle yazılan uygulamalar derleme işlemi sonrası bu byte-code adı verilen bir formata dönüştürülür ve aynı bir değer transferi işlemi gibi ağ üzerinde madenciler tarafından çalıştırılmak üzere ağa gönderilir.

Bu işlem bir kere yaratılıp, madenciler tarafından onaylandıktan sonra artık bir daha değiştirilmesi mümkün olmaz. Yani eğer bir akıllı kontratı derleyip ağa gönderirsem, bu kurduğum uygulama artık bu ağ hayatta kaldığı sürece değiştirilemez ve daha da önemlisi geri alınamaz. Eğer bu uygulamada bir güvenlik açığı ya da başka bir hata varsa o zaman bu hata sonsuza kadar orada kalacaktır. Bu da haliyle bizim klasik yazılım geliştirme yöntemlerinden alışkın olduğumuz, ‘çalışmazsa yeniden kurarız, ne olacak’ yaklaşımından uzaklaşmamıza sebep olmaktadır. Dolayısıyla, uygulama geliştirme sürecinin klasik uygulama geliştirme yöntemlerinden biraz daha farklı olması gerektiğini söylemek yanlış olmaz. Ancak bu konuyu bir sonraki sayıda ele almak üzere burada bırakıp asıl konumuza geri dönüyorum.

Ethereum Saldırı Vektörleri

Yukarıda anlattığım sebeplerden dolayı Ethereum üzerinde geliştirilen uygulamalarda yapılacak hatalar, sonrasında geri dönülemez sonuçlara yol açabilir. Bu yüzden uygulama geliştirirken dikkat edilmesi gereken önemli güvenlik açıklarını ve saldırı vektörlerini göz önünde bulundurmanız ve uygulamalarınızın bunlardan arındırılmış olduğuna dikkat etmeniz çok önemlidir.

Tam Sayı Değeri Taşırma Saldırısı
Eğer blok zinciri ve kripto para alanıyla ilgileniyorsanız, bu yazıyı okuyorsanız ilgilendiğinizi varsayıyorum, sosyal medyada geçtiğimiz haftalarda sıkça karşımıza çıkan ‘ERC20 token’ların güvenlik açığı bulundu’ şeklindeki haberleri görmüş olabilirsiniz. Tabi ki ne yazık ki Türkiye’deki bir çok teknoloji konusunda olduğu gibi blok zinciri alanında da sansasyonel haber yapmak çok revaçta. Bu yüzden sanki bulunan güvenlik açığının ERC20 token’larla ilgili olduğunu düşünüyor olabilirsiniz, ancak gerçek bundan biraz daha farklı.

Yazımın başında anlattığım EVM üzerinde hafızayı yönetmek için kullanılan farklı veri tipleri belirlenmiştir. Ethereum doğası gereği çok büyük sayılarla uğraşacağı için genellikle bu veri tipleri daha büyük değerlerin ifade edilebildiği işaretsiz yani İngilizcesiyle ‘unsigned’ olan değişken tiplerine de yer vermektedir. Bu veri tipleri içerisinde en büyük değeri alabilecek tam sayı tipi UINT256 yani 256 bitlik işaretsiz tam sayı tipidir. Özetle bu veri tipi 0 ile 2256 arasındaki tam sayı değerlerini alabilir. Yani bu veri tipini kullanan bir değişkenin 256 bit ile ifade edebileceği en büyük sayı değeri 115792089237316195423570985008687907853269984665640564039457584007913129639935 olacağından dolayı bu sayıya eğer 1 eklersek oluşacak sayıyı 256 bit ile ifade etmek mümkün olmayacaktır. İşte sorun da tam bu noktada oluşuyor, 2256 + 1 değeri UINT256’nın sınırları dışına taştığı için EVM bu sayıyı 0’a yuvarlıyor. Neden mi? Performans için tabi ki!! Nasıl yani? Yıl olmuş 2018 sen hala daha ‘arithmetic overflow check’ maliyetinden bahsediyorsun, dediğinizi duyar gibiyim hatta belki şu anda diğer yazılım dillerinde bu sorunu yaşamıyoruz diyor olabilirsiniz. Durun, durun sakin olun açıklayacağım. Bu kararın sebebini anlamak için öncelikle Ethereum’un amacını biraz daha anlamamız gerekiyor.

Ethereum, sunucular üzerinde çalışacak şekilde tasarlanmış bir uygulama platformundan farklı olarak merkeziyetsizliği sağlamaya çalışan bir bilgisayar olarak düşünülmüş ve tüm teknik kararlar bu çerçevede verilmiştir. Merkeziyetsizliğin temelinde yatan iki önemli konu vardır, ödül ve kriptografik algoritmalar. Kriptografi kısmına şu anda değinmeyeceğim ancak işlem onaylayan madencilerin ödüllendirilmesi bu ağın devamlılığının sağlanmasının ve yüksek hash oranlarına sahip olmasının tek sebebidir. Çünkü bugün itibariyle madenciler hem buldukları bloklar hem de o blok içerisinde çalıştırdıkları yazılımların işlem adımlarıyla orantılı bir şekilde ödüllendirirler. Bu işlem adımlarına gas adı verilir ve sizin çalıştırmak istediğiniz uygulama madencinin işlemcisini ne kadar meşgul tutarsa o kadar çok ödeme yapmanızı gerektirir. Böylece hem madenciler ödüllendirilirken hem de sistemin DoS saldırılarına karşı korunması sağlanmış olur.

Durum böyle olunca, yani her çalıştırılacak işlem karşılığında bir ödeme yapılıyorsa, bu durumda bizim gözümüze batmayan işlemlerin maliyetleri de haliyle bu işlemlerin içerisinde yer almış olsaydı, işlem ücretleri artmış olacaktı.

Diğer yazılım dilleri bir değerin taşması sonucunda hata fırlatarak bu sorunları önlemekteyken yine EVM için bu sağlıklı bir çözüm olarak kabul görmemektedir. Bunun yine sebebi Ethereum’un çalıştırılacak her işleminin gas tüketmesi ve bu gas’ın karşılığında para ödüyor olmanız ve hata fırlatıldığı taktirde ödediğiniz tüm parayı harcamanıza sebep olacak olmasıdır. Bunun sebebi hata fırlatma işleminin maliyetinin çok yüksek olmasından kaynaklanıyor, çünkü yönetilemeyen bir hata fırlatıldığında oluşturulmuş tüm hafıza yığınlarının (stack) yok edilmesi gerekmekte, tüm yığınların en tepesine kadar geri dönmesi gerekmektedir. Bu durum da haliyle hata fırlatılmasıyla birlikte ciddi bir işlem gücüne ihtiyaç duymakta ve gas limit adı verilen bir işlemin kullanabileceği en fazla gas miktarına eşit ya da fazla olmasına sebep olmaktadır. Peki biraz ikna olduysanız hatanın nasıl oluştuğuna ve ne şekilde suistimal edilebildiğine bir göz atalım.

Senaryomuz çok basit; elimizde bir ERC20 token kontratı, ArkaKapıToken, olsun. Diyelim ki bir hesap kendisine ait 100 ArkaKapıToken’ı 10 farklı adrese transfer etmek istiyor. Bu durumda 10 farklı işlem göndererek 10 kere işlem ücreti ödemek yerine kontrat üzerindeki ‘batchTransfer’ fonksiyonunu kullanarak, parametre olarak vereceği 10 farklı adresin her birine ne kadar transfer yapabileceğini göndersin. Böylece 10 ayrı işlem yapmak yerine sadece tek bir işlem ile bu transferleri bir kerede gerçekleştirebileceği için çok daha az işlem ücreti ödeyecektir. Bu özellik birçok ERC20 token kontratı tarafından sağlanan bir özelliktir ve genellikle, ne yazık ki düşünülmeden kopyala yapıştır yapılarak yazılımcılar tarafından kontratlara eklenir. Şimdi aşağıda gerçek bir akıllı kontrattan alınmış koda bakalım.

Image for post
Image for post

Yukarıdaki örnekte hatayı görebiliyor musunuz? Hemen yardımcı olayım, hatanın oluştuğu kısım 3. Satır. Diyelim ki _value değeri 2^256 maksimum değerine eşit olsun ve _receivers dizisinde de iki tane eleman olsun. Bu durumda 2^(256 x 2) değeri UINT256 veri tipinin sınırları dışına taşacağı için amount değişkeninin değeri 0 olacak, 4. Satırdaki cnt değeri dizideki eleman sayısı olacağı için bu satır başarılı bir şekilde geçilecek, 5. Satırdaki kontrol de yine amount değeri 0 olduğu için başarılı bir şekilde geçtikten sonra 8–11 arasında _receivers dizisinin elemanlara çok büyük sayıda bir token transferi gerçekleşecektir.

Şüphesiz bu sorun sadece transferBatch fonksiyonuna sahip ERC20 token’larda değil, bu şekilde UINT256 ile işlem yapan her akıllı kontrat için geçerlidir. Aynı zamanda yine sorun sadece yukarıya doğru taşmayı sağlayan çarpma ve toplama işlemlerinde değil, aşağıya doğru yapılan işlemlerde de karşımıza çıkmaktadır. Bu soruna da underflow denilmektedir. Aşağıdaki kod parçası hem overflow hem de underflow sorunlarını güzel bir şekilde örneklemektedir.

Image for post
Image for post

Zaman Damgası Bağımlılığı

Eğer siz de C#, Java, Javascript ya da Python gibi yazılım dilleriyle yazdığınız uygulamaları kendi kontrolünüzdeki ya da bulut üzerindeki sunuculara kurmaya alıştıysanız yine akıllı kontratlar yazarken göz önünde bulundurmanız gereken bir önemli konu var.

Ethereum ve neredeyse tüm blok zinciri protokolleri tamamen güvenmemeyi baz alır. Bu temel prensip ‘Trustlessness’ olarak anılır ve temelde protokolde yapılacak işlemlerde hiçbir şekilde sistemdeki tarafların birbirine güvenmesine ihtiyaç duymamasını hedefler ve bu alanda çözümler sunar. Bunun en önemli sebeplerinden biri tamamen dağıtık bir sistem olduğundan dolayı tarafların birbirlerini tanımaması ve bu yüzden kötü niyetli kişilerin sistemde her zaman olduğunu kabullenmesidir.

Bunun en güzel örneği 30 saniye kuralıdır. Ethereum ağına dahil olan tüm node’ların zaman ayarlarının doğru olması gerekir. Yani ağa bağlanan her bilgisayar, ilk bağlantı sırasında zaman dilimini ve bilgisayarın sistem saatini ilişki kurduğu taraflarla paylaşmak zorundadır. Eğer sistem saati farklı ise o zaman ağa girişi engellenir. Bunun en önemli sebebi tabi ki manipülasyonların yapılmasını engellemektir. Ancak tüm işlemlerin bire bir olduğunu ve arada mesaj kayıpları olduğunu düşünürsek, iletişimde aksaklıklar olması her zaman mümkün olacağı için 30 saniyelik zaman farklılıkları göz ardı edilmektedir. Bu da ağdaki madencilerden en az birinin 30 saniye ileri ya da geri olabileceği anlamına gelmektedir.

Bu durumda eğer block.timestamp özelliğini kullanarak işlemler yapıyor ve zamana bağlı bir takım kararlar vermek istiyorsanız bu 30 saniyelik hata payını göz önünde bulundurmanız gerekmektedir. Yine bu 30 saniyelik zaman farkı sizin için önemliyse kötü niyetli madenciler bu zaman farkını kullanarak kontratınızda manipülasyonlar yapabilir.

Kısa Adres Saldırısı

Şimdi sıra geldi yazının anlatması en zor saldırı yöntemine. Bu yöntem Golem ekibi tarafından geçtiğimiz yıl bulundu ve birçok kripto borsasının Ethereum bazlı token’larını etkiledi. Büyük aktörleri etkilediği için pek fazla duymamış olabilirsiniz ama ben biraz detaylarını anlatmaya çalışacağım, umarım başarabilirim!

Bildiğiniz gibi Ethereum adresleri 20 byte uzunluğundadır. Örneğin 0x071a8A7a1cb42F0300202a8374c1DDFA14895500 adresi geçerli bir Ethereum adresidir. Bu adresin sonundaki sıfırlara göz atalım şimdi. Eğer ben bu adresi, sonundaki sıfırlarını atarak 0x071a8A7a1cb42F0300202a8374c1DDFA148955 şeklinde ifade etseydim ne olurdu?

Yukarıdaki soruya geri döneceğim, ancak öncesinde Ethereum işlemlerinin nasıl çalıştığını da biraz anlatmazsam bu problemi anlatmak çok zor olacak.

Diyelim bir ERC20 kontratının Transfer fonksiyonunu çağırıyorsunuz. Transfer fonksiyonu address ve uint256 tipinde olmak üzere iki parametre ile çalıştırılabilir. Dolayısıyla Transfer(0xaaabbbccc00, 100) gibi bir çağırım yapmak isteseydiniz, Ethereum protokolü Transfer fonksiyonunun o kontrat içerisindeki imzasını ve diğer tüm parametrelerini arka arkaya ekleyerek bir işlem (transaction) hazırlayıp imzaladıktan sonra ağdaki madencilerin onaylaması için gönderecekti. Transfer fonksiyonunun kontrattaki imzasının 0x12345 olduğunu varsayarsak Transfer(0xaaabbbccc00, 1) fonksiyon çağrımı; 0x12345aaabbbccc0000000001 işlemi şeklinde ifade edildikten sonra imzalanıp ağa gönderilir.

Artık konunun nereye gittiğini anladığınızı düşünüyorum. Eğer sonu iki tane sıfır ile biten bu adresi sıfırlarını koymadan işleme sokarsam ve bu işlemi oluşturan borsa bu adres boyutunu kontrol etmeden işleme sokmaya çalışırsa ne olur? Transfer(0xaaabbbccc, 1) için (adresin sonunda sıfırlar olmadığına dikkat edin) 0x12345aaabbbccc00000001 şeklinde bir işlem oluşturulacaktır. Bu işlem madenci tarafından anlaşılmaya çalışıldığında parçalara bölünecek ve aşağıdaki tablo ortaya çıkacaktır;

Fonksiyonun imzası: 0x12345
Birinci parametre address veri tipinde: 0xaaabbbccc00
İkinci parametre UINT256 veri tipinde: 0x000001??

olan birinci parametre olmasına rağmen soldan başlayarak işlemi parçalara ayırdığı için eksik olan adres parametresini ikinci parametre olan 0x00000001 değerinin başındaki 1 byte’dan tamamlayarak 0x000001 değerini elde etmekte ve tahmin edileceği gibi bu değeri 0x00000100 şeklinde yani 256 tam sayı değerine tamamlamaktadır.

Bu şekilde eğer gönderim yapacak borsanın hesabında bu işlemi karşılayacak kadar token mevcutsa günün sonunda borsa sadece 1 tane token göndereceğini düşünürken akıllı kontrat üzerinde 256 tane gönderecektir. Bu saldırı yöntemi aynı ‘SQL injection’ saldırısı gibi basit ve çabucak farkına varılabilecek diye düşünülse de emin olun bu hatayı barındıran uygulamaların sayısı şaşırtıcı derecede fazladır.

Eğer buraya kadar sabredip okuduysanız şu anda saldırı yöntemlerinden daha da önemlisi uygulamalar geliştirdiğiniz EVM’i ve Ethereum’u biraz daha anlamış olmalısınız.

Yazımı tamamlarken bu yazıyı gece yarısı okuyup, sayısız yazım hatasını düzeltmeme yardımcı olan Unichain ekibinden Fuat Cem Özyazıcı, Serkan Ayyıldız ve Şafak Kayran’a teşekkür etmek istiyorum.

Written by

I do stuff. with code.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store