Pada Mei 2021, kami menyaksikan beberapa peretasan yang menargetkan produk BSC DeFi. Secara khusus, celah yang terkait dengan pencetakan hadiah di agregator hasil, PancakeBunny, dieksploitasi untuk mencetak ~7 juta token BUNNY dari nol, yang menyebabkan kerugian finansial sebesar $45 juta. Setelah peretasan berdarah, tiga proyek bercabang — AutoShark, Merlin Labs, dan PancakeHunny — diserang dengan teknik serupa. Tim Keamanan Blockchain Grup Amber, yang dipimpin oleh Dr. Chiachih Wu, menguraikan celah dan memberikan akun langkah demi langkah eksploitasi dengan mereproduksi serangan terhadap PancakeBunny.
Permukaan Serangan Tersembunyi: balanceOf()
Banyak orang percaya bahwa komposisi sangat penting untuk keberhasilan DeFi. Kontrak Token (misalnya, ERC20) memainkan peran penting di lapisan bawah lego DeFi. Namun, pengembang mungkin mengabaikan beberapa kondisi yang tidak dapat dikendalikan dan tidak dapat diprediksi saat mengintegrasikan ERC20 ke dalam proyek DeFi mereka. Misalnya, Anda tidak dapat memprediksi kapan dan berapa banyak token yang akan Anda terima saat Anda mengambil saldo token saat ini. Ketidakpastian ini menciptakan permukaan serangan yang tersembunyi.
Dalam banyak kasus, kontrak pintar merujuk pada keseimbangan ERC20 dalam logika bisnis mereka. Misalnya, ketika pengguna menyetor beberapa token XYZ ke dalam kontrak pintar, XYZ.balanceOf() dipanggil untuk memeriksa berapa banyak uang yang diterima. Jika Anda terbiasa dengan basis kode Uniswap, Anda mungkin tahu bahwa kontrak UniswapV2Pair memiliki banyak panggilan balanceOf().
Dalam potongan kode, UniswapV2Pair.mint() menggunakan saldo saat ini (saldo0, saldo1) dan data pembukuan (jumlah0, jumlah1) untuk mendapatkan jumlah yang disetorkan oleh pengguna (jumlah0, jumlah1 ). Namun, jika aktor jahat mentransfer beberapa aset (token1 atau token2) tepat sebelum pemanggilan mint(), korban akan memberikan lebih banyak likuiditas daripada yang diharapkan, yaitu, lebih banyak token LP yang dicetak. Jika hadiah dihitung berdasarkan jumlah token LP, pelaku kejahatan dapat memperoleh keuntungan saat hadiah melebihi pengeluaran.
UniswapV2Pair.burn() memiliki risiko yang sama. Pemanggil fungsi mint() mungkin membahayakan dirinya sendiri tanpa pemahaman menyeluruh tentang risiko yang terlibat. Inilah yang terjadi dalam kasus PancakeBunny.
Pada cuplikan kode di atas, baris 140 mengambil saldo token LP melalui balanceOf() dan menyimpannya ke dalam likuiditas. Pada baris 144–145, bagian dari total token LP yang dimiliki oleh UniswapV2Pair (yaitu, likuiditas dari _totalSupply) digunakan untuk menurunkan (jumlah0, jumlah1) dengan saldo saat ini (saldo0, saldo1) dari dua aset (yaitu, token0 dan tanda1). Kemudian, (jumlah0, jumlah1) dari dua aset ditransfer ke alamat di baris 148-149.
Di sini, aktor jahat dapat memanipulasi (saldo0, saldo1) dan likuiditas dengan mengirimkan beberapa token0+ token1 atau token LP ke dalam kontrak UniswapV2Pair tepat sebelum fungsi mint() dipanggil untuk membuat pemanggil mengeluarkan lebih banyak token0+token1. Kami akan memandu Anda melalui kode sumber PancakeBunny dan menunjukkan kepada Anda bagaimana aktor jahat dapat mengambil untung dari melakukan hal ini.
Analisis Celah: BunnyMinterV2
Dalam kode sumber PancakeBunny, fungsi BunnyMinterV2.mintForV2() bertugas mencetak token BUNNY sebagai hadiah. Secara khusus, jumlah yang akan dicetak (yaitu, mintBunny) berasal dari parameter input, _withdrawalFees, dan _performanceFee. Perhitungan terkait dengan tiga fungsi: _zapAssetsToBunnyBNB() (baris 213), priceCalculator.valueOfAsset() (baris 219) dan jumlahBunnyToMint() (baris 221). Karena aktor jahat dapat mencetak BUNNY dalam jumlah besar, masalahnya terletak pada salah satu dari tiga fungsi yang disebutkan di atas.
Mari kita mulai dari fungsi _zapAssetsToBunnyBNB(). Ketika aset yang diteruskan adalah Cake-LP (baris 267), sejumlah token LP digunakan untuk menghilangkan likuiditas dan mengambil (amountToken0, amountToken1) dari (token0, token1) dari kumpulan likuiditas (baris 278). Dengan bantuan kontrak zapBSC, aset tersebut ditukar dengan token LP BUNNY-BNB (baris 287–288). Jumlah token LP BUNNY-BNB yang sesuai kemudian dikembalikan ke pemanggil (baris 298). Di sini, kita punya masalah. Apakah jumlahnya sesuai dengan jumlah token LP yang Anda asumsikan akan dibakar?
Dalam implementasi PancakeV2Router.removeLiquidity(), likuiditas token LP (jumlah dalam zapAssetsToBunnyBNB()) akan dikirim ke kontrak PancakePair (baris 500) dan PancakePair.burn() akan dipanggil. Jika saldo token LP PancakePair saat ini lebih besar dari 0, jumlah sebenarnya yang akan dibakar akan lebih besar dari jumlah, yang secara tidak langsung meningkatkan jumlah BUNNY yang akan dicetak.
Masalah lain di _zapAssetsToBunnyBNB( ) adalah panggilan zapBSC.zapInToken(). Logika di balik ini adalah untuk menukar dua aset yang dikumpulkan oleh removeLiquidity() menjadi token BUNNY-BNB LP. Karena zapBSC menukar aset melalui PancakeSwap, pelaku kejahatan dapat menggunakan pinjaman kilat untuk memanipulasi jumlah BUNNY-BNB yang ditukar.
Kembali ke BunnyMinterV2.mintForV2(), bunnyBNBAmount yang dikembalikan oleh zapAssetsToBunnyBNB() akan diteruskan ke priceCalculator.valueOfAsset() untuk mengutip nilai berdasarkan BNB (yaitu, vauleInBNB), mirip dengan mekanisme oracle.
Namun, priceCalculator.valueOfAsset() mereferensikan jumlah BNB dan BUNNY (reserve0, reserve1) di BUNNY_BNB PancakePair sebagai umpan harga, yang memungkinkan pelaku jahat untuk menggunakan pinjaman kilat untuk memanipulasi jumlah token BUNNY yang dicetak.
Saya ountBunnyToMint() fungsi adalah perhitungan matematika sederhana. Kontribusi input dikalikan lima (bunnyPerProfitBNB=5e18), yang dengan sendirinya tidak memiliki permukaan serangan, tetapi amplifikasi memperbesar manipulasi yang disebutkan di atas.
Bersiaplah untuk Tempur
Karena serangan itu dipicu oleh getReward(), kita harus memenuhi syarat untuk mendapatkan hadiah terlebih dahulu.
Sebagai ditunjukkan pada tangkapan layar Etherscan di atas, peretas PancakeBunny memanggil fungsi init() dari kontrak eksploit untuk menukar 1 WBNB ke token WBNB-USDT-LP dan menyetorkannya ke dalam kontrak VaultFlipToFlip, sehingga ia akan mendapatkan beberapa hadiah dengan meminta getReward().
Seperti yang ditunjukkan di atas, menggunakan fungsi Exp.prepare()kami mereproduksi panggilan vaultFlipToFlip.deposit() (baris 62). Kami juga menggunakan kontrak ZapBSC untuk menyederhanakan perolehan token LP (baris 54-57). Namun, seseorang tidak bisa mendapatkan hadiah sampai penjaga PancakeBunny memicu panggilan harvest() berikutnya. Karena alasan ini, peretas PancakeBunny tidak memicu serangan hingga panen pertama() transaksi mengikuti init() transaksi.
Dalam simulasi kami, tidak ada penjaga yang dapat memicu panen(). Oleh karena itu, kami memanfaatkan fitur eth-brownie untuk menyamar sebagai penjaga dan secara manual memulai transaksi harvest() (baris 25).
Pinjaman Flash Rekursif
Untuk memanfaatkan dana, PancakeBunny exploiter menggunakan delapan kumpulan dana yang berbeda termasuk tujuh kontrak PancakePair dan ForTube Bank. Di sini, tim Keamanan Blockchain Amber Group hanya menggunakan tujuh fitur flash-swap kontrak PancakePair berikut untuk meminjamkan 2,3 juta WBNB:
address[7] pairs=[ address(0x897c2de73dd55d77), alamat(0x897c2de73dd55d77 , alamat (0x897c2de73dd55d77), alamat (0x897c2de73dd55d77), alamat (0x897c2de73dd55d77)
Untuk menyederhanakan panggilan flash-swap, kami mengemas dua parameter ke dalam argumen input keempat dari PancakePair.swap() panggilan (baris 72 atau baris 74): level dan aset. Variabel level menunjukkan level panggilan swap() yang kita gunakan; variabel aset adalah 0 atau 1, artinya kita perlu meminjam token0 atau token1.
Menggunakan fungsi callback pancakeCall(), kita memanggil PancakePair.swap() secara rekursif dengan level+1 hingga mencapai level ketujuh. Di tingkat atas, kita memanggil shellcode() untuk melakukan tindakan nyata di baris 98. Ketika shellcode() kembali, variabel aset mengembalikan aset yang dipinjam di setiap level yang sesuai (baris 102-104).
Tarik Pemicunya
Fungsi shellcode() dipanggil oleh pancakeCall tingkat ketujuh( ) adalah kode eksploit yang sebenarnya. Pertama, kami menyimpan saldo WBNB saat ini di wbnbAmount (baris 108), menukar 15.000 WBNB menjadi token WBNB-USDT-LP (baris 112), dan mengirimkannya ke kontrak yang mencetak token LP tersebut (yaitu kontrak PancakePair) di baris 113. Langkah ini bertujuan untuk memanipulasi panggilan removeLiquidity() di dalam fungsi _zapAssetsToBunnyBNB() seperti yang dianalisis di atas, memungkinkan kita untuk menerima lebih banyak WBNB+USDT dari yang diharapkan.
Langkah kedua adalah memanipulasi harga USDT yang dirujuk oleh _zapAssetsToBunnyBNB() untuk menukar USDT ke WBNB. Karena _zapAssetsToBunnyBNB() menggunakan WBNB-USDT PancakePair untuk menukar USDT ke WBNB, kita dapat menukar sisa flash yang dipinjamkan WBNB ke USDT di PancakeSwap. Melakukan hal itu akan membuat WBNB menjadi sangat murah, dan _zapAssetsToBunnyBNB() akan menerima jumlah WBNB yang tidak proporsional ketika ditukar dari USDT. Perhatikan bahwa manipulasi harga di sini terjadi pada kumpulan Pancake V1, bukan PancakePair Pancake V2 seperti pada langkah sebelumnya.
Langkah terakhir adalah panggilan getReward(). Panggilan kontrak sederhana dapat menghasilkan 6,9 juta token BUNNY (baris 125). Token BUNNY kemudian dapat ditukar dengan WBNB di PancakeSwap untuk membayar kembali pinjaman flash.
Dalam simulasi kami, aktor jahat membayar 1 WBNB dan pergi dengan 104rb WBNB + 3,8 juta USDT (setara dengan ~$45 juta).
Tentang Amber Group
Amber Group adalah penyedia layanan keuangan kripto global terkemuka yang beroperasi di seluruh dunia dan sepanjang waktu dengan kehadiran di Hong Kong, Taipei, Seoul, dan Vancouver. Didirikan pada tahun 2017, Amber Group melayani lebih dari 500 klien institusional dan telah secara kumulatif memperdagangkan lebih dari $500 miliar di 100+ bursa elektronik, dengan lebih dari $1,5 miliar aset yang dikelola. Pada tahun 2021, Amber Group mengumpulkan $100 juta dalam pendanaan Seri B dan menjadi unicorn FinTech terbaru senilai lebih dari $1 miliar. Untuk informasi lebih lanjut, silakan kunjungi: www.ambergroup.io.