# [Line CTF 2023] Malcheeeeese WriteUp
1. ๋ฌธ์ ํ๋ฉด์ ๋ค์ด๊ฐ ๋ฌธ์ ํ์ธ ๋ฐ ์ฒจ๋ถํ์ผ ๋ค์ด๋ก๋
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 ์ผ๋ก ์ ์ํ๋ ๊ณผ์ ์์ ๋ฌธ์ ๊ฐ ์๊ฒจ ๊ฒฐ๊ตญ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ง๋ ๋ชปํ์๋ค.
'โ๏ธ Capture The Flag (CTF)' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[PwnMe CTF 2023] Just a XOR WriteUp (0) | 2023.05.06 |
---|---|
[UMass CTF 2023] wrathsweatingbuddha WriteUp (Unsolved) (0) | 2023.03.25 |
[Digital Overdose CTF] This isn't bitrot WriteUp (0) | 2022.11.20 |
[Square CTF 2022] Alex Hanlon Has The Flag! WriteUp (0) | 2022.11.19 |
[WPI CTF 2022] Muffin Hater WriteUp (0) | 2022.09.25 |