📜 ⬆️ ⬇️

Cryptocurrency Ethereum: write an exploit under a vulnerable smart contract and get tokens

How many copies are already broken in talking about cryptocurrency? Banks and government agencies argue about its legal status, and private organizations are coming up with different ways to use the blockchain . We thought about the safety of this technology and related products.

Using the NeoQUEST-2017 job as an example, we deal with smart contracts Ethereum - the second most popular cryptocurrency after Bitcoin. Competitors had to write an exploit to a vulnerable contract. How to make it - we read under a cat!

What is Ethereum?


Ethereum is a cryptocurrency , that is, a distributed database that stores information about how much money there is. Users can interact with the database with the help of transactions - commands that are checked for correctness and are collected in blocks by special users - miners.

When a transaction enters the block, it is considered confirmed, and its effect takes effect. A chain of transaction blocks (blockchain) ensures data integrity and synchronization of database status between all users.
')
A feature of Ethereum is the support of smart contracts. A smart contract is a code that is entered into the blockchain and manages a separate cryptocurrency account. It can receive and send money, process information and store important data. Account management is carried out without human intervention according to a predetermined, unchanged algorithm.

The code of smart contracts can be written in different languages, but the most common is Solidity ( here you can read a curious habrastya about the implementation of a smart contract for Solidity). This language is similar to JavaScript with the addition of special constructions for working with the blockchain. An important difference from JS is that the code is compiled into byte code for the Ethereum virtual machine (EVM). Publication of bytecode is carried out using a special transaction.

After entering the blockchain, a smart contract receives an address that can be used to send and receive cryptocurrency. Users can invoke various contract methods using transactions. Such transactions are processed using a contract code.



One application of smart contracts is the creation of decentralized autonomous organizations (DAO). Such organizations usually sell tokens, exchanging cryptocurrency for them. Owners of tokens can manage the organization, for example, make decisions about issuing new tokens or sponsoring an enterprise with the money accumulated in a smart contract. In the DAO contract, a fairly large amount of cryptocurrency can be collected, which makes it a desirable target for hackers, because an error in the code of a smart contract may allow it to seize its wealth.

Initial data to the task


According to legend, the participants need to get the engine and fuel for the spacecraft, and they can do this through the StarDAO website. Information on the site indicates that the company uses a smart contract based on the cryptocurrency Ethereum to trade in space equipment.

Smart contract code:
contract StarDAO { address owner; function StarDAO() payable { owner = msg.sender; } function GetOwner() returns (address) { return owner; } modifier onlyOwner { if (msg.sender != owner) throw; _; } function TransferOwnership(address newOwner) onlyOwner { owner = newOwner; } mapping (address => uint) starTokens; uint starTokensTotalSupply = 0; function BuyTokens() payable { starTokensTotalSupply += msg.value; starTokens[msg.sender] += msg.value; } function SellTokens(uint amount) { if (starTokens[msg.sender] >= amount) { if (msg.sender.call.value(amount)() == false) throw; starTokensTotalSupply -= amount; starTokens[msg.sender] -= amount; } } function GetBalance(address addr) constant returns (uint) { return starTokens[addr]; } function GetTotalSupply() constant returns (uint) { return starTokensTotalSupply; } function SendTokens(address addr, uint amount) { if (starTokens[msg.sender] >= amount) { starTokens[msg.sender] -= amount; starTokens[addr] += amount; } } mapping (uint => bool) bonusCodes; function AddBonusCode(uint code) onlyOwner { bonusCodes[code] = true; } function HashReverse(bytes s) constant returns (uint8[32]) { uint8[32] res; bytes32 z = sha3(s); for (uint8 i = 0; i < 32; i++) res[31-i] = uint8(z[i]); return res; } function CalcCodeHash(bytes code) constant returns (uint) { var tmp = HashReverse(code); uint codeHash = 0; for (uint8 i = 0; i < 32; i++) codeHash = codeHash * 256 + tmp[i]; return codeHash; } mapping (address => bool) fuelAccess; function BuyFuel() { uint fuelPrice = 1000000000000000000000000; if (starTokens[msg.sender] >= fuelPrice) { starTokens[msg.sender] -= fuelPrice; starTokensTotalSupply -= fuelPrice; fuelAccess[msg.sender] = true; } } function HasFuel(address addr) constant returns (bool) { return fuelAccess[addr]; } mapping (address => bool) driveAccess; function GetDrive(bytes code) { uint codeHash = CalcCodeHash(code); if (bonusCodes[codeHash]) { bonusCodes[codeHash] = false; driveAccess[msg.sender] = true; } } function HasDrive(address addr) constant returns (bool) { return driveAccess[addr]; } } 



