Skip to content

Instantly share code, notes, and snippets.

@shingonu
Last active May 17, 2018 07:02
Show Gist options
  • Save shingonu/8e42ce04c91bf700d9bfc5504258cc80 to your computer and use it in GitHub Desktop.
Save shingonu/8e42ce04c91bf700d9bfc5504258cc80 to your computer and use it in GitHub Desktop.

Parity Multisig Hacked

address of sender

contract A {
    B b;

    function A(address _b) public {
        b = B(_b);
    }

    function callB() public {
        b.call();
    }
}

contract B {
    address public sender;
    function call() public {
        sender = msg.sender;
    }
}

If you call the callB function in A contract, the sender of B contract becomes the address of contract A. On the other hand, if you call call function of B contract, sender becomes address of EOA.

call, callcode, delegatecall

call, callcode and delegatecall: To interface with contracts that do not adhere to the ABI, the function call is provided which takes an arbitrary number of arguments of any type.

  • call - Execute code of another contract
  • delegatecall - Execute code of another contract, but with the state(storage) of the calling contract.
  • callcode(deprecated)

call

example:

address nameReg = 0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2;
nameReg.call("register", "MyName”);`

p.recipient.call.value(p.amount * 1 ether)(transactionBytecode);

This would be similar to sending a normal transaction with the web3 api. pseudo-code:

tx = {
  from: (in the example, the calling contracts address)
  to: (p.recipient is the address),
  value: (p.amount * 1 ether),
  data: (transactionByteCode),
  ...
}
web3.eth.sendTransaction(tx)

delegatecall

The delegatecall method was a bug fix for callcode, which did not preserve msg.sender and msg.value, so callcode is deprecated and will be removed in the future.

It is important to note, that delegatecall involves a security-risk for the calling contract, as the called contract can access/manipulate the calling contracts storage.

There are, of course, inherent security risks with calling a function on a contract from a given address and this way of calling contracts breaks type-safety in Solidity. Therefore, call, callcode and delegatecall are supposed to be used only as a last resort.

If Alice invokes Bob who does DELEGATECALL to Charlie, the msg.sender in the DELEGATECALL is Alice (whereas if CALLCODE was used the msg.sender would be Bob).

Details

When D does CALL on E, the code runs in the context of E: the storage of E is used.

When D does CALLCODE on E, the code runs in the context of D. So imagine that the code of E is in D. Whenever the code writes to storage, it writes to the storage of account D, instead of E.

pragma solidity ^0.4.18;

contract D {
  uint public n;
  address public sender;
  uint256 public amount;

  function callSetN(address _e, uint _n) public {
    _e.call.value(10).gas(6712353)(bytes4(keccak256("setN(uint256)")), _n);
    // E's storage is set, D is not modified
  }

  function callcodeSetN(address _e, uint _n) public {
    _e.callcode.value(10).gas(6712353)(bytes4(keccak256("setN(uint256)")), _n);
    // D's storage is set, E is not modified
  }

  function delegatecallSetN(address _e, uint _n) public {
    // TypeError: Member "value"
    // _e.delegatecall.value(10).gas(250000)(bytes4(sha3("setN(uint256)")), _n);
    
    _e.delegatecall.gas(6712353)(bytes4(keccak256("setN(uint256)")), _n);
    // D's storage is set, E is not modified
  }
}

contract E {
  uint public n;
  address public sender;
  uint256 public amount;

  function setN(uint _n) {
    n = _n;
    sender = msg.sender;
    amount = msg.value;
    // msg.sender is D if invoked by D's callcodeSetN. None of E's storage is updated
    // msg.sender is C if invoked by C.foo(). None of E's storage is updated

    // the value of "this" is D, when invoked by either D's callcodeSetN or C.foo()
  }
}

contract C {
    function foo(D _d, E _e, uint _n) {
        _d.delegatecallSetN(_e, _n);
    }
}

When D does CALLCODE on E, msg.sender inside E is D as commented in the code above.

When an account C invokes D, and D does DELEGATECALL on E, msg.sender inside E is C. That is, E has the same msg.sender and msg.value as D.

패리티 해킹 사건 분석

2017년 7월 19일에 parity 해킹 사건이 발생한다. 이 사건으로 인해 150,000 ETH가 도난당한다. 이 사건은 parity에서 만든 multisig 기능을 제공하는 contract의 잘못된 코드로 인해 발생했다.

이 contract에는 multisig의 owner들을 초기화하는 함수가 있다.

// constructor - just pass on the owner array to the multiowned and  // the limit to daylimit  
function initWallet(address[] _owners, uint _required, uint _daylimit) {  
  initDaylimit(_daylimit);    
  initMultiowned(_owners, _required);  
}

그런데 이 함수를 delegatecall로 누구나 initWallet 함수를 호출할 수 있다.

function() payable {
  // just being sent some cash?
  if (msg.value > 0)
    Deposit(msg.sender, msg.value);
  else if (msg.data.length > 0)
    _walletLibrary.delegatecall(msg.data);
}

해커는 delegatecall을 이용해 initWallet 함수를 호출했고 execute 함수를 호출해서 ETH를 빼갔다.

이후 parity 사에서는 코드를 수정하고 다시 배포한다.

function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
     initDaylimit(_daylimit);	     initDaylimit(_daylimit);
     initMultiowned(_owners, _required);	     initMultiowned(_owners, _required);
   }	   }

수정된 코드를 보면 only_uninitialized인 조건에서만 동작하도록 변경했다. 이말은 initWallet 함수가 한번 초기화되면 다시 호출될 수 없음을 의미한다.

그런데 아무도 contract에 초기화 함수를 호출하지 않았다. 그러던 와중 스마트 컨트랙트를 공부하는 누군가 initWallet 함수를 호출했고, 실수로 kill 함수를 호출하는 황당한 사건이 발생한다.

그래서 이 multisig contract를 사용하는 contract에 multisig에 묶여있는 모든 ETH와 token들을 사용할 수 없게 되는 문제가 발생했다.

Library

추가로 multisig를 library로 만들지 않고 contract로 만들어서 발생한 문제도 포함하고 있다.

  1. 자체의 owner를 허용한다. 제대로 선언된 라이브러리 함수는 많은 사람들이 사용하기 때문에 owner가 없어야 한다. 더 심각한 것은, owner를 가지면서 누구나 이 owner를 변경할 수 있다는 것이다.
  2. contract의 경우 owner는 kill 기능을 통해 소유하고 있는 contract를 삭제할 수 있다.

Reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment