AceBear Security Contest Writeup

この大会は2018/1/27 11:00(JST)~2018/1/28 23:00(JST)に開催されました。
今回もチームで参戦。結果は3987点で139チーム中10位でした。
自分で解けた問題をWriteupとして書いておきます。

CNVService (Crypt/ACM)

$ nc cnvservice.acebear.site 1337
***************************CNVService*****************************
* Challenge created by CNV                                       *
* My blog: https://chung96vn.blogspot.com                        *
***************************CNVService*****************************
********************Menu********************
* 1 - Register                             *
* 2 - Login                                *
********************************************
Your choice: 1
*****************************REGISTER*****************************
Name: 1
Username: 2
Cookie: PWg77PMNr9+uFv7AJVlwzob7DLRKNm7bpk7wY6l+JBBxnlH1lDq2HJXf53CIAgzXz70wj6nMNmgICMiq6l7CELxf80XHSkf5UsRnDr+CHOM=
***************************END REGISTER***************************
********************Menu********************
* 1 - Register                             *
* 2 - Login                                *
********************************************
Your choice:

ソースコードを見ると、以下のような処理になっている。

■1を選択
Nameを入力
Usernameを入力(rootはNG)
すると、暗号化結果を表示

◇Cookie.register(Name, Username)
name: Nameをpadding
iv: nameと特定文字列とのxor
cookie: CNVService*user=[username]*[ctime()]*[__SECRET__]
iv+cookieのAES暗号したものをBase64エンコードして返す。

◇AES暗号
cookieをpadding
ECB暗号モード利用
1ブロック目:ivとのxor --AES暗号-->
2ブロック目:1ブロック目暗号化文字列のmd5とのxor --AES暗号-->

■2を選択
クッキーを入力

◇Cookie.authentication(cookie)
cookieをBase64デコードする。
name: cookie前半16バイトと特定文字列とのxor
name: unpaddingする

AES復号する。
*区切りでチェック
1つ目:CNVService
最後:__SECRET__
2つ目の最初の5文字:user=
2つ目:=区切りで2つ

username=rootの暗号化文字列を割り出す必要があるが、当然rootで登録はできない。以下のようなことを念頭に暗号化文字列を作成することを考える。

"CNVService*user=" ^ iv                             --AES ECB--> 暗号1ブロック目
"root*Sat Jan 27 " ^ md5(暗号1ブロック目).digest()  --AES ECB--> 暗号2ブロック目
"13:43:37 2018*SS" ^ md5(暗号2ブロック目).digest()  --AES ECB--> 暗号3ブロック目
"SSSSSS??????????" ^ md5(暗号3ブロック目).digest()  --AES ECB--> 暗号4ブロック目

いろいろ試したが、__HEDDEN__, __SECRET__の値を知らずに解くことはできそうにない。
まず md5(__HIDDEN__).digest()を求めてみる。

import socket
from hashlib import md5
from base64 import b64decode
from base64 import b64encode

def xor(dest, src):
    if len(dest) == 0:
        return src
    elif len(src) == 0:
        return dest
    elif len(dest) >= len(src):
        return ''.join(chr(ord(dest[i])^ord(src[i])) for i in range(len(src)))
    else:
        return ''.join(chr(ord(dest[i])^ord(src[i])) for i in range(len(dest)))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('cnvservice.acebear.site', 1337))

data = s.recv(1024)
data += s.recv(1024)
data += '1'
print data

s.sendall('1\n')
data = s.recv(1024)
data += s.recv(1024)
print data

name = 'xxxxxxxxxxxxxxxx'
print name
s.sendall(name + '\n')
data = s.recv(1024)
print data
username = 'aaaa'
print username
s.sendall(username + '\n')

data = s.recv(1024)
data += s.recv(1024)
print data

cookie = data[8:].strip()

cookie = b64decode(cookie)
iv = cookie[:16]
cipher_text = cookie[16:]

HIDDEN = xor(iv, name)
print HIDDEN.encode('hex')

この結果、md5(__HIDDEN__).digest()の16進表記は以下の通りであることがわかる。

0c6734e3fc02a0d0a119f1cf2a567fc1

次に__SECRET__を求めてみる。最初は以下のコードで__SECRET__の長さを確認する。

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('cnvservice.acebear.site', 1337))

data = s.recv(1024)
data += s.recv(1024)
data += '1'
print data

s.sendall('1\n')
data = s.recv(1024)
data += s.recv(1024)
name = 'test'
print data + name
s.sendall(name + '\n')
data = s.recv(1024)
username = 'aaaaaaa' #<- change this string length
print data + username
s.sendall(username + '\n')
data = s.recv(1024)
print data

cookie = data[8:].strip()

print len(cookie.decode('base64'))
・aのとき、80
・aaaaaaaのとき、80
・aaaaaaaaのとき、96

CNVService*user=
aaaaaaaa*Sat Jan
 27 13:43:37 201
8*SSSSSSSSSSSSSS
\x16........\x16

この結果__SECRET__の長さは14。
次に__SECRET__の後ろから1文字ずつはみ出させ、usernameに同じデータを作るように指定し、BFで該当ブロックの暗号データが同じになるものを探す。最初の1文字は以下のようなイメージ。

CNVService*user=
aaaaaaaaa*Sat Ja
n 27 13:43:37 20
18*SSSSSSSSSSSSS
S\x15.......\x15
import socket
from hashlib import md5
from base64 import b64decode
from base64 import b64encode

HIDDEN = '0c6734e3fc02a0d0a119f1cf2a567fc1'.decode('hex')
P_HEAD = 'CNVService*user='
NAME = 'xxxxxxxxxxxxxxxx'
BLOCK_SIZE = 16

pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
                chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)

def xor(dest, src):
    if len(dest) == 0:
        return src
    elif len(src) == 0:
        return dest
    elif len(dest) >= len(src):
        return ''.join(chr(ord(dest[i])^ord(src[i])) for i in range(len(src)))
    else:
        return ''.join(chr(ord(dest[i])^ord(src[i])) for i in range(len(dest)))

def right_enc(s, nth, plain, initFlg):
    data = s.recv(1024)
    if initFlg:
        data += s.recv(1024)
    data += '1'
    print data
    s.sendall('1\n')

    data = s.recv(1024)
    data += s.recv(1024)
    print data + NAME
    s.sendall(NAME + '\n')

    data = s.recv(1024)
    username = 'a' * (8 + nth)
    print data + username
    s.sendall(username + '\n')

    data = s.recv(1024)
    print data
    cookie = data[8:].strip()

    cookie = b64decode(cookie)
    iv = cookie[:16]
    cipher_text = cookie[16:]

    enc_block4 = cipher_text[48:64]
    enc_block5 = cipher_text[64:]
    return xor(plain, md5(enc_block4).digest()), enc_block5

def getname(xor_val):
    return xor(xor_val, xor(P_HEAD, HIDDEN))

def enc(s, name):
    data = s.recv(1024)
    data += '1'
    print data
    s.sendall('1\n')

    data = s.recv(1024)
    data += s.recv(1024)
    print data + name
    s.sendall(name + '\n')

    data = s.recv(1024)
    username = 'a'
    print data + username
    s.sendall(username + '\n')

    data = s.recv(1024)
    print data
    cookie = data[8:].strip()

    cookie = b64decode(cookie)
    iv = cookie[:16]
    cipher_text = cookie[16:]

    enc_block1 = cipher_text[:16]
    return enc_block1

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('cnvservice.acebear.site', 1337))

secret = ''
initFlg = True
for i in range(1, 15):
    for code in range(32, 127):
        print '[-] code =', code
        try_secret = pad(chr(code) + secret)
        p, c = right_enc(s, i, try_secret, initFlg)
        initFlg = False

        if enc(s, getname(p)) == c:
            secret = chr(code) + secret
            break
        print '[-] secret =', secret

print '[+] secret =', secret

この結果、__SECRET__の値は以下の通りであることがわかる。

__SECRET__ = 'Thi5_i5_s3cr3t'

あとは適当なNameで1ブロック目から順に求めていく。

import socket
from hashlib import md5
from base64 import b64decode
from base64 import b64encode

BLOCK_SIZE = 16

pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
                chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)

def xor(dest, src):
    if len(dest) == 0:
        return src
    elif len(src) == 0:
        return dest
    elif len(dest) >= len(src):
        return ''.join(chr(ord(dest[i])^ord(src[i])) for i in range(len(src)))
    else:
        return ''.join(chr(ord(dest[i])^ord(src[i])) for i in range(len(dest)))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('cnvservice.acebear.site', 1337))

data = s.recv(1024)
data += s.recv(1024)
data += '1'
print data

s.sendall('1\n')
data = s.recv(1024)
data += s.recv(1024)

name = 'xxxxxxxxxxxxxxxx'
print data + name
s.sendall(name + '\n')

data = s.recv(1024)
username = 'aaaa'
print data + username
s.sendall(username + '\n')

data = s.recv(1024)
print data
cookie = b64decode(data[8:].strip())
iv = cookie[:16]
cipher_text = cookie[16:]

HIDDEN = xor(iv, name)

p = [
    'CNVService*user=',
    'root*Sat Jan 27 ',
    '13:43:37 2018*Th',
    pad('i5_i5_s3cr3t')]

# ciphertext 1st block
c = [cipher_text[:16]]

# ciphertext 2nd, 3rd, 4th block
for i in range(3):
    data = s.recv(1024)
    print data + '1'

    s.sendall('1\n')
    data = s.recv(1024)
    data += s.recv(1024)

    name2 = xor(xor(p[i+1], md5(c[i]).digest()), xor(p[0], HIDDEN))
    print data + name2
    s.sendall(name2 + '\n')
    data = s.recv(1024)
    username = 'a'
    print data + username
    s.sendall(username + '\n')

    data = s.recv(1024)
    print data

    cookie = b64decode(data[8:].strip())
    cipher_text = cookie[16:]

    c.append(cipher_text[:16])

# make cookie
data = s.recv(1024)
print data + '2'

cookie = iv
for i in range(4):
    cookie += c[i]

b64_cookie = b64encode(cookie)

s.sendall('2\n')
data = s.recv(1024)
data += s.recv(1024)
print data + b64_cookie

s.sendall(b64_cookie + '\n')
data = s.recv(1024)
print data
data = s.recv(1024)
print data

実行結果は以下の通り。

           :
********************Menu********************
* 1 - Register                             *
* 2 - Login                                *
********************************************
Your choice: 2
*******************************LOGIN******************************
Cookie: dB9Mm4R62KjZYYm3Ui4HuX2M/X3aDOaMYiURc62G0iNH1X1MYq3cT/KLGBii3waxANcmeINSqA86qHKfYpL2fagUrLmrhK2qCQmiDLHHxxc=
**************************LOGIN SUCCESS***************************

Welcome CNV service: xxxxxxxxxxxxxxxx
Username: root
Time register: Sat Jan 27 13:43:37 2018
***************************Root Servive***************************
This is flag: AceBear{AES_CNV_is_s3cure_but_CNV_S3rvic3_i5_not_s3cure}
AceBear{AES_CNV_is_s3cure_but_CNV_S3rvic3_i5_not_s3cure}