Capturetheether 题解(Accounts)

Fuzzy identity

目标合约

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
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 开始)预先计算出来的:

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

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

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
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

部署的合约如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 合约地址生成规则如下:

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

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

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

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

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

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
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

目标合约

1
2
3
4
5
6
7
8
9
10
11
12
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 得到答案:

1
2
3
4
5
6
7
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

目标合约

1
2
3
4
5
6
7
8
9
10
11
12
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):

1
2
3
4
5
6
7
8
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) 就可以计算出私钥。

计算脚本如下:

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
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 方法即可通关。