Tags: engineering reverse
Rating:
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<...>.
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
decrypt
programdecrypt
is a commodore basic v2 program that loads and runs automatically. it performs the following actions:
opens and reads secret into a variable a$
asks the user to enter an 8-character key
the key is exactly 8 characters long
the 8th character is equal to character 6 XOR character 7
runs a small embedded 6502 machine code routine to calculate a starting “decrypt key” (we'll call it dk, as in derived/decrypt key)
uses that dk in a loop to decrypt the contents of a$, byte by byte
execution starts at line 2 with:
2 gosub 1000
4 input b$
10 gosub 4000
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 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
-> 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.
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
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 ^_^
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:
temp = byte xor dk
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?
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
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