Back to Blog

~$39.5M Kaybedildi: Allbridge, Wanchain ve Daha Fazlası | BlockSec Haftalık

Code Auditing
July 30, 2026
17 min read
Key Insights

Geçen hafta (2026/07/20 - 2026/07/26) boyunca, toplam yaklaşık 39,5 milyon dolarlık kayıpla ilgili aşağıdaki 8 önemli güvenlik olayı öne çıkmaktadır.

Tarih Olay Tür Tahmini Kayıp
2026/07/19* Allbridge Hatalı Girdi Doğrulaması ~$1.65M
2026/07/20 Zilliqa Hatalı Nonce Üretimi ~$400K
2026/07/20 Wanchain Hatalı Mesaj Kodlaması ~$500K
2026/07/22 42DAO Özel Anahtar Ele Geçirilmesi ~$900K
2026/07/22 AFX Trade Özel Anahtar Ele Geçirilmesi ~$24.15M
2026/07/22 B² Network Özel Anahtar Ele Geçirilmesi ~$3.8M
2026/07/23 Verus Özel Anahtar Ele Geçirilmesi ~$7.6M
2026/07/24 Lien Finance Hatalı Doğrulama Mantığı ~$542K

*Allbridge olayı 19 Temmuz'da (17:51 UTC) yaşanmış olup geçen haftaki raporda yer almamıştır. Eksiksizlik açısından buraya dahil edilmiştir.

  • Zilliqa, zincir dışı cüzdan uygulamasında uzun süre tespit edilemeyen ve 2019'dan bu yana eski yerel ZIL hesabı kullanıcılarını özel anahtar ele geçirilme riskiyle karşı karşıya bırakan bir açığı gün yüzüne çıkardığı için seçilmiştir.
  • Allbridge, Solana'nın konumsal hesap modeli tarafından mümkün kılınan bir hesap takma adı (account-aliasing) açığını gözler önüne serdiği için seçilmiştir. Kaynak kodu mevcut olmadığından açığın dağıtılmış program ikilisinden yeniden oluşturulması gerekmiştir.
  • Wanchain, bir köprünün kriptografi tehlikeye atılmadan nasıl tamamen boşaltılabileceğini gösterdiği için seçilmiştir. İmzalar geçerli ve doğru şekilde doğrulanmıştı; ancak enjektif olmayan, sınırlayıcı içermeyen mesaj kodlaması, 3.097,56 token için verilen bir yetkinin Cardano üzerinde 203.001.692,164714 token olarak yeniden yorumlanmasına izin vermiştir.
  • Lien Finance, eşleşen öğelerin kimliğini ve çokluğunu kontrol etmeksizin bakiyelerin sayılarla örtüşüp örtüşmediğini doğrulayan bir mekanizmanın nasıl atlatılabileceğini örneklediği için seçilmiştir.

Web3 için En İyi Güvenlik Denetçisi

Lansmanı yapmadan önce tasarımı, kodu ve iş mantığını doğrulayın

Haftanın Öne Çıkanı: Allbridge Core

Bu olay, hesap takma adı (account-aliasing) örüntüsünün aynı değiştirilebilir hesabı iki talimat rolünde kabul eden herhangi bir Solana programına uygulanabilmesi nedeniyle öne çıkarılmıştır. Temel mekanizma — aynı durumun iki değiştirilebilir görünümünün bulunduğu ve ikinci yazmanın birincisini sessizce üzerine yazdığı durum — klasik ERC-20 öz-transfer hatasıyla (from == to olduğu transferFrom) aynı yapıya sahiptir. Projenin olay sonrası analizi [1] üst düzey bir özet sunmakta ancak kod düzeyindeki mekanizmayı ayrıntılı biçimde ele almamaktadır. Aşağıdaki analiz, bu boşluğu doldurmak amacıyla dağıtılmış program ikilisinden yeniden oluşturulmuştur.

19 Temmuz 2026'da (17:51 UTC), Solana üzerinde faaliyet gösteren bir zincirler arası köprü protokolü olan Allbridge Core, yaklaşık 1,65 milyon dolarlık (~1,12M USDC ve ~539K USDT) bir saldırıya uğramıştır. Temel neden, swap talimatının aynı Pool hesabını hem gönderme hem de alma rolünde kabul etmesi ve ikisinin farklı olması gerektiğini zorunlu kılmamasıydı. Aynı hesap iki kez iletildiğinde, bir rolden gelen dahili muhasebe güncellemeleri diğeri tarafından sessizce üzerine yazılırken gerçek token transferleri zaten tamamlanmıştı. Beş öz-swap işlemi, Pool'un kayıtlı durumunu yeterince bozarak saldırganın küçük bir USDT girdisini ~2,24M USDC'ye dönüştürmesine olanak tanıdı.

