yoshikingのがんばる日記

あたまよくないけどがんばります

Securinets CTF Quals 2019 writeup

 3/24~25にSecurinets CTFが開催されており、参加しました. チームzer0ptsとして参加し、結果は23393ptsで2位でした.

 2位!!!!!

 すごいですね(他人事感).例に漏れず、チームメンバーがつよつよでした. 今回はinsecureではなく、theoldmoon0602, ptr-yudai, st98プロと僕の4人でzer0ptsというチームで出ました.

 僕がフラグを提出した問題は、Sanity Checkぐらいなんですが... まあ、先に解いてるけど、空いた(手をつける問題がない)時間に解いた問題もあるので、それらのwriteupを書こうと思います. あとは、部分的に手伝った問題とかですね.

[Crypto] Useless Admin (869pts)

 OTP(One Time Password)を使ってるよ的な問題文と以下のファイルcipher.jsonが渡されます.

{
    "cipher_list": [
        "1b0605000e14000d1b524802190b410700170e10054c11480807001806004e4f1f4f01480d411400531158141e1c100016535a480c000c031a000a160d421e004113010f13451e0c0100100a020a1a4e165f500d0c1e041a090b001d0515521c0a0410000a4f4b4d1d1c184d071600071c0a521d1706540940",
        "1e10524e001f11481c010010070b13024f0704590903094d0c000e4f0711000615001911454217161a1a45040149000a5218404f1e0012060b1b590a1048171741140c01174c0d49174f0c8d4fc7520211531b0b0c1e4f",
        "1d0c04451352001a000154431b014109450a0a0b000045490403520a1d16490008535848085942071c0d0c57101c0045111c40430c4e111c0b1b1c451d4f071712010508475518061d00060a1b0a1a4c165d",
        "160d074300061d071b524e06190b134e450a0b0a4d4c12411d004f014045491b4649074804001100011d4504520612451e165d53064e164e1d060d0d44541a0041031b0b06540d1a070004001d4b074800531c04101d4f",
        "1a1d524912521548120045021b4e1506490a0859150345531d12521b4e094909030003011148420453074d161e05540b071e4c451b000a084a1d1c04084c0b45060b060a4742070618534218070210484512020043100e191e5956111a1c001c1f0b5c",
        "1a1d5248000154041a1c47430d0b04000005015900140c4f04534f094e08490103000000045442111b11001b1b1d000917535a48004e021d4a0e0b0044491c03080a001a024c11490748074f02040054451a1d150c1b150d020d0e",
        "1a1d5249125215481613500a1b0f0d4e4d0d1c0d000700001d1c001b06004f1d0f5a11480745040a011100181c0c540d13000e44085404404a061716014e010c0308104e084e0d4911450506011853540a5304120a1a154c0a1843001b45541c481607051b431f480d001e0400000c531d01011d00124441010200190d0800000000000e54060001100a1b4d0b040d105347",
        "0a0607000913020d551300041d0f0f0a0003061f154c034f1b53530602004e0c030c541f0454110a1d5a001e0649190419165d00104f104e1b1a101101001b0b1705051b0642040c5341114f0e4b104f0803110b0a060f42",
        "160d074300061d071b524e06190b134e450a0b0a4d4c12411d004f014045491b4649074804001100011d4504520612451e165d53064e16424a1810110c00060d04440e1c02411c0c00544209001953540d165009021a1542",
        "1e10524e001f11481c010010070b13024f0704590903094d0c000e4f0711000615001911454217161a1a45040149000a5218404f1e0012060b1b590a1048171741140c01174c0d49174f4201001f534b0b1c074b",
        "1a49134d4113540a0713490d434e160f541700174f4c11480c53520a1d1100000000190d4549114512544d12000c540402034b4e0d491d40"
    ],
    "cipher_flag": "1a4905410f06110c55064f430a00054e540c0a591603174c0d5f000d1b110006414c1848164516111f1100111d1b54001c17474e0e001c011f1d0a4b"
}

 OTPを使った問題の経験がなかったので、とりあえず調べた結果、こちらのサイト

dann.com.br

を見つけました.サイトを参照してもらえればわかるんですが、ほとんど同じなので今回の問題にしたものにコードを変更します.

import string
import collections
import sets, sys

ciphers = ["1b0605000e14000d1b524802190b410700170e10054c11480807001806004e4f1f4f01480d411400531158141e1c100016535a480c000c031a000a160d421e004113010f13451e0c0100100a020a1a4e165f500d0c1e041a090b001d0515521c0a0410000a4f4b4d1d1c184d071600071c0a521d1706540940",
        "1e10524e001f11481c010010070b13024f0704590903094d0c000e4f0711000615001911454217161a1a45040149000a5218404f1e0012060b1b590a1048171741140c01174c0d49174f0c8d4fc7520211531b0b0c1e4f",
        "1d0c04451352001a000154431b014109450a0a0b000045490403520a1d16490008535848085942071c0d0c57101c0045111c40430c4e111c0b1b1c451d4f071712010508475518061d00060a1b0a1a4c165d",
        "160d074300061d071b524e06190b134e450a0b0a4d4c12411d004f014045491b4649074804001100011d4504520612451e165d53064e164e1d060d0d44541a0041031b0b06540d1a070004001d4b074800531c04101d4f",
        "1a1d524912521548120045021b4e1506490a0859150345531d12521b4e094909030003011148420453074d161e05540b071e4c451b000a084a1d1c04084c0b45060b060a4742070618534218070210484512020043100e191e5956111a1c001c1f0b5c",
        "1a1d5248000154041a1c47430d0b04000005015900140c4f04534f094e08490103000000045442111b11001b1b1d000917535a48004e021d4a0e0b0044491c03080a001a024c11490748074f02040054451a1d150c1b150d020d0e",
        "1a1d5249125215481613500a1b0f0d4e4d0d1c0d000700001d1c001b06004f1d0f5a11480745040a011100181c0c540d13000e44085404404a061716014e010c0308104e084e0d4911450506011853540a5304120a1a154c0a1843001b45541c481607051b431f480d001e0400000c531d01011d00124441010200190d0800000000000e54060001100a1b4d0b040d105347",
        "0a0607000913020d551300041d0f0f0a0003061f154c034f1b53530602004e0c030c541f0454110a1d5a001e0649190419165d00104f104e1b1a101101001b0b1705051b0642040c5341114f0e4b104f0803110b0a060f42",
        "160d074300061d071b524e06190b134e450a0b0a4d4c12411d004f014045491b4649074804001100011d4504520612451e165d53064e16424a1810110c00060d04440e1c02411c0c00544209001953540d165009021a1542",
        "1e10524e001f11481c010010070b13024f0704590903094d0c000e4f0711000615001911454217161a1a45040149000a5218404f1e0012060b1b590a1048171741140c01174c0d49174f4201001f534b0b1c074b",
        "1a49134d4113540a0713490d434e160f541700174f4c11480c53520a1d1100000000190d4549114512544d12000c540402034b4e0d491d40"]

target_cipher = "1a4905410f06110c55064f430a00054e540c0a591603174c0d5f000d1b110006414c1848164516111f1100111d1b54001c17474e0e001c011f1d0a4b"

def strxor(a, b):
    return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)])

# To store the final key
final_key = [None]*150
# To store the positions we know are broken
known_key_positions = set()

# For each ciphertext
for current_index, ciphertext in enumerate(ciphers):
    counter = collections.Counter()
    # for each other ciphertext
    for index, ciphertext2 in enumerate(ciphers):
        if current_index != index: # don't xor a ciphertext with itself
            for indexOfChar, char in enumerate(strxor(ciphertext.decode('hex'), ciphertext2.decode('hex'))): # Xor the two ciphertexts
                # If a character in the xored result is a alphanumeric character, it means there was probably a space character in one of the plaintexts (we don't know which one)
                if char in string.printable and char.isalpha(): counter[indexOfChar] += 1 # Increment the counter at this index
    knownSpaceIndexes = []

    # Loop through all positions where a space character was possible in the current_index cipher
    for ind, val in counter.items():
        # If a space was found at least 7 times at this index out of the 9 possible XORS, then the space character was likely from the current_index cipher!
        if val >= 7: knownSpaceIndexes.append(ind)
    #print knownSpaceIndexes # Shows all the positions where we now know the key!

    # Now Xor the current_index with spaces, and at the knownSpaceIndexes positions we get the key back!
    xor_with_spaces = strxor(ciphertext.decode('hex'),' '*150)
    for index in knownSpaceIndexes:
        # Store the key's value at the correct position
        final_key[index] = xor_with_spaces[index].encode('hex')
        # Record that we known the key at this position
        known_key_positions.add(index)

# Construct a hex key from the currently known key, adding in '00' hex chars where we do not know (to make a complete hex string)
final_key_hex = ''.join([val if val is not None else '00' for val in final_key])
# Xor the currently known key with the target cipher
output = strxor(target_cipher.decode('hex'),final_key_hex.decode('hex'))

print "Fix this sentence:"
print ''.join([char if index in known_key_positions else '*' for index, char in enumerate(output)])+"\n"

# WAIT.. MANUAL STEP HERE 
# This output are printing a * if that character is not known yet
# fix the missing characters like this: "Let*M**k*ow if *o{*a" = "cure, Let Me know if you a"
# if is too hard, change the target_cipher to another one and try again
# and we have our key to fix the entire text!

#sys.exit(0) #comment and continue if u got a good key

target_plaintext = "i wanted to end the world, but i'll settle for ending yours."
print "Fixed:"
print target_plaintext+"\n"

key = strxor(target_cipher.decode('hex'),target_plaintext)

print "Decrypted msg:"
for cipher in ciphers:
    print strxor(cipher.decode('hex'),key)

print "\nPrivate key recovered: "+key+"\n"
Fix this sentence:
* *anted to ind t** y*rl*, but i'*l*settle for endin* ****s.

Fixed:
i wanted to end the world, but i'll settle for ending yours.

Decrypted msg:
how often have i said that when you have excluded the imposs
my name is sherlock holmes. it is my business to know what o
never trust to general impressions, my boy, but concentrate 
education never ends, watson. it is a series of lessons with
it is a great thing to start life with a small number of rea
it has long been an axiom of mine that the little things are
it is a capital mistake to theorize before one has data. ins
you have a grand gift for silence, watson. it makes you quit
education never ends, watson. it is a series of lessons, wit
my name is sherlock holmes. it is my business to know what o
i am a brain, watson. the rest of me is a mere appendix.

Private key recovered: sir arthur conan doyale is one of the best writers. i enjoye

 Fixedには上のFix this sentenceを直したものを直接コードに書きます. Fix this sentenceを直すのが大変で、ひたすら調べてi wanted to ind t** y*rl*, but i'll settle for ending yours.までたどり着いたんですが前半がわかりませんでした. 後にptr-yudaiが名言?であることを見つけ、Fixedはi wanted to end the world, but i'll settle for ending yours.であることがわかりました.

Securinets{i wanted to end the world, but i'll settle for ending yours.}

[Crypto] RSA/Encoding error (1000pts) first solve!

 zipファイルが渡されます. 解凍すると、以下のようなpngファイルが出てきます. f:id:y05h1k1ng:20190325160735p:plain  読み取ると、Hint: Every single information mattersで大した情報ではないです. stringsコマンドにかけるとoutput.txtというものが見えるので、foremostコマンドで取り出します. output.txtが取れたので、中身を見ると以下のようなビット列が複数行に渡って書かれています.

11111.11111.00011.10110.00000.00000.00000.10111.10000.10001.00000.11111.10111.01111.00100.00000.10101.10011

 ここからが本当にわからなくて、大変でした. theoldmoon0602とptr-yudaiによるとどうやら反復符号というものらしいです. これでoutput.txtの解釈はできても、RSA要素が全く出てきてません. 色々調べると、はじめのpngexif情報でした. exiftoolにかけると以下になります.

$ exiftool ctf_image.png 
ExifTool Version Number         : 10.80
File Name                       : ctf_image.png
Directory                       : .
File Size                       : 11 kB
File Modification Date/Time     : 2019:03:23 22:53:12+09:00
File Access Date/Time           : 2019:03:25 16:07:35+09:00
File Inode Change Date/Time     : 2019:03:24 13:14:23+09:00
File Permissions                : rw-r--r--
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 887
Image Height                    : 521
Bit Depth                       : 8
Color Type                      : RGB with Alpha
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
SRGB Rendering                  : Perceptual
Gamma                           : 2.2
Pixels Per Unit X               : 3780
Pixels Per Unit Y               : 3780
Pixel Units                     : meters
Comment                         : e = 2^16+1
Warning                         : [minor] Trailer data after PNG IEND chunk
Image Size                      : 887x521
Megapixels                      : 0.462

 これで、e = 2^16+1、あとはエスパー?で画像の縦横のサイズが素数だとわかり、必要なものが揃いました. dを計算し、output.txtを1行ずつ復号してやるとでます.

from Crypto.Util.number import inverse

with open("./output/zip/output.txt", "rb") as f:
    buf = f.read()
#buf = buf.replace(b"\n", ".")

p = 887
q = 521
e = 2**16 + 1
N = p*q
phi = (p-1)*(q-1)

d = inverse(e, phi)

m = ''
for a in buf.split(b"\n"):
    result = ''
    for block in a.split(b'.'):
        if block.count(b'1') > 2:
            result += '1'
        else:
            result += '0'
        c = int(result, 2)
    tmp = pow(c, d, N)
    m += hex(tmp)[2:-1].decode('hex')
print m
A repetition code is a coding scheme that repeats the bits across a channel to achieve error-free communication. Given a stream of data to be transmitted, the data are divided into blocks of bits. Each block is transmitted some predetermined number of times. For example, to send the bit pattern "1011", the four-bit block can be repeated three times, thus producing "1011 1011 1011". However, if this twelve-bit pattern was received as "1010 1011 1011"  where the first block is unlike the other two  it can be determined that an error has occurred.  hope you have understand it, please validate the task with the following Flag : xJbht0oWpsOa1e3WnXo9FDnUj3VZpZsuxMPVlYEN

Securinets{xJbht0oWpsOa1e3WnXo9FDnUj3VZpZsuxMPVlYEN}

[Forensics] Easy Trade (200pts)

 pcapファイルが渡されます. wiresharkで見ると、いくつかのtcpがあるのでtcp streamします(無条件反射). あるパケットの中にPKflag.txtを含んだバイト列があるので、zipだろうということで、摘出します.f:id:y05h1k1ng:20190325163847p:plain  tcp streamで内容を保存できますが、デフォルトではASCIIの保存になるので、下のShow and sava data asrawに変更して保存します. f:id:y05h1k1ng:20190325163716p:plain  これでzipを手に入れることができたので、あとは解凍するだけですが、ここでパスワードを聞かれます. ここは、tcpgive me the keyとあったのでその後にあるパケットのsecurinetsXDを入れると解凍できます. これでflag.txtを手に入れることができます.

Securinets{c2VjdXJpbmV0c3s5NTRmNjcwY2IyOTFlYzI3NmIxYTlmZjg0NTNlYTYwMX0}

 これは、ptr-yudaiが秒で(5分もかからないぐらい)解いたのですが、僕は20分ぐらいかかりました. 圧倒的マッスルですね.

まとめ

 1位、2位の繰り返しで激アツでした. また、今回は初チームということで楽しく24時間過ごせました(4時間ほど寝たけど...).

 最近はCryptoを練習しているので、更にやっていきます.

 チームメンバー、運営の皆様ありがとうございました.お疲れ様です.