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