- 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 |