27 Mart 2022'de, Ethereum üzerindeki staking DeFi projesi Revest Finance, ERC-1155 geri çağırma (call-back) mekanizması nedeniyle saldırıya uğradı ve yaklaşık 2 milyon dolar değerinde token (BLOCKS, ECO, LYXe ve RENA) çalındı. Saldırıyı ilk olarak analiz ettik ve o gece (UTC+8) analizimizi tweetledik.
Aslında, Twitter'ı yazarken, Revest TokenVault sözleşmesindeki bir fonksiyon hakkında hâlâ bazı şüphelerimiz vardı. İşlevselliğini anlamaya çalışarak sözleşmeyi inceledik. Daha sonra bunun, çok daha basit bir şekilde istismar edilebilecek ve aynı büyük kayıplara yol açabilecek (gerçekleşen saldırıda olduğu gibi) başka bir kritik sıfır-gün açığı olduğunu keşfettik.
Ardından hemen Revest Finance ekibiyle iletişime geçtik; ekip hızlı yanıt vererek açık için bir geçici çözüm önerdi. Açığın tetiklenemeyeceğini doğruladıktan sonra bu blogu yayımlamaya karar verdik.
Bu blogun geri kalanı üç bölümden oluşmaktadır: Revest Finance'ın mekanizması, orijinal yeniden giriş (re-entrancy) saldırısı ve yeni sıfır-gün açığı.
Revest Finance FNFT Nedir?
Revest Finance'ın Finansal Değiştirilemez Token'ı (FNFT), kilitli varlıklara gelecekteki hakların güvensiz (trustless) şekilde transfer edilmesini mümkün kılar. Giriş sözleşmesi (Revest sözleşmesi), temel varlıkları kilitleyerek FNFT basmak için üç farklı arayüz sunar:
mintTimeLock: temel varlık, belirli bir süre sonra serbest bırakılır.mintValueLock: temel varlık, değeri belirlenmiş bir değerin üzerine çıktığında veya altına düştüğünde serbest bırakılır.mintAddressLock: temel varlık, belirlenmiş bir hesap tarafından serbest bırakılır.
Revest sözleşmesi, temel varlıkları kilitlemek ve serbest bırakmak için diğer üç sözleşmeyi birbirine bağlar.
-
FNFTHandler: ERC-1155 token'ından miras alır. Her kilit için artan
fnftIdile yeni bir FNFT oluşturur. Kilit, oluşturma sırasında yeni FNFT'nin toplam arzını belirler. FNFT başka bir şekilde basılamaz, ancak temel varlıkların serbest bırakılması için yakılabilir. -
LockManager: oluştururken her kilit için kilitleme koşullarını kaydeder ve kilidi açarken kilidin açılıp açılamayacağına karar verir.
-
TokenVault: temel varlıkları alır ve gönderir; her FNFT için belirli bir FNFT'nin değeri gibi meta verileri kaydeder.
FNFT basma sürecini açıklamak için mintAddressLock'u örnek alıyoruz.


Yukarıdaki iki şekil, bir FNFT'nin nasıl oluşturulduğunu, basıldığını ve yakıldığını açıklamaktadır.
Özellikle, A kullanıcısı Revest Finance'a 100 WETH kilitleyerek fnftId değeri 1 olan karşılık gelen FNFT'yi oluşturur. Son olarak, belirlenmiş alıcılara belirlenmiş paylarla 100 adet 1-FNFT basar.
Temel varlık serbest bırakıldıktan sonra, her 1-FNFT'nin bir (*1e18) WETH almak için yakılabileceğini unutmayın. Şekil 2'de gösterildiği gibi, B kullanıcısı 25 adet 1-FNFT yakarak 25 (*1e18) WETH çeker.
Ayrıca, Revest sözleşmesi depositAdditionalToFNFT adında başka bir arayüz daha sunar; bu arayüz, aşağıda ele alınacak iki güvenlik açığına yol açmaktadır.
Bu fonksiyonun normal kullanımını açıklamak için önce aşağıdaki iki şekli kullanıyoruz.


depositAdditionalToFNFT fonksiyonu, mevcut bir kilide (fnftId ile belirtilen) daha fazla temel varlık kilitler. Makul olarak (Şekil 3), belirtilen miktarın belirtilen FNFT'nin toplam arzıyla aynı olmasını gerektirir ve ardından eklenen varlıkları her belirtilen FNFT'ye eşit olarak dağıtır.
Aksi takdirde (Şekil 4), en son fnftId ile yeni bir kilit oluşturur, eski FNFT'nin belirtilen miktarlarını yakar ve belirtilen miktarda yeni FNFT basar; ardından aşağıdaki kodda gösterildiği gibi yeni kilidin depositAmount değerini eski kilidin depositAmount değeri ile belirtilen miktarın toplamı olarak kaydeder.
// Şimdi token vault'a transfer yapıyoruz
if(fnft.asset != address(0)){
IERC20(fnft.asset).safeTransferFrom(_msgSender(), vault, quantity * amount);
}
ITokenVault(vault).handleMultipleDeposits(fnftId, newFNFTId, fnft.depositAmount + amount);
emit FNFTAddionalDeposited(_msgSender(), newFNFTId, quantity, amount);
TokenVault sözleşmesinde kaydedilen depositAmount, belirli bir FNFT'nin çekebileceği temel varlık miktarını gösterdiğinden, bu işlem belirtilen miktardaki eski FNFT'nin değerini eski kilit'ten yeni kilide aktarır.
(Belirtilen miktarın toplam arzdan büyük olması işlemi geri alacaktır)
Yeniden Giriş Açığı Nedir?
Bu bölümde, yeniden giriş saldırısının nasıl çalıştığını açıklayacak ve temel nedeni ile düzeltme yöntemini tartışacağız.



Yukarıdaki üç şekil, yeniden giriş saldırısının tüm sürecini temel olarak açıklamaktadır. Özellikle, saldırgan önce değersiz 2 adet 1-FNFT basmak için sıfır RENA token kilitler. Ardından saldırgan yine sıfır RENA token kilitler ancak bu sefer de değersiz (şimdilik) 360.000 adet 2-FNFT basar. Son adımda saldırgan, ERC-1155 token standardından miras alınan FNFTHandler'ın geri çağırma mekanizması aracılığıyla Revest sözleşmesinin depositAdditionalToFNFT fonksiyonuna yeniden girer; bu, fnftId güncellenmeden önce fnftId değeri 2 olan kilidin depositAmount değerinin üzerine yazar. Sonuç olarak, saldırgan depositAmount değeri 1e18 olan 360.001 adet 2-FNFT elde eder; bu da TokenVault sözleşmesinden 360.001 * 1e18 RENA çekebileceği anlamına gelir. Üstelik tek maliyet yalnızca 1e18 RENA'dır.
Düzeltme Yöntemi
Revest Finance'ın kodları, klasik yeniden giriş kalıbıyla tamamen örtüşmektedir: fnftId kullan -> geri çağırma mekanizmalı harici çağrı -> fnftId'yi güncelle. Bu nedenle, sorunları çözmenin en doğrudan yolu bu kalıbı kırmaktır. Düzeltilmiş kod aşağıda gösterilmektedir:
function mint(
address account,
uint id,
uint amount,
bytes memory data
) external override onlyRevestController {
require(amount > 0, "Geçersiz miktar");
require(supply[id] == 0, "Aynı FNFT için tekrarlanan basım");
supply[id] += amount;
fnftsCreated += 1;
_mint(account, id, amount, data);
}
İlk olarak, güncelleme işlemini harici çağrıdan (_mint) önceye taşır; bu da saldırıyı önleyebilir. İkinci olarak, sistem sıfır FNFT basmaya ve aynı FNFT'yi tekrar basmaya izin vermediğinden, sistemin beklendiği gibi çalışmasını sağlamak için iki kontrol ekler; bu da sistemin güvenliğini artırabilir.
Yeni Sıfır-Gün Açığı
Revest Finance kodunu analiz ederken, TokenVault sözleşmesindeki handleMultipleDeposits fonksiyonu bizi her zaman şaşırttı; fonksiyonun kodu aşağıda gösterilmektedir.
function handleMultipleDeposits(
uint fnftId,
uint newFNFTId,
uint amount
) external override onlyRevestController {
require(amount >= fnfts[fnftId].depositAmount, 'E003');
IRevest.FNFTConfig storage config = fnfts[fnftId];
config.depositAmount = amount;
mapFNFTToToken(fnftId, config);
if(newFNFTId != 0) {
mapFNFTToToken(newFNFTId, config);
}
}
depositAdditionalToFNFT fonksiyonuna yapılan çağrı sırasında, handleMultipleDeposits fonksiyonu eski kilidin depositAmount değerini değiştirir veya yeni kilidin değerini kaydeder. newFNFTId sıfır olduğunda, yeni kilidin depositAmount değerini kaydetmez; çünkü bu, mevcut kilide ek varlık ekleme işlemidir.
Sağduyuya göre, newFNFTId sıfır olmadığında, yalnızca yeni kilidin depositAmount değerini kaydeder ancak eskisini değiştirmez. Ancak kod, yalnızca yeni kilidin depositAmount değerini kaydetmekle kalmayıp eskisini de değiştirdiğini göstermektedir.
Bunun ciddi bir sıfır-gün mantık açığı olduğuna inanıyoruz ve bunu doğrulamak için bir PoC yazıyoruz. Aşağıdaki üç şekil, PoC'nin nasıl çalıştığını açıklamaktadır.



Özellikle, saldırgan önce 360.000 adet 1-FNFT basmak için sıfır RENA kilitler. Ardından saldırgan, yeni bir kilit oluşturmak için doğrudan depositAdditionalToFNFT fonksiyonunu çağırır. Mantık hatası nedeniyle, TokenVault sözleşmesi eski kilidin depositAmount değerini hatalı biçimde sıfırdan 1e18'e değiştirir. Sonuç olarak, saldırgan 359.999 RENA değerinde 359.999 adet 1-FNFT kazanır. Açıkça görüldüğü üzere, PoC gerçek yeniden giriş saldırısından çok daha basittir.
Açığı Düzeltmek İçin Geçici Çözüm
Bu bir mantık hatasıdır ve düzeltmek için aşağıdaki kodun kullanılmasını öneririz.
function handleMultipleDeposits(
uint fnftId,
uint newFNFTId,
uint amount
) external override onlyRevestController {
require(amount >= fnfts[fnftId].depositAmount, 'E003');
IRevest.FNFTConfig memory config = fnfts[fnftId];
config.depositAmount = amount;
if(newFNFTId != 0) {
mapFNFTToToken(newFNFTId, config);
} else {
mapFNFTToToken(fnftId, config);
}
}
Açıklı olan iki sözleşme (TokenVault ve FNFTHandler) çok sayıda kritik durum (state) sakladığından, proje durumları taşımadan TokenVault ve FNFTHandler sözleşmelerini yeniden dağıtamaz. Bu açığa yönelik olası saldırıları önlemek için proje, olası saldırganlara açık olan yüzeyleri azaltmak amacıyla daha karmaşık fonksiyonları devre dışı bırakan Revest sözleşmesinin lite sürümünü yeniden dağıttı. Geçici çözümü kontrol ettikten sonra, lite Revest sözleşmesinin bu blogda bahsedilen olası saldırıları hafifletebileceğine inanıyoruz.
Sonuç
Bir DeFi projesini güvenli kılmak kolay bir iş değildir. Kod denetiminin yanı sıra, topluluğun proje durumunu izlemek için proaktif bir yöntem benimsemesi ve saldırı gerçekleşmeden önce engel olması gerektiğini düşünüyoruz.
BlockSec Hakkında
BlockSec, 2021 yılında küresel çapta tanınan güvenlik uzmanlarından oluşan bir grup tarafından kurulan öncü bir blok zinciri güvenlik şirketidir. Şirket, kitlesel benimsenmesini kolaylaştırmak amacıyla gelişmekte olan Web3 dünyasının güvenliğini ve kullanılabilirliğini artırmaya kararlıdır. Bu doğrultuda BlockSec; akıllı sözleşme ve EVM zinciri güvenlik denetim hizmetleri, güvenlik geliştirme ve tehditleri proaktif olarak engelleme için Phalcon platformu, fon takibi ve soruşturma için MetaSleuth platformu ve kripto dünyasında verimli gezinen web3 geliştiricileri için MetaDock eklentisi sunmaktadır.
Bugüne kadar şirket, MetaMask, Uniswap Foundation, Compound, Forta ve PancakeSwap gibi 300'den fazla saygın müşteriye hizmet vermiş; Matrix Partners, Vitalbridge Capital ve Fenbushi Capital gibi önde gelen yatırımcılardan iki finansman turunda onlarca milyon ABD doları almıştır.
Resmi web sitesi: https://blocksec.com/
Resmi Twitter hesabı: https://twitter.com/BlockSecTeam



