bg-ctf

Challenge 5: Count my Assembly

In this challenge, you need to construct specific arrays that, when read via inline Assembly, match specific values.

Contract Overview

The contract uses assembly to read from memory pointers data1 and data2.

Hints

Hint 1 In Solidity, the memory variable for a dynamic array (like uint[] memory data2) points to the start of the array data. What is the very first thing stored at that memory location?
Hint 2 The EVM works with 32-byte words. `0xD0` is 208 bytes. - The array length takes up the first 32 bytes. - Array elements start at offset 32 (`0x20`). - Each element is 32 bytes. How is the offset `208` mapped to a specific index in the `data1` array?
Hint 3 mload(p) reads 32 bytes starting at memory address `p`. If `p` is not aligned to a 32-byte boundary (like the start of an array element), it will read bytes from two adjacent elements. Remember that the EVM is Big Endian.

Solution

Click to reveal solution 1. **Calculate `counter2`**: `mload(data2)` reads the first 32 bytes at the memory location of `data2`. For dynamic arrays, this is the **length** of the array. So, `data2` must have a length of `tokenIdCounter % 128`. 2. **Calculate `counter1`**: We need `mload(data1 + 208)` to equal `tokenIdCounter`. Let's trace the memory layout of `data1`: - `data1 + 0`: Length - `data1 + 32`: Index 0 - ... - `data1 + 192` (`0xC0`): Index 5 - `data1 + 224` (`0xE0`): Index 6 The offset is `208` (`0xD0`). This is exactly halfway between Index 5 (`192`) and Index 6 (`224`). `208 = 192 + 16`. `mload` reads 32 bytes. Starting at `data1 + 208` means we read: - The last 16 bytes of `data1[5]` (These become the **high** 128 bits of our result) - The first 16 bytes of `data1[6]` (These become the **low** 128 bits of our result) We want the result to be `tokenIdCounter`. Since this is a small number, the high bits must be 0, and the low bits must be `tokenIdCounter`. - `data1[5]` low bytes -> High bits of Result. Set `data1[5] = 0`. - `data1[6]` high bytes -> Low bits of Result. Since `data1[6]` is a `uint256`, its high bytes are the "beginning" of the word in Big Endian. To place `tokenIdCounter` there, we shift it left by 128 bits. Set `data1[6] = tokenIdCounter << 128`. 3. **Construct the arrays**: ```solidity uint256 target = nftFlags.tokenIdCounter(); // Logic for counter2 uint256[] memory data2 = new uint256[](target % 0x80); // Logic for counter1 uint256[] memory data1 = new uint256[](7); // Size 7 to reach index 6 data1[5] = 0; data1[6] = target << 128; challenge5.mintFlag(data1, data2); ``` Why this works: - You manipulated the array length to satisfy the first assembly read. - You manipulated specific bits across two array elements to construct a specific integer when read from an unaligned memory offset.

Why This Matters

Inline assembly (assembly { ... }) allows developers to bypass Solidity’s safety checks and interact directly with memory and storage. While powerful (and often used for gas optimization), it requires a deep understanding of the EVM.

Misunderstanding offsets or memory pointers can lead to reading “garbage” data or corrupting memory, often leading to bugs that are difficult to track down. Moreover, Solidity’s memory handling is a detail that’s not often thought about, as it is all done for you. This challenge gives you a chance to dig into the specific details of how it really works.