Security Innovation Smart Contract CTF入门

题目地址 wp

参考文章

智能合约安全系列文章之反编译篇 opcode逆向基础 关于evm中数据存储 以太坊中智能合约中的存储

Solidity数据存储

这里记录一下安装web3.py的过程,装的头皮发麻,通常肯定是使用命令pip3 install web3,但是我的报错是几个模块安装出错并且visual c++环境没有。

解决过程: 最开始直接下载web3-5.28.0-py3-none-any.whl安装还是报错,这个和用pip3安装没啥区别似乎,后来手动下载web3的包用python setup.py install下载,这个过程可以成功下载一些包,但到最后还会在visual c++报错,然后用VisualCppBuildTools_Full.exe安装环境。最后又试了一下直接用pip3安装,就成功了。可能需要的包在前面都装好了吧。

Lock Box

考点:合约反汇编,EVMstorage 存储的读取。太菜了,这题确实不会,第一次了解到了EVM的反汇编知识,学到了很多知识。这里也认真的分析一遍合约反汇编代码。

合约源码,源码比较容易看懂,主要就是满足pin == _pin,但这里肯定无法猜出来。但这里可以看到pin的初始定义用了uint256 private pin;。这告诉我们这个值是储存在storage中的,那我们如果可以知道它在storage上的地址就能够通过合约交互获取到它的值了

EVM里有三种存储方式:

  • stack “PUSH”命令
  • memory 用“MSTORE”指令
  • stroage 用“SSTORE”指令存储数据,类似于存储在硬盘上,最耗费gas的存储 针对这三种存储方式,展开又有一些内容了
pragma solidity 0.4.24;
 
contract CtfFramework{
    
    event Transaction(address indexed player);
 
    mapping(address => bool) internal authorizedToPlay;
    
    constructor(address _ctfLauncher, address _player) public {
        authorizedToPlay[_ctfLauncher] = true;
        authorizedToPlay[_player] = true;
    }
    modifier ctf() { 
        require(authorizedToPlay[msg.sender], "Your wallet or contract is not authorized to play this challenge. You must first add this address via the ctf_challenge_add_authorized_sender(...) function.");
        emit Transaction(msg.sender);
        _;
    }
    
    // Add an authorized contract address to play this game
    function ctf_challenge_add_authorized_sender(address _addr) external ctf{
        authorizedToPlay[_addr] = true;
    }
 
 
pragma solidity 0.4.24;
 
contract Lockbox1{
 
    uint256 private pin;
 
    constructor(address _ctfLauncher, address _player) public payable
        
    {
        pin = now%10000;
    }
    
    function unlock(uint256 _pin) external{
        require(pin == _pin, "Incorrect PIN");
        msg.sender.transfer(address(this).balance);
    }
 
}

首先是main函数的反汇编代码。对于memory[0x40:0x60] = 0x80;这个几乎每个合约的前几个操作码,对应的opcode如下,简单讲就是把0x80 写到[0x40 ,0x40 + 0x20] 这块内存里面,因为内存是空的,这会创建新的内存,分配了32字节的内存空间。不过其实后续也不太会用到只是了解一下吧。

下面的0x3c3e1662也是可以通过签名网站查到的,不过也就限于一些常见的函数名字。

在这里插入图片描述

在这里插入图片描述

关于opcode操作的分析还是先放一放吧。。。先把反汇编代码看懂

contract Contract {
    function main() {
 		#进入main函数,把0x80写到[0x40 ,0x40 + 0x20]这块内存里面,因为内存是空的,这会创建新的内存
 		#对应的opcode操作
 		#PUSH1 0x80 
 		#PUSH1 0x40 
 		#MSTORE  即为  MSTORE(arg0, arg1)从栈中获取两个参数,表示MEM[0x40:(0x40+32)=0x60] = 0x80
 		#正在做的是分配96个字节的存储器并将指针移动到第64个字节的开头。 我们现在有64个字节用于临时空间,32个字节用于临时存储器存储。
 #可靠性文档声明如下:"Solidity以一种非常简单的方式管理内存:内存中位置0x40有一个"空闲内存指针"。如果你想分配内存,只需使用该点的内存并相应地更新指针。"
        memory[0x40:0x60] = 0x80;
        
        #判断用户输入的data内容长度是否小于4,如果满足就revert(关于revert,assert,require的比较)
        if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }
    
    	#与上0xffffffff(即四字节)获取data的低4位,赋值给var0
        var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;
    
    	#这里的0x3c3e1662代表的是函数进行sha3加密后的前四位。所以就是判断是否调用这个函数()
        if (var0 == 0x3c3e1662) {
            // Dispatch table entry for ctf_challenge_add_authorized_sender(address)
            var var1 = msg.value;
        	
        	#判断var1是否存在 
            if (var1) { revert(memory[0x00:0x00]); }
        	
        	#var1赋值为0x0092
            var1 = 0x0092;
            
            #传入的参数,并且与0xffffffffffffffffffffffffffffffffffffffff,说明低位20bytes数值保留,高位12bytes数值置0。这应该是address数据
            #并且这里0x04前面四个字节是赋值给了var0的,所以从0x04开始取
            var var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
            
            #调用函数并且传入参数
            ctf_challenge_add_authorized_sender(var2);
            stop();
            
         #如果调用的是另一个函数,这里也可以搜到到	unlock(uint256)函数名,下面的操作是一样的
        } else if (var0 == 0x6198e339) {
            // Dispatch table entry for unlock(uint256)
            var1 = msg.value;
        
            if (var1) { revert(memory[0x00:0x00]); }
        
            var1 = 0x00bf;
            var2 = msg.data[0x04:0x24];
            unlock(var2);
            stop();
        } else { revert(memory[0x00:0x00]); }
    }

然后是两个函数,这里重点看unlock函数

function unlock(var arg0) {
		#将msg.sender 放在0x00:0x2032字节内存中
        memory[0x00:0x20] = msg.sender;
        
        #0x200x40 赋值为0x00
        memory[0x20:0x40] = 0x00;
    
    	#if里面首先通过keccak256(memory[0x00:0x40])加密了0x000x40的内容,获取了低位的一个字节的内容。这里可以推测出是一个bool类型数据。所以这里所取得数据其实对应源码里面得这一行代码mapping(address => bool) internal authorizedToPlay;
    	#storage[keccak256(memory[0x00:0x40])]等同于authorizedToPlay[msg.sender] 
          if (storage[keccak256(memory[0x00:0x40])] & 0xff) {
            var temp0 = memory[0x40:0x60];
            log(memory[temp0:temp0 + memory[0x40:0x60] - temp0], [0xf0c55d049e61d6dcd81c1f2715f135c87551e981a34759c240851595cfcb38c7, msg.sender]);
        
        #if判断storage[0x01]中0x01插槽存储的内容是否和arg0相等,这里也可以推测这个插槽中存储的就是pin的值
            if (storage[0x01] == arg0) {
                var temp1 = address(this).balance;
                var temp2 = memory[0x40:0x60];
                var temp3;
                
           #这里进行了转账操作,更加确定了推测
                temp3, memory[temp2:temp2 + 0x00] = address(msg.sender).call.gas(!temp1 * 0x08fc).value(temp1)(memory[temp2:temp2 + memory[0x40:0x60] - temp2]);
                var var0 = !temp3;
           
           #是否转账成功
                if (!var0) { return; }
            
                var temp4 = returndata.length;
                memory[0x00:0x00 + temp4] = returndata[0x00:0x00 + temp4];
                revert(memory[0x00:0x00 + returndata.length]);
            } else {
                var temp5 = memory[0x40:0x60];
                memory[temp5:temp5 + 0x20] = 0x08c379a000000000000000000000000000000000000000000000000000000000;
                var temp6 = temp5 + 0x04;
                var temp7 = temp6 + 0x20;
                memory[temp6:temp6 + 0x20] = temp7 - temp6;
                memory[temp7:temp7 + 0x20] = 0x0d;
                var temp8 = temp7 + 0x20;
                memory[temp8:temp8 + 0x20] = 0x496e636f72726563742050494e00000000000000000000000000000000000000;
                var temp9 = memory[0x40:0x60];
                revert(memory[temp9:temp9 + (temp8 + 0x20) - temp9]);
            }
        } else {
            var temp10 = memory[0x40:0x60];
            memory[temp10:temp10 + 0x20] = 0x08c379a000000000000000000000000000000000000000000000000000000000;
            var temp11 = temp10 + 0x04;
            var temp12 = temp11 + 0x20;
            memory[temp11:temp11 + 0x20] = temp12 - temp11;
            memory[temp12:temp12 + 0x20] = 0x9c;
            var temp13 = temp12 + 0x20;
            memory[temp13:temp13 + 0x9c] = code[0x03c2:0x045e];
            var temp14 = memory[0x40:0x60];
            revert(memory[temp14:temp14 + (temp13 + 0xa0) - temp14]);
        }
    }
}
 

直接在控制台交互就行了。

在这里插入图片描述

PiggyBank challenge

考点:CharliesPiggyBank合约重写collectFunds没有使用onlyOwner修饰符,导致不是owner也能调用withdraw从而转移amount。还是考察的一些基础语法知识。考点很简单没啥好说的,这题也没看wp就做出来了。

合约源码如下。简单分析一下,本菜鸡在看完一些基础语法之后也能看懂了。

PiggyBank合约中核心点在于collectFunds方法中,调用了withdraw方法来像调用者账户发送币,但是这里用onlyOwner修饰限制了调用者必须是合约拥有者,再看另外一个合约CharliesPiggyBank,重写了collectFunds方法,但是可以看到用了public修饰为公共方法,并且没有onlyOwner限制。很显然,这里就存在漏洞了,我们直接调用这个函数就行了。

contract PiggyBank{
 
    using SafeMath for uint256;
 
    uint256 public piggyBalance;
    string public name;
    address public owner;
    
    constructor(address _ctfLauncher, address _player, string _name) public payable  
    {
        name=_name;
        owner=msg.sender;
        piggyBalance=piggyBalance.add(msg.value);
    }
    
    function() external payable{
        piggyBalance=piggyBalance.add(msg.value);
    }
 
    
    modifier onlyOwner(){
        require(msg.sender == owner, "Unauthorized: Not Owner");
        _;
    }
 
    function withdraw(uint256 amount) internal{
        piggyBalance = piggyBalance.sub(amount);
        msg.sender.transfer(amount);
    }
 
    function collectFunds(uint256 amount) public onlyOwner{
        require(amount<=piggyBalance, "Insufficient Funds in Contract");
        withdraw(amount);
    }
    
}
 
 
contract CharliesPiggyBank is PiggyBank{
    
    uint256 public withdrawlCount;
    
    constructor(address _ctfLauncher, address _player) public payable PiggyBank(_ctfLauncher, _player, "Charlie") 
    {
        withdrawlCount = 0;
    }
    
    function collectFunds(uint256 amount) public{
        require(amount<=piggyBalance, "Insufficient Funds in Contract");
        withdrawlCount = withdrawlCount.add(1);
        withdraw(amount);
    }
    
}

SI

合约源码

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 {
 
    uint256 public feeAmount;
    uint256 public etherCollection;
    address public developer;
 
    constructor(address _ctfLauncher, address _player) public payable
        
    {
        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{
        purchaseTokens(msg.value);
    }
 
    // Allow users to refund their tokens for half price ;-)
    function refundTokens(uint256 _value) external{
        require(_value>0, "Cannot Refund Zero Tokens");
        transfer(this, _value);
        etherCollection -= _value/2;
        msg.sender.transfer(_value/2);
    }
 
    function withdrawEther() external{
        require(msg.sender == developer, "Unauthorized: Not Developer");
        require(balances[this] == 0, "Only Allowed Once Sale is Complete");
        msg.sender.transfer(etherCollection);
    }
 
}