Tags: engineering reverse 

Rating:

# challenge overview

the challenge presents a .d64 commodore 64 disk image containing two files:

***decrypt*** — a basic v2 program that loads and executes a decryption routine
***secret*** — a sequential file containing an encrypted message

we are informed that the message in **secret** has been encrypted using the logic implemented in **decrypt**, and that a passphrase is required to unlock it. our objective is to reverse-engineer the decryption process and recover the flag, which follows the format BTSCTF<...>.

## disk image analysis

we begin by examining the disk image using c1541 (a tool from the VICE emulator suite)

```
c1541 disk.d64 -list

0 "bts_disk" 2a
1 "secret" seq
1 "decrypt" prg
```

we extract the files for offline inspection

```
c1541 disk.d64 -read "secret" secret.seq
c1541 disk.d64 -read "decrypt" decrypt.prg
```

## **analyzing the ``decrypt`` program**

``decrypt`` is a commodore basic v2 program that loads and runs automatically. it performs the following actions:

1. opens and reads secret into a variable a$

2. asks the user to enter an 8-character key

3. checks that:

* the key is exactly 8 characters long

* the 8th character is equal to character 6 XOR character 7

4. runs a small embedded 6502 machine code routine to calculate a starting “decrypt key” (we'll call it dk, as in derived/decrypt key)

5. uses that dk in a loop to decrypt the contents of a$, byte by byte

## program control flow analysis

execution starts at line 2 with:
```
2 gosub 1000
4 input b$
10 gosub 4000
```

* line 2 loads the file secret into string variable a$
* line 4 prompts the user to input an 8-character decryption key, stored in b$
* line 10 initiates validation and transformation routines

### loading the encrypted data

the subroutine at line 1000 opens and reads the secret file:

```
1000 print "searching for a secret file on drive 8.."
1001 open 2,8,2,"secret,s,r"
1002 input#2,a$
1003 if st<>0 then print "cannot find a secret file": close 2: end
1004 print "secret file found!"
1005 print "enter decryption key (8 chars)"
1006 poke 6, len(a$)
1008 lli = 0
1009 return
```

the important part here is that the encrypted contents are read into the string a$

### key validation logic

key validation is performed at line 4000:

```
4002 if len(b$) <> 8 then print "invalid key length!": goto 3000
4020 y = asc(mid$(b$,7,1))
4025 gosub 1500
4026 dk = peek(250)
4030 if y <> asc(mid$(b$,8,1)) then print "invalid key!": goto 3000
```

* the key must be exactly 8 characters
* y is set to the ascii value of the 7th character
* the subroutine at line 1500 uses b$[0]..b$[6] to compute a derived value (dk)
* finally, a condition checks that b$[7] == b$[5] xor b$[6]

-> this is the core of the key (secret passphrase) validation. only keys where the 8th byte equals the xor of characters 6 and 7 are accepted.

### machine code routines

the program defines two short 6502 subroutines via a data block at line 10000 and loads them into memory address $c000 (decimal 49152) using

```
2020 for i=0 to 17
2030 read a: poke 49152+i, a
2040 next
```

the first subroutine (at``` $c000```) performs xor:

```
lda $fb ; load operand1
eor $fc ; xor with operand2
sta $fd ; store result
rts
```

the second subroutine (at ```$c007```) performs a 1-bit left shift and merges in a new lsb:

```
asl $fa ; shift accumulator
lda $fb
and #$01
ora $fa
sta $fa
rts
```

### computing the initial decrypt key (dk)

subroutine 1500 computes the initial value used for decryption, stored at ```$fa``` (memory address 250):

```
1501 poke 250,0
1505 poke 251,y ; y = key[6]
1506 sys 49159 ; perform initial shift with key[6]

1510 for x=1 to 6
1515 z = asc(mid$(b$,x,1))
1520 poke 251,z
1530 poke 252,y
1540 sys 49152 ; xor key[x] with y
1546 sys 49159 ; shift result into $fa
1550 y = peek(253)
1560 next x
```

this builds a 7-bit value by combining the least significant bits (LSB) of the first 7 key characters.this is VERY important and a blatant flaw. due to its importance im also inserting equivalent pseudocode :

```
def derive_dk(key):
y = key[6]
result = y & 1
for i in range(6):
x = key[i]
tmp = x ^ y
result = ((result << 1) | (x & 1)) & 0x7f
y = tmp
return result
```

this function determines the initial decryption key, dk ^_^

### decryption logic

the main loop begins at line 30:

```
13 poke 11, tt - 47 ; tt was 165 → value 118 stored at $0b
30 lli = lli + 1
31 xd = asc(mid$(a$,lli,1))
35 if xd > 127 then xd = xd - 160
40 poke 251, xd
41 poke 252, dk
50 sys 49152 ; dk = xd xor dk
51 dk = peek(253)
52 poke 252, dk
53 poke 251, peek(11) ; constant 118
54 sys 49152
98 print chr$(peek(253));
99 if peek(6) <= lli then goto 3000
100 goto 30
```

this loop processes one byte of a$ at a time and decrypts it. inserting equivalent pseudocode :

```
dk = initial_dk
for byte in secret_data:
if byte > 127:
byte -= 160
temp = byte ^ dk
dk = temp
char = temp ^ 118
print(chr(char), end='')
```

we can see that each character is decrypted by applying two xor operations:

1. ```temp = byte xor dk```
2. ```output = temp xor 0x76```

then dk is updated to the last temp for the next iteration. this is a simple xor-based stream cipher seeded by the dk (decrypted key) from the user supplied key
see it now?

### keyspace weakness and brute-force strategy

#### keyspace flaw

* only the least significant bit of each of the first 7 characters affects dk
* this gives only 2⁷ = 128 possible dk values

#### validation weakness

only constraint on characters 6–8 is:

```
key[7] == key[5] xor key[6]
```

the other characters can be freely selected to match a desired lsb pattern

#### brute-force plan

i tested all 128 possible dk values until printable ASCII text was found.

after some bruteforcing... derived ```dk``` turns out to be 0x51

i interpreted ```dk = 0x51``` as the LSB pattern of the key.
this meant that the LSBs of characters 1–7 in the key must be:
```1, 0, 1, 0, 0, 0, 1```

this gives us a constructed key ```122211Ap```

running :

```
with open("secret.seq","rb") as f:
ct = bytearray(b & 0x7f for b in f.read())

key = b"122211Ap"
flag = decrypt(ct, key).decode("ascii", "replace")
print(flag)
```

prints out the flag

```BTSCTF<M0N3Y-I$-HIDDEN-1NSI0E-THE-4M1GA>```

solved by tlsbollei