Registering on the site gives you access to your personal account, where you can carry out basic operations with cryptocurrency: make transactions, create smart contracts and interact with existing contracts. In addition, a small amount of cryptocurrency is issued at registration: 1 ETH, or (which is the same) 10 18 WEI, the smallest ETH particles.



There is also a tab "Get the goods", but when you try to find out the issuing code, an error occurs. It looks like the web server checks the availability of the engine and fuel for the selected account using a smart contract (for good reason, it has HasFuel () and HasDrive () functions!). An issuance code is provided only if the result of the check is successful.

Part 1: we get fuel


 function HasFuel(address addr) constant returns (bool) { return fuelAccess[addr]; } 


The HasFuel () function checks the array fuelAccess and returns true only if the value true is entered into an array cell corresponding to the passed address. Therefore, you need to achieve the condition fuelAccess [our_address] == true.

 function BuyFuel() { uint fuelPrice = 1000000000000000000000000; if (starTokens[msg.sender] >= fuelPrice) { starTokens[msg.sender] -= fuelPrice; starTokensTotalSupply -= fuelPrice; fuelAccess[msg.sender] = true; } } 


The BuyFuel () function sets the fuel cost to 10 24 WEI, that is, 1 million ETH. Then there is a check that the user who called the function has a sufficient number of tokens. If successful, the tokens are written off, and information on the purchase of fuel is entered into the fuelAccess array. Therefore, all you need to get the key is to get enough funds to buy.

 function SellTokens(uint amount) { if (starTokens[msg.sender] >= amount) { if (msg.sender.call.value(amount)() == false) throw; starTokensTotalSupply -= amount; starTokens[msg.sender] -= amount; } } 


The exchange of cryptocurrencies for tokens, the transfer of tokens from one account to another and the reverse exchange of tokens for cryptocurrency are performed by the functions BuyTokens () , SendTokens () and SellTokens () . The latter is of particular interest. This function accepts as input the number of tokens that the user wants to exchange for a cryptocurrency, and checks if the user has enough tokens.

If the test is successful, the function attempts to send the requested WEI number to the user. If the user for some reason does not accept the money sent, the throw statement is called, which terminates the execution of the contract and rolls back all changes. If the money comes, then the number of derived tokens is subtracted from the number of user tokens.

At first glance, the algorithm of the function is correct. This is true, but ... Not in one special case! The thing is that this function can be called not by an ordinary user, but by another smart contract. This contract can be implemented the so-called fallback function, which is called whenever the contract accepts money. Therefore, an arbitrary code can be executed between checking the condition and writing off the tokens.

Of course, the code will be executed in the context of the contract receiving the payment and will not be able to directly affect the StarDAO contract. But nothing prevents you from calling out some function of the StarDAO contract and breaking the atomicity of SellTokens () .

 bool attack = true; function () payable { if (attack) { attack = false; dao.SellTokens(1); } } 


How can this be used? Suppose there is a contract that now has exactly 1 token in StarDAO. Suppose there is a logical variable in the contract: attack = true and fallback, which checks the attack and, if successful, resets it to false and sells 1 token. Let's see what happens if the contract tries to sell one of its tokens.

And something interesting is happening! The contract calls SellTokens (1) . StarDAO checks that starTokens [our_address] ≥ 1 - the condition is met, because the contract has just 1 token. StarDAO sends the contract to 1 WEI, causing the fallback function to be called. It clears the attack flag and calls SellTokens (1) again.

StarDAO again checks that starTokens [our_address] ≥ 1 - the condition is still met, because 1 token has not yet been written off. Therefore, StarDAO once again sends 1 WEI to the contract (and the fallback function does nothing, since attack = false now). After that, StarDAO subtracts a unit from each contract tokens for each WEI sent.

After the first subtraction, the number of tokens becomes zero, and after the second, due to overflow of the integer variable, the contract becomes the happy owner of (2 256 -1) tokens. It is enough for fuel (and with it the first key)!

The full contract code for the attack is shown below. It shows the features of interaction with the contract StarDAO and contains all the steps of the attack.

Exploit code:
 contract StarDAO { function BuyTokens() payable; function SellTokens(uint amount); function GetBalance(address addr) constant returns (uint); function SendTokens(address addr, uint amount); } contract Bad { address owner; bool attack; StarDAO dao = StarDAO(0x91d6561b996fba322b1a5ecfdf462b4ee0b130d7); function Bad() payable { owner = msg.sender; } function launchAttack() { attack = true; dao.BuyTokens.value(1)(); dao.SellTokens(1); dao.SendTokens(owner, dao.GetBalance(this)); } function () payable { if (attack) { attack = false; dao.SellTokens(1); } } } 



Part 2: get the engine


  function GetDrive(bytes code) { uint codeHash = CalcCodeHash(code); if (bonusCodes[codeHash]) { bonusCodes[codeHash] = false; driveAccess[msg.sender] = true; } } 


The GetDrive () function is responsible for getting the engine. It takes some code as input, calculates its modified keccak-256 hash, and checks for the presence of this hash in the bonusCodes array. If there is a hash, it is deleted, and the user gets the engine.

 address owner; modifier onlyOwner { if (msg.sender != owner) throw; _; } function AddBonusCode(uint code) onlyOwner { bonusCodes[code] = true; } 


You can add a hash to the bonusCodes array using the AddBonusCode () function. The catch is that the onlyOwner modifier only allows the owner of an account whose address is stored in the variable owner. So, to add code and then use it, you need to become the owner of the contract.

 function StarDAO() payable { owner = msg.sender; } function TransferOwnership(address newOwner) onlyOwner { owner = newOwner; } 


The variable owner is changed in the contract in just two places.

First, it is set in the StarDAO contract constructor (). The constructor is called only once - when creating a contract. So, using it to change the owner will not work.

Secondly, there is the TransferOwnership () function, but it has the onlyOwner modifier. It turns out that it is also not suitable for changing the owner.

At this moment it seems that it is impossible to change the owner. But it is not so! Although Solidity is similar to JavaScript, it compiles, and the resulting bytecode does not work with variables, but with addresses in memory. So you can try to determine where the owner value is stored, and rewrite it right there, at a low level.

To do this, you need to figure out how memory works in the Ethereum virtual machine. When executing a contract, two types of memory are used - memory and storage (in fact, there are code, stack and calldata, but these are nuances). Memory is a temporary memory whose contents are not recorded on the blockchain and are not saved between contract calls. Storage, on the contrary, is in the blockchain and stores the values ​​of the contract constant variables.

Each time a variable is declared in Solidity without explicitly stating in which memory it is stored, the type of memory is automatically assigned to it. For example, all global variables and all arrays are stored by default in storage. Storage is an address space with an address length of 2,256 bits, divided into cells of 32 bytes. All static variables (integers, arrays of constant size, etc.) are stored in storage sequentially, starting at address 0x0. Variables that resize dynamically are stored in a more complex way, their address is calculated using the keccak-256 hash.

 function HashReverse(bytes s) constant returns (uint8[32]) { uint8[32] res; bytes32 z = sha3(s); for (uint8 i = 0; i < 32; i++) res[31-i] = uint8(z[i]); return res; } 


How does this help to change the contract owner? Looking at the HashReverse () function, you can see that it uses the res array. When it is declared, the type of memory is not specified, which means that it is stored in storage. In addition, this array is only declared, but not initialized.

Since the Ethereum virtual machine by default initializes all data with zeros, the array will be assigned the address 0x0, and it will play the role of a peculiar NULL POINTER. So, when writing to the array, the first 32 bytes of storage will be changed, in which the first declared variable in the contract is stored - owner.

And since no version of Solidity supports the constant modifier (this word is reserved for the future to protect the function from making any changes to the blockchain), it becomes possible to overwrite the address of the owner of the contract.

It is not possible to write your address to the owner variable, simply passing it as a parameter to the HashReverse () function, since the argument is hashed beforehand. Fortunately, the address of Ethereum is nothing but a hash of the public key of the account, which can be viewed in the user's personal account.



In the above example, the public key - 0xe969598d9dcacebd89d0ca96f0a66c6908f9c3ff4f6652ac2d110fc49ae8f7d18313f2ecbbb612778d815c22cb858438a504e76c70de013c26c4c86e72dc07a4. Hence, in order to become the owner of the contract, you should call (method transact) HashReverse () with the argument [0xe9, 0x69, 0x59, 0x8d, 0x9d, 0xca, 0xce, 0xbd, 0x89, 0xd0, 0xca, 0x96, 0xf0, 0xa6, 0x6c, 0x69 , 0x08, 0xf9, 0xc3, 0xff, 0x4f, 0x66, 0x52, 0xac, 0x2d, 0x11, 0x0f, 0xc4, 0x9a, 0xe8, 0xf7, 0xd1, 0x83, 0x13, 0xf2, 0xec, 0xbb, 0xb6, 0x12, 0x77, 0x8d , 0x81, 0x5c, 0x22, 0xcb, 0x85, 0x84, 0x38, 0xa5, 0x04, 0xe7, 0x6c, 0x70, 0xde, 0x01, 0x3c, 0x26, 0xc4, 0xc8, 0x6e, 0x72, 0xdc, 0x07, 0xa4]. By calling the GetOwner () function after this, you can make sure that the address of the account and the owner of the contract are the same.



The next step is to add a code to get the engine. For this you need:
  1. Take some set of bytes (for example, [0x01, 0x02, 0x03]).
  2. Calculate from him the hash keccak-256 (for this example, we get 0xf1885eda54b7a053318cd41e2093220dab15d65381b1157a3633a83bfd5c9239).
  3. Turn it over (0x39925cfd3ba833367a15b18153d615ab0d2293201ed48c3153a0b754da5e88f1).
  4. To present in the form of a decimal number (26040433828516858466028575311317889779993153936426418137092284197924182591729).
  5. Call AddBonusKey () by passing this number as a parameter.

For conversions you can, for example, use the pysha3 library for Python.



Finally, the added code needs to be used. To do this, you can call GetDrive () , transferring the bytes from which the code was created. Voila, the spacecraft engine (and second key) are received!

Finally


Nowadays, the blockchain begins to be used in real life. For example, it is used for decentralized file storage , electronic voting , and some banks issue special cryptocurrency cards .

The blockchain guarantees the integrity of the data entered into it, but does not protect against errors in the underlying applications. A good example is the attack on The DAO , during which the attacker used an error in an organization’s smart contract to steal a huge amount of cryptocurrency (equivalent to $ 60 million). This attack led to the death of The DAO and shook the stability of Ethereum. Fortunately, the money was returned to their rightful owners. Nevertheless, the incident stressed the importance of ensuring information security in the application of blockchain technology.

We demonstrated the security problems associated with the use of cryptocurrency in one of the NeoQUEST tasks, and we very much hope that our participants have learned a lot in the process of passing the task and from this write-up! By the way, while the site with tasks is still available, those who have not completed the task can finally "finish off" it!

Source: https://habr.com/ru/post/324456/


All Articles