Skip to content

marigold-dev/training-security-2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

title authors last_update
Part 2: Leaks
Benjamin Fuentes
date
11 January 2024

Note : clone this project for compiling and testing this tutorial

Replay attack

A replay attack on a smart contract is a type of security vulnerability that allows an attacker to reuse a signed valid transaction multiple times. We saw in the previous chapter how to do offchain replay attacks, but it is possible to do also onchain replay attacks. Besides, Tezos prevents this kind of vulnerability, it is possible to write a code that does this attack sending the same operation several times for execution.

Compile and simulate the replay attack :

taq init
taq compile 1-replay.jsligo
taq simulate 1-replay.tz --param=1-replay.parameter.parameter.tz

The simulation will tell you that several internal transactions will be executed. But if you deploy the code and try to execute it :

taq deploy 1-replay.tz --mutez 1000 -e testing

taq transfer KT1VMt7t4CboRP6jYBUdBQowHb4NR1UtmDrz -e testing

Then, the Tezos will detect the flaw

  "message": "(transaction) proto.017-PtNairob.internal_operation_replay"

Memory overflow

Memory overflow is a kind of attack that fulfills the memory of a smart contract resulting in making this contract unusable. Even simply loading the data into memory and deserializing it at the beginning of the call, could use so much gas that any call to the contract would fail. All the funds would be forever locked into the contract.

Here is the list of dangerous types to use carefully :

  • Integers and nats: as they can be increased to an arbitrarily large value
  • Strings: as there is no limit on their lengths
  • Lists, sets, maps: that can contain an arbitrary number of items

SOLUTION:

  • Ask the user to pay a minimum tez for each call
  • Set a threshold limit
  • Store data on a big_map
  • Avoid unnecessary onchain computation that can be done offchain. Ex: do not loop onchain and just update a part of a map

Example with the current FA1.2 implementation: https://inference.ag/blog/2023-10-09-FA12_spenders/

You can have a look at the LIGO implementation of fa1.2 on the Ligo registry here

The code follows the standard but you can see that the Allowance type is a map. It would have been better to change the Standard and use a big_map instead of a map. If you implement the Standard differently, then your smart contract storage definition and entrypoint signatures will not match anymore and will not be supported by other platforms

Re-entrancy

These attacks allow an attacker to repeatedly call a contract function in a way that drains the contract’s resources, leading to a denial of service (DoS) attack

One of the most well-known examples of a re-entrancy attack occurred in 2016 when an attacker exploited a vulnerability in the DAO (Decentralized Autonomous Organization) contract on the Ethereum blockchain. But this popular hack is still actively used :

  • Uniswap/Lendf.Me hacks (April 2020) – $25 mln, attacked by a hacker using a re-entrancy.
  • The BurgerSwap hack (May 2021) – $7.2 mln, because of a fake token contract and a re-entrancy exploit.
  • The SURGEBNB hack (August 2021) – $4 mln, seems to be a re-entrancy-based price manipulation attack.
  • CREAM FINANCE hack (August 2021) – $18.8 mln, re-entrancy vulnerability allowed the exploiter for the second borrow.
  • Siren protocol hack (September 2021) – $3.5 mln, AMM pools were exploited through re-entrancy attack.

This kind of attack is quite simple to put in place with Solidity because of the way it works.

Consider this scenario :

sequenceDiagram
  User->>MaliciousContract: deposit funds
  MaliciousContract->>LedgerContract: deposit funds
  User->>MaliciousContract: call withdraw
  MaliciousContract->>LedgerContract: call withdraw
  Note right of LedgerContract: checkBalance
  Note right of LedgerContract: sendFunds
  LedgerContract->>MaliciousContract: sendFunds operation
  Note right of MaliciousContract: loop calling withdraw ... x times
  MaliciousContract->>LedgerContract: call withdraw
  LedgerContract->>MaliciousContract: sendFunds operation  ... x times
  Note right of LedgerContract: ... Once finish ... UpdateBalance
Loading

Why this scenario is not possible on Solidity? On Solidity, the operation will call directly the smart contract like doing a stop, calling a synchronous execution and continuing the flow.

Why this scenario is not possible on Tezos? On Tezos, the first transaction will update the state and will execute a list of operations at the end of execution. Next executions will encounter an updated state

Let's implement a more complex scenario, now :

sequenceDiagram
  User->>MaliciousContract: deposit cookies
  MaliciousContract->>LedgerContract: deposit cookies
  User->>MaliciousContract: sell cookies
  MaliciousContract->>OfferContract: sell cookies
  Note right of OfferContract: checkBalance
  OfferContract->>LedgerContract: call hasCookies view
  Note right of OfferContract: prepare post operations (sendFund + changeOwner)
  OfferContract->>MaliciousContract: sendFund
  Note right of MaliciousContract: while receiving fund on default entrypoint will loop on selling cookies
  MaliciousContract->>OfferContract: sell cookies
  Note right of OfferContract: checkBalance
  OfferContract->>LedgerContract: call hasCookies view
  Note right of OfferContract: prepare post operations (sendFund + changeOwner)
  OfferContract->>MaliciousContract: sendFund
  MaliciousContract->>OfferContract: sell cookies
  Note right of OfferContract: checkBalance
  OfferContract->>LedgerContract: call hasCookies view
  Note right of OfferContract: prepare post operations (sendFund + changeOwner)
  OfferContract->>MaliciousContract: sendFund
  OfferContract->>LedgerContract: call changeOwner
Loading

The issue here is clearly that we send money without updating the state first

SOLUTION :

  • Mutex safeguard: The goal is to avoid multiple internal operations being generated. A boolean isRunning will lock only one operation for the full transaction flow.

    1. Check the isRunning is false
    2. Set isRunning to true
    3. Do logic code ...
    4. Create a last operation transaction to reset the boolean to false
  • Check-and-send pattern: Principle of separating state changes from external contract interactions. First, update the contract’s state, then interact with other contracts

Compile/Run the hack test first

taq test 3-reentrancyTest.jsligo

The logs seem to be fine, but it is hard to guess the internal transactions and to separate the fees from the hack on the attacker's balance

┌─────────────────────────┬─────────────────────────────────────────────┐
│ Contract                │ Test Results                                │
├─────────────────────────┼─────────────────────────────────────────────┤
│ 3-reentrancyTest.jsligo │ "ledgerContract"                            │
│                         │ KT1LQyTHEZeaecRj7hWgkzPEBD6vMEKXYzoo(None)  │
│                         │ "offerContract"                             │
│                         │ KT1M4nPCej4va4Q2iMPX2FKt8xLw5cfGjBv9(None)  │
│                         │ "maliciousContract"                         │
│                         │ KT1B7RgF6j7UpAybpdfxhLCp7hf41pNFcxyS(None)  │
│                         │ "admin initialize cookies to malicious KT1" │
│                         │ Success (1299n)                             │
│                         │ "COOKIES OWNERS"                            │
│                         │ {KT1B7RgF6j7UpAybpdfxhLCp7hf41pNFcxyS}      │
│                         │ "BALANCE OF SENDER"                         │
│                         │ 3799985579750mutez                          │
│                         │ Success (1798n)                             │
│                         │ "AFTER RUN - BALANCE OF SENDER"             │
│                         │ 3799984579749mutez                          │
│                         │ {KT1LQyTHEZeaecRj7hWgkzPEBD6vMEKXYzoo}      │
│                         │ "END RUN - BALANCE OF SENDER"               │
│                         │ 3799984579749mutez                          │
│                         │ {KT1LQyTHEZeaecRj7hWgkzPEBD6vMEKXYzoo}      │
│                         │ Everything at the top-level was executed.   │
│                         │ - testReentrancy exited with value true.    │
│                         │                                             │
│                         │ 🎉 All tests passed 🎉                      │
└─────────────────────────┴─────────────────────────────────────────────┘

To have a better visualization of the hack, the contract should be deployed

Compile the first contract, the Ledger contract, and deploy it

taq compile 3-reentrancyLedgerContract.jsligo
taq deploy 3-reentrancyLedgerContract.tz -e testing

Copy the contract address, in my case KT1BJZfhC459WqCVJzPmu3vJSWFFkvyi9k1u, and paste it on the file 3-reentrancyOfferContract.storageList.jsligo with your value

Compile/deploy the second contract, the Offer contract, putting some money on the contract for the thieves

taq compile 3-reentrancyOfferContract.jsligo
taq deploy 3-reentrancyOfferContract.tz -e testing --mutez 10000000

Copy the contract address, in my case KT1CHJgXEdBPktNNPGTDaL8XEAzJV9fjSkrZ, and paste it on the file 3-reentrancyMaliciousContract.storageList.jsligo with your value

Compile/deploy the last contract, the Malicious contract which will loop and steal the funds of the Offer contract

taq compile 3-reentrancyMaliciousContract.jsligo
taq deploy 3-reentrancyMaliciousContract.tz -e testing

Copy the contract address, in my case KT1NKLZE9HkGJxjopowLqxA4pswutgMrrXyE, and initialize the Ledger contract as the Malicious contract as some cookies on its storage. Paste the value in the file 3-reentrancyLedgerContract.parameterList.jsligo

Once done, compile the Ledger contracts and call with this parameter

taq compile 3-reentrancyLedgerContract.jsligo
taq call 3-reentrancyLedgerContract --param 3-reentrancyLedgerContract.parameter.default_parameter.tz -e testing

Context is ready :

  • the Malicious contract has cookies on the Ledger contract
  • all deployed contract points to the correct addresses

Now the Malicious contract will try to steal funds from the Offer contract, run the command to start the attack the transaction flow

octez-client transfer 0 from alice to KT1NKLZE9HkGJxjopowLqxA4pswutgMrrXyE --entrypoint attack --arg 'Unit' --burn-cap 1

Here you can see the result on the Ghostnet: https://ghostnet.tzkt.io/KT1NKLZE9HkGJxjopowLqxA4pswutgMrrXyE/operations/

3 refunds will be emitted instead of one

SOLUTION: on the 3-reentrancyOfferContract.jsligo file, line 34, swap the order of operation execution

from

return [list([opTx, opChangeOwner]), s];

to

return [list([opChangeOwner,opTx]), s];

and rerun the scenario from scratch redeploying the contracts. It should be impossible to run the attack, as the transaction will fail

"message":"user do not have cookies"
  • Authorize withdraw transfer only to a user account: As User wallet cannot do callback loops, it solves the issue but this solution is not always feasible and limiting. To check if an address is implicit, the Tezos.get_sender and the Tezos.get_source are always equal.

  • Audit External Contract calls: This is very hard to check, for example on withdrawal for a token transfer, any contract can receive funds.

  • Call third-party security experts or employ automated security tools: If you are not sure about your code, they will identify weaknesses and validate the contract’s security measures.

Overflow

Manipulating arithmetic operations can lead to overflows and underflows

  • On Solidity: SafeMath is a library in Solidity that was designed to provide safe mathematical operations. It prevents overflow and underflow errors when working with unsigned integers (uint), which can lead to unexpected behavior in smart contracts. However, since Solidity v0.8.0, this library has been made obsolete as the language itself starts to include internal checking.

  • On LIGO: For the nat, int, and timestamp types, the Michelson interpreter uses arbitrary-precision arithmetic provided by the OCaml Zarith library. It means that their size is only limited by gas or storage limits. You can store huge numbers in a contract without reaching the limit. However, in LIGO, an overflow will cause the contract to fail.

SOLUTION :

  • For large Tez values, do operation on int or nat as it has larger memory values
  • There is no other solution than using types with larger values as the default behavior is to reject the transaction in case of overflow

Do not confuse with Ligo MathLib library providing manipulation of floats and rationals instead of using basic types.

Go to Part 3: User trust & management.

Releases

No releases published

Packages

No packages published