Please note that I’m neither a programmer, nor a pentester, nor a native english speaker.
I regularly participate to some CTFs, online and on-site. It was my 4th experience at NorthSec.
Due to Covid-19, the event was 100% online. Still, the yearly event stays the most immersive and the most complete CTF I’m participating to. There is always a main scenario with many different tracks, including: trivia, RE, web, crypto, hardware (badge hacking), programming, forensics, (when on-site) IRL investigation…
The NSEC team always delivers, they are doing an impressing job: check nsec.io! I’m looking forward to the next NorthSec!
With the team SummerJedi team, we ended 33 on 84 (nearly 30 on 84, as we unlocked a 3pts flag, but 15 min late :P ). We are satisfied with the result as, we were ~6 people while the max was 20, amd, none of us is a professionnal pentester.
We are provided with one file: challenge.cabal. First, I’m checking the content and the file type:
jayjay@kalisse:~$ more challenge.cabal
@@@@@@@��������������@��������K%@@@@@@@�������`��K@���`����K%@@@@@@@%@@@@@@@����@��������K%@@@@@@@�������`�������@
�������K%@@@@@@@��@���`���@@@@@@@@@@�����@@@��K%@@@@@@@��@������`��`����@@@�����@@@��K%@@@@@@@��@����`�����@@@@@@@
���@@@@@��K%@@@@@@@��@����`���@@@@@@@@@���@@@@@�M���`���]K%@@@@@@@��@���`�����K%@@@@@@@@@@@��@���`�����@@@@���@@@@
@�M��]@@@@������@���`���@�����K@@@@@%@@@@@@@��@��`�����@@@@@@@@@���@@@@@�M�]@@@@@����K%@@@@@@@��@����@@@@@@@@@@@@@...
jayjay@kalisse:~$ file challenge.cabal
challenge.cabal: EBCDIC text
EBCDIC is an old character encoding made by IBM. I quickly found a EBCDIC converter : dcode - codage-ebcdic that takes decimals or hex as an input.
I extract the hex:
jayjay@kalisse:~$ xxd -p challenge.cabal
40404040404040c9c4c5d5e3c9c6c9c3c1e3c9d6d540c4c9e5c9e2c9d6d5
4b2540404040404040d7d9d6c7d9c1d460c9c44b40c7c5e360c6d3c1c74b
25404040404040402540404040404040c4c1e3c140c4c9e5c9e2c9d6d54b
2540404040404040e6d6d9d2c9d5c760e2e3d6d9c1c7c540e2c5c3e3c9d6...
Dcode.fr converts it to a COBOL script:
IDENTIFICATION DIVISION.
PROGRAM-ID. GET-FLAG.
DATA DIVISION.
WORKING-STORAGE SECTION.
78 KEY-LEN VALUE 28.
78 ANSWER-OF-LIFE VALUE 42.
01 CHAR-INDEX PIC 99.
01 USER-KEY PIC X(KEY-LEN).
01 KEY-TABLE.
05 KEY-VALUE PIC X(09) OCCURS KEY-LEN TIMES.
01 IS-VALID PIC 9(5) COMP.
01 ARG1 PIC 9(5) COMP.
01 ARG2 PIC 9(5) COMP.
01 RETRN PIC 9(5) COMP.
01 RSD1 PIC 9(5) COMP.
01 RSD2 PIC 9(5) COMP.
01 QTN1 PIC 9(5) COMP.
01 QTN2 PIC 9(5) COMP.
01 BIT-VAL PIC 9(5) COMP.
PROCEDURE DIVISION.
000-MAIN.
DISPLAY "PLEASE ENTER THE KEY ("KEY-LEN" chars)".
ACCEPT USER-KEY.
PERFORM 001-SET-FLAG-KEY.
MOVE 1 TO IS-VALID
MOVE 1 TO CHAR-INDEX.
PERFORM UNTIL CHAR-INDEX > KEY-LEN
COMPUTE ARG1 = FUNCTION ORD(USER-KEY(CHAR-INDEX:1)) - 1
MOVE KEY-VALUE(CHAR-INDEX) TO ARG2
PERFORM 002-MAGIC-OP
IF RETRN IS NOT EQUAL TO ANSWER-OF-LIFE
MOVE 0 TO IS-VALID
END-IF
ADD 1 TO CHAR-INDEX
END-PERFORM.
IF IS-VALID IS EQUAL TO 1
DISPLAY "VALID KEY ENTERED. WELCOME TO THE CABAL."
ELSE
DISPLAY "INVALID KEY ENTERED."
END-IF.
STOP RUN.
001-SET-FLAG-KEY.
MOVE 108 TO KEY-VALUE(1).
MOVE 102 TO KEY-VALUE(2).
MOVE 107 TO KEY-VALUE(3).
MOVE 109 TO KEY-VALUE(4).
MOVE 7 TO KEY-VALUE(5).
MOVE 105 TO KEY-VALUE(6).
MOVE 101 TO KEY-VALUE(7).
MOVE 104 TO KEY-VALUE(8).
MOVE 101 TO KEY-VALUE(9).
MOVE 102 TO KEY-VALUE(10).
MOVE 27 TO KEY-VALUE(11).
MOVE 121 TO KEY-VALUE(12).
MOVE 126 TO KEY-VALUE(13).
MOVE 98 TO KEY-VALUE(14).
MOVE 111 TO KEY-VALUE(15).
MOVE 105 TO KEY-VALUE(16).
MOVE 107 TO KEY-VALUE(17).
MOVE 104 TO KEY-VALUE(18).
MOVE 107 TO KEY-VALUE(19).
MOVE 102 TO KEY-VALUE(20).
MOVE 108 TO KEY-VALUE(21).
MOVE 106 TO KEY-VALUE(22).
MOVE 124 TO KEY-VALUE(23).
MOVE 101 TO KEY-VALUE(24).
MOVE 120 TO KEY-VALUE(25).
MOVE 99 TO KEY-VALUE(26).
MOVE 126 TO KEY-VALUE(27).
MOVE 25 TO KEY-VALUE(28).
002-MAGIC-OP.
MOVE 1 TO BIT-VAL.
MOVE ZERO TO RETRN.
IF ARG1 IS NOT EQUAL TO ZERO OR ARG2 IS NOT EQUAL TO ZERO
PERFORM 003-MAGIC-OP-SUB
UNTIL ARG1 IS EQUAL TO ZERO AND ARG2 IS EQUAL TO ZERO.
003-MAGIC-OP-SUB.
DIVIDE ARG1 BY 2 GIVING QTN1.
COMPUTE RSD1 = ARG1 - QTN1 * 2.
DIVIDE ARG2 BY 2 GIVING QTN2.
COMPUTE RSD2 = ARG2 - QTN2 * 2.
IF RSD1 IS NOT EQUAL TO RSD2 THEN
ADD BIT-VAL TO RETRN
END-IF.
MULTIPLY BIT-VAL BY 2 GIVING BIT-VAL.
MOVE QTN1 TO ARG1.
MOVE QTN2 TO ARG2.
Now we can make a link with the challenge title. My first reaction was: “NOOOOO, I don’t want to COBOL anything…”, I studied it at university and I didn’t like it much :D
I found an online compiler and I played a bit with the code by printing some values. I understood that:
I tested a random “FLAG-AAAAAAAAAAAAAAAA”, and by printing values I could validate that the flag was beginning with “FLAG-“. As there are just 20 remaining characters to decode (some characters are used more than once), we can manually test characters, or reverse the program.
Mea Culpa, I did it manually.
Still, I made a python resolver after the CTF that finds the flag all alone. I translated all COBOL instructions to python3. Instead of checking the user input, for each new char, I test all of the printable characters until finding THE Answer to the Universe:
import string
def magic_op_sub(arg1, arg2, retrn, bit_val):
qtn1 = arg1 // 2
rsd1 = arg1 - qtn1 * 2
qtn2 = arg2 // 2
rsd2 = arg2 - qtn2 * 2
if rsd1 != rsd2:
retrn = retrn + bit_val
bit_val = bit_val * 2
arg1 = qtn1
arg2 = qtn2
return arg1, arg2, retrn, bit_val
def magic_op (arg1, arg2):
bit_val = 1
retrn = 0
while arg1 != 0 or arg2 != 0:
arg1, arg2, retrn, bit_val = magic_op_sub (arg1, arg2, retrn, bit_val)
return retrn
key_len=28
answer_of_life=42
user_key=input ('PLEASE ENTER THE KEY ("KEY-LEN" chars): ')
key_value=[108,102,107,109,7,105,101,104,101,102,27,121,126,98,111,105,107,104,107,102,108,106,124,101,120,99,126,25]
is_valid=1
char_index=0
retrn=''
result=''
while char_index < key_len:
arg2 = key_value[char_index]
for char in string.printable:
if magic_op(ord(char), arg2) == answer_of_life:
result += char
print (result)
break
char_index += 1
if is_valid == 1:
print ("VALID KEY ENTERED. WELCOME TO THE CABAL.")
else:
print ("INVALID KEY ENTERED.")
Here is the final result:
jayjay@kalisse:~$ python3 cabal_to_python.py
PLEASE ENTER THE KEY ("KEY-LEN" chars):
F
FL
FLA
FLAG
FLAG-
FLAG-C
FLAG-CO
FLAG-COB
FLAG-COBO
FLAG-COBOL
FLAG-COBOL1
FLAG-COBOL1S
FLAG-COBOL1ST
FLAG-COBOL1STH
FLAG-COBOL1STHE
FLAG-COBOL1STHEC
FLAG-COBOL1STHECA
FLAG-COBOL1STHECAB
FLAG-COBOL1STHECABA
FLAG-COBOL1STHECABAL
FLAG-COBOL1STHECABALF
FLAG-COBOL1STHECABALF@
FLAG-COBOL1STHECABALF@V
FLAG-COBOL1STHECABALF@VO
FLAG-COBOL1STHECABALF@VOR
FLAG-COBOL1STHECABALF@VORI
FLAG-COBOL1STHECABALF@VORIT
FLAG-COBOL1STHECABALF@VORIT3
VALID KEY ENTERED. WELCOME TO THE CABAL.
We are provided with a picture “Alphonse Dorimene”.
Seeing the picture, we clearly see that the top has been scrambled. I first thought about fnding the original pic in order to do a diff with the provided one.
It was simpler. I checked the top content with hexdump:
jayjay@kalisse:~$ hexdump -C Alphonse_Dorimene.jpeg |more
[...]
00000300 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b ff dd 00 |................|
00000310 04 00 20 ff da 00 0c 03 01 00 02 11 03 11 00 46 |.. ............F|
00000320 4c 41 47 7b 64 33 73 6a 30 72 64 31 6e 35 64 66 |LAG{d3sj0rd1n5df|
00000330 67 37 36 35 7d 3f 00 fe 17 12 2f 2a 19 91 99 a6 |g765}?..../*....|
00000340 79 23 6d cc a7 7f 72 7a 71 8e b5 c4 ea 2f 0b 96 |y#m...rzq..../..|
[...]
It could be resolved with strings too.
We are provided with two files:
The code uses some specifc language functions like SalStrLength
.
It was enough to determine that the language was the “.NET server-side application language” or SAL.
I converted the SAL script to python3. I separated it in 4 blocs:
def encrypt (clearPassword):
## BLOC 1
binaryNumber=''
preSwapBinaryText=''
encryptPassword=''
charASCIINum=0
length=0
origLength=0
swapPosition=0
position=0
index=0
length = len( clearPassword )
origLength = length
## BLOC 2
while index < length:
charASCIINum = ord( clearPassword[index] )
binaryNumber = bin( charASCIINum )[2:]
preSwapBinaryText = preSwapBinaryText + binaryNumber
index = index + 1
length = len( preSwapBinaryText )
print (length)
# ==259
## BLOC 3
while len( preSwapBinaryText ) < 70:
binaryNumber = bin( 90 + int(str(int(len(preSwapBinaryText)/2))[:2] ))[2:]
preSwapBinaryText = preSwapBinaryText + binaryNumber
## BLOC 4
swapPosition = 3 # Simplified as the result was always the same
length = len( preSwapBinaryText )
index = 0
position=0
while True:
encryptPassword = encryptPassword + preSwapBinaryText[position:position+2]
position = position + 2
encryptPassword = encryptPassword + preSwapBinaryText[position+1:position+2]
encryptPassword = encryptPassword + preSwapBinaryText[position:position+1]
position = position + 2
if position >= length:
break
return encryptPassword
In order to decode the cipher, we need to invert the execution of the blocs and the different operations: 4, 3, 2.
I made a function to deswap the 3rd and 4th bits of each 4-bits bloc:
def deswap(binaire):
i=0
res=''
while i < len (binaire):
res+=binaire[i:i+2]
i=i+2
res+=binaire[i+1:i+2]
res+=binaire[i:i+1]
i=i+2
return res
My comprehension of the bloc 3 was not immediate. We understood that we didn’t have to invert it: in our case the cipher is +200 binary long, while BLOC3 processing requires it to be under 70.
In the end we need to cut the cipher and convert each bloc to a character. I first tried to cut to 8bit long, it was not working. Then, I cut it to 7bit long.
f=open('crapto_century.cipher','r')
data=f.read()
f.close()
data=deswap(data)
while len (data)>1:
result +=chr(int(data[-7:],2))
data=data[:-7]
print (result)
Here is the final result:
jayjay@kalisse:~$ python3 crapto.py
01326d7c229f121b91283e47da3e4bbaaea4-GALF
jayjay@kalisse:~$ python3 crapto.py |rev
FLAG-4aeaabb4e3ad74e38219b121f922c7d62310