Writeup: Old Calculator - upCTF (Reverse)
Challenge
Found my old TI in a box of high school stuff. Weird, there's a program on here I don't remember installing. Wrap the code in upCTF{...}
We're given a single file: PROG.8xp - a TI-83/84 calculator program.
Flag
upCTF{1F41L3DC4LCF0RTH1S}
Analysis
File Format
The .8xp format is the standard TI-83+ program file. The header identifies it as **TI83F*, generated by SPASM (a popular TI assembler). The type byte 0x06 and the BB 6D (AsmPrgm) token confirm this is a Z80 assembly program, not TI-BASIC.
Memory Layout
By cross-referencing string addresses in LD HL instructions with their file offsets, the base address is determined:
| Component | RAM Address | File Offset |
|---|---|---|
| AsmPrgm token (BB 6D) | $9D93 | 0x4A |
| First instruction (JP $9E57) | $9D95 | 0x4C |
| Encoded data (18 bytes) | $9D98 | 0x4F |
| Encrypted function (57 bytes) | $9DAA | 0x61 |
| "= CODE CHECKER =" | $9DE5 | 0x9C |
| "CORRECT! :)" | $9E13 | 0xCA |
| "WRONG. Try again." | $9E1F | 0xD6 |
Program Structure
The program has four major components:
1. Main UI ($9E57) - Clears the screen, displays "= CODE CHECKER =", "Enter code (CLEAR=reset):", and "> ", then enters a key-reading loop. Accepts digits (keycodes 0x8E-0x97) and letters (0x9A-0xB3), storing up to 18 raw keycodes in a buffer at $86EC. Pressing CLEAR resets input; pressing ENTER triggers verification.
2. Custom Interrupt Handler ($9E31) - The program switches to IM 2 and installs a custom ISR. On each hardware interrupt tick, if input has been entered, the ISR advances an 8-bit LFSR:
SRL A ; shift right, carry = bit 0
JR NC, skip
XOR $B8 ; feedback polynomial
skip:
DJNZ loop ; repeat (input_count) times
The LFSR is seeded with $A5 and stored at $8749. The number of shifts is timing-dependent (based on interrupt ticks between keypresses).
3. Self-Decrypting Verification ($9F18) - When ENTER is pressed with exactly 18 characters:
- The LFSR state is XORed with
$17to produce a decryption key - 57 bytes of encrypted data at $9DAA are XORed with this key
- The result is written to $8710 as executable Z80 code
- The decrypted function is called, then zeroed out (anti-forensics)
4. Decrypted Comparison Function ($8710) - The hidden function performs the actual code check using its own internal LFSR.
Breaking the Encryption
Since the decryption key is timing-dependent, we brute-force all 256 possible XOR keys. Key 0xA7 produces clean, valid Z80 code:
verify:
PUSH DE ; save user input pointer ($86EC)
LD DE, $86FE ; temp buffer
; Decrypt expected keycodes using LFSR
.loop1:
LD A, C ; C starts at $A5
SRL A ; LFSR step
JR NC, .no_xor
XOR $B8 ; polynomial feedback
.no_xor:
LD C, A ; save new state
LD A, (HL) ; load encoded expected byte
XOR C ; decrypt with LFSR state
LD (DE), A ; store decrypted keycode
INC HL / INC DE
DJNZ .loop1
; Compare user input against decrypted expected
LD HL, $86FE
POP DE ; DE = user input at $86EC
.loop2:
LD A, (DE)
CP (HL) ; compare byte-by-byte
JR NZ, .wrong
INC HL / INC DE
DJNZ .loop2
AND A ; clear carry = CORRECT
RET
.wrong:
SCF ; set carry = WRONG
RET
Computing the Code
The verification function decrypts the 18 stored bytes at $9D98 using a self-contained LFSR (seed $A5, polynomial $B8). This is independent of the timing-dependent ISR state - the function carries its own LFSR.
Encoded data: 65 EA 10 CE 3D DD BB 8F 23 45 EC A7 92 A5 AA 1A 6A 66
LFSR seed: $A5
Step: SRL, if carry XOR $B8, then XOR with data byte
Step 1: LFSR=$EA 0x65^0xEA = 0x8F -> '1'
Step 2: LFSR=$75 0xEA^0x75 = 0x9F -> 'F'
Step 3: LFSR=$82 0x10^0x82 = 0x92 -> '4'
Step 4: LFSR=$41 0xCE^0x41 = 0x8F -> '1'
Step 5: LFSR=$98 0x3D^0x98 = 0xA5 -> 'L'
Step 6: LFSR=$4C 0xDD^0x4C = 0x91 -> '3'
Step 7: LFSR=$26 0xBB^0x26 = 0x9D -> 'D'
Step 8: LFSR=$13 0x8F^0x13 = 0x9C -> 'C'
Step 9: LFSR=$B1 0x23^0xB1 = 0x92 -> '4'
Step 10: LFSR=$E0 0x45^0xE0 = 0xA5 -> 'L'
Step 11: LFSR=$70 0xEC^0x70 = 0x9C -> 'C'
Step 12: LFSR=$38 0xA7^0x38 = 0x9F -> 'F'
Step 13: LFSR=$1C 0x92^0x1C = 0x8E -> '0'
Step 14: LFSR=$0E 0xA5^0x0E = 0xAB -> 'R'
Step 15: LFSR=$07 0xAA^0x07 = 0xAD -> 'T'
Step 16: LFSR=$BB 0x1A^0xBB = 0xA1 -> 'H'
Step 17: LFSR=$E5 0x6A^0xE5 = 0x8F -> '1'
Step 18: LFSR=$CA 0x66^0xCA = 0xAC -> 'S'
TI-83+ _GetKey keycodes: 0x8E-0x97 = digits 0-9, 0x9A-0xB3 = letters A-Z.
Result
The code is 1F41L3DC4LCF0RTH1S, which in leet speak reads:
I FAILED CALC FOR THIS
A fitting message for a mysterious program found on an old high school calculator.