DSCTF 初赛+决赛 智能合约题目
DSCTF 初赛+决赛 智能合约题目
初赛 Inject
这题其实在我的星球里面分析过了,当时赣网杯看过了这道字节码填空题,于是看到了这个就非常熟悉。但是由于这题的两次code转换给我绕晕了 没有非常快的解出,在临近结束的时候才拿到flag。这里不在逆向说明了,直接把源码放出来:
pragma solidity ^0.4.23;
contract EasyInject {
address public target = 0x0;
uint public randomnum = 0;
bool public flag = false;
function PayForFlag() public {
address a = target;
a.delegatecall(abi.encodeWithSignature(""));
}
function create_new_target(uint blockNumber) public returns(address addr) {
uint[8] memory a;
a[0] = 0x6300;
a[1] = blockNumber;
a[2] = 0x43;
a[3] = 0x10;
a[4] = 0x58;
a[5] = 0x57;
a[6] = 0x33;
a[7] = 0xff;
uint code = to_code8(a);
uint[10] memory b;
b[0] = 0;
b[1] = 0x7d;
b[2] = code;
b[3] = 0x34;
b[4] = 0x52;
b[5] = 0x600b;
b[6] = 0x6015;
b[7] = 0xf3;
b[8] = 0x33;
b[9] = 0xff;
uint initcode = to_code10(b);
uint sz = GetSize(initcode);
uint offset = 32 - sz;
assembly {
let p := mload(0x40)
mstore(p, initcode)
addr := create(0, add(p, offset), sz)
}
require(addr != 0x0);
target = addr;
}
function to_code8(uint[8] chunks) internal pure returns(uint code) {
for(uint i=chunks.length; i>0; i--) {
code ^= chunks[i-1] << 8 * GetSize(code);
}
}
function to_code10(uint[10] chunks) internal pure returns(uint code) {
for(uint i=chunks.length; i>0; i--) {
code ^= chunks[i-1] << 8 * GetSize(code);
}
}
function GetSize(uint256 chunk) internal pure returns(uint) {
bytes memory b = new bytes(32);
assembly {
mstore(add(b, 32), chunk)
}
for(uint32 i = 0; i< b.length; i++) {
if(b[i] != 0) {
return 32 - i;
}
}
return 0;
}
}
题目目标 是这个: bool public flag = false;
位于slot2 的位置。 而题目给了一个delegatecall的机会。在Payforflag里面。
但是call的合约是什么构成的呢?
首先是:
a[0] = 0x6300; PUSH4 0x00??????
a[1] = blockNumber;
a[2] = 0x43; // 堆块高度 blockNumber
a[3] = 0x10; // LT
a[4] = 0x58; // PC
a[5] = 0x57; // JUMPI
a[6] = 0x33; //msg.sender
a[7] = 0xff; //selfdestruct
其中blocknumber自己可控。然后这个结果 被用于转化to_code8。
因为他将此视为一个只有8个部分的字节码。分别是
PUSH4
参数
Blocknumber
LT
PC
JUMPI
msg.sender
selfdestruct
共8个部分。然后将此整合为一个整数。继续向下填。
b[0] = 0; //stop
b[1] = 0x7d;//PUSH30
b[2] = code;//上面整合好的整数
b[3] = 0x34;//CALLVALUE
b[4] = 0x52;//MSTORE
b[5] = 0x600b; //PUSH1 0x0b
b[6] = 0x6015;// PUSH1 0x15
b[7] = 0xf3; // return
b[8] = 0x33; // msg.sender
b[9] = 0xff; //return
也就是他将上面的参数直接转了下来。用作这里的参数。 利用to_code10
将这段视为10个部分进行转化。
那么在这个不同长度的转化中就出现了问题,在转化8的过程中,如果当前code的部分多于8 那么就会有指令错误的进行偏移。甚至有指令会被抹去。
转10也就同理。
最后他要部署一个合约用于call,那么就一定需要返回东西。所以其内容应该为:
6560016002550034526006601af3
PUSH6 0x600160025500
CALLVALUE //0
MSTORE
PUSH1 0x06
PUSH1 0x1a
RETURN
这样我们就可以部署一个
600160025500
的合约上去。在delegatecall的时候就可以满足条件了。
而这些一共是 14字节。 现在我们需要考虑的就是如何绕过他做为填空的那些字节。这个时候其实我并没有去详细计算怎么样才能让我这里开始运行。而是使用了fuzz 字节长度的办法。 首先我想到的是,PUSH4那里还需要3个字节。首先随便填一个也可以自己用作部署东西的字节:616060
然后后面开始不断的填充60 直到32字节 结合调试去查看我最终部署时候的字节长什么样。
最后在26字节的时候,可以发现其内容刚好可以覆盖前面的所有内容运行
65开头的所有内容。
填入的最后结果为:0x6160606060606060606560016002550034526006601af3000000
决赛 Stake
pragma solidity >=0.8.0;
interface IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint);
function balanceOf(address owner) external view returns (uint);
function allowance(address owner, address spender) external view returns (uint);
function approve(address spender, uint value) external returns (bool);
function transfer(address to, uint value) external returns (bool);
function transferFrom(address from, address to, uint value) external returns (bool);
}
contract CTF { // basic ERC20 with permit
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
uint256 internal immutable INITIAL_CHAIN_ID;
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;
uint randomNumber = 0;
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
_mint(msg.sender, 1e24);
}
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transfer(address to, uint256 amount) public returns (bool) {
balanceOf[msg.sender] -= amount;
unchecked {
balanceOf[to] += amount;
}
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public returns (bool) {
uint256 allowed = allowance[from][msg.sender];
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;
balanceOf[from] -= amount;
unchecked {
balanceOf[to] += amount;
}
return true;
}
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
unchecked {
address recoveredAddress = ecrecover(
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
)
)
),
v,
r,
s
);
require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");
allowance[recoveredAddress][spender] = value;
}
}
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
}
function computeDomainSeparator() internal view virtual returns (bytes32) {
return
keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256("1"),
block.chainid,
address(this)
)
);
}
function _mint(address to, uint256 amount) internal {
totalSupply += amount;
unchecked {
balanceOf[to] += amount;
}
}
function _burn(address from, uint256 amount) internal {
balanceOf[from] -= amount;
unchecked {
totalSupply -= amount;
}
}
}
contract FLAG {
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
address public underlying;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
uint randomNumber = 0;
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals,
address _underlying
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
underlying = _underlying;
}
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transfer(address to, uint256 amount) public returns (bool) {
balanceOf[msg.sender] -= amount;
unchecked {
balanceOf[to] += amount;
}
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public returns (bool) {
uint256 allowed = allowance[from][msg.sender];
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;
balanceOf[from] -= amount;
unchecked {
balanceOf[to] += amount;
}
return true;
}
function deposit(uint amount) external{
IERC20(underlying).transferFrom(msg.sender, address(this), amount);
_mint(msg.sender, amount);
}
function withdraw(uint amount) external{
_burn(msg.sender, amount);
IERC20(underlying).transfer(msg.sender, amount);
}
function _mint(address to, uint256 amount) internal {
totalSupply += amount;
unchecked {
balanceOf[to] += amount;
}
}
function _burn(address from, uint256 amount) internal {
balanceOf[from] -= amount;
unchecked {
totalSupply -= amount;
}
}
fallback() external{}
}
contract staking{
uint a=0;
mapping(address => uint) public stakedAmount;
mapping(address => uint) public startTime;
FLAG public flag;
uint private goal = 800000 * 1e18;
uint public ratePerBlock = 1e16; // 0.01 token per block
uint public max_income = 100 * 1e18; //max token income
bool public claimed = false;
bool public isSolved = false;
uint randomNumber = 0;
function solve() public{
isSolved = flag.balanceOf(msg.sender) >= goal;
}
constructor() {
CTF ctf = new CTF("CTF", "CTF", uint8(18));
flag = new FLAG("FLAG", "FLAG", uint8(18), address(ctf));
ctf.approve(address(flag), type(uint).max);
ctf.approve(address(this), type(uint).max);
flag.deposit(ctf.balanceOf(address(this)));
flag.approve(address(this), type(uint).max);
}
function airdrop() public {
require(claimed == false);
flag.transfer(msg.sender, 10 * 1e18);
claimed = true;
}
function stake(uint amount) public {
uint curBlock = block.number;
if(startTime[msg.sender] == 0){
startTime[msg.sender] = curBlock;
stakedAmount[msg.sender] = amount;
}
else{
uint income = _calculateAccumulateIncome(stakedAmount[msg.sender], startTime[msg.sender], curBlock);
if(income > max_income){
income = max_income;
}
startTime[msg.sender] = curBlock;
stakedAmount[msg.sender] += (income+amount);
}
flag.transferFrom(msg.sender, address(this), amount);
}
function _calculateAccumulateIncome(uint principal, uint start, uint end) internal view returns(uint){
return principal * (end - start) * ratePerBlock / 1e18;
}
function unstake(uint amount) public{
uint curBlock = block.number;
uint principal = stakedAmount[msg.sender];
uint income = _calculateAccumulateIncome(principal, startTime[msg.sender], curBlock);
income = (income > max_income)?max_income:income;
uint userTotalStake = principal + income;
if(amount == type(uint).max){
flag.transfer(msg.sender, userTotalStake);
stakedAmount[msg.sender] = 0;
startTime[msg.sender] = curBlock;
}
else{
require(amount <= userTotalStake, "Nice Try");
stakedAmount[msg.sender] = userTotalStake - amount;
startTime[msg.sender] = curBlock;
flag.transfer(msg.sender, amount);
}
}
function transferUnderlyingWithPermit(address from, address _flag, address to, uint amount, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
address _ctf = FLAG(_flag).underlying();
CTF(_ctf).permit(from, address(this), amount, deadline, v, r, s);
CTF(_ctf).transferFrom(from, to, amount);
}
}
公链题,赛时摆了直接开抄。 但其实漏洞点也很明显。 前两个合约 由于0.8.0 没有任何溢出问题,甚至第一个合约我还找到了相关的原合约进行对比,几乎100%相同。那么只可能出在staking里面。
读一下逻辑,它实现的其实是一个类似于存款 然后获利的一个情景。利用了ERC20代币的效果进行铸币等等操作。
STAKE和UNSTAKE就是模拟存款和取款(或结束投资)时的一些盈利。而我们的目标是要短时间内拿到大量代笔。只可能未授权从铸币者处转钱出来。
这个时候我们关注到了transferUnderlyingWithPermit
这个函数。
他有一个transfrom的方法可以支持任意人进行转账。
但有一个permit操作是EIP-2612: permit – 712-signed approvals
这个提案里面定义的。需要ecrecover签名通过。才能实现任意转账。
我们可以发现 这个方法中的_flag 地址是可控的,那么我们可以将_ctf 的地址就指向flag自己。这只需要实现一个合约即可。将flag放到对应的 underlying上就可以。而permit方法怎么办?flag中并没这个方法,但是他有callback,可以处理任何不满足条件的calldata,所以并不会revert。然后就可以顺理成章的进行转账操作了。从而实现盗窃大量代币的效果。
这也就是其完整攻击链了。