Capture The Ether 题解(Accounts)

Capture The Ether 题解(Accounts)

·

5 min read

Fuzzy identity

目标合约

pragma solidity ^0.4.21;

interface IName {
    function name() external view returns (bytes32);
}

contract FuzzyIdentityChallenge {
    bool public isComplete;

    function authenticate() public {
        require(isSmarx(msg.sender));
        require(isBadCode(msg.sender));

        isComplete = true;
    }

    function isSmarx(address addr) internal view returns (bool) {
        return IName(addr).name() == bytes32("smarx");
    }

    function isBadCode(address _addr) internal pure returns (bool) {
        bytes20 addr = bytes20(_addr);
        bytes20 id = hex"000000000000000000000000000000000badc0de";
        bytes20 mask = hex"000000000000000000000000000000000fffffff";

        for (uint256 i = 0; i < 34; i++) {
            if (addr & mask == id) {
                return true;
            }
            mask <<= 4;
            id <<= 4;
        }

        return false;
    }
}

通关条件

构造一个合约,实现 name 接口且合约地址中包含 "badc0de"

题目分析

实现 name 接口是很简单的,重点在于合约地址得包含 "badc0de"

CREATE

在 ETH 中,合约地址是可以根据部署者的地址和它的 nonce(也就是发送过的交易量,根据 EIP-161,普通地址从 0 开始,合约地址从 1 开始)预先计算出来的:

// 这种方法使用的是 CREATE 操作码,也是合约部署的默认方式
address = keccak(RLP([deployer, nonce]))[12:]

所以对于 "badc0de" 的要求,我们采用暴力搜索的方式解决--生成大量的地址,对于每个地址使用适量的 nonce 去计算合约地址,检查每个地址是否满足条件。我用 Elixir 写了个脚本进行搜索,跑了十分钟左右找出 11 条符合条件的记录。脚本如下:

defmodule Ethereum.AddressGenerator do
  @moduledoc false

  alias Ethereum.Crypto

  def con_generate_badcode do
    timeout = 20000

    1..200
    |> Enum.map(fn _ -> Task.async(fn -> generate_badcode() end) end)
    |> Enum.map(&(Task.yield(&1, timeout) || Task.shutdown(&1, timeout)))
    |> Enum.filter(&(is_tuple(&1) and elem(&1, 0) == :ok))
    |> Enum.map(fn {:ok, result} -> result end)
    |> Enum.filter(fn
      {:error, :not_found} -> false
      {:ok, _, _, _} -> true
    end)
  end

  def generate_badcode do
    Enum.each(1..50000, fn _ ->
      {addr, _priv} = pair = generate_address()

      Enum.each(1..10, fn nonce ->
        contract_addr = contract_address_with_nonce(addr, nonce)

        if String.contains?(contract_addr, "badc0de") do
          throw({:badcode, contract_addr, nonce, pair})
        end
      end)
    end)

    {:error, :not_found}
  catch
    {:badcode, contract_addr, nonce, pair} ->
      {:ok, contract_addr, nonce, pair}
  end

  def generate_address do
    {pub, priv} = Crypto.generate_keypair()
    {pubkey_to_address(pub), Base.encode16(priv, case: :lower)}
  end

  def contract_address_with_nonce(addr, nonce)
      when is_binary(addr) and is_integer(nonce) do
    addr_bytes =
      addr
      |> String.trim("0x")
      |> Base.decode16!(case: :lower)

    [addr_bytes, nonce]
    |> ExRLP.encode()
    |> bytes_to_address()
  end

  defp pubkey_to_address(pub) do
    pub
    |> strip_leading_byte()
    |> bytes_to_address()
  end

  defp bytes_to_address(data) do
    data
    |> Crypto.kec()
    |> extract_addr_bytes()
    |> Base.encode16(case: :lower)
    |> String.replace_prefix("", "0x")
  end

  defp strip_leading_byte(<<_::8, data::binary>>), do: data

  defp extract_addr_bytes(<<_::bytes-size(12), data::bytes-size(20)>>), do: data
end

部署的合约如下:

contract IdentifierHacker is IName {

    function name() external view override returns (bytes32) {
        return bytes32("smarx");
    }

    function auth(FuzzyIdentityChallenge c) public {
        c.authenticate();
    }

    function checkCompleted(FuzzyIdentityChallenge c) public view returns (bool) {
        return c.isComplete();
    }
}

CREATE2

这篇文章 介绍了用 CREATE2 来解决本题的思路。

除了 CREATE 外,EVM 在 EIP-1014 中添加了一个新的操作码 CREATE2 ,也可以用来生成合约地址。和 CREATE 相比, CREATE2 的好处在于其生成的合约地址并不依赖于生成者的状态(nonce)。 CREATE2 合约地址生成规则如下:

keccak256(0xff ++ deployer ++ salt ++ keccak256(bytecode))[12:]

其中 bytecode 是合约的字节码,salt 是 32 字节的随机盐值。

由于 CREATE2 不是默认生成合约的操作码,所以我们得通过一个合约来部署 IdentityHacker 合约:

contract Deployer {
    function deploy(bytes32 salt) public {
        // solidity 0.6.2 之后不需要用 assembly 也能使用 create2 了
        new IdentifierHacker{salt: salt}();
    }
}

部署 Deployer 后,我们就得到 IdentityHacker 的 deployer 地址了。使用 CREATE2 我们不需要生成大量地址,而是对于同一个地址暴力搜索 salt,脚本如下:

defmodule Ethereum.Create2AddressGenerator do
  @moduledoc false

  alias Ethereum.Crypto

  @bytecode "IDENTITY_HACKER_BYTECODE"
  @deployer "DEPLOYER_ADDRESS"
  @kec_bytecode @bytecode |> Base.decode16!(case: :lower) |> Crypto.kec()
  @addr_bytes @deployer |> String.trim("0x") |> Base.decode16!(case: :mixed)

  def con_generate_badcode do
    timeout = 20000

    1..500
    |> Enum.map(fn _ -> Task.async(fn -> generate_badcode() end) end)
    |> Enum.map(&(Task.yield(&1, timeout) || Task.shutdown(&1, timeout)))
    |> Enum.filter(&(is_tuple(&1) and elem(&1, 0) == :ok))
    |> Enum.map(fn {:ok, result} -> result end)
    |> Enum.filter(fn
      {:error, :not_found} -> false
      {:ok, _, _} -> true
    end)
  end

  def generate_badcode do
    1..50000
    |> Enum.each(fn _ ->
      salt = :crypto.strong_rand_bytes(32)
      contract_addr = contract_address_with_salt(salt)

      if String.contains?(contract_addr, "badc0de") do
        throw({:badcode, contract_addr, salt})
      end
    end)

    {:error, :not_found}
  catch
    {:badcode, contract_addr, salt} ->
      {:ok, contract_addr, "0x" <> Base.encode16(salt, case: :lower)}
  end

  def contract_address_with_salt(salt) when byte_size(salt) == 32 do
    [<<0xFF>>, @addr_bytes, salt, @kec_bytecode]
    |> IO.iodata_to_binary()
    |> Crypto.kec()
    |> extract_addr_bytes()
    |> Base.encode16(case: :lower)
    |> String.replace_prefix("", "0x")
  end

  defp extract_addr_bytes(<<_::bytes-size(12), data::bytes-size(20)>>), do: data
end

这份脚本花了一分钟就找到了满足条件的 salt

Public Key

目标合约

pragma solidity ^0.4.21;

contract PublicKeyChallenge {
    address owner = 0x92b28647ae1f3264661f72fb2eb9625a89d88a31;
    bool public isComplete;

    function authenticate(bytes publicKey) public {
        require(address(keccak256(publicKey)) == owner);

        isComplete = true;
    }
}

通关条件

找到 owner 的 publicKey

题目分析

ECDSA 中,有了消息+签名是可以恢复出公钥的,可以参考这里这里

从 etherscan 上找到 owner 发送过的交易的 rawtx,可以从 rawtx 中得到签名和交易信息,有了交易信息、签名,就可以恢复出公钥。

简单处理的话,可以直接用 ethereumjs 得到答案:

const EthereumTx = require('ethereumjs-tx').Transaction

const rawtx = '0xf87080843b9aca0083015f90946b477781b0e68031109f21887e6b5afeaaeb002b808c5468616e6b732c206d616e2129a0a5522718c0f95dde27f0827f55de836342ceda594d20458523dd71a539d52ad7a05710e64311d481764b5ae8ca691b05d14054782c7d489f3511a7abf2f5078962'

let tx = new EthereumTx(rawtx, {chain: 'ropsten'})
let publicKey = tx.getSenderPublicKey().toString('hex')
console.log(publicKey)

Account takeover

目标合约

pragma solidity ^0.4.21;

contract AccountTakeoverChallenge {
    address owner = 0x6B477781b0e68031109f21887e6B5afEAaEB002b;
    bool public isComplete;

    function authenticate() public {
        require(msg.sender == owner);

        isComplete = true;
    }
}

通关条件

得到 owner 的私钥

题目分析

写过钱包的人第一反应应该就是签名的随机数有问题。但是具体从签名推出私钥还没尝试过,所以参考了这篇题解

查看 owner 发送的最早两笔交易,解析出来可以发现他们签名的 r 值是相同的(以太坊签名 signature = (r, s, v),其中 v 是 recovery_id):

const EthereumTx = require('ethereumjs-tx').Transaction

const rawtx1 = '0xf86b80843b9aca008252089492b28647ae1f3264661f72fb2eb9625a89d88a31881111d67bb1bb00008029a069a726edfb4b802cbf267d5fd1dabcea39d3d7b4bf62b9eeaeba387606167166a07724cedeb923f374bef4e05c97426a918123cc4fec7b07903839f12517e1b3c8'
const rawtx2 = '0xf86b01843b9aca008252089492b28647ae1f3264661f72fb2eb9625a89d88a31881922e95bca330e008029a069a726edfb4b802cbf267d5fd1dabcea39d3d7b4bf62b9eeaeba387606167166a02bbd9c2a6285c2b43e728b17bda36a81653dd5f4612a2e0aefdb48043c5108de'

let tx1 = new EthereumTx(rawtx1, {chain: 'ropsten'})
let tx2 = new EthereumTx(rawtx2, {chain: 'ropsten'})
console.log(tx1.r.toString('hex') === tx2.r.toString('hex')) // true

在 secp256k1 中有:

其中(r, s) 是签名,G 是椭圆曲线上的基点,k 是随机数,M 是消息,H(M) 表示对 M 进行 sha256,k-1 表示的是 k 的模反元素。可以看出,相同的 r 代表着相同的 k。 从 (1)(2)(3) 可以推得:

所以在 k 暴露的情况下,私钥是可以被计算出来的。接下来的任务就是尝试得到 k:

所以有:

结合 (4)(6) 就可以计算出私钥。

计算脚本如下:

const bigintModArith = require('bigint-mod-arith')

const EthereumTx = require('ethereumjs-tx').Transaction

const rawtx1 = '0xf86b01843b9aca008252089492b28647ae1f3264661f72fb2eb9625a89d88a31881922e95bca330e008029a069a726edfb4b802cbf267d5fd1dabcea39d3d7b4bf62b9eeaeba387606167166a02bbd9c2a6285c2b43e728b17bda36a81653dd5f4612a2e0aefdb48043c5108de'
const rawtx2 = '0xf86b80843b9aca008252089492b28647ae1f3264661f72fb2eb9625a89d88a31881111d67bb1bb00008029a069a726edfb4b802cbf267d5fd1dabcea39d3d7b4bf62b9eeaeba387606167166a07724cedeb923f374bef4e05c97426a918123cc4fec7b07903839f12517e1b3c8'

let tx1 = new EthereumTx(rawtx1, {chain: 'ropsten'})
let tx2 = new EthereumTx(rawtx2, {chain: 'ropsten'})

let z1 = BigInt('0x' + tx1.hash(false).toString('hex'))
let z2 = BigInt('0x' + tx2.hash(false).toString('hex'))

let s1 = BigInt('0x' + tx1.s.toString('hex'))
let s2 = BigInt('0x' + tx2.s.toString('hex'))

let r = BigInt('0x' + tx1.r.toString('hex'))

let p = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141')

let z = z1 - z2

let k = bigintModArith.modInv(s1 - s2, p) * z % p
let priv = (k * s1 - z1) * bigintModArith.modInv(r, p) % p
let privNeg = (-s1 * (-k % p) - z1) * bigintModArith.modInv(r, p) % p
if (priv == privNeg) {
    console.log('Privkey is: ', priv.toString(16))
}

k = bigintModArith.modInv(s1 + s2, p) * z % p
priv = (k * s1 - z1) * bigintModArith.modInv(r, p) % p
privNeg = (-s1 * (-k % p) - z1) * bigintModArith.modInv(r, p) % p
if (priv == privNeg) {
    console.log('Privkey is: ', priv.toString(16))
}

得到私钥,利用 owner 地址调用目标的 authenticate 方法即可通关。