☢️ Unintended Solution.. zzlol
이 또한 저번 포스팅에 이어 언인텐 풀이이다. 지금까지 봐왔던 언인텐 풀이와는 결이 다를 정도로 심각한 언인텐 풀이인데,, 이 방법을 어떻게 생각해낸건지 모르겠다. 개천재인듯;; (저 말고 어떤 선배입니다)
🖲️ Code Analysis
꽤나 나를 힘들게 했던 문제이다.. 언인텐 풀이만으로 너무 많은 힘을 써버려서 인텐 풀이를 알고싶게 하지도 않는다.. 먼저 코드를 살펴보자.
이 문제는 2가지 코드가 있다. 바로 Client 와 Server 이 서로 통신하면서 파일을 만들어내게 된다. 코드를 모두 설명하기에는 무리가 있기 때문에 문제를 푸는 데에 필요한 코드들만 살펴보도록 하자.
Server.py
#!/usr/bin/python3
# server.py
from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os
def aes_enc(pt : bytes, key : bytes):
pt_pad = pad(pt, 16)
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(pt_pad)
def aes_dec(ct : bytes, key : bytes):
cipher = AES.new(key, AES.MODE_ECB)
pt_pad = cipher.decrypt(ct)
return unpad(pt_pad, 16)
def input_enc(session_key : bytes) -> str:
dat_enc = bytes.fromhex(input())
return aes_dec(dat_enc, session_key).decode()
def print_enc(session_key : bytes, data : bytes):
data_enc = aes_enc(data.encode(), session_key)
print(data_enc.hex())
def key_parser(client_id : str):
try:
f = open(f"{client_id}/key.txt")
enc_p = f.readline()[:-1]
enc_q = f.readline()[:-1]
enc_d = f.readline()[:-1]
enc_u = f.readline()[:-1]
n = int(f.readline())
f.close()
except:
print("[-] Error has been occured during key parsing. Please try later.")
exit(-1)
return enc_p, enc_q, enc_d, enc_u, n
def login():
client_id = input()
if not client_id.islower():
print("[-] Only lowercase alphabet is allowed in ID")
exit(-1)
if os.path.exists(client_id): # Already registered
enc_p, enc_q, enc_d, enc_u, n = key_parser(client_id)
print(f"Hi {client_id}! Glad to see you again")
print(enc_p)
print(enc_q)
print(enc_d)
print(enc_u)
else: # Not registered account
print(f"Hi {client_id}! To create an account, please send your RSA private key encrypted with your pw")
enc_p = input()
enc_q = input()
enc_d = input()
enc_u = input()
n = int(input())
try:
os.makedirs(client_id)
os.makedirs(f"{client_id}/src")
f = open(f"{client_id}/key.txt", 'w')
f.write(enc_p + '\n')
f.write(enc_q + '\n')
f.write(enc_d + '\n')
f.write(enc_u + '\n')
f.write(str(n) + '\n')
f.close()
except:
print("Error has been occured during key storing. Please try later.")
exit(-1)
print("Key is successfully saved")
return client_id, n, 65537
def gen_and_send_session_key(n, e):
session_key = os.urandom(32)
while True:
prefix_padding = os.urandom(1)
if prefix_padding != b'\x00':
break
postfix_padding = os.urandom(256 - 32 - 1 - 1)
rsa_plain = bytes_to_long(prefix_padding + session_key + postfix_padding)
rsa_enc_session_key = long_to_bytes(pow(rsa_plain, e, n))
print(rsa_enc_session_key.hex())
return session_key
def save_file(session_key : bytes, client_id : str):
name = input_enc(session_key)
if name == "BACK":
return False
if not name.islower():
print_enc(session_key, "Invalid filename")
return False
print_enc(session_key, "OK")
try:
data_hex = input_enc(session_key)
if data_hex == "BACK":
return False
data = bytes.fromhex(data_hex)
if len(data) > 1000:
print_enc(session_key, "File too large")
return False
except:
print_enc(session_key, "Wrong hex data")
return False
try:
filepath = f"{client_id}/src/{name}.enc"
if os.path.exists(filepath):
print_enc(session_key, "File already exists")
return False
f = open(filepath, "wb")
f.write(data)
f.close()
except:
print_enc(session_key, "Failed to save a file")
return False
print_enc(session_key, "OK")
return True
def load_file(session_key : bytes, client_id : str):
name = input_enc(session_key)
if name == "BACK":
return False
if not name.islower():
print_enc(session_key, "Invalid filename")
return False
try:
filepath = f"{client_id}/src/{name}.enc"
data_enc = open(filepath, "rb").read()
except:
print_enc(session_key, "File does not exist")
return False
print_enc(session_key, "OK")
print_enc(session_key, data_enc.hex())
return True
def menu(session_key : bytes, client_id : str):
while True:
c = input_enc(session_key)
if c == 'save_file':
save_file(session_key, client_id)
elif c == 'load_file':
load_file(session_key, client_id)
elif c == 'logout':
exit(-1)
else:
print("Wrong choice.")
exit(-1)
def go():
client_id, n, e = login()
session_key = gen_and_send_session_key(n, e)
menu(session_key, client_id)
go()
Client.py
#!/usr/bin/python3
# client.py
from Crypto.Util.number import *
import os
import socket
from Crypto.Util.Padding import pad, unpad
from hashlib import sha256
from Crypto.Cipher import AES
CREDENTIALS = {
"codegate": "*******************"
}
def aes_enc(pt, key):
pt_pad = pad(pt, 16)
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(pt_pad)
def aes_dec(ct, key):
cipher = AES.new(key, AES.MODE_ECB)
pt_pad = cipher.decrypt(ct)
#return pt_pad
return unpad(pt_pad, 16)
def packet_recv_plain(sock):
data = b''
while True:
c = sock.recv(1)
if c == b'\n':
break
data += c
return data
def packet_send_plain(sock, data):
if type(data) != bytes or b'\n' in data:
print("[-] Invalid packet")
exit(-1)
sock.sendall(data + b'\n')
def packet_recv_aes_enc(sock, session_key):
data = b''
while True:
c = sock.recv(1)
if c == b'\n':
break
data += c
data = data.decode()
data = bytes.fromhex(data)
return aes_dec(data, session_key)
def packet_send_aes_enc(sock, data, session_key):
if type(data) != bytes or b'\n' in data:
print("[-] Invalid packet")
exit(-1)
data_enc = aes_enc(data, session_key).hex().encode()
sock.sendall(data_enc + b'\n')
# Garner's formula
def rsa_crt_dec(p, q, d, u, c):
mp = pow(c, d % (p-1), p)
mq = pow(c, d % (q-1), q)
m = ((mp - mq) * u % p) * q + mq
return m
# Derive file encryption key from RSA prvate key
def file_encryption_key(p, q, d, u):
rsa_priv_key = long_to_bytes(p) + long_to_bytes(q) + long_to_bytes(d) + long_to_bytes(u)
return sha256(rsa_priv_key).digest()
def login(sock):
client_id = input("id (Only lowercase) > ")
if not client_id.islower():
print("[-] Invalid ID")
exit(-1)
if client_id in CREDENTIALS:
client_pw = CREDENTIALS[client_id]
else:
client_pw = input("pw > ")
sock.send(client_id.encode())
def generate_rsa_private_key():
while True:
p = getPrime(1024)
q = getPrime(1024)
e = 65537
phi = (p-1) * (q-1)
if phi % e == 0: continue
if p == q: continue
d = inverse(e, phi)
u = inverse(q, p)
return p, q, d, u
def init_connection():
ip = input("ip > ")
port = int(input("port > "))
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect((ip, port))
except:
print("[-] Connection error")
exit(-1)
print("[+] Connection established")
return sock
def login(sock):
client_id = input("id > ")
if client_id in CREDENTIALS:
client_pw = CREDENTIALS[client_id]
else:
client_pw = input("pw > ")
sock.sendall((client_id + '\n').encode())
resp = packet_recv_plain(sock).decode()
print("(From server)", resp)
# Already registered
if resp == f"Hi {client_id}! Glad to see you again":
pw_hash = sha256(client_pw.encode()).digest()
p = int(aes_dec(bytes.fromhex(packet_recv_plain(sock).decode()), pw_hash))
q = int(aes_dec(bytes.fromhex(packet_recv_plain(sock).decode()), pw_hash))
d = int(aes_dec(bytes.fromhex(packet_recv_plain(sock).decode()), pw_hash))
u = int(aes_dec(bytes.fromhex(packet_recv_plain(sock).decode()), pw_hash))
# Not registered
elif resp == f"Hi {client_id}! To create an account, please send your RSA private key encrypted with your pw":
pw_hash = sha256(client_pw.encode()).digest()
p, q, d, u = generate_rsa_private_key()
n = p * q
enc_p = aes_enc(str(p).encode(), pw_hash).hex().encode()
enc_q = aes_enc(str(q).encode(), pw_hash).hex().encode()
enc_d = aes_enc(str(d).encode(), pw_hash).hex().encode()
enc_u = aes_enc(str(u).encode(), pw_hash).hex().encode()
packet_send_plain(sock, enc_p)
packet_send_plain(sock, enc_q)
packet_send_plain(sock, enc_d)
packet_send_plain(sock, enc_u)
packet_send_plain(sock, str(n).encode())
resp = packet_recv_plain(sock).decode()
print("(From server)", resp)
# Error has been occured
else:
exit(-1)
return p,q,d,u
def recv_session_key(sock, p, q, d, u):
rsa_enc_session_key = int(packet_recv_plain(sock).decode(), 16)
rsa_plain = long_to_bytes(rsa_crt_dec(p, q, d, u, rsa_enc_session_key))
session_key = rsa_plain[1:33]
print("[+] Session key received")
return session_key
def save_file(sock, session_key, file_enc_key):
packet_send_aes_enc(sock, "save_file".encode(), session_key)
name = input("filename(Only lowercase) > ")
if not name.islower():
print("[-] Invalid filename")
packet_send_aes_enc(sock, "BACK".encode(), session_key)
return False
packet_send_aes_enc(sock, name.encode(), session_key)
resp = packet_recv_aes_enc(sock, session_key).decode()
if resp != "OK":
print("(From server)", resp)
return False
data = bytes.fromhex(input("data(in hex) > "))
if len(data) > 1000:
print("[-] File too large")
packet_send_aes_enc(sock, "BACK".encode(), session_key)
return False
data_enc = aes_enc(data, file_enc_key)
packet_send_aes_enc(sock, data_enc.hex().encode(), session_key)
resp = packet_recv_aes_enc(sock, session_key).decode()
if resp != "OK":
print("(From server)", resp)
return False
print(f"[+] File {name} successfully saved")
return True
def load_file(sock, session_key, file_enc_key):
packet_send_aes_enc(sock, "load_file".encode(), session_key)
name = input("filename(Only lowercase) > ")
if not name.islower():
print("[-] Invalid filename")
packet_send_aes_enc(sock, "BACK".encode(), session_key)
return False
packet_send_aes_enc(sock, name.encode(), session_key)
resp = packet_recv_aes_enc(sock, session_key).decode()
if resp != "OK":
print("(From server)", resp)
return False
data_enc_hex = packet_recv_aes_enc(sock, session_key).decode()
data_enc = bytes.fromhex(data_enc_hex)
'''
Sorry, I won't let you know a plain file content. But I will give you a encrypted one. You can easily decrypt this without my help because file_enc_key is derived from your own rsa private key. isn't it????
'''
# data = aes_dec(data_enc, file_enc_key)
# print(f"[+] {name}(in hex) : {data.hex()}")
print(f"[+] {name}.enc(in hex) : {data_enc.hex()}")
return True
def menu(sock, session_key, file_enc_key):
menu = '''1. Save a file
2. Load a file
3. Logout'''
while True:
print(menu)
c = input("> ")
if c == '1':
save_file(sock, session_key, file_enc_key)
elif c == '2':
load_file(sock, session_key, file_enc_key)
elif c == '3':
packet_send_aes_enc(sock, "logout".encode(), session_key)
print("[+] Bye...")
break
else:
continue
def go():
### INIT
sock = init_connection()
p,q,d,u = login(sock)
session_key = recv_session_key(sock, p, q, d, u)
file_enc_key = file_encryption_key(p, q, d, u)
### MAIN ROUTINE
menu(sock, session_key, file_enc_key)
sock.close()
go()
코드가 많이 길지만 그래도 대충 감만 잡도록 하자.. ㅠㅠ 처음에 보고 기겁함
Client 와 Server 이 서로 통신을 하게 되는데, 여기서 통신에는 암호화 과정이 들어가게 된다. session key 를 생성함으로써, 통신할 때 메시지를 암호화해서 socket 을 보내게 되고, 수신자는 이를 다시 session key 로 복호화해서 정확한 메시지를 전달받는다.
그렇다면 이 session key 는 어떻게 생성될까??
—> 아마 인텐 풀이를 위해서는 필수적으로 봐야 할 요소이지만, python int error 이라는 미친 성능의 언인텐 풀이를 위해서는 있어봤자 독이 되는 요소이다… 여기서 삽질을 3시간 이상 한 것 같다…..
결국 우리는 flag.enc 를 읽어야 한다. 그런데 flag.enc 는 어떤 이상한 문자로 되어있고, 곧 암호화되어 저장됨을 인식할 수 있겠다. 그렇다면 어떻게 flag.enc 가 암호화되는 것일까?
def file_encryption_key(p, q, d, u):
rsa_priv_key = long_to_bytes(p) + long_to_bytes(q) + long_to_bytes(d) + long_to_bytes(u)
return sha256(rsa_priv_key).digest()
data_enc = aes_enc(data, file_enc_key)
바로 이 두 부분이다. p, q, d, u 를 이용해서 file_enc_key 를 생성해내고, 이를 aes 의 key 로 사용함으로써 기존의 FLAG 를 암호화하는 것 같다.
결국 우리가 알아야 할 것은 p, q, d, u 즉, RSA 암호의 모든 정보가 되겠다.
현재는 N만이 나와있는 상태이고, 우리는 p , q 중 하나만 구할 수 있다면 file_enc_key 도 구할 수 있을 것이다. ㅎㅎ
💡 Main Idea
바로 python 에서 int error 를 나타낼 때, 값까지 모두 나타내어주는 error 를 사용한다.. 어떻게 이런 생각을 했는지 신기할 정도로 신선한 방법이다. (다시봐도 미친 풀이..)
여튼 그래서 어떤 부분에서 에러를 발생시킬 수 있냐..?
def login(sock):
client_id = input("id > ")
if client_id in CREDENTIALS:
client_pw = CREDENTIALS[client_id]
else:
client_pw = input("pw > ")
sock.sendall((client_id + '\n').encode())
resp = packet_recv_plain(sock).decode()
print("(From server)", resp)
# Already registered
if resp == f"Hi {client_id}! Glad to see you again":
pw_hash = sha256(client_pw.encode()).digest()
p = int(aes_dec(bytes.fromhex(packet_recv_plain(sock).decode()), pw_hash))
q = int(aes_dec(bytes.fromhex(packet_recv_plain(sock).decode()), pw_hash))
d = int(aes_dec(bytes.fromhex(packet_recv_plain(sock).decode()), pw_hash))
u = int(aes_dec(bytes.fromhex(packet_recv_plain(sock).decode()), pw_hash))
바로 이 부분이다. Server 에서는 암호화된 P를 전송하는데, Client 에서 이 암호화된 값을 통해서 기존의 P 를 구하는 코드가 있다. 그런데, 여기서 int 에서 오류를 발생시켜야 한다.
첨부되어있는 ket.txt 를 살펴보면, 이미 enc_p 의 값이 정의되어 있다는 것을 확인할 수 있다. 그렇다면 우리는 우리가 서버인척을 하고 client 에 enc_p 를 살짝 변형시킨 값을 반환시켜 오류를 발생시킬 수 있을 것이다.
우리가 주목해야 할 점은 2가지이다.
1. Padding 문제를 피할 것
aes_dec 부분에서 unpad 함수가 쓰인다. 하지만, 만약 enc_p 를 변형시켰을 때, 변형된 데이터를 복호화 시에 unpad 함수에서 오류가 발생한다면 우리는 int error 를 발생시킬 수 없게 된다.
2. int error를 발생시킬 것
당연하게도, int error 를 발생시켜야 한다. enc_p 를 그대로 넣어주게 되면, int error 가 발생하지 않아서 우리가 p 에 대한 정보를 얻을 수 없게 된다. 따라서 enc_p 중간에 이상한 값을 첨가하여 int string 으로 변형될 수 없게 해야 한다.
- Padding 문제를 피하기 위해서는 enc_p 의 마지막 블럭이 항상 포함되어야 한다. Pad 에 영향을 끼치는 것은 마지막 블럭인데, 이 블럭이 있어야 정상적으로 unpad 를 진행할 수 있게 된다.
- int error 문제는 중간에 00 이 16번 반복되는 하나의 블럭을 넣어줌으로써 해결할 수 있다.
📖 Exploit Code
이제 내가 Server 인 척을 하고, 거짓된 데이터를 넘겨주어야 한다.
선배님이 주신 서버를 이용해서 ssh 에서 서버를 열고, 다음과 같은 코드를 작성해주자.
import socket
HOST = '0.0.0.0'
PORT = 18425
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((HOST, PORT))
server_socket.listen()
client_socket, addr = server_socket.accept()
print('Connected by', addr)
client_id = client_socket.recv(1024)
enc_p = "5901d4d62364a649837b4638748fd17beb3942388cce66f7595ea589820b7ca86fa1c33d92435f6820c06e09752b4bcf1f0a3936d173d482fb414cabe446ed00fbcc7de15b79670e8cf0ee478be647b6bf637e4a69eac25b2d8567fd400b01b3bbf9ec3a6cee1718d43cb8c5d16b0ebd35e3cffc2bc58662338e56f3a7a9478d958d8abac38a84d801e11cd209510782b5e9f5bcc277b5e7518c38571c923771c2a22c1dc11abf5a82d32c1c4e3ae84300a11fd09a978304cf65c6e31c8286685020c63c1ed465a1f0e2423bafff2cf1b951266cacfd99580693c0cdd80d8224a41e56bfc9ae430fc219a6a4d8570d487de2dc67ec5610734181e72bc5cf8691f04ffb9997d259d757e4779966d1fd0c911c50928345a5c8fedfd81f56c07340051d97c41c6529a5593b854af0f10f832c158b049a53325d13e68b6d34b297be"
client_socket.sendall(f"Hi codegate! Glad to see you again\n".encode())
client_socket.sendall((enc_p[:-32] + "00" * 16 + enc_p[-32:] + "\n").encode())
client_socket.close()
server_socket.close()
이 코드를 실행시켜서 서버를 열고, Client 서버에서 이 서버에 접속을 해주도록 해보자.
그러면 다음과 같이 int error 를 발견할 수 있다.
아마 00 으로 구성된 블럭이 복호화 후에 int string 이 아닌 다른 이상한 바이트로 변질되기에 오류가 발생하는 것 같다. 하지만 이유는 모르겠지만 끝까지 오류가 나오지 않고 중간에서 끊겨 모두 확인할 수는 없었다.
그래도 이 과정을 통해 P 의 앞쪽 바이트를 모두 leak 해볼 수 있었다.
그렇다면 반대로 client_socket.sendall((enc_p[:-32] + "00" * 16 + enc_p[-32:] + "\\n").encode()) 이 부분을 다르게 변질시켜서 이번에는 뒤의 int 를 leak 해보도록 하자. 앞부분을 없애고 뒷부분을 매우 늘려주면 된다.
후후,, 결국 Python 의 Int error 을 통해서 이렇게 p의 원하는 자리수를 leak 할 수 있었다.
따라서 이런 식으로 P 의 모든 수를 확인할 수 있고, P 를 최종적으로 leak 한 결과는 다음과 같다.
from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from hashlib import sha256
p = 139167972701556923691719729095826674624022800081683439292056773540293879230416702328728312842627704836270110246831665146105905423916568797355302669673976390440372528742403601639760049783384774945975256639743140994000549663310264767368556146118016837819793263414602490398883213775195758088528334919695125670223
N = 12659214462730739290777710676401716129364537461971321037157877540193780746910540896819650182970880505880808257029478576966922788012182813161567264480789412487933555048594859387373262444873139956649127172509365160527593844518913325580532608517683946927197373934501869789520791006507064633048240282420120292353010385218838558491852148859208850993839899492722657498287727598894181537635087547896216039380309309759245366183056244723928135316860412330991393425352005295678522825817645977696006156843729550866483053456177362041805254686689400982564781219502910693511366956648759975359689060253588108513703355540146694238377
q = N // p
e = 0x10001
d = inverse(e,(p-1)*(q-1))
u = inverse(q,p)
이렇게 RSA 에 대한 모든 정보를 구할 수 있게 된다.
결국 앞서 말했던 file_enc_key 를 모두 구할 수 있게 되었고, 이 file_enc_key 를 구해서 aes key로써 사용하게 되면, flag 를 복호화할 수 있을 것이다. 최종적인 exploit 코드는 다음과 같다.
이 데이터를 코드에 추가해주면, 최종적으로 다음과 같은 코드가 완성된다.
from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from hashlib import sha256
p = 139167972701556923691719729095826674624022800081683439292056773540293879230416702328728312842627704836270110246831665146105905423916568797355302669673976390440372528742403601639760049783384774945975256639743140994000549663310264767368556146118016837819793263414602490398883213775195758088528334919695125670223
N = 12659214462730739290777710676401716129364537461971321037157877540193780746910540896819650182970880505880808257029478576966922788012182813161567264480789412487933555048594859387373262444873139956649127172509365160527593844518913325580532608517683946927197373934501869789520791006507064633048240282420120292353010385218838558491852148859208850993839899492722657498287727598894181537635087547896216039380309309759245366183056244723928135316860412330991393425352005295678522825817645977696006156843729550866483053456177362041805254686689400982564781219502910693511366956648759975359689060253588108513703355540146694238377
q = N // p
e = 0x10001
d = inverse(e,(p-1)*(q-1))
u = inverse(q,p)
def aes_dec(ct, key):
cipher = AES.new(key, AES.MODE_ECB)
pt_pad = cipher.decrypt(ct)
return unpad(pt_pad, 16)
def file_encryption_key(p, q, d, u):
rsa_priv_key = long_to_bytes(p) + long_to_bytes(q) + long_to_bytes(d) + long_to_bytes(u)
return sha256(rsa_priv_key).digest()
flag = bytes.fromhex('05317e8e878cf267b924c9aebca52bfa9169813439997054db6a5522e488a0b34594104098ae60ed147a465d12d797af')
file_enc_key = file_encryption_key(p,q,d,u)
print(aes_dec(flag,file_enc_key).decode())
Flag : codegate2022{977afe81a5fabee1ff0f7437cea81a50}
이번 문제를 통해서 Python int error 뿐만 아니라 server 과 client 가 소통하는 방식, 즉 socket 에 대해서 더욱 자세히 살펴볼 수 있었다. 다시 한번 선배님께 감사...
'Cryptography > CTF' 카테고리의 다른 글
[Kaist-Postech 2020] - fixed point revenge (0) | 2023.04.19 |
---|---|
[Kaist-Postech 2020] - Baby Bubmi (0) | 2023.04.18 |
[CodeGate 2022] - Hidden Command Service (0) | 2023.04.17 |
[HITCON 2022] - SuperPrime (0) | 2023.04.17 |
[HITCON 2022] - Babysss (0) | 2023.04.17 |