HTB CyberApocalypse 2025

This writeup consists of challenges I solved and did not solve but looked back on to learn more

Blockchain/Eldorion

Setup.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import { Eldorion } from "./Eldorion.sol";

contract Setup {
    Eldorion public immutable TARGET;
    
    event DeployedTarget(address at);

    constructor() payable {
        TARGET = new Eldorion();
        emit DeployedTarget(address(TARGET));
    }

    function isSolved() public view returns (bool) {
        return TARGET.isDefeated();
    }
}
Eldorion.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract Eldorion {
    uint256 public health = 300;
    uint256 public lastAttackTimestamp;
    uint256 private constant MAX_HEALTH = 300;
    
    event EldorionDefeated(address slayer);
    
    modifier eternalResilience() {
        if (block.timestamp > lastAttackTimestamp) {
            health = MAX_HEALTH;
            lastAttackTimestamp = block.timestamp;
        }
        _;
    }
    
    function attack(uint256 damage) external eternalResilience {
        require(damage <= 100, "Mortals cannot strike harder than 100");
        require(health >= damage, "Overkill is wasteful");
        health -= damage;
        
        if (health == 0) {
            emit EldorionDefeated(msg.sender);
        }
    }

    function isDefeated() external view returns (bool) {
        return health == 0;
    }
}

From the setup contract, we can see that the challenge is solved once the target is defeated. However, there is a modifier in the attack function in the eldorion contract. This modifier checks the block timestamp and if the current timestamp is greater than the previous one, the HP will be restored to full. This means we cannot simply call attack() 3 times as we are an Externally Owned Account (EOA) because each attack() is in a different transaction and the timestamp will not match. The only way to solve this is by writing you own smart contract and calling the attack() 3 times as contracts can execute the 3 functions in 1 transaction.

Solve.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "forge-std/Script.sol";
import "../src/Eldorion.sol";

contract Exploit {
    function win(address _target) public {
        Eldorion eldorion = Eldorion(_target);
        eldorion.attack(100);
        eldorion.attack(100);
        eldorion.attack(100);
}
}

Then, deploy the smart contract with

forge create src/Exploit.sol:Exploit --rpc-url $RPC --private-key $PVK

Blockchain/HeliosDEX

Setup.sol
HeliosDEX.sol

This Solidity contract, HeliosDEX, is a decentralized exchange (DEX) for three custom ERC-20 tokens:

  • Eldorion Fang (ELD)

  • Malakar Essence (MAL)

  • Helios Lumina Shards (HLS)

The DEX maintains reserves of each token and has exchange ratios:

  • ELD: 2 ELD per 1 ETH

  • MAL: 4 MAL per 1 ETH

  • HLS: 10 HLS per 1 ETH

Users can swap 1 wei for a token and get a one time refund aswell.

Vulnerability

These 2 token use Math Rounding modes which does

Means you can just give 1 wei and you will be rounded up to 1 token

OfficialSolve.py

Blockchain/Eldoria Gate

Setup.sol
EldoraGate.sol
EldoraGateKernel.sol

Analysis

From the code above, our goal will be to successfully authenticate but not be assigned any roles (bit mask 0).

Vulnerability

The vulnerability lies in the way the identity is evaluated. The enter() function takes msg.value and casts it to a uint8 value.

But the line above is vulnerable to integer overflow because we can donate 255 ethereum and cause it to overflow in the add() because the default role value is 1 and 255 + 1 = will overflow back to 0. Integer overflow and underflow protection was introduced by default starting with Solidity version 0.8.0. However, we can see the evaluateIdentity() uses low level yul assembly to do arithmetic. In assembly, there is no concept of data types and all values are treated as 32 byte values.

OfficialSolve.py

Rev/Single Step

Opening in Ghidra we can see the main function only calls one function

The disassembly does not show anything meaningful. Hence, we will analyse this in gdb.

Now that we reach the breakpoint, we will step 1 by 1 and examine the behaviour of the file.

After the XOR instruction at RIP + 0x1, we can see that valid instructions have appeared. Which means, the binary is doing some metamorphic behaviour which decrypts itself dynamically during runtime. Each valid instruction is put in between a series of decryption instructions.

Going further down, we can see the program re encrypts itself by using RIP+(-15) due to two's complement. Hence, we will need to deobfuscate it by writing our own script

Deobfuscator.py

Rev/Heart Protector

This challenge features virtual machine implementation in the program which has custom instructions created by the author to try and make reversing harder. When dealing with a VM based challenge, it is important to write your own emulator to understand what the VM is d doing. For example

Rev/Gateway

Looking at the main function, we can see its implementing the Heaven's Gate technique whereby its switching execution context from 32 bit to 64 bit during run time. Before the ret far syscall is called, we can see the value 0x33 and sub_80499b9 is pushed onto the stack. This is important because the retfar instruction will pop the top 2 items off the stack. The first value sub_80499b9 is the function that will be executed in 64 bit mode and 0x33 is the value that will go into the CS register. The value in the CS register will determine the execution mode of the CPU

  • 0x33 for 64 bit

  • 0x23 for 32 bit

Binary ninja has a helpful feature which allows you to disassemble functions in another architecture which can help in a polygot binary implementing Heaven's Gate.

Last updated