๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
โœ’๏ธ Capture The Flag (CTF)

[Line CTF 2023] Malcheeeeese WriteUp (Unsolved)

by A Lim Han 2023. 3. 25.

# [Line CTF 2023] Malcheeeeese WriteUp

 

1. ๋ฌธ์ œ ํ™”๋ฉด์— ๋“ค์–ด๊ฐ€ ๋ฌธ์ œ ํ™•์ธ ๋ฐ ์ฒจ๋ถ€ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ

ํ„ฐ๋ฏธ๋„์„ ํ†ตํ•ด nc 34.85.9.81 13000 ์œผ๋กœ ์ ‘์†ํ•ด์•ผ ํ•˜๋Š” ๋“ฏ ํ•˜๋‹ค.

 

 

 

 

2. ํŒŒ์ผ ์†์˜ challenge_server.py๋ฅผ ์—ด์–ด ๋ถ„์„

+ [Line 1 ~ 3]

:  'server' ๋ชจ๋“ˆ์—์„œ 'decrypt', 'generate_new_auth_token' ํ•จ์ˆ˜ import + 'BaseRequestHandler', 'TCPServer', 'ForkingMixIn' ํด๋ž˜์Šค import

 

+ [Line 5]

: ์‚ฌ์šฉํ•  ์„œ๋ฒ„์˜ ์ฃผ์†Œ ์„ค์ •

 

 

+ [Line 9 ~ 13]

ChallengeHandler ํด๋ž˜์Šค  -->  'BaseRequestHandler' ํด๋ž˜์Šค ์ƒ์† + ์‚ฌ์šฉ์ž ์š”์ฒญ ์ฒ˜๋ฆฌ ์—ญํ•  ๋‹ด๋‹น

 

+ [Line 15 ~ 36]

handle ๋ฉ”์†Œ๋“œ  -->  ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ ๋ฐ›์•„ ์‹คํ–‰

new_auth_token, verifier ์ƒ์„ฑ ํ›„ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†ก  -->  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ณด๋‚ธ 'authtoken' ์ˆ˜๋ น ํ›„ 'decrypt' ํ•จ์ˆ˜ ํ˜ธ์ถœ + ๋ณตํ˜ธํ™”

:  ๋ณตํ˜ธํ™” ๊ฒฐ๊ณผ๋ฅผ JSON ํ˜•ํƒœ๋กœ ์ธ์ฝ”๋”ฉํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†กํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ์š”์ฒญ์ด ์˜ค์ง€ ์•Š์„ ๋•Œ๊นŒ์ง€ ์š”์ฒญ ์ฒ˜๋ฆฌ

 

 

+ [Line 38 ~ 57]

:  'ChallengeServer' ํด๋ž˜์Šค 

-->  'ForkingMixIn' ํด๋ž˜์Šค, 'TCPServer' ํด๋ž˜์Šค ๋‹ค์ค‘์ƒ์† + ์„œ๋ฒ„ ๊ตฌ๋™ ์—ญํ• 

 

 

 

 

3. ํŒŒ์ผ ์†์˜ client.py๋ฅผ ์—ด์–ด ๋ถ„์„

#!/usr/bin/env python3
# crypto + misc challenge

# How to generate the authentication token
# Given as secret; key for AES, sign_key_pair for EdDSA ( sign_key, verify_key ), token, password
# key for AES, token, password were pre-shared to server.
# 1. Generate Signature for token over Ed25519 ( RFC8032 ) 
# 2. Encrypt password, token and signature with AES-256-CTR-Enc( base64enc(password || token || signature), key for AES, iv for AES )
# 3. Generate authentication token ; base64enc( iv for AES) || AES-256-CTR-Enc( base64enc(password || token || signature), key for AES, iv for AES )
# - password : 12 bytes, random bit-string
# - token : 15 bytes, random bit-string
# - signature : 64 bytes, Ed25519 signature ( RFC8032 )
# - key for AES : 32 bytes, random bit-string
# - iv for AES : 8 bytes, random bit-string
# - payload = base64enc(password || token || signature) : 124 bytes
# - authentication_token ( encrypted_payload ) = base64enc(iv) || AES-256-CTR-Enc ( payload ) : 136 bytes

# ๋ฌธ์ œ ๊ฐœ์š”: ์„œ๋ฒ„์—์„œ ๋ฐ›์€ ์ธ์ฆ ํ† ํฐ์„ ํ•ด๋… + ์ƒˆ๋กœ์šด ์ธ์ฆ ํ† ํฐ์„ ์ƒ์„ฑ

