HTB CyberApocalypse 2025
This writeup consists of challenges I solved and did not solve but looked back on to learn more
Blockchain/Eldorion
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.
Then, deploy the smart contract with
forge create src/Exploit.sol:Exploit --rpc-url $RPC --private-key $PVK
Blockchain/HeliosDEX
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
function swapForMAL() external payable underHeliosEye {
uint256 grossMal = Math.mulDiv(msg.value, exchangeRatioMAL, 1e18, Math.Rounding(1));
function swapForHLS() external payable underHeliosEye {
uint256 grossHLS = Math.mulDiv(msg.value, exchangeRatioHLS, 1e18, Math.Rounding(3));
These 2 token use Math Rounding modes which does
In OpenZeppelin’s Math library, rounding modes are:
0 → Round down (towards zero).
1 → Round up (away from zero).
2 → Round to nearest integer.
3 → Bankers' rounding (round to the nearest even number).
Means you can just give 1 wei and you will be rounded up to 1 token
Blockchain/Eldoria Gate
Analysis
function checkUsurper(address _villager) external returns (bool) {
(uint id, bool authenticated , uint8 rolesBitMask) = kernel.villagers(_villager);
bool isUsurper = authenticated && (rolesBitMask == 0);
emit UsurperDetected(
_villager,
id,
"Intrusion to benefit from Eldoria, without society responsibilities, without suspicions, via gate breach."
);
return isUsurper;
}
}
From the code above, our goal will be to successfully authenticate but not be assigned any roles (bit mask 0).
Vulnerability
function enter(bytes4 passphrase) external payable {
bool isAuthenticated = kernel.authenticate(msg.sender, passphrase);
require(isAuthenticated, "Authentication failed");
uint8 contribution = uint8(msg.value);
(uint villagerId, uint8 assignedRolesBitMask) = kernel.evaluateIdentity(msg.sender, contribution);
string[] memory roles = getVillagerRoles(msg.sender);
emit VillagerEntered(msg.sender, villagerId, isAuthenticated, roles);
}
function evaluateIdentity(address _unknown, uint8 _contribution) external onlyFrontend returns (uint id, uint8 roles) {
assembly {
mstore(0x00, _unknown)
mstore(0x20, villagers.slot)
let villagerSlot := keccak256(0x00, 0x40)
mstore(0x00, _unknown)
id := keccak256(0x00, 0x20)
sstore(villagerSlot, id)
let storedPacked := sload(add(villagerSlot, 1))
let storedAuth := and(storedPacked, 0xff)
if iszero(storedAuth) { revert(0, 0) }
let defaultRolesMask := ROLE_SERF
roles := add(defaultRolesMask, _contribution)
if lt(roles, defaultRolesMask) { revert(0, 0) }
let packed := or(storedAuth, shl(8, roles))
sstore(add(villagerSlot, 1), packed)
}
}
The vulnerability lies in the way the identity is evaluated. The enter() function takes msg.value and casts it to a uint8 value.
let defaultRolesMask := ROLE_SERF
roles := add(defaultRolesMask, _contribution)
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.
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.
pushf
xor
<INST>
popf

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
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
while true:
opcode = chunk[PC]
if DEBUG:
print(f'PC @ 0x{PC:x}')
print(f'[OP={opcode}]')
if opcode == 0:
# store
arg1 = chunk[PC+1]
memory[MP] = regs[arg1]
MP += 1 # memory pointer
print(f'store R{arg1}')
PC += 2 # increment IP

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