Course update: lessons, CTF 0x0011a_cb, and documentation

- 0x0011a_cb (Operation Dark Vector): nation-state CTF redesign with an
  AES-128-ECB sealed target and a plaintext launch origin; RP2350 firmware with
  bearing-driven servo, tri-color LEDs, GSV stats, and a realistic no-fix path
- docs: story-driven classified brief, GDB and Ghidra tutorials with deep
  step-throughs, regenerated artifacts and PDFs
- scripts: docstring standard, AES per-student randomizer, telemetry monitor
- week 3 to week 5 lessons: Ghidra patching tutorial, CMSIS-SVD hardware RE,
  double floating-point and GPIO architecture chapters, README structure
This commit is contained in:
Kevin Thomas committed 2026-09-27 14:18:56 -04:00
1 parent 5201ee4b6b
commit 35eacd2c0e
162 files changed
+125658 -232

No files matched your search

+192 -44
View File
@@ -108,26 +108,48 @@ Use this exact process any time you need to encode a decimal float manually.
2. Convert the number to binary.
- Integer part: `42 = 101010 (base 2)`
- $42 = 32 + 8 + 2 = 2^5 + 2^3 + 2^1$
- In 6-bit binary: `101010`
- Fractional part: use repeated multiply-by-2 on the fraction.
- Start with `0.5`
- `0.5 * 2 = 1.0` -> integer part is `1` (this is the first binary fractional bit)
- $0.5 \times 2 = 1.0 \implies$ integer part is `1` (this is the first binary fractional bit)
- Remaining fractional part is now `0.0`, so we stop.
- Therefore `0.5 = 0.1 (base 2)`.
- Combined: `42.5 = 101010.1 (base 2)`
- Combined fixed-point binary: `42.5 = 101010.1 (base 2)`
3. Normalize to the form `1.xxxxx * 2^n`.
- `101010.1 (base 2) = 1.010101 (base 2) * 2^5`
- So the true exponent is `n = 5`.
3. Normalize to scientific form ($1.\text{mantissa} \times 2^n$) - Where the 5 comes from!
- **Why is the exponent 5?** Look at the powers of 2 for the integer part (42):
- $2^4 = 16$
- $2^5 = 32$
- $2^6 = 64$
- Because $32 \le 42 < 64$ (that is, $2^5 \le 42 < 2^6$), the largest power of 2 contained within 42 is $2^{\mathbf{5}}$. This mathematical bound guarantees that when normalized, the exponent **must be 5**.
- Now count the binary point shifts to bring the number into $1.xxxx$ form:
4. Compute the stored exponent (bias 127 for float).
- `stored exponent = n + 127 = 5 + 127 = 132`
- `132` in binary is `10000100` (8 bits).
```
Original position: 1 0 1 0 1 0 . 1 x 2^0 (value: 42.5)
Shift 1 place left: 1 0 1 0 1 . 0 1 x 2^1
Shift 2 places left: 1 0 1 0 . 1 0 1 x 2^2
Shift 3 places left: 1 0 1 . 0 1 0 1 x 2^3
Shift 4 places left: 1 0 . 1 0 1 0 1 x 2^4
Shift 5 places left: 1 . 0 1 0 1 0 1 x 2^5
^
Binary point is now right after the first '1'!
```
- We shifted the binary point **exactly 5 places to the left**, so the true exponent is $n = \mathbf{5}$:
$$101010.1_2 = 1.010101_2 \times 2^{\mathbf{5}}$$
4. Compute the stored exponent (bias 127 for float, 1023 for double).
- For a 32-bit `float`:
$$\text{stored exponent} = n + 127 = 5 + 127 = 132 = 10000100_2$$
- For a 64-bit `double`:
$$\text{stored exponent} = n + 1023 = 5 + 1023 = 1028 = 10000000100_2$$
> Tip: **Why 127?** The exponent field is 8 bits wide, giving $2^8 = 256$ total values. Half of that range should represent negative exponents and half positive. The midpoint is $(2^8 / 2) - 1 = 127$. So a stored exponent of `127` means a real exponent of **0**, values below `127` are negative exponents, and values above `127` are positive exponents. Doubles use an 11-bit exponent field so their midpoint (bias) is $( 2^{11} / 2) - 1 = 1023$ instead.
5. Build the mantissa (fraction bits).
- Take bits after the leading `1.` from `1.010101` -> `010101`
- Pad with zeros to 23 bits:
- Pad with zeros to 23 bits (for 32-bit float):
- `01010100000000000000000`
6. Assemble all fields.
@@ -147,32 +169,42 @@ Given the 32-bit pattern:
- `0 | 10000100 | 01010100000000000000000`
Decode it field by field:
Decode it field by field back to decimal:
1. Sign bit
- Sign bit is `0` -> number is positive.
- So the sign multiplier is `(+1)`.
- Sign bit is `0` -> number is positive: `(+1)`.
2. Exponent field
- Exponent bits are `10000100`.
- Convert to decimal: `10000100 (base 2) = 132`.
- Float bias is `127`, so true exponent is:
- `132 - 127 = 5`.
- Convert to decimal: $10000100_2 = 128 + 4 = 132$.
- Float bias is `127`, so subtract the bias to recover the true exponent:
$$132 - 127 = \mathbf{5}$$
- Recovering **5** tells us the significand was scaled by $2^5$.
3. Mantissa field
- Stored mantissa bits are `01010100000000000000000`.
- IEEE 754 normal numbers use an implicit leading `1`, so significand becomes:
- `1.010101 (base 2)`.
- IEEE 754 normal numbers restore the implicit leading `1.`, so significand becomes:
$$1.010101_2$$
4. Rebuild the value
- Formula: `value = (+1) * 1.010101 (base 2) * 2^5`.
- Shift binary point right by 5:
- `1.010101 * 2^5 = 101010.1 (base 2)`.
4. Rebuild the value (undo the normalization)
- Formula: $\text{value} = (+1) \times 1.010101_2 \times 2^5$.
- Multiplying by $2^5$ shifts the binary point **5 places to the right**:
5. Convert `101010.1 (base 2)` to decimal
- Integer part: `101010 = 32 + 8 + 2 = 42`
- Fraction part: `.1 = 1/2 = 0.5`
- Total: `42 + 0.5 = 42.5`
```
Start: 1 . 0 1 0 1 0 1 x 2^5
Shift 1 place right: 1 0 . 1 0 1 0 1 x 2^4
Shift 2 places right: 1 0 1 . 0 1 0 1 x 2^3
Shift 3 places right: 1 0 1 0 . 1 0 1 x 2^2
Shift 4 places right: 1 0 1 0 1 . 0 1 x 2^1
Shift 5 places right: 1 0 1 0 1 0 . 1 x 2^0 = 101010.1
```
- Resulting fixed-point binary: $101010.1_2$.
5. Convert $101010.1_2$ to decimal
- Integer part: $101010_2 = 32 + 8 + 2 = 42$
- Fraction part: $.1_2 = 1/2 = 0.5$
- Total: $42 + 0.5 = \mathbf{42.5} \checkmark$
So the decoded value is exactly `42.5`.
@@ -423,12 +455,12 @@ r2 (low 32 bits): 0x00000000 = 0000 0000 0000 0000 0000 0000 0000 0000
Laid out as a single 64-bit value with every bit numbered:
```
Bit: 63 62-52 (11 bits) 51-32 (20 bits) 31-0 (32 bits)
+---+-----------------------+------------------------------------------+----------------------------------+
| 0 | 1 0 0 0 0 0 0 0 1 0 0 | 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 | 00000000000000000000000000000000 |
+---+-----------------------+------------------------------------------+----------------------------------+
Sign Exponent (11) Mantissa high 20 bits Mantissa low 32 bits
(from r3 bits 19-0) (from r2, all zero)
Bit: 63 62-52 (11b) 51-32 (20b from r3) 31-0 (32b from r2)
+---+-------------+---------------------+-------------------------+
| 0 | 10000000100 | 01010100000000000000| 00000000...00000000 |
+---+-------------+---------------------+-------------------------+
Sign Exponent Mantissa (High 20) Mantissa (Low 32)
(from r3 bits 19-0) (from r2, all zero)
```
**Step-by-step field extraction:**
@@ -488,7 +520,7 @@ $$\text{real exponent} = \text{stored exponent} - \text{bias}$$
$$\text{real exponent} = 1028 - 1023 = \mathbf{5}$$
This means the number is scaled by $2^5 = 32$. In other words, the mantissa gets shifted left by 5 binary places.
This confirms the number is scaled by $2^5 = 32$. When reconstructing the value, multiplying the significand $1.010101_2$ by $2^5$ shifts the binary point 5 places to the right, recovering $101010.1_2 = 42.5$.
**3. Mantissa - bits 51-0 of the 64-bit value**
@@ -772,7 +804,7 @@ int main(void) {
1. Hold the BOOTSEL button on your Pico 2
2. Plug in the USB cable (while holding BOOTSEL)
3. Release BOOTSEL - a drive called "RPI-RP2" appears
4. Drag and drop `0x000A_intro-to-doubles.uf2` onto the drive
4. Drag and drop `0x0011_double-floating-point-data-type.uf2` onto the drive
5. The Pico will reboot and start running!
### Step 2: Verify It's Working
@@ -809,7 +841,7 @@ Ghidra will open. Now we need to create a new project.
1. Click **File** -> **New Project**
2. Select **Non-Shared Project**
3. Click **Next**
4. Enter Project Name: `0x000A_intro-to-doubles`
4. Enter Project Name: `0x0011_double-floating-point-data-type`
5. Click **Finish**
### Step 5: Import the Binary
@@ -978,12 +1010,12 @@ r2 (low 32 bits): 0x645A1CAC = 0110 0100 0101 1010 0001 1100 1010 1100
Laid out as a single 64-bit value with every bit numbered:
```
Bit: 63 62-52 (11 bits) 51-32 (20 bits) 31-0 (32 bits)
+---+-----------------------+------------------------------------------+------------------------------------------+
| 0 | 1 0 0 0 0 0 0 0 1 0 0 | 0 1 0 1 0 1 0 0 0 0 1 1 0 0 1 1 1 0 1 1 | 01100100010110100001110010101100 |
+---+-----------------------+------------------------------------------+------------------------------------------+
Sign Exponent (11) Mantissa high 20 bits Mantissa low 32 bits
(from r3 bits 19-0) (from r2)
Bit: 63 62-52 (11b) 51-32 (20b from r3) 31-0 (32b from r2)
+---+-------------+---------------------+-------------------------+
| 0 | 10000000100 | 01010100001100111011| 01100100...10101100 |
+---+-------------+---------------------+-------------------------+
Sign Exponent Mantissa (High 20) Mantissa (Low 32)
(from r3 bits 19-0) (from r2)
```
**Step-by-step field extraction:**
@@ -1011,11 +1043,127 @@ Exponent bits: `10000000100`
Convert to decimal: $2^{10} + 2^{2} = 1024 + 4 = 1028$
Subtract the bias (same formula as Part 2 - the bias is 1023 for all doubles):
Subtract the bias (the bias is 1023 for all 64-bit doubles):
$$\text{real exponent} = 1028 - 1023 = \mathbf{5}$$
This means the mantissa gets shifted left by 5 binary places (i.e. multiplied by $2^5 = 32$).
#### Deep Dive: Where Did the Exponent 5 Come From?
If you are wondering why the real exponent is **5**, let's walk through the foundational math step-by-step using `42.5`. This shows both how decimal `42.5` converts into IEEE 754 format, and how that converted value breaks down step-by-step back to `42.5`.
##### 1. Forward Conversion: Decimal 42.5 to IEEE 754
**Step A: The Power-of-2 Bounding Rule (Why 5?)**
Why is the exponent 5 and not 4 or 6? Look at the powers of 2 around the integer part (42):
- $2^4 = 16$
- $2^5 = 32$
- $2^6 = 64$
Because $32 \le 42 < 64$ (that is, $2^{\mathbf{5}} \le 42 < 2^6$), the largest power of 2 contained within 42 is $2^{\mathbf{5}}$. This mathematical bound guarantees that when normalized, the exponent **must be 5**.
**Step B: Convert 42.5 to Fixed-Point Binary**
- Integer part (42):
- $42 - 32 = 10 \implies 2^5$ bit is `1`
- $10 < 16 \implies 2^4$ bit is `0`
- $10 - 8 = 2 \implies 2^3$ bit is `1`
- $2 < 4 \implies 2^2$ bit is `0`
- $2 - 2 = 0 \implies 2^1$ bit is `1`
- $0 \implies 2^0$ bit is `0`
- Result: $42_{10} = 101010_2$
- Fractional part (0.5):
- $0.5 \times 2 = 1.0 \implies$ integer part `1`, remainder `0.0`
- Result: $0.5_{10} = 0.1_2$
- Combined fixed-point binary:
$$42.5_{10} = 101010.1_2$$
**Step C: Normalize to Scientific Notation (Counting the 5 Shifts Left)**
IEEE 754 requires every normal non-zero number to be represented in scientific form:
$$1.\text{mantissa} \times 2^n$$
We start with $101010.1$ and shift the binary point to the left until exactly one non-zero bit (`1`) remains before the point:
```
Original position: 1 0 1 0 1 0 . 1 x 2^0 (value: 42.5)
Shift 1 place left: 1 0 1 0 1 . 0 1 x 2^1
Shift 2 places left: 1 0 1 0 . 1 0 1 x 2^2
Shift 3 places left: 1 0 1 . 0 1 0 1 x 2^3
Shift 4 places left: 1 0 . 1 0 1 0 1 x 2^4
Shift 5 places left: 1 . 0 1 0 1 0 1 x 2^5
^
Point is now immediately after the first 1!
```
Notice we shifted the binary point **exactly 5 places to the left**. That shift count is our true exponent:
$$101010.1_2 = 1.010101_2 \times 2^{\mathbf{5}}$$
**That is exactly where the 5 comes from!**
**Step D: Add the Exponent Bias**
IEEE 754 adds a fixed bias so exponents are stored as unsigned positive integers:
- For `float` (bias 127): $\text{stored exponent} = 5 + 127 = 132 = 10000100_2$
- For `double` (bias 1023): $\text{stored exponent} = 5 + 1023 = 1028 = 10000000100_2$
**Step E: Assemble the 64-Bit Double Pattern for 42.5**
- Sign (1 bit): `0` (positive)
- Exponent (11 bits): `10000000100` (1028)
- Mantissa (52 bits): `010101` followed by 46 zeros (the leading `1.` is implied)
- Register distribution:
- `r3` (high 32 bits): `0x40454000`
- `r2` (low 32 bits): `0x00000000`
##### 2. Reverse Breakdown: IEEE 754 Double Back to Decimal 42.5
Now take `r3 = 0x40454000` and `r2 = 0x00000000` and break it down step-by-step back to `42.5`:
**Step A: Recover the Real Exponent 5**
- Extract the 11 exponent bits from `r3` (bits 30-20):
$$\text{Stored exponent} = 10000000100_2 = 1024 + 4 = 1028$$
- Subtract the 1023 bias:
$$\text{Real Exponent} = 1028 - 1023 = \mathbf{5}$$
Subtracting 1023 recovers our shift count of **5**, telling us the significand was scaled by $2^{\mathbf{5}}$.
**Step B: Restore the Significand (Reattach Implicit 1)**
- Stored mantissa bits from `r3`: `01010100000...`
- Reattach the implicit leading `1.`:
$$\text{Significand} = 1.010101_2$$
**Step C: Multiply by $2^5$ (Shift Binary Point 5 Places Right)**
$$\text{Value} = 1.010101_2 \times 2^{\mathbf{5}}$$
Multiplying by $2^5$ shifts the binary point **5 places to the right**:
```
Start: 1 . 0 1 0 1 0 1 x 2^5
Shift 1 place right: 1 0 . 1 0 1 0 1 x 2^4
Shift 2 places right: 1 0 1 . 0 1 0 1 x 2^3
Shift 3 places right: 1 0 1 0 . 1 0 1 x 2^2
Shift 4 places right: 1 0 1 0 1 . 0 1 x 2^1
Shift 5 places right: 1 0 1 0 1 0 . 1 x 2^0 = 101010.1
```
The un-normalized fixed-point binary representation is:
$$101010.1_2$$
**Step D: Convert Fixed-Point Binary to Decimal**
- Integer part (`101010`):
- $1 \times 2^5 = 32$
- $0 \times 2^4 = 0$
- $1 \times 2^3 = 8$
- $0 \times 2^2 = 0$
- $1 \times 2^1 = 2$
- $0 \times 2^0 = 0$
- Sum: $32 + 8 + 2 = 42$
- Fractional part (`.1`):
- $1 \times 2^{-1} = 0.5$
- Final Total:
$$42 + 0.5 = \mathbf{42.5} \checkmark$$
##### 3. Why 42.52525 Shares the Exact Same Exponent 5
Now return to `42.52525` in our binary (`r3 = 0x4045433B`):
- The integer part of `42.52525` is still **42**.
- Since $32 \le 42.52525 < 64$ ($2^{\mathbf{5}} \le 42.52525 < 2^6$), the largest power of 2 fitting in the number is still $2^{\mathbf{5}}$.
- Normalizing $101010.10000110011101..._2$ to $1.xxxx...$ still requires shifting the binary point **5 places to the left**:
$$1.0101010000110011101..._2 \times 2^{\mathbf{5}}$$
- Therefore, the stored exponent in `r3` is identically $5 + 1023 = 1028$ (`10000000100`), producing the exact same `0x404...` in the upper 12 bits of `r3`!
- The only difference between `42.5` (`0x40454000_00000000`) and `42.52525` (`0x4045433B_645A1CAC`) is in the remaining mantissa bits that capture the fractional difference between `0.5` and `0.52525`.
**3. Mantissa - bits 51-0**
@@ -1282,10 +1430,10 @@ fav_num: 99.990000
| - Split into high/low words |
+-----------------------------------------------------------------+
| 5. Patch the constant(s) in Ghidra |
| - Edit bytes in the Bytes window (Pencil mode) |
| - Edit bytes in the Bytes window (Pencil mode) |
| - Replace the old encoding with the new one |
+-----------------------------------------------------------------+
| 6. Export -> Convert to UF2 -> Flash -> Verify |
| 6. Export -> Convert to UF2 -> Flash -> Verify |
| - Same workflow as integer patching |
+-----------------------------------------------------------------+
```