์„œ๋ช… ์ƒ์„ฑ ํŒจ์Šค์›Œ๋“œ, ํ† ํฐ, ์„œ๋ช… ์•”ํ˜ธํ™”
Ed25519 AES-256-CTR ๋ชจ๋“œ

 

# ๋ฌธ์ œ ์ƒ์„ธ: 

PW 12 ๋ฐ”์ดํŠธ, ๋žœ๋คํ•œ ๋น„ํŠธ ๋ฌธ์ž์—ด
Token 15 ๋ฐ”์ดํŠธ, ๋žœ๋คํ•œ ๋น„ํŠธ ๋ฌธ์ž์—ด
Sign  64 ๋ฐ”์ดํŠธ, Ed25519 ์„œ๋ช… (RFC8032)
AES Key 32 ๋ฐ”์ดํŠธ, ๋žœ๋คํ•œ ๋น„ํŠธ ๋ฌธ์ž์—ด
AES IF 8 ๋ฐ”์ดํŠธ, ๋žœ๋คํ•œ ๋น„ํŠธ ๋ฌธ์ž์—ด
payload  124 ๋ฐ”์ดํŠธ
์ธ์ฆ ํ† ํฐ 136 ๋ฐ”์ดํŠธ

 

 

+ [Line 18 ~ 26] 

client_secret.py ํŒŒ์ผ, common_secret.py ํŒŒ์ผ์—์„œ SIGN_KEY_PAIR_HEX, AES_KEY_HEX, TOKEN_HEX, PASSWORD_HEX ํ˜ธ์ถœ

 

+ [Line 27 ~ 50]

:  token์„ sign_key_pair๋กœ ์„œ๋ช… ( ed25519 ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์‚ฌ์šฉ )

:  password, token, signature๋ฅผ ์ด์šฉํ•˜์—ฌ payload ์ƒ์„ฑ 

-->  AES-CTR ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•ด payload๋ฅผ ์•”ํ˜ธํ™”ํ•œ ํ›„ encrypted_payload ์ƒ์„ฑ

 

+ [Line 51]

PREVIOUS_ENCRYPTED_PWD_HEX ์ถœ๋ ฅ

-->  encrypted_payload์˜ ์ฒ˜์Œ 16๋ฐ”์ดํŠธ๋งŒ ์ถ”์ถœํ•˜์—ฌ 16์ง„์ˆ˜ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ถœ๋ ฅ

 

 

 

 

4. ํŒŒ์ผ ์†์˜ server.py๋ฅผ ์—ด์–ด ๋ถ„์„

+ [Line 7 ~ 10]

sys, os ๋ชจ๋“ˆ( = ์‹œ์Šคํ…œ ๊ด€๋ จ ๊ธฐ๋Šฅ ์ œ๊ณต ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ) ์ž„ํฌํŠธํ•˜์—ฌ ์‚ฌ์šฉ

: ์•„๋ž˜์˜ ํŒŒ์ผ๋กœ๋ถ€ํ„ฐ ๊ฐ ๊ฐ’์„ ๊ฐ€์ ธ์˜ด

ํŒŒ์ผ๋ช… ๊ฐ€์ ธ์˜ค๋Š” ๊ฐ’
server_secret.py FLAG ๊ฐ’
common_secret.py AES_KEY_HEX, TOKEN_HEX, PASSWORD_HEX ๊ฐ’

 

+ [Line 12 ~ 16]

  • base64 ๋ชจ๋“ˆ: ๋ฌธ์ž์—ด ๋ฐ ๋ฐ”์ดํŠธ ๋””์ฝ”๋”ฉ & ์ธ์ฝ”๋”ฉ ๊ธฐ๋Šฅ ์ œ๊ณต
  • Crypto.PublicKey.ECC ๋ชจ๋“ˆ: ํƒ€์› ๊ณก์„  ์•”ํ˜ธํ™”๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ณต๊ฐœํ‚ค ์•”ํ˜ธํ™” ๊ตฌํ˜„ ๊ธฐ๋Šฅ ์ œ๊ณต
  • Crypto.Signature.eddsa ๋ชจ๋“ˆ: Ed25519 ์„œ๋ช… ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ ๊ธฐ๋Šฅ ์ œ๊ณต
  • Crypto.Cipher.AES ๋ชจ๋“ˆ: AES ๋Œ€์นญํ‚ค ์•”ํ˜ธํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ ๊ธฐ๋Šฅ ์ œ๊ณต
  • Crypto.Random.get_random_bytes ๋ชจ๋“ˆ: ์•”ํ˜ธํ™”์šฉ ๋ฌด์ž‘์œ„ ๋ฐ”์ดํŠธ ์ƒ์„ฑ ๊ธฐ๋Šฅ ์ œ๊ณต

 

