Bingo CTF Writeup

Isopach · November 13, 2020

Attempted this CTF on a workday at 2am. Zero bingos, but learnt a lot during post-CTF discussion so I’m going to write about it. Btw, Misc = Crypto and Forensics.


Misc


KCD

Category: Misc | 20 solves | Easy

Challenge Description KCD : Korean Caesar Dressing
Sometimes you have to learn a foreign language.

HINT1: Find a string with the least strokes!
HINT2: The flag contains Korean characters 'ㅇ' and 'ㅈ'.

We’re given the source.py and output.txt in a Google Drive here.

Analyzing the source, it seems like the key is generated at a range between 0 and 11172.

I’ll start off with saying that my generated list of flags was wrong, so there was no way I could have reached the correct flag.

Wrong Code

for i in range(0,11172):
    key = i
    assert 0 <= key < 11172

    out = ""
    for c in inp:
        if ord('a') <= ord(c) <= ord('z'):
            out += chr(ord('a') + (ord(c) - ord('a') + key) % 26)
        elif ord('A') <= ord(c) <= ord('Z'):
            out += chr(ord('A') + (ord(c) - ord('A') + key) % 26)
        elif ord('가') <= ord(c) <= ord('힣'):
            out += chr(ord('가') + (ord(c) - ord('가') + key) % 11172)
        else:
            out += c

    
    if out[:5] == "Bingo":
        with open("flags.txt","a") as myfile:
                myfile.write(out)

I had incorrectly assumed that you could just do key += 1 to enumerate all the flags, but it was only after the CTF had ended that I sat down with my teammate @waituck and did an analysis of what was wrong with my code, did I realise that korean letters do not work like Caesar’s Cipher.

The correct way was to make a decryption function and then do the following, as seen in the author’s solution.

from jamo import j2hcj, h2j
for key in range(24, 11172, 26):
    hcj = j2hcj(h2j(decrypt(inp, key)))

Because 11172 is not divisible by 26, we cannot simply divide it and have to subtract the key instead.

Also, I was not aware of the jamo library so I did my own implementation of stroke counting by referencing three repos on github.

Credits to neotune for the parser to split korean words into their alphabets, arutarimu for the KR-EN converter which I modified into KR-Stroke conversion, and jeonghanjoo for converting each alphabet into their stroke count.

My additions are basically only the regex to find each digit

sum(map(int, re.findall('\d{1}', str1)))

As no alphabet would be more than a single digit worth of strokes, I only looked for singular digits \d{1}.

And then sum them up in a function find_sum(flag), and then print the flag data[count.index(min(count))].

Here’s the full code to parsing and counting the strokes, which would have solved the challenge if I wasn’t such a crypto newbie.

countstrokes.py

import re
import sys

FIRST_CHAR = ["ㄱ", "ㄲ", "ㄴ", "ㄷ", "ㄸ", "ㄹ", "ㅁ", "ㅂ", "ㅃ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅉ", "ㅊ", "ㅋ", "ㅌ",
              "ㅍ", "ㅎ"]
MID_CHAR = ["ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅘ", "ㅙ", "ㅚ", "ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ",
            "ㅠ", "ㅡ", "ㅢ", "ㅣ"]
FINAL_CHAR = [" ", "ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ", "ㄶ", "ㄷ", "ㄹ", "ㄺ", "ㄻ", "ㄼ", "ㄽ", "ㄾ", "ㄿ", "ㅀ", "ㅁ",
              "ㅂ", "ㅄ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"]
KR_STROKE_DICTIONARY = {'ㄱ':2, 'ㄲ':4, 'ㄴ':2, 'ㄷ':3, 'ㄸ':6, 'ㄹ':5, 'ㅁ':4, 'ㅂ':4, 'ㅃ':8, 'ㅅ':2, 'ㅆ':4, 'ㅇ':1, 'ㅈ':3, 'ㅉ':6, 'ㅊ':4, 'ㅋ':3, 'ㅌ':4, 'ㅍ':4, 'ㅎ':3, 'ㅏ':2, 'ㅐ':3, 'ㅑ':3, 'ㅒ':4, 'ㅓ':2, 'ㅔ':3, 'ㅕ':3, 'ㅖ':4, 'ㅗ':2, 'ㅛ':3, 'ㅜ':2, 'ㅠ':3, 'ㅡ':1, 'ㅣ':1, 'ㅢ':2, 'ㅚ':3,'ㄶ':4, 'ㅙ':5, 'ㅝ':4,'ㅞ':5,'ㄽ':6,'ㄻ':6, 'ㅟ':3,'ㅘ':4,'ㅄ':6,'ㄿ':7,'ㅄ':6}
keys_values = KR_STROKE_DICTIONARY.items()

KR_STROKE_DICTIONARY = {str(key): str(value) for key, value in keys_values}

def find_sum(str1): 
    # Regular Expression that matches 
    # digits in between a string 
    return sum(map(int, re.findall('\d{1}', str1)))
    
def decompose(korean):  # this is a method to break down each characteristic as individual characters.
    word_list = list(korean)
    result = []
    for word in word_list:   # iterate through each characteristic
        if re.match('.*[ㄱ-ㅎㅏ-ㅣ가-힣]', word):   # regex to make sure the characteristic is in Korean.
            char = ord(word) - 44032   # Korean unicode starts from 44032.
            first = int(char / 588)
            result.append(FIRST_CHAR[first])
            mid = int((char - (first * 588)) / 28)
            result.append(MID_CHAR[mid])
            final = int((char - (first * 588) - (mid * 28)))
            if FINAL_CHAR[final] == " ":   # the final character doesn't have to exist, and if doesn't exist, skip.
                pass
            else:
                result.append(FINAL_CHAR[final])
        else:
            result.append(word)
    return result   # keeping the result in a list data structure helps to convert into English letters much easier.


def convert(word_list):   # a simple conversion algorithm using dictionary.
    result = ""
    for i in word_list:
        if i in KR_STROKE_DICTIONARY:
            result += (KR_STROKE_DICTIONARY[i])
        else:
            result += i
    return result


if __name__ == '__main__':
    f = open("flags.txt","r")
    data = f.readlines()
    count = []
    flags = []
    for e in data:
        flag = "".join(convert(decompose(e)))
        flags.append(flag)
        print(flag)
        count.append(find_sum(flag))
    #print(count)
    print(data[count.index(min(count))])
    print(flags[count.index(min(count))])

This will give us a list of potential flags with the fewest stroke count of 55.

Bingo{Did_어_thInk_뎨세_예제_EASY?_쒜세_This_is_예-졔.}
Bingo{Did_유_thInk_디스_이즈_EASY?_예스_This_is_이-지.}
Bingo{Did_좌_thInk_뚀쐐_죠쫴_EASY?_이쐐_This_is_죠-쬬.}
Bingo{Did_째_thInk_러야_쩌챠_EASY?_죠야_This_is_쩌-처.}

Only the second one makes sense when read aloud, so that must be the flag.

FLAG Bingo{Did_유_thInk_디스_이즈_EASY?_예스_This_is_이-지.}

Twitter, Facebook