티스토리 뷰
1. 문제
주어진 인스턴스의 소유권을 탈취하라.
도움이 될만한 것:
저수준의 함수 'delegatecall'이 어떻게 동작하는지, 어떻게 온체인상의 라이브러리에게 동작을 위임하는지, 그리고 실행 범위에 어떤 영향을 미치는지 알아보라.
Fallback 메서드
메서드 Ids
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Delegate {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
컨트랙트가 두 개가 있는데 Delegate 컨트랙트는 소유권 탈취가 너무 쉽기 때문에 당연히 아니고 Delegation 컨트랙트의 소유권을 탈취하는 것이 맞습니다.
2. 해법
await contract.sendTransaction({data: "0xdd365b8b"})
콘솔창에서 컨트랙트의 fallback 함수를 호출해 줍니다. 이때 data 필드에 4바이트 값을 넣어주는데 이 값은 Delegation 컨트랙트의 fallback 함수에서 Delegate 컨트랙트에 대해 delegatecall을 실행할 메서드의 id(함수 선택자)입니다.
트랜잭션이 컨펌되고 owner가 누구인지 확인하면 자신의 지갑 주소로 변경된 것을 확인할 수 있습니다.
3. 메서드 Id (함수 선택자)
메서드 id는 함수 프로토타입의 Keccak-256 해시의 첫 4바이트입니다. 이 값은 컨트랙트에서 어떤 함수를 호출해야 하는지 정확하게 식별하기 위해 사용됩니다. 예를 들어, ERC-20 컨트랙트의 transfer 함수의 프로토타입은 'transfer(address,uint256)'이며 메서드 id는 '0xa9059cbb'입니다.
이 문제에서는 fallback 함수의 delegatecall을 통해 Delegate 컨트랙트의 pwn 함수를 호출하기 위해 'pwn()'의 Keccack-256 해시의 첫 4바이트를 calldata로 넘겨주었습니다.
메서드 id는 web3.js 같은 라이브러리를 사용해도 좋고, 아래와 같이 컨트랙트를 작성하여 Remix IDE에서 테스용 evm에 배포하여 확인할 수도 있습니다.
contract MethodID {
function get() public pure returns(bytes4) {
return bytes4(keccak256("pwn()"));
또는
return Delegate.pwn.selector;
}
}
4. delegatecall
delegatecall은 컨트랙트 내에서 다른 컨트랙트를 호출하는 저수준의 함수입니다. 비슷한 함수인 call과의 차이점은 delegatecall은 컨트랙트 B를 호출한 컨트랙트 A의 msg.sender, msg.sender 그리고 스토리지를 컨트랙트 B에서 그대로 접근할 수 있다는 것입니다. 즉, 현재 컨트랜트의 컨텍스트 내에서 다른 컨트랙트의 코드를 실행하는 것입니다.
그림을 덭붙여서 설명하자면 다음과 같습니다. EOA에서 컨트랙트 A를 호출하고 컨트랙트 A는 컨트랙트 B를 호출합니다. 일반적인 트랜잭션 호출이라면 컨트랙트 A의 msg.sender는 EOA 주소이며 자신의 스토리지를 사용할 테고 컨트랙트 B의 msg.sender는 컨트랙트 A이며 역시 자신의 스토리지를 사용할 것입니다. 따라서 컨트랙트 B에서 pwn 함수를 호출하면 컨트랙트 B의 owner를 컨트랙트 A의 주소로 변경할 것입니다.
반면 delegatecall은...
이런 느낌입니다. 글로 설명하려니 조금 어려워서 의식의 흐름대로 만들어 보았습니다. 실제로 이렇다는 건 아님!
그림으로 보시는 바와 같이 Delegation 컨트랙트에서 fallback 함수에서 delegatecall을 통해 호출한 Delegate 컨트랙트의 pwn 함수는 Delegation 컨트랙트의 컨텍스트에서 실행됩니다. 따라서 msg.sender는 EOA 주소가 되며 이를 사용해 스토리지에 저장된 owner 값을 변경하는데, 이는 실제로 Delegate 컨트랙트의 owner 변수가 스토리지 상태 변수 슬롯에 첫 번째로 배치되어 있기 때문에 pwn 함수는 Delegation 컨트랙트의 스토리지에 첫 번째로 배치되어 있는 변수의 값을 변경하게 됩니다. 우연의 일치로 변수의 이름이 동일했을 뿐이지 변수명이 다르더라도 레이아웃 배치가 동일하다면 변경이 발생합니다. 이는 아래에서 직접 실행해 본 결과와 함께 확인할 수 있습니다.
4.1. 스토리지 상태 변수 슬롯에서 같은 위치에 있고 변수명이 다른 경우
마찬가지로 player 값이 변경됩니다.
4.2. 스토리지 상태 변수 슬롯에서 같은 위치에 있고 타입이 다른 경우
생성자에서 1로 초기화를 했었는데 fallback 함수를 호출하니 msg.sender를 uint256 정수로 변환한 값이 player 변수에 할당되어 있는 것을 확인할 수 있습니다.
4.3. 스토리지 상태 변수 슬롯이 다른 경우
uint256 타입의 number를 스토리지 슬롯의 첫 번째 자리에 배치하고 address 타입의 owner는 두 번째로 배치하였습니다. 이 상태에서 fallback 함수를 호출하면 첫 번째 자리에 있는 number의 값이 msg.sender의 uint256 정숫값으로 변경되는 것을 확인할 수 있습니다.
5. 결론
컨트랙트 내부에서 다른 컨트랙트를 호출하는 것은 주의가 필요하다.
'Solidity > Hacking' 카테고리의 다른 글
[Ethernaut] 8. Vault (0) | 2024.01.18 |
---|---|
[Ethernaut] 7. Force (0) | 2024.01.17 |
[Ethernaut] 5. Token (0) | 2024.01.15 |
[Ethernaut] 4. Telephone (0) | 2024.01.08 |
[Ethernaut] 3. CoinFlip (0) | 2024.01.07 |