+ [Line 18]

: ENCRYPTED_PAYLOAD_SIZE ๋ณ€์ˆ˜ ํฌ๊ธฐ = 136 = ์•”ํ˜ธํ™”๋œ ํŽ˜์ด๋กœ๋“œ ํฌ๊ธฐ

 

+ [Line 19]

: PREVIOUS_ENCRYPTED_PWD ๋ณ€์ˆ˜ ๊ฐ’ = ์•”ํ˜ธํ™”๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ

 

+ [Line 20 ~ 21]

: AES_IV_HEX ๋ณ€์ˆ˜ ๊ฐ’ =  AES ์•”ํ˜ธ์—์„œ์˜ ์ดˆ๊ธฐํ™” ๋ฒกํ„ฐ(IV)

: previous_aes_iv ๋ณ€์ˆ˜  -->  AES_IV_HEX ๊ฐ’์˜ ๋ฐ”์ดํŠธ ํ‘œํ˜„

 

+ [Line 22 ~ 29]

: AES_KEY_HEX, PASSWORD_HEX ๋ณ€์ˆ˜์— "common_secret.py" ํŒŒ์ผ์—์„œ ๊ฐ€์ ธ์˜จ ๊ฐ’์„ ์‚ฌ์šฉํ•˜์—ฌ bytes ํ˜•ํƒœ๋กœ ์ €์žฅ

: previous_iv_b64 ๋ณ€์ˆ˜์— "previous_aes_iv" ๋ณ€์ˆ˜ ๊ฐ’์„ base64 ์ธ์ฝ”๋”ฉํ•œ ๊ฐ’ ์ €์žฅ

 

+ [Line 30 ~ 31]

: replay_attack_filter_for_iv ๋ณ€์ˆ˜๋Š” previous_iv_b64 ๋ณ€์ˆ˜ ๊ฐ’์„ ๋ฆฌ์ŠคํŠธ๋กœ ์ €์žฅ

-->  IV๋ฅผ ํ•„ํ„ฐ๋งํ•˜์—ฌ ๋ฆฌํ”Œ๋ ˆ์ด ๊ณต๊ฒฉ ๋ฐฉ์ง€์— ์‚ฌ์šฉ

: replay_attack_filter_for_sig ๋ณ€์ˆ˜๋ฅผ ๋นˆ ๋ฆฌ์ŠคํŠธ๋กœ ์ดˆ๊ธฐํ™”

-->  ์„œ๋ช…์„ ํ•„ํ„ฐ๋งํ•˜์—ฌ ๋ฆฌํ”Œ๋ ˆ์ด ๊ณต๊ฒฉ ๋ฐฉ์ง€์— ์‚ฌ์šฉ

 

+ [Line 32 ~ 33]

: acceptable_token ๋ณ€์ˆ˜๋ฅผ ๋นˆ ๋ฆฌ์ŠคํŠธ๋กœ ์ดˆ๊ธฐํ™”

: acceptable_password ๋ณ€์ˆ˜์— "cheeeeese" ๊ฐ’์„ bytes ํ˜•ํƒœ๋กœ ์ €์žฅ

 

 

+ [Line 40 ~ 65]

: ์ƒˆ๋กœ์šด ์ธ์ฆ ํ† ํฐ์„ ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜ generate_new_auth_token() ์ •์˜

--> AES ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ํ†ตํ•ด ์•”ํ˜ธํ™”๋œ ์ธ์ฆ ํ† ํฐ๊ณผ EdDSA ๊ฒ€์ฆ์ž๋ฅผ ๋ฐ˜ํ™˜๊ฐ’์œผ๋กœ ๊ฐ€์ง

 

: ์ดํ›„ ์•„๋ž˜์˜ ์ฝ”๋“œ์— ๊ตฌํ˜„๋œ ํ•จ์ˆ˜๋ฅผ ๋”ฐ๋ผ ์•”ํ˜ธํ™” ํŽ˜์ด๋กœ๋“œ๋ฅผ ๋ณตํ˜ธํ™” ํ›„ ํŽ˜์ด๋กœ๋“œ์˜ ์ง„์œ„ ๊ฒ€์ฆ ๊ณผ์ •์„ ๊ฑฐ์นจ

