Processing math: 100%

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

  1. checks that:
  • the key is exactly 8 characters long

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

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

  2. 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