BlockChain

Solidity 취약점 #6

부산대보금자리 2022. 1. 18. 17:35

 

 

  • Requirement violation

 

Solidity에서 require 외부 입력에 대해서 validate한. 대부분의 경우에서 입력은 caller에게 들어오지만 return callee 처리한다.

이러한 require violation 일어나면 두가지 경우이다.

번째는 입력에 의해서 버그가 발견된 것이고,

번째는 requirement 조건이 너무 강하게 제한된 것이다.

너무 강하게 조건이 정해져 있다면 올바른 입력을 처리 못할수가 tek.

                                                  

예시>

 

 

 

  • Signature Replay

 

Off-Chain에서의 데이터에 대해서 인증된 유저임을 나타내기 위해서 Sign 하고 on-chain에 업데이트되면 이를 검증하게 된다.

 

 

러한 서명이 재전송된다면 만약 한번만 전송되길 원하는 signer에게는 위험할수 있다.

 

예시> 취약 코드

 

// SPDX-License-Identifier: MITpragmasolidity^0.8.10;pragmaexperimentalABIEncoderV2;import"github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.3/contracts/cryptography/ECDSA.sol";

contractMultiSigWallet{
   
usingECDSAforbytes32;

address[2] publicowners;

constructor(address[2] memory_owners) payable{
        owners = _owners;
    }

functiondeposit() externalpayable{}

functiontransfer(
       
address_to,
       
uint_amount,
       
bytes[2] memory_sigs
    )
external{
       
bytes32txHash = getTxHash(_to, _amount);
        require(_checkSigs(_sigs, txHash),
"invalid sig");

(boolsent, ) = _to.call{value: _amount}("");
        require(sent,
"Failed to send Ether");
    }

Function getTxHash(address_to, uint_amount) publicviewreturns(bytes32) {
       
returnkeccak256(abi.encodePacked(_to, _amount));
    }

function_checkSigs(bytes[2] memory_sigs, bytes32_txHash)
       
privateviewreturns(bool)
    {
       
bytes32ethSignedHash = _txHash.toEthSignedMessageHash();

for(uinti = 0; i < _sigs.length; i++) {
           
addresssigner = ethSignedHash.recover(_sigs[i]);
           
boolvalid = signer == owners[i];

if(!valid) {
               
returnfalse;
            }
        }

returntrue;
    }
}

 

-> nonce 추가

// SPDX-License-Identifier: MIT pragmasolidity^0.8.10; pragmaexperimentalABIEncoderV2; import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.3/contracts/cryptography/ECDSA.sol";

Contract MultiSigWallet{

using ECDSA for bytes32;

address[2] public owners;

mapping(bytes32 => bool) public executed;

 

constructor(address[2] memory_owners) payable{

owners = _owners;

}

function deposit() externa lpayable{}

Function transfer(
       
address_to,
       
uint_amount,
       
uint_nonce,
       
bytes[2] memory_sigs
    )
external{
bytes32 txHash = getTxHash(_to, _amount, _nonce);

require(!executed[txHash], "tx executed");

require(_checkSigs(_sigs, txHash), "invalid sig");

executed[txHash] = true;

(bool sent, ) = _to.call{value: _amount}("");

require(sent, "Failed to send Ether");

}

Function getTxHash(
        address_to,
        uint_amount,
        uint_nonce
    ) public
view returns(bytes32) {

return keccak256(abi.encodePacked(address(this), _to, _amount, _nonce));

}

function_checkSigs(bytes[2] memory_sigs, bytes32_txHash)
        private
view returns(bool)
    {

bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();

for (uint i = 0; i < _sigs.length; i++) {

address signer = ethSignedHash.recover(_sigs[i]);

bool valid = signer == owners[i];

if (!valid) {

return false;

}

}

return true; }

}

 

 

 

  • Exception disorder

외가 일어날수 있는 부분에 대해서 조건에 따라 처리하는 코드가 없다면 Attacker에 의해서 취약한 코드를 실행할 있다.

이는 address.send() address.call.value() 같은 메소드를 포함한다.

 

2016 king of the Ether Throne 사례

 

 

contract KotET {

    address public king;

    uint public claimPrice = 100;

    address owner;

    //constructor, assigning ownership

    constructor() {

        owner = msg.sender;

        king = msg.sender;

    }

    //for contract creator to withdraw commission fees

    function sweepCommission(uint amount) {

        owner.send(amount);

    }

    //fallback function

    function() {

        if (msg.value < claimPrice) revert;

        uint compensation = calculateCompensation();

       

        king.send(compensation);

        king = msg.sender;

        claimPrice = calculateNewPrice();

    }

}

 

Msg.value가 claimprice보다 크면 보상을 king에게 주고 sender 된다.

이때 send 함수는 2300 가스이므로 fallback function기능이 제대로 수행못할 있다. 그럼 해당 보상은 그대로 contract wallet에 보관되게 된다.

이에 따른 예외 처리가 필요하며 가스 제한이 없는 call을 쓸수가 있다.

 

이때를 기준으로 출금 패턴이 등장하였다.

 

pragma solidity >0.4.99 <0.6.0

contract WithdrawalContract {

    address public richest;

    uint public mostSent;

    mapping (address => uint) pendingWithdrawals;

    constructor() public payable {

        richest = msg.sender;

        mostSent = msg.value;

    }

    function becomeRichest() public payable returns (bool) {

        if (msg.value > mostSent) {

            pendingWithdrawals[richest] += msg.value;

            richest = msg.sender;

            mostSent = msg.value;

            return true;

        } else {

            return false;

        }

    }

    function withdraw() public {

        uint amount = pendingWithdrawals[msg.sender];

        // 리엔트란시(re-entrancy) 공격을 예방하기 위해

        // 송금하기 전에 보류중인 환불을 0으로 기억해 두십시오.

        pendingWithdrawals[msg.sender] = 0;

        msg.sender.transfer(amount);

    }

}

 

이는 가장 richest 되는걸 기록 하는 메소드와

출금하는 메소드를 따로 구현하여 재진입 공격과 exception disorder 모두 방지하였다.

 

'BlockChain' 카테고리의 다른 글

Solidity re-enterancy  (0) 2022.01.20
Solidity 취약점 #7  (0) 2022.01.19
Solidity 취약점 #5  (0) 2022.01.17
Solidity 취약점 #4  (0) 2022.01.14
Solidity 취약점 #3  (0) 2022.01.13