# Input : b64password : base64 encoded password
# Output:
# - Is password verification successful? ( True / False )
# - raw passowrd length
# - Error Code ( 0, 1, 2 )
# - Error Message
def verify_password(b64password):
    try:
        password = base64.b64decode(b64password)
    except:
        return False, -1, 1, "Base64 decoding error"

    if password in acceptable_password:
        return True, len(password), 0, "Your password is correct!"
    return False, len(password), 2, "Your password is incorrect."

# Input : b64token_signature : base64 encoded token+signature, verifier, verify_counter
# Output:
# - Is signature verification successful? ( True / False )
# - Error Code ( 0, 1, 2, 3, 4 )
# - Error Message
def verify_signature(b64token_signature, verifier, verify_counter):
    b64token = b64token_signature[:20]
    b64signature = b64token_signature[20:]

    if verify_counter > 1:
        return False, 1, "Err1-Verification limit Error"

    if b64signature in replay_attack_filter_for_sig:
        return False, 2, "Err2-Deactived Token"
    
    try:
        token = base64.b64decode(b64token)
        signature = base64.b64decode(b64signature)
    except:
        return False, 3, "Err3-Base64 decoding error"
    
    try:
        verifier.verify(token, signature)
        if token in acceptable_token:
            return True, 0, "verification is successful"
    except ValueError:
        pass

    return False, 4, "Err4-verification is failed"

def decrypt(hex_ciphertext, verifier, verify_counter):

    flag = ""

    # Length check
    ciphertext = bytes.fromhex(hex_ciphertext)
    if len(ciphertext)!=ENCRYPTED_PAYLOAD_SIZE:
        ret = {
            "is_iv_verified" : False,
            "is_pwd_verified" : False,
            "pwd_len" : -1,
            "pwd_error_number" : -1,
            "pwd_error_reason": "",
            "is_sig_verified" : False,
            "sig_error_number" : -1,
            "sig_verification_reason": "authentication token size MUST be 136 bytes",
            "flag" : ""
        }
        return ret
    
    iv_b64 = ciphertext[:12]
    ciphertext = ciphertext[12:]

    # iv reuse detection
    if iv_b64 in replay_attack_filter_for_iv:
        ret = {
            "is_iv_verified" : False,
            "is_pwd_verified" : False,
            "pwd_len" : -1,
            "pwd_error_number" : -1,
            "pwd_error_reason": "",
            "is_sig_verified" : False,
            "sig_error_number" : -1,
            "sig_verification_reason": "iv reuse detected",
            "flag" : ""
        }
        return ret
    

    cipher = AES.new(aes_key, AES.MODE_CTR, nonce=base64.b64decode(iv_b64))
    pt_b64 = cipher.decrypt(ciphertext)

    # password authentication
    is_pwd_verified, pwd_len, pwd_error_number, pwd_error_reason = verify_password(pt_b64[:16])

    # authentication using EdDSA
    is_sig_verified, sig_error_number, sig_error_reason = verify_signature(pt_b64[16:], verifier, verify_counter)

    if True==is_pwd_verified and True==is_sig_verified:
        flag = FLAG
    
    ret = {
        "is_iv_verified" : True,
        "is_pwd_verified" : is_pwd_verified,
        "pwd_len" : pwd_len,
        "pwd_error_number" : pwd_error_number,
        "pwd_error_reason": pwd_error_reason,
        "is_sig_verified" : is_sig_verified,
        "sig_error_number" : sig_error_number,
        "sig_error_reason": sig_error_reason,
        "flag" : flag
    }

    return ret

 

 

 

 

+ ์ฝ”๋“œ ๋ถ„์„๊นŒ์ง€๋Š” ๋งˆ๋ฌด๋ฆฌํ•˜์˜€์ง€๋งŒ ํ„ฐ๋ฏธ๋„์—์„œ ๋ฌธ์ œ ์ƒ์— ๋‚˜ํƒ€๋‚˜ ์žˆ๋Š” IP์ธ nc 34.85.9.81 13000 ์œผ๋กœ ์ ‘์†ํ•˜๋Š” ๊ณผ์ •์—์„œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒจ ๊ฒฐ๊ตญ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜์ง€๋Š” ๋ชปํ•˜์˜€๋‹ค.