jayjay7777.github.io

View My GitHub Profile

Write-up - NorthSec 2021 CTF - may 2021

Warning

Please note that I’m neither a programmer, nor a pentester, nor a native english speaker.

Intro

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!

Scoreboard / team SummerJedi

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.

The challenges

The Cabal

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:

  1. the key length was 28
  2. each character was tested individually
  3. the answer to the universe is still 42

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.

Trivia Desjardins

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.

Crapto Century

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:

  1. Initialization
  2. Each character is converted to its decimal value, then to its binary value. The binary value is added to the preSwapBinaryText variable.
  3. While the length of preSwapBinaryText is under 70, it adds some bnary value
  4. For each bloc of 4 bit, it swaps the third and the forth characters
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