17.SWC-117_Signature Malleability
2023-07-15 17:26:26 # 09.SWC

SWC-117_Signature Malleability

Signature Malleability

  • Description: The implementation of a cryptographic signature system in Ethereum contracts often assumes that the signature is unique, but signatures can be altered without the possession of the private key and still be valid. The EVM specification defines several so-called ‘precompiled’ contracts one of them being ecrecover which executes the elliptic curve public key recovery. A malicious user can slightly modify the three values v, r and s to create other valid signatures. A system that performs signature verification on contract level might be susceptible to attacks if the signature is part of the signed message hash. Valid signatures could be created by a malicious user to replay previously signed messages.

  • Remediation: A signature should never be included into a signed message hash to check if previously messages have been processed by the contract.

vulnerability contract 1:

我的理解

(1)这个合约txid如果包含sig,那么sig可以最多被修改成四个不同的,这四个不同的sig对应的消息m是相同的,也就是说value,to,gasprice,nonce是相同的。
(2)然后这四个sig都可以执行transfer方法,因为算出来的txid不一样,但是解析出来的签名者是一样的,因此该签名者会被最多扣除4次余额。
(3)照这么说,以太坊的消息为啥不会被重放呢?因为以太坊会拒绝相同的nonce,无法重放。但是在这里不存在这个问题
(4)然后这个SWC把sig从txid中的hash移除了,但是这会造成DoS(因为同一个value,to,gasprice,nonce被执行之前,不能被其他人再次执行),在GitHub讨论的修改方法如链接:https://github.com/SmartContractSecurity/SWC-registry/issues/173

ECDSA链接:
4篇连载:https://coders-errand.com/malleability-ecdsa-signatures/
https://eklitzke.org/bitcoin-transaction-malleability
https://hackernoon.com/what-is-the-math-behind-elliptic-curve-cryptography-f61b25253da3
https://www.derpturkey.com/inherent-malleability-of-ecdsa-signatures/

然后我去stackoverflow问了,答案很清晰

This is how someone can exploit it:

  1. Alice sends some tokens to Bob using the transfer function. All normal so far, since txid wasn’t seen before then signatureUsed[txid] == false and the payment goes through.
  2. Bob is our exploiter. He picks the signature = (r, s, v) used by Alice and creates a new one signature2 = (r2, s2, v2) like this:
1
2
3
r2 = r
s2 = s
v2 = v<27 ? v+27 : v-27
  1. Bob calls transfer with the same parameters used by Alice but using signature2. txid will be different than before so signatureUsed[txid] == false. The signature is also recognized as a valid Alice signature (see how ecrecoverFromSig handles v …). So the payment goes through.
  2. At then end Bob stole an extra payment from Alice.

The root of the problem is that it’s possible to use a signature to make another valid one. This opens up to “replay attacks”.

This particular code tries to prevent replay attacks by checking if txid was seen before. However txid was calculated using the signature, thus failing to prevent this exploit. The fix is just to remove the signature from txid.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
pragma solidity ^0.4.24;

contract transaction_malleablity{

mapping(address => uint256) balances;
mapping(bytes32 => bool) signatureUsed;

constructor(address[] owners, uint[] init){
require(owners.length == init.length);
for(uint i=0; i < owners.length; i ++){
balances[owners[i]] = init[i];
}
}

function transfer(bytes _signature, address _to, uint256 _value, uint256 _gasPrice, uint256 _nonce) public returns (bool){
bytes32 txid = keccak256(
abi.encodePacked(
getTransferHash(
_to, _value, _gasPrice, _nonce
),
_signature
)
); // something wrong

require(!signatureUsed[txid]);

address from = recoverTransferPreSigned(_signature, _to, _value, _gasPrice, _nonce);
require(balances[from] > _value);

balances[from] -= _value;
balances[_to] += _value;
signatureUsed[txid] = true;
}

function recoverTransferPreSigned(bytes _sig, address _to, uint256 _value, uint256 _gasPrice, uint256 _nonce) public view returns (address recovered) {
return ecrecoverFromSig(getSignHash(getTransferHash(_to, _value, _gasPrice, _nonce)), _sig);
}

function getTransferHash(address _to, uint256 _value, uint256 _gasPrice, uint256 _nonce) public view returns (bytes32 txHash) {
return keccak256(address(this), bytes4(0x1296830d), _to, _value, _gasPrice, _nonce);
}

function getSignHash(bytes32 _hash) public pure returns (bytes32 signHash){
return keccak256("\x19Ethereum Signed Message:\n32", _hash);
}

function ecrecoverFromSig(bytes32 hash, bytes sig) public pure returns (address recoveredAddress) {
bytes32 r;
bytes32 s;
uint8 v;
if (sig.length != 65) return address(0);

assembly {
r := mload(add(sig, 32))
s := mload(add(sig, 64))
v := byte(0, mload(add(sig, 96)))
}

if (v < 27) {
v += 27;
}

if (v != 27 && v != 28) return address(0);
return ecrecover(hash, v, r, s);
}
}

fix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// instead of: mapping(bytes32 => bool) signatureUsed;
mapping(address => mapping(uint256 => bool)) saltsUsed;

function transfer(
bytes _signature,
address _to,
uint256 _value,
uint256 _gasPrice,
uint256 _salt
) public returns (bool) {
address from = recoverTransferPreSigned(_signature, _to, _value, _gasPrice, _salt);

require(!saltsUsed[from][_salt]);

require(balances[from] > _value);

balances[from] -= _value;
balances[_to] += _value;

saltsUsed[from][_salt] = true
}

or

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

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;
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 view returns (bytes32) {
return keccak256(abi.encodePacked(address(this), _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;
}
}

/*
// owners
0xe19aea93F6C1dBef6A3776848bE099A7c3253ac8
0xfa854FE5339843b3e9Bfd8554B38BD042A42e340

// to
0xe10422cc61030C8B3dBCD36c7e7e8EC3B527E0Ac
// amount
100
// nonce
0
// tx hash
0x12a095462ebfca27dc4d99feef885bfe58344fb6bb42c3c52a7c0d6836d11448

// signatures
0x120f8ed8f2fa55498f2ef0a22f26e39b9b51ed29cc93fe0ef3ed1756f58fad0c6eb5a1d6f3671f8d5163639fdc40bb8720de6d8f2523077ad6d1138a60923b801c
0xa240a487de1eb5bb971e920cb0677a47ddc6421e38f7b048f8aa88266b2c884a10455a52dc76a203a1a9a953418469f9eec2c59e87201bbc8db0e4d9796935cb1b
*/

vulnerability contract 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pragma solidity 0.4.24;

contract Missing{
address private owner;

modifier onlyowner {
require(msg.sender==owner);
_;
}

function Constructor() // 改成constructor
public
{
owner = msg.sender;
}

function () payable {}

function withdraw()
public
onlyowner
{
owner.transfer(this.balance);
}

}
Prev
2023-07-15 17:26:26 # 09.SWC
Next