1. Investigation
>> chall.py
from hashlib import sha256
from Crypto.Util.Padding import pad, unpad
import signal
import subprocess
import socketserver
import os
allowed_commands = [b'whoami', b'ls', b'cat secret.txt', b'pwd']
BLOCK_SIZE = 32
def encrypt_block(block, secret):
enc_block = b''
for i in range(BLOCK_SIZE):
val = (block[i]+secret[i]) % 256
enc_block += bytes([val])
return enc_block
def encrypt(msg, password):
h = sha256(password).digest()
if len(msg) % BLOCK_SIZE != 0:
msg = pad(msg, BLOCK_SIZE)
blocks = [msg[i:i+BLOCK_SIZE] for i in range(0, len(msg), BLOCK_SIZE)]
ct = b''
for block in blocks:
enc_block = encrypt_block(block, h)
h = sha256(enc_block + block).digest()
ct += enc_block
return ct.hex()
def run_command(cmd):
if cmd in allowed_commands:
try:
resp = subprocess.run(
cmd.decode().split(' '), capture_output=True)
output = resp.stdout
return output
except:
return b'Something went wrong!\n'
else:
return b'Invalid command!\n'
def challenge(req):
req.sendall(b'This is Jenny! I am the heart and soul of this spaceship.\n' +
b'Welcome to the debug terminal. For security purposes I will encrypt any responses.')
while True:
req.sendall(b'\n> ')
command = req.recv(4096).strip()
output = run_command(command)
response = b'Command executed: ' + command + b'\n' + output
password = os.urandom(32)
ct = encrypt(response, password)
req.sendall(ct.encode())
class incoming(socketserver.BaseRequestHandler):
def handle(self):
signal.alarm(30)
req = self.request
challenge(req)
class ReusableTCPServer(socketserver.ForkingMixIn, socketserver.TCPServer):
pass
def main():
socketserver.TCPServer.allow_reuse_address = True
server = ReusableTCPServer(("0.0.0.0", 1337), incoming)
server.serve_forever()
if __name__ == "__main__":
main()
문제에서 주어진 파일을 다운로드 해봤더니 위와 같은 파이썬으로 작성된 소스코드를 확인할 수 있었습니다.
대략적인 로직을 살펴보니 아래와 같은 방식으로 동작하고 있었습니다.
- 사용자로부터 command 를 입력받음
- 입력받은 command 가 allowed_commands 에 있는지 확인하고 subprocess 로 해당 command 실행한 결과를 output 에 저장 (allowed_commands = [b'whoami', b'ls', b'cat secret.txt', b'pwd'])
- b'Command executed: ' + command + b'\n' + output 형태의 평문을 만들어 랜덤으로 생성된 32 바이트 크기의 password 와 함께 encrypt 를 수행
- 반환된 ciphertext 를 출력
encrypt 는 다음과 같은 로직으로 동작하고 있었습니다.
- password 의 sha256 해시값을 h 에 저장
- 평문을 32 바이트 단위로 패딩한 후 블록으로 나눔
- 첫번째 블록, h 와 함께 encrypt_block 수행한 결과를 enc_block 에 저장
- enc_block 과 첫번째 블록을 이어붙여 만든 문자열의 sha256 해시값을 h 에 저장
- ciphertext 에 enc_block 붙임
- 이를 반복하여 완성된 ciphertext 를 반환 (ciphertext = enc_block_1 + enc_block_2 + ... + enc_block_n)
encrypt_block 은 다음과 같은 로직으로 동작하고 있었습니다.
- 블록과 h 를 한자리씩 10진수로 더한 값에 256 으로 나머지 연산 수행 -> val = (block[i] + h[i]) % 256
- 연산된 값을 바이트 형태로 변환하여 enc_block 에 붙임 -> enc_block += bytes([val])
- 이를 반복하여 완성된 enc_block 을 반환 (enc_block = val_1 + val_2 + ... + val_31 + val_32)
결론적으로는 FLAG 를 찾기 위해 allowed_commands 중에 "cat secret.txt" 를 실행하고, 그 결과를 암호화하여 반환된 ciphertext 를 위 로직에서 약점을 찾아 초기에 랜덤으로 생성된 32 바이트 크기의 password 를 모르더라도 복호화하여 평문을 알아내야 한다는 것을 알 수 있습니다.
2. Solution
일단 문제 서버에 접속해 "cat secret.txt" 를 command 로 입력하여 다음과 같은 ciphertext 를 반환 받았습니다.
ct = '8f0684e23cd3dec423f4a9668226c06e022430c66c76684ef5eef210b7c60222fb44b765be60dc328266cd2b06a66f4f699da138e497b4de6c6d9e111ec743ba4b1183d4574843634e4c3bbcdeb5a92f3c71a7e6398b516cfd0fd52d96346a961868a3ad7d2e2e405c1014b8517acd226deae2de4981715684e056764c20b98e72c35ac16ed49deb2928be3096939861cff45a0474f52048f16d3aabbbd78881b25e74f0830a87f093a1c0fa613119a046e679ac498d3efd8681741814db2dd48cca92b22acd37f5303079a0d8fa019d32bf20f5162d0dc8a04122f65535f13bae55a31c03c583ff947fdf2ab7add9fb4a159c3210ad17c68ba56d827d1a29d7'
# 블록 단위로 쪼갬
CBLOCK1 = '8f0684e23cd3dec423f4a9668226c06e022430c66c76684ef5eef210b7c60222' (Length : 32 bytes)
CBLOCK2 = 'fb44b765be60dc328266cd2b06a66f4f699da138e497b4de6c6d9e111ec743ba' (Length : 32 bytes)
CBLOCK3 = '4b1183d4574843634e4c3bbcdeb5a92f3c71a7e6398b516cfd0fd52d96346a96' (Length : 32 bytes)
CBLOCK4 = '1868a3ad7d2e2e405c1014b8517acd226deae2de4981715684e056764c20b98e' (Length : 32 bytes)
CBLOCK5 = '72c35ac16ed49deb2928be3096939861cff45a0474f52048f16d3aabbbd78881' (Length : 32 bytes)
CBLOCK6 = 'b25e74f0830a87f093a1c0fa613119a046e679ac498d3efd8681741814db2dd4' (Length : 32 bytes)
CBLOCK7 = '8cca92b22acd37f5303079a0d8fa019d32bf20f5162d0dc8a04122f65535f13b' (Length : 32 bytes)
CBLOCK8 = 'ae55a31c03c583ff947fdf2ab7add9fb4a159c3210ad17c68ba56d827d1a29d7' (Length : 32 bytes)
위에서 평문은 b'Command executed: ' + command + b'\n' + output 와 같은 형태로 이루어진다고 했으므로 command 부분에 "cat secret.txt" 를 넣어보면 다음과 같은 평문이 만들어집니다. (output 은 cat secret.txt 실행 결과)
pt = b'Command executed: cat secret.txt' + b'\n' + output
위 평문을 password 와 함께 encrypt 한 것의 결과가 바로 반환받은 ciphertext 입니다.
문제의 소스코드에서 32 바이트 단위로 블록을 나누고 있기 때문에 평문을 블록으로 나눠보겠습니다.
PBLOCK1 : b'Command executed: cat secret.txt' (length: 32)
PBLOCK2 : b'\n' + output????????????????????? (length: 32)
PBLOCK3 : ...
...
PBLOCKn : ?????padpadpadpadpadpadpadpadpadpad (length : 32)
대략 위와 같이 평문 블록이 구성되며 공교롭게도 첫번째 블록이 정확히 32 바이트인 것을 확인할 수 있습니다.
첫번째 블록만을 가지고 encrypt 가 어떻게 수행되는지 생각해보겠습니다.
| PBLOCK1 : b'Command executed: cat secret.txt'
+ | h : b'sha256(password)=???????????????'
---------------------------------------------------
| ??????????????????????????????????
% | mod 256 in each digit
---------------------------------------------------
enc_block_1
이를 식으로 나타내면 enc_block_n[i] = (PBLOCK_n[i] + h_n[i]) % 256 과 같은 형식이 됩니다.
여기서 우리는 enc_block 을 이어붙인 것이 곧 ciphertext 이고, 첫번째 평문 블록의 값들을 알고 있기 때문에 식에 대입해보면 0x8f(=ciphertext 첫번째 블록의 첫번째 바이트) = (b'C'(=0x43) + ?) % 256 라는 식이 나오며 ? 는 h 가 sha256 해시값이기 때문에 한 바이트 당 최대 0xff 까지 표현이 가능하여 합리적인 시간 내에 Brute-Force 를 통해 ? 값을 알아낼 수 있게 됩니다.
-> 모든 32 자리의 바이트에 대해서 Brute-Force 를 수행하면 password 의 sha256 해시값을 알 수 있음. (문제풀이에는 불필요)
for i in range(0x100):
if(0x8f == (0x43 + i) % 256):
print(i) # 76 == 0x4c
위와 같은 과정을 머리 속에 담아두고 두번째 블록으로 넘어가보면 이제는 평문을 모르고 h 값은 sha256(enc_block1 + pblock1) 을 통해 알아낼 수 있기 때문에 enc_block_2[i] = (? + h_2[i]) % 256 와 같은 식이 만들어지며 ? 는 평문이라 ASCII 범위 내의 문자일 것이기 때문에 똑같이 최대 0xff 까지 Brute-Force 를 수행해보면 됩니다.
for i in range(0x100):
if(0xfb == (i + 0xf1) % 256):
print(i) # 10 == 0x0a == b'\n'
결론
- ciphertext 를 알고 있기 때문에 각 enc_block 또한 알 수 있음.
- 첫번째 평문 블록이 공교롭게도 정확히 32 바이트 길이기 때문에 다음 두번째 블록의 encryption 에 사용되는 h 값을 구할 수 있음. (h = sha256(enc_block_1 + pblock_1))
- 0x00 ~ 0xff 범위 내의 Brute-Force 를 수행하여 enc_block_n[i] = (PBLOCK_n[i] + h_n[i]) % 256 식을 만족하는 평문을 알아냄
>> sol.py
from hashlib import sha256
def decrypt(cblocks, pblock):
plaintext = b''
for i in range(7):
enc_block1 = bytes.fromhex(cblocks[i])
print(f"[+] enc_block_{i+1} + pblock_{i+1}: ", enc_block1.hex(), "+", pblock)
h = sha256(enc_block1 + pblock).digest()
print("[+] h: ", h.hex())
enc_block2 = bytes.fromhex(cblocks[i+1])
pblock = b''
for a, b in zip(enc_block2, h):
for j in range(0x100):
if(a == (j + b) % 256):
pblock += bytes([j])
plaintext += pblock
print(f"[+] pblock_{i+2}: ", pblock)
print('='*64)
return plaintext
def main():
# b'Command executed: ' + command + b'\n' + output
BLOCK_SIZE = 64
ct = '8f0684e23cd3dec423f4a9668226c06e022430c66c76684ef5eef210b7c60222fb44b765be60dc328266cd2b06a66f4f699da138e497b4de6c6d9e111ec743ba4b1183d4574843634e4c3bbcdeb5a92f3c71a7e6398b516cfd0fd52d96346a961868a3ad7d2e2e405c1014b8517acd226deae2de4981715684e056764c20b98e72c35ac16ed49deb2928be3096939861cff45a0474f52048f16d3aabbbd78881b25e74f0830a87f093a1c0fa613119a046e679ac498d3efd8681741814db2dd48cca92b22acd37f5303079a0d8fa019d32bf20f5162d0dc8a04122f65535f13bae55a31c03c583ff947fdf2ab7add9fb4a159c3210ad17c68ba56d827d1a29d7'
cblocks = [ct[i:i+BLOCK_SIZE] for i in range(0, len(ct), BLOCK_SIZE)]
for i, block in enumerate(cblocks):
print(f"[+] enc_block_{i+1}: ", block)
print('='*64)
pblock1 = b'Command executed: cat secret.txt'
plaintext = decrypt(cblocks, pblock1)
print("[+] plaintext: ", plaintext)
# [+] plaintext: b'\nIn case Jenny malfunctions say the following phrase: Melt My Eyez, See Your Future \nThe AI system will shutdown and you will gain complete control of the spaceship.\n- Danbeer S.A.\nHTB{b451c*****************w34k!!!}\n\x07\x07\x07\x07\x07\x07\x07'
main()
🚩 FLAG : HTB{b451c*****************w34k!!!}
'CTF writeups > Crypto' 카테고리의 다른 글
[Cyber Apocalypse CTF 2022] How The Columns Have Turned (0) | 2022.05.20 |
---|---|
[Cyber Apocalypse CTF 2022] The Three-Eyed Oracle (0) | 2022.05.20 |
[picoCTF 2022] NSA Backdoor (0) | 2022.05.04 |
[picoCTF 2022] Sum-O-Primes (0) | 2022.05.04 |
[picoCTF 2022] Sequences (0) | 2022.05.03 |