Arka Plan

Allbridge Core, takasları vUSD adı verilen dahili bir muhasebe birimi üzerinden yönlendiren bir zincirler arası köprüdür. Bir swap iki muhasebe aşamasından oluşur:

kaynak token -- swap_to_v_usd(gönderme_havuzu) --> vUSD
vUSD        -- swap_from_v_usd(alma_havuzu) --> hedef token

vUSD bir SPL token değildir. Yalnızca her Pool'un zincir üstü verisindeki bir alan olarak mevcuttur. Her Pool, token_balance, v_usd_balance ve reserves değerlerini takip eder. Gönderme aşaması, kaynak tokenı kullanıcıdan gönderme köprü kasasına aktarır, gönderme Pool'unun token taraflı muhasebesini artırır ve vUSD çıktısını hesaplar. Alma aşaması ise bu vUSD'yi alma Pool'una ekler, token taraflı muhasebesini azaltır ve hedef tokenı alma köprü kasasından kullanıcıya aktarır.

Solana programları, hesaplarını çağırandan konumsal bir dizi olarak alır. Programın talimat işleyicisi hangi dizi konumlarının hangi rollere karşılık geldiğini belirtir (send_pool, receive_pool, send_mint vb.), ancak çalışma zamanı çağıranın iki konuma aynı hesap adresini iletmesini engellemez. Program hesapları iki farklı konumda seri dışı bıraktığında (deserialize ettiğinde), her seri dışı bırakma işlemi ayrı bir bellek içi nesne üretir. Bir nesnede yapılan değişiklikler diğerini etkilemez. Aynı hesap iki kez iletildiğinde her iki nesne de aynı zincir üstü veriyle başlar; ancak herhangi biri değiştirilir değiştirilmez birbirinden ayrışırlar.

İşleyici tamamlandığında, programın çıkış mantığı (örn. Anchor'ın AccountsExit'i) her hesap nesnesini sabit bir sırayla hesabın verisine geri serileştirir. İki nesne aynı hesapla eşleniyorsa ikinci serileştirme birincinin üzerine tamamen yazar.

Açık Analizi

Etkilenen Allbridge Core dağıtımının tam kaynak kodu mevcut değildi. Analiz, ProgramData'da depolanan 1.770.736 baytlık ELF'den yeniden oluşturulmuştur (SHA-256: 40f776...346bb6). Son dağıtım slotu (204.727.029), saldırı slotunun (433.941.722) öncesindedir.

Hatalı program BrdgN2...ceWB adresidir. Saldırı işleminin Swap talimatlarında (3'ten 7'ye kadar olan talimatlar), dört gönderme ve alma hesap çifti özdeşti:

Roller Hesap
send_mint / receive_mint Es9vMF...wNYB
send_pool / receive_pool DW4a2E...wCX
send_bridge_token / receive_bridge_token 2xY9TD...vohV
send_user_token / receive_user_token 817UdW...CVct

Ayrıştırıcı, her rolü beklenen mint, kasa, sahip, PDA, yetki ya da Token programına bağlayan 16 adet kurtarılmış 32 baytlık karşılaştırma içermektedir. Ancak send_pool.key() != receive_pool.key() koşulunu zorunlu kılan herhangi bir karşılaştırma bulunmamaktadır. İki Pool rolü, işleyici çalışmadan önce bağımsız olarak seri dışı bırakılmaktadır.

Dağıtılan ELF, sabit bir sırayla iki Pool yapısı, gönderme ve alma hesaplamaları ile iki Pool çıkışını göstermektedir:

L54249-L54252  function_9881(accounts[5])     -> yerel gönderme_havuzu
L54294-L54297  function_9881(accounts[6])     -> yerel alma_havuzu

L74177-L74195  function_12623(send_pool)      -> swap_to_v_usd
L74368-L74389  function_12940(receive_pool)   -> swap_from_v_usd

L55932-L55936  function_8300(send_pool)       -> tam Pool serileştirme
L55940-L55944  function_8300(receive_pool)    -> tam Pool serileştirme

