[Solidity] Signature Replay Attack

임형석·2023년 11월 15일
0

Solidity


Signature Replay Attack

시그니쳐 리플레이는 전자서명 과정에서 일어나는 해킹 공격이다.

다음과 같이 Multi Signature Wallet 을 구현한 컨트랙트와 함께 설명하면,

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";

contract MultiSigWallet {
    using ECDSA for bytes32;

    address[2] public owners;

    constructor(address[2] memory _owners) payable {
        owners = _owners;
    }

    function deposit() external payable {}

    function transfer(address _to, uint _amount, bytes[2] memory _sigs) external {
        bytes32 txHash = getTxHash(_to, _amount);
        require(_checkSigs(_sigs, txHash), "invalid sig");

        (bool sent, ) = _to.call{value: _amount}("");
        require(sent, "Failed to send Ether");
    }

    function getTxHash(address _to, uint _amount) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_to, _amount));
    }

    function _checkSigs(
        bytes[2] memory _sigs,
        bytes32 _txHash
    ) private view returns (bool) {
        bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();

        for (uint i = 0; i < _sigs.length; i++) {
            address signer = ethSignedHash.recover(_sigs[i]);
            bool valid = signer == owners[i];

            if (!valid) {
                return false;
            }
        }

        return true;
    }
}

A, B 라는 두명의 오너가 MultiSigWallet 컨트랙트를 관리한다.

컨트랙트에는 2 ETH 가 예치되어 있다. A 가 transfer 함수를 통해 컨트랙트에서 자신의 지갑으로 1 ETH 를 전송하고 싶다면 B 의 서명을 받아야 한다.

서명은 getTxHash 함수를 통해 ETH를 받을 주소, 전송할 ETH 갯수 로 해시하고 이 해시는 _checkSigs 함수를 통해 서명한 주소와 서명이 일치하는지 검증한 후 ETH 를 전송하게 된다.

이 과정에서 A 가 B 의 서명을 받았을 때, 이 서명을 계속 사용하여 transfer 함수를 여러번 호출할 수 있다. 이것을 Signature Replay 공격이라고 부른다.


방어 방법

Signature Replay 공격을 방어하는 방법은 두 가지가 있다.

첫 번째 방법은 execute 매핑을 추가하여 해당 서명이 실행되었는지 확인하고, nonce 값을 인자로 받아 서명을 한번만 사용 가능하도록 만드는 것이다.

contract MultiSigWallet {
    using ECDSA for bytes32;

    address[2] public owners;
    mapping(bytes32 => bool) public executed;

    constructor(address[2] memory _owners) payable {
        owners = _owners;
    }

    function deposit() external payable {}

    function transfer(
        address _to,
        uint _amount,
        uint _nonce,
        bytes[2] memory _sigs
    ) external {
        bytes32 txHash = getTxHash(_to, _amount, _nonce);
        require(!executed[txHash], "tx executed");
        require(_checkSigs(_sigs, txHash), "invalid sig");

        executed[txHash] = true;

        (bool sent, ) = _to.call{value: _amount}("");
        require(sent, "Failed to send Ether");
    }

    function getTxHash(
        address _to,
        uint _amount,
        uint _nonce
    ) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_to, _amount, _nonce));
    }

    function _checkSigs(
        bytes[2] memory _sigs,
        bytes32 _txHash
    ) private view returns (bool) {
        bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();

        for (uint i = 0; i < _sigs.length; i++) {
            address signer = ethSignedHash.recover(_sigs[i]);
            bool valid = signer == owners[i];

            if (!valid) {
                return false;
            }
        }

        return true;
    }
}

이렇게 컨트랙트를 만들 경우, 해당 컨트랙트에 한해 Signature Replay 공격 방어가 가능하다.

하지만 tx 를 실행 후, selfdestruct 를 이용해 다시 컨트랙트를 배포하여 논스 값을 초기화 시킬 수 있다.

두 번째 방법으로, 이러한 공격을 막기 위해 서명 생성 과정에서 컨트랙트의 주소도 포함시킨다면 컨트랙트를 다시 배포하더라도 같은 서명을 생성할 수 없기에 공격을 방어할 수 있다.

    function getTxHash(
        address _to,
        uint _amount,
        uint _nonce
    ) public view returns (bytes32) {
        return keccak256(abi.encodePacked(address(this), _to, _amount, _nonce));
    }

피싱

일부 사이트는 처음 지갑을 연결할 때 아래와 같이 서명을 요청하는 경우가 있다.

아래 사진의 opensea 는 트랜잭션이나 가스비를 소모하는 서명은 아니라고 친절히 알려주고 있다..

만약 피싱사이트에 잘못 접속하게 된다면 이 서명을 통해 지갑에 보유하고 있는 자산을 털릴 수 있다.

자산을 제어할 수 있는 서명은 위 사진처럼 빨간 글씨로 경고 문구를 보여주므로 주의하면 된다.

개발자도 사용자가 생성한 서명이 의도대로 사용되는 지와 서명을 이용한 공격을 주의하며 컨트랙트를 작성해야 한다.


참조 : https://solidity-by-example.org/hacks/signature-replay/

0개의 댓글