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