Her function_9881 çağrısı ayrı bir 176 baytlık yerel Pool nesnesi tahsis eder ve kodu çözülmüş alanları buna kopyalar. Hesap anahtarları eşit olduğunda her iki nesne de aynı veriyle başlar; ancak gönderme nesnesinde yapılan bir değişiklik alma nesnesini etkilemez.

Yeniden oluşturulmuş sözde-Rust kodu (dağıtılan davranış, kurtarılan kaynak değil) [2] [3]:

let mut send_pool = Account::<Pool>::try_from(&accounts[5])?;
let mut receive_pool = Account::<Pool>::try_from(&accounts[6])?;

require_keys_eq!(send_pool.mint, send_mint.key());
require_keys_eq!(receive_pool.mint, receive_mint.key());

// Kasa, sahip, PDA, yetki ve Token programı bağlantıları kontrol edilmektedir.
// EKSİK: require_keys_neq!(send_pool.key(), receive_pool.key());

token::transfer(send_user_token, send_bridge_token, amount)?;
let v_usd = swap_to_v_usd(&mut send_pool, amount)?;
let receive_amount =
    swap_from_v_usd(&mut receive_pool, v_usd, receive_amount_min)?;
token::transfer(receive_bridge_token, receive_user_token, receive_amount)?;

// İşleyici döndükten sonra AccountsExit:
send_pool.exit(program_id)?;     // birinci yazma
receive_pool.exit(program_id)?;  // ikinci yazma, birincinin üzerine yazar

function_8300, yazıcısını 0. konumdan başlatır ve tanımlı 131 Pool baytının tamamını serileştirir. Birinci çıkış send_pool'u, ikinci çıkış ise receive_pool'u aynı baytların üzerine yazar. Dolayısıyla nihai hesap durumu, iki yerel güncellemenin birleşimi değil, yalnızca eskimiş alma tarafı değeridir.

SPL token transferleri ayrı CPI'lardır. Pool çıkışlarından önce SPL Token programına ait kasa hesaplarını güncellerler; bu nedenle ikinci Pool serileştirmesi girdi transferini geri alamaz. Her takma adlı swap, transfer edilen tokenleri kasada bırakırken karşılık gelen gönderme tarafı Pool güncellemesini atar. Alma tarafı güncellemesi ise kalır ve Pool'un kayıtlı durumunu daha yüksek v_usd_balance ve daha düşük token tarafı muhasebesine doğru iter.

Temel kusur, programın send_pool ve receive_pool'un farklı hesaplar olduğunu kontrol etmemesidir. Diğer tüm doğrulamalar (mint bağlantısı, kasa sahipliği, PDA türetme) geçerlidir; çünkü her iki rol de aynı Pool'a meşru olarak aittir.

Saldırı Analizi

Aşağıdaki analiz, 3LNLaG...Y39Q işlemine dayanmaktadır.

Saldırı, bir Kamino flaş borçlanması, yedi Allbridge Swap talimatı ve bir Kamino geri ödemesini içeren tek bir işlemde tamamlandı.

  • Adım 1: Saldırgan Kamino'dan ~1,12M USDC ödünç aldı ve Allbridge üzerinden ~949K USDT için swap yaptı (talimatlar 1-2). Bu ilk swap, sonraki öz-swaplar için gereken USDT'yi sağladı ve hem USDC hem de USDT Pool durumlarını değiştirdi.
  • Adım 2: Saldırgan, hem gönderme hem de alma için özdeş mint, Pool, kasa ve kullanıcı token hesaplarını kullanarak beş adet ~100K USDT öz-swap işlemi gerçekleştirdi (talimatlar 3-7). Alma Pool'u her çağrıda son olarak serileştirildiğinden, kalıcı muhasebe alma tarafı azalmasını kaydetti; gönderme tarafı eklemeyi ise kaydetmedi. Gözlemlenen çıktılar, bozulma biriktikçe her çağrıda azaldı:
Öz-swap Girdi Kayıtlı vUSD Fiyat (vUSD/USDT) USDT çıktısı
1 ~100K ~162K ~5,24 ~47,8K
2 ~100K ~256K ~16,6 ~25,4K
3 ~100K ~471K ~57,2 ~12,5K
4 ~100K ~918K ~190 ~5,73K
5 ~100K ~1,82M ~563 ~2,36K

Beş çağrı kasaya ~500K USDT aktardı ve ~93,8K USDT geri döndürdü. Pool'un kayıtlı token_balance değeri her çağrıda düşerken gerçek kasa bakiyesi büyüdü; bu durum, bir sonraki adımın istismar ettiği açığı genişletti.

  • Adım 3: Saldırgan yalnızca ~3,99K USDT sağladı (talimat 8). Bozulmuş USDT Pool'u ~2,24M vUSD üretti ve USDC Pool'u bunu ~2,24M USDC'ye dönüştürdü. Şişirilmiş vUSD çıktısı mümkün oldu; çünkü Pool'un kayıtlı token bakiyesi, beş tur atılan gönderme tarafı güncellemesinin ardından gerçek kasa bakiyesinin çok altındaydı.
  • Adım 4: Saldırgan ~1,12M USDC Kamino flaş kredi anaparasını ve ~11,2 USDC ücretini geri ödedi (talimat 9). Saldırganın nihai bakiyeleri ~1,12M USDC ve ~539K USDT oldu.

Sonuç

Bu olayın temel nedeni, send_pool ve receive_pool'un farklı hesaplara atıfta bulunduğunu doğrulayan kontrolün eksikliğiydi. Tek bir değiştirilebilir Pool, iki yerel muhasebe nesnesine seri dışı bırakıldı; Token programı kasa transferlerini zaten tamamlamışken ikinci tam serileştirme birincinin üzerine yazdı. Bu durum, Pool'un kayıtlı muhasebesini gerçek kasa bakiyesinden ayırdı. Hesap takma adları, ELF kontrol akışı, serileştirme sırası, işlem kayıtları ve kasa bakiyeleri aynı mekanizmayı desteklemektedir [1].

Solana programları için, aynı hesap türünü iki veya daha fazla değiştirilebilir rolde kabul eden herhangi bir talimat ya tekliği zorunlu kılmalı (require_keys_neq!) ya da çıkıştan önce değişiklikleri birleştirmelidir. Anchor çerçevesinin #[account] kısıtlama sistemi varsayılan olarak roller arası tekliği zorunlu kılmaz; bu nedenle bu kontrol açıkça eklenmek zorundadır. Genel örüntü — aynı durumun iki değiştirilebilir görünümünün bulunduğu ve ikinci yazmanın birincisini sessizce üzerine yazdığı durum — programların kendi serileştirme sıralarını yönettiği her yerde ortaya çıkabilir.

Kaynaklar

Phalcon Explorer ile Başlayın

İşlemlere Dalın, Akıllıca Hareket Edin

Şimdi ücretsiz deneyin

Bu Haftanın Diğer Olayları

Zilliqa Ledger Cüzdanı

20 Temmuz 2026'da Zilliqa, yaklaşık 400K dolar kayıpla birlikte eski yerel ZIL hesaplarının aktif olarak istismar edildiğiyle tutarlı zincir üstü aktivite tespit etti. Temel neden, Zilliqa Ledger uygulamasının EC-Schnorr imzalama yolundaki önyargılı bir nonce'du: nonce üretim kodu, modüler indirgemeden sonra 40 baytlık bir arabellekten yanlış 32 baytı kopyaladı ve en anlamlı 64 biti sıfıra sabitledi. Aynı hesaptan birkaç genel imzayla saldırgan, kafes tabanlı teknikler aracılığıyla özel anahtarı kurtarabilir ve hesabı boşaltabilirdi.

Arka Plan

Zilliqa yerel işlemleri, secp256k1 üzerinde EC-Schnorr imzaları kullanır. Her imza için imzalayan, 0k<N0 \le k < N (secp256k1 eğri mertebesi) koşulunu sağlayan taze, tam genişlikte, öngörülemeyen bir geçici nonce kk örneklemek zorundadır. İmzalama akışı bir taahhüt Q=compress(kG)Q = \text{compress}(kG), bir meydan okuma r=SHA256(QpublicKeymsg)Nr = \text{SHA256}(Q \| \text{publicKey} \| \text{msg}) \bmod N ve bir yanıt s=krxNs = k - r \cdot x \bmod N üretir; burada xx özel anahtardır. Yayınlandığında (r,s)(r, s) herkese açık hale gelir.

s=krxNs = k - rx \bmod N doğrusal ilişkisi kritik noktadır. Doğru bir uygulama, her kk taze ve düzgün dağıtıldığından güvenli kalır. Nonce önyargılı veya çok küçükse her genel imza, aynı özel anahtar hakkında bilgi sızdırır.

Açık Analizi

İlgili nonce üretim yolu, Zilliqa Ledger uygulamasının "Ledger ekibi incelemesine dayalı hata düzeltmeleri" adlı commit'inde tanıtıldı. Amaçlanan akış şuydu: 40 bayt rastgelelik üret, eğri mertebesi NN modülo indirgeme yap ve sonuçta elde edilen skaleri kk olarak kullan. Geniş bir rastgele tamsayı üretip eğri mertebesiyle modülo indirgemek tek başına sorun değildir. Kusur, nonce arabelleğine kopyalama sırasında ortaya çıktı:

unsigned char nonce[size+8];
cx_rng(nonce, size+8);
cx_math_modm(nonce, size+8, (unsigned WIDE char *) PIC(domain->n), size);
os_memcpy(T->K, nonce, size);

cx_math_modm sonrasında 32 baytlık skaler sonuç, 40 baytlık arabellek içinde sağa hizalı olarak yer alır. os_memcpy(T->K, nonce, size) ilk size (32) baytı kopyalar; bu işlem baştaki 8 sıfır dolgu baytını korurken entropi taşıyan son 8 baytı atar. Üretilen nonce bu nedenle k<N2256k < N \approx 2^{256} yerine k<2192k < 2^{192} koşulunu sağlar.

Bu, her nonce'un 64 bitinin sıfıra sabitlendiği anlamına gelir. Schnorr yanıt denkleminden ki=si+rixNk_i = s_i + r_i x \bmod N, her imza sonucun 21922^{192} ile sınırlandırıldığı genel bir modüler doğrusal ilişki verir. Bu bir Gizli Sayı Problemi (HNP) örneğidir. Aynı anahtardan yaklaşık dört veya daha fazla etkilenmiş imzayla, standart kafes indirgeme algoritması özel anahtarı sıradan donanımda saniyeler içinde kurtarabilir [1].

Kusur, Temmuz 2026'da tespit edilene kadar Zilliqa Ledger uygulamasının 2019'dan itibaren yayınlanan her sürümünde mevcuttu [2].

Bu olay için Saldırı Analizi sunulmamaktadır. İstismar tamamen zincir dışında gerçekleştirildi: saldırgan, kafes indirgeme kullanarak halihazırda herkese açık zincir üstü imzalardan özel anahtarları kurtardı ve ardından standart transfer işlemlerini imzaladı. Analiz edilecek çok adımlı bir zincir üstü saldırı dizisi bulunmamaktadır.

Sonuç

Bu olay, zincir üstü bir akıllı sözleşme hatasından değil, zincir dışı bir imzalama uygulama açığından kaynaklandı. Zilliqa Ledger uygulaması, k<2192k < 2^{192} ile kısıtlanmış nonce'larla EC-Schnorr imzaları üretti; bu durum, aynı hesaptan birkaç yerel işlemin ardından özel anahtar kurtarmak için yeterli yapısal bilgi sızdırdı.

Birincil hafifletme önlemi, Ledger uygulamasını güncellemek değil anahtar emekliliğidir. Düzeltilmiş bir uygulama yeni zayıflatılmış imzaları engeller; ancak zincire kaydedilmiş ve halihazırda herkese açık olan imzaları silip yok edemez. Yeterince savunmasız imza üretmiş etkilenmiş her anahtar, ele geçirilmiş olarak değerlendirilmelidir [2].

Cüzdan ve protokol ekipleri için: nonce üretimi ve kodlamasını güvenlik açısından kritik kod olarak ele alın, uygulanabilir olduğunda iyi incelenmiş bir standardı izleyen deterministik nonce üretimini tercih edin ve etkilenen kullanıcılar için, aynı özel anahtarı zaten elinde bulundurabilen saldırganlar tarafından yapılabilecek öne geçme (front-running) girişimlerini hesaba katan koordineli bir geçiş yolu sunun.

Kaynaklar


Wanchain Cardano Köprüsü

20 Temmuz 2026'da Wanchain Cardano köprüsü, Cardano TreasuryCheck Plutus doğrulayıcısındaki enjektif olmayan bir mesaj kodlaması nedeniyle yaklaşık 515,2 milyon NIGHT (~500K dolar) kaybetti [1]. Projenin olay sonrası analizi [2] üst düzey mekanizmayı açıklamakta ancak bayt düzeyinde kodlama ayrıntıları veya kod sunmamaktadır; aşağıdaki analiz tam çarpışmayı yeniden oluşturmaktadır. Köprü düğümü imzaları geçerli ve doğru şekilde doğrulanmıştı; ancak değişken uzunluklu alanların sınırlayıcısız birleştirmesi, saldırganın iki bitişik sayısal alan arasındaki bayt sınırlarını kaydırmasına ve 3.097,56 NIGHT için verilen bir yetkiyi 203.001.692,164714 NIGHT çekimi olarak yeniden yorumlamasına olanak tanıdı.

Arka Plan

Etkilenen bileşen, Wanchain'in Cardano zincirler arası hazine sistemidir. Kaynak zincirdeki bir kullanıcı token yakar veya kilitler, köprü düğümleri ayrıştırılmış çözücü alanlarından oluşturulan bir yetkilendirme mesajını imzalar ve imzalı kanıt, hazineden varlık serbest bırakmak için Cardano TreasuryCheck Plutus doğrulayıcısına gönderilir.

Açık Analizi

TreasuryCheck doğrulayıcısı, köprü düğümü imzasını 14 ayrıştırılmış çözücü alanının ham birleştirmesi üzerinde doğrular:

hashRedeemer = sha3_256 $ mconcat
  [ toPkhPay, toPkhStk, policy, assetName
  , packInteger amount, packInteger adaAmount
  , txHash, packInteger index, packInteger mode
  , uniqueId, packInteger txType, packInteger ttl
  , packInteger outputCount, userData
  ]

packInteger tarafından kullanılan tamsayı kodlaması değişken uzunlukludur ve birleştirmede uzunluk öneki, tür ayırıcısı ya da etki alanı ayrıştırılmış türlenmiş serileştirme bulunmamaktadır. Bu nedenle farklı anlamsal demetler aynı imzalanmış bayt dizisini üretebilir.

Temsili saldırıda köprü düğümleri normal bir çekimi imzaladı:

amount    = 3097560000     -> packInteger = b8a103c0
adaAmount = 1206800        -> packInteger = 126a10
birleşik                                  = b8a103c0126a10

Saldırgan şu şekilde ayrıştırılmış bir Cardano çözücüsü gönderdi:

amount    = 203001692164714 -> packInteger = b8a103c0126a
adaAmount = 16              -> packInteger = 10
birleşik                                   = b8a103c0126a10

Bayt dizileri özdeştir. İmza kontrolü geçti ve Cardano sözleşmesi çözücüyü, yetkilendirilenin yaklaşık 65.000 katı büyüklüğünde bir çekim olarak yorumladı.

Saldırı Analizi

Aşağıdaki analiz, temsili Cardano işlemine 0a4861...2ea1 ve BSC kaynak işlemine 0xe90111...d5f26b atıfta bulunmaktadır.

  • Adım 1: Saldırgan normal görünümlü kaynak zinciri köprü talepleri oluşturdu. Temsili durumda BSC işlemi 3.110 NIGHT yaktı ve Wanchain API'si beklenen Cardano alım miktarını 3.097,56 NIGHT olarak kaydetti [3].

  • Adım 2: Köprü düğümleri ham, birleştirilmiş alanlardan oluşturulan yetkilendirme karmasını imzaladı.

  • Adım 3: Saldırgan imzalanmış bayt dizisini değiştirmeden tutarken amount ile adaAmount arasındaki Cardano çözücü sınırlarını değiştirdi.

  • Adım 4: Cardano TreasuryCheck doğrulayıcısı çözücüyü yüksek değerli bir çekim olarak ayrıştırdı ve yeniden kullanılan imzayı başarıyla doğruladı. İşlem saldırgana 203.001.692,164714 NIGHT ödedi.

  • Adım 5: Aynı örüntü birden fazla Cardano işlemine uygulandı ve toplam yaklaşık 515.206.545,426856 NIGHT (~500K dolar) boşaltıldı.

Sonuç

Hiçbir kriptografi kırılmadı. İmzalayanlar geçerli imzalar üretti ve TreasuryCheck doğrulayıcısı bunları doğru biçimde doğruladı. Kusur, mesaj yapımındaydı: hashRedeemer, 14 değişken uzunluklu alanı hiçbir sınırlayıcı veya uzunluk öneki olmaksızın tek bir bayt dizisinde birleştirdi ve bu, kodlamayı enjektif olmayan hale getirdi. Farklı alan demetleri aynı ön görüntüyü, aynı karmayı ve aynı geçerli imzayı üretiyor.

Düzeltme, imzalanmış kodlamayı enjektif yapmaktır; böylece herhangi bir bayt dizisini yalnızca bir alan demeti üretebilir. Sabit genişlikli tamsayı kodlaması veya her alana uzunluk öneki ekleme, saldırganın hareket ettirdiği sınırları sabitler. Standart bir yapılandırılmış kodlayıcı aynı sonucu elde eder ve hataya daha az açıktır. Genel kural: ham birleştirme yerine kanonik serileştirmeyi imzalayın. Bu, değişken uzunluklu argümanlarla kullanılan Solidity'nin abi.encodePacked ile aynı hata sınıfıdır; farklı girdi demetleri özdeş bayt dizileri üretebilir. Değişken uzunluklu alanlardan derlenen herhangi bir imzalı mesaja uygulanır ve özellikle mesajı oluşturan ve ayrıştıran tarafların farklı sistemlerde çalıştığı köprüler için geçerlidir.

Kaynaklar


Lien Finance

24 Temmuz 2026'da Ethereum üzerinde faaliyet gösteren merkeziyetsiz tahvil OTC protokolü Lien Finance, yaklaşık 542K dolar USDC için istismar edildi. Temel neden, tahvil takas fonksiyonundaki bir doğrulama açığıydı: fonksiyon, girdi ve çıktı grupları arasındaki paylaşılan tahvil toplam sayısının eşleşip eşleşmediğini kontrol etti; ancak hangi spesifik tahvillerin eşleştirildiğini takip etmedi. Bu durum, saldırganın tüm girdi tahvilleri için yakma adımını atlamasına ve teminatsız bir tahvil basmasına olanak tanıdı; basılan tahvil daha sonra protokolün OTC havuzları aracılığıyla gerçek USDC karşılığında satıldı.

Arka Plan

Lien Finance, ETH'ye karşı teminatlı tahviller çıkaran bir DeFi protokolüdür. BondMakerCollateralizedEth sözleşmesi, kullanıcıların tahvil token'ları basmak için ETH teminat kilitlemesine izin verir. Bir tahvilin getirisi bir fnMap ile tanımlanır; bu, teminat fiyatını vadeye kadar ödeme miktarıyla eşleştiren parçalı doğrusal bir fonksiyondur.

Anlamlı birim, tahvil grubudur. registerNewBondGroup(), bir gruptaki tüm tahvillerin getirilerinin, birleştirilmiş fnMap'lerinin her kırılma noktasında ETH fiyatını topladığını doğrular. Bu koşulu sağlayan bir grup tam bir teminat birimini yeniden oluşturur. Grupların birbirinin yerine geçebilmesinin nedeni budur: koşulu sağlayan herhangi iki grup eşit değerdedir ve exchangeEquivalentBonds() dönüştürme yolu bu garantiye dayanır.

Hem tahvil hem de grup kaydı izinsizdir: herhangi bir adres vade ve keyfi bir fnMap sağlayarak yeni bir tahvil kaydedebilir; herhangi bir adres, yalnızca getiri toplamı kontrolüne tabi olmak kaydıyla halihazırda kayıtlı tahvil kimliklerinin herhangi bir listesini grup olarak kaydedebilir.

Açık Analizi

Hatalı sözleşmeler 0xDA6F...BEf0 ve 0x8432...7de0 adresleridir.

Bir tahvil hem girdi hem de çıktı grubunda göründüğünde onu yakıp hemen yeniden basmak gereksiz işlemdir. exceptionBonds parametresi bu tahvilleri isimlendirir; böylece takas her iki adımı da atlayabilir. Fonksiyon bunu tek bir sayaçla uygular: exceptionCount, girdi grubunu tararken her eşleşmede bir artırılır ve çıktı grubunu tararken her eşleşmede bir azaltılır; hangi bondID'nin eşleştiği hiçbir zaman kaydedilmez.

Bu, exceptionBonds = [BondA, BondB] ile [BondA, BondB] girdi grubu ve [BondC, BondA, BondA] çıktı grubunun doğrulamayı geçtiği anlamına gelir. Sayaç girdi tarafında 2'ye ulaşır (BondA ve BondB için birer eşleşme) ve çıktı tarafında 0'a geri döner; ancak her iki azaltma da BondA'nın iki girişinden gelir. Takas hiçbir şey yakmaz ve yalnızca BondC'yi basar: tüketilen girdi olmaksızın oluşturulan çıktı grubu token'ları.

Saldırı Analizi

Saldırı iki işleme bölünmüştür: Adım 1, 0xe8689a...284d0f işleminde; Adım 2-5 ise 0xb96d57...48e0e7 işleminde gerçekleşmektedir.

  • Adım 1: Saldırgan aynı vadeyi paylaşan BondA, BondB ve BondC'yi tanımlamak için izinsiz registerNewBond() fonksiyonunu üç kez çağırdı. BondA ve BondB, teminatı _assertBondGroup() koşulunun gerektirdiği şekilde ETH fiyatına toplandığı 3.200 dolar altında eşit olarak bölen kaldıraçlı alım çağrılarıdır. BondC, 1.870 dolarlık spot fiyata karşı 3.200 dolar kullanım fiyatıyla derin bir kârsız LBT'dir: sıfır içsel değer; ancak bir opsiyon olarak IMT başına yaklaşık 7,64 dolarlık zaman değeri.

  • Adım 2: Saldırgan iki tahvil grubu kaydetti. registerNewBondGroup([BondA, BondB]), Grup 36'yı (girdi) döndürdü; registerNewBondGroup([BondC, BondA, BondA]) ise Grup 37'yi (çıktı) döndürdü. BondC'nin düz sıfır getirisi kırılma noktası toplamlarına hiçbir şey eklemez; bu nedenle varlığı denklik koşulunu bozmaz. BondA, Grup 37'de iki kez listelenmiştir ve bu durum takasın doğrulamayı geçmesini sağlar.

  • Adım 3: Saldırgan exchangeEquivalentBonds(inputGroup = 36, outputGroup = 37, amount = 6968565865905, exceptionBonds = [BondA, BondB]) fonksiyonunu çağırdı. Girdi grubu taranırken: BondA ve BondB her biri bir istisnayla eşleşir, dolayısıyla ikisi de yakma işlemini atlar ve exceptionCount 2'ye ulaşır. Çıktı grubu taranırken: BondC hiçbir şeyle eşleşmez ve basılır, ardından BondA'nın iki girişi her biri bir istisnayla eşleşerek sayacı 0'a düşürür.

  • Adım 4: Saldırgan basılan BondC'yi protokolün OTC havuzları (GeneralizedDotc) aracılığıyla USDC karşılığında sattı ve 532.144 USDC elde etti.

  • Adım 5: Saldırgan aynı örüntüyü ikinci BondMakerCollateralizedEth sözleşmesine karşı tekrarladı ve toplam kârı yaklaşık 542.144,63 USDC'ye çıkardı.

Sonuç

Paylaşılan tahvillerin yeniden basımını atlamak amacıyla tasarlanan exceptionBonds mekanizması, çıktı grubunda bir bondID'yi çoğaltarak tüm girdi tahvillerine aynı anda uygulanabiliyordu. Bu durum, tahvil token'larının yalnızca issueNewBonds() aracılığıyla yatırılan teminat karşılığında oluşturulduğu değişmezi bozdu. Düzeltme, her istisnanın her iki grupta da tam olarak bir kez tüketilmesini sağlamak için hangi spesifik bondID'lerin eşleştirildiğini takip etmektir (örn. bit haritası veya küme kullanarak).

Phalcon Security ile Başlayın

Her tehdidi tespit edin, önemli olanlar için uyarı alın ve saldırıları engelleyin.

Şimdi ücretsiz deneyin

BlockSec Hakkında

BlockSec, tam yığın blockchain güvenliği ve kripto uyumluluk sağlayıcısıdır. Müşterilerin kod denetimi (akıllı sözleşmeler, blockchain ve cüzdanlar dahil) gerçekleştirmesine, saldırıları gerçek zamanlı olarak engellemesine, olayları analiz etmesine, yasadışı fonları takip etmesine ve protokoller ile platformların tam yaşam döngüsü boyunca AML/CFT yükümlülüklerini karşılamasına yardımcı olan ürün ve hizmetler geliştiriyoruz.

BlockSec, prestijli konferanslarda birden fazla blockchain güvenlik makalesi yayımladı, çeşitli DeFi uygulamalarındaki sıfır gün saldırılarını raporladı, 20 milyon doların üzerinde kurtarmak amacıyla birden fazla saldırıyı engelledi ve milyarlarca dolarlık kripto para birimini güvence altına aldı.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit