# Please ensure that you solved the challenge properly at the local.
# If things do not run smoothly, you generally won't be allowed to make another attempt.
from secret.network_util import check_client, ban_client
import sys
import os
import subprocess
import tempfile
OFFSET_PLAINTEXT = 0x4010
OFFSET_KEY = 0x4020
def main():
if not check_client():
return
key = os.urandom(16)
with open("encrypt", "rb") as f:
content = bytearray(f.read())
# input format: hex(plaintext) i j
try:
plaintext_hex, i_str, j_str = input().split()
pt = bytes.fromhex(plaintext_hex)
assert len(pt) == 16
i = int(i_str)
assert 0 <= i < len(content)
j = int(j_str)
assert 0 <= j < 8
except Exception as err:
print(err, file=sys.stderr)
# ban_client()
return
# update key, plaintext, and inject the fault
content[OFFSET_KEY:OFFSET_KEY + 16] = key
content[OFFSET_PLAINTEXT:OFFSET_PLAINTEXT + 16] = pt
content[i] ^= (1 << j)
tmpfile = tempfile.NamedTemporaryFile(delete=True)
with open(tmpfile.name, "wb") as f:
f.write(content)
os.chmod(tmpfile.name, 0o775)
tmpfile.file.close()
# execute the modified binary
try:
ciphertext = subprocess.check_output(tmpfile.name, timeout=1.0)
print(ciphertext.hex())
except Exception as err:
print(err, file=sys.stderr)
ban_client()
return
# please guess the AES key
if bytes.fromhex(input()) == key:
with open("secret/flag.txt") as f:
print(f.read())
from datetime import datetime
print(datetime.now(), plaintext_hex, i, j, file=sys.stderr)
main()
Recon
Sau một hồi đọc code thì mình thấy được bài này sẽ cho người dùng nhập vào input dạng hex(plaintext) i j, với plaintext dài 16 bytes, i là một số từ 0 tới độ dài nội dung file binary check và j chạy từ 0 tới 7. Hệ thống sẽ tiến hành ghi plaintext và key (được gen từ hàm os.random(16)) vào offset của nó trong file check sau đó lưu thành một file tạm và khởi chạy. Kết quả sẽ trả về ciphertext của nó được mã hóa bằng AES_ECB. Yêu cầu của chúng ta là phải nhập đúng key và chương trình sẽ trả về flag.
Về 2 giá trị i và j, nó được sử dụng ở đoạn code content[i] ^= (1 << j), có nghĩa rằng chúng ta được quyền sửa 1 byte bất kì ở trong đoạn file binary trước khi nó được khởi chạy. Hẳn sẽ có cách nào đó để ta có thể làm chương trình in ra key hoặc tạo ra ciphertext gì đó dễ dàng tính được key? Nghe vế đầu sẽ có vẻ khả thi hơn.
Lưu ý một điều nữa là ở đoạn code khởi chạy file binary tạm thời
# execute the modified binary
try:
ciphertext = subprocess.check_output(tmpfile.name, timeout=1.0)
print(ciphertext.hex())
except Exception as err:
print(err, file=sys.stderr)
ban_client()
return
Có thể thấy nếu có lỗi xảy ra trong quá trình khởi chạy, hàm ban_client() sẽ được thực thi. Ta không biết nó sẽ làm gì, cơ mà không nên chơi liều. Challenge có cho sẵn Dockerfile nên ta có thể dựng lại và xóa dòng đó đi cho an toàn để thử nghiệm payload.
Unintended solution
Vì ở bài này, ta chỉ được quyền sửa duy nhất 1 byte, và nội dung của file binary ngoài phần key và plaintext ra thì mọi thứ luôn không đổi vì nó được lấy từ file check. Chắc chắn sẽ có 1 byte khiến cho file binary này hoạt động không như mong muốn và giúp ta có được key. Nhưng để biết là byte nào thì mình không biết, cho nên mình đã nghĩ tới chuyện là sẽ thử thay đổi từng byte một và quan sát output trả về.
Mình sử dụng một đoạn code như sau:
import os
import subprocess
import tempfile
from tqdm import tqdm
OFFSET_PLAINTEXT = 0x4010
OFFSET_KEY = 0x4020
def main():
key = os.urandom(16)
for i in tqdm(range(0, 21032)):
for j in range(0, 8):
with open("encrypt", "rb") as f:
content = bytearray(f.read())
plaintext_hex = ''0' * 32'
pt = bytes.fromhex(plaintext_hex)
content[OFFSET_KEY:OFFSET_KEY + 16] = key
content[OFFSET_PLAINTEXT:OFFSET_PLAINTEXT + 16] = pt
content[i] ^= (1 << j)
tmpfile = tempfile.NamedTemporaryFile(delete=True)
with open(tmpfile.name, "wb") as f:
f.write(content)
os.chmod(tmpfile.name, 0o775)
tmpfile.file.close()
try:
ciphertext = subprocess.check_output(tmpfile.name, timeout=0.001)
if(key in ciphertext):
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
print("Found key! Position i = " + str(i) + " j = " + str(j))
except:
continue
main()
Mình sử dụng rất nhiều hàm print() là do khi sửa đổi các bit của file binary có thể sẽ xảy ra lỗi khi thực thi (không phải lỗi từ python nên mình không chưa tìm ra cách bắt exception). In ra nhiều như vậy sẽ dễ dàng để nhận ra lúc nào có kết quả hơn.
Các kết quả mình thu được là
Found key! Position i = 4539 j = 2
Found key! Position i = 4551 j = 5
Found key! Position i = 5463 j = 1
Found key! Position i = 8871 j = 2
Tiếp theo mình sẽ dựng Docker để test xem khi nhập payload nó sẽ hiện ra như nào. Mình cũng sửa lại file main một chút để nó in ra key cho mình so sánh
Có thể thấy các payload sau đều chỉ in ra key luôn chứ không in thêm những thứ không quan trọng như payload đầu. Mình sẽ thử dùng payload thứ hai để lấy flag
[x] Opening connection to 139.162.24.230 on port 31339
[x] Opening connection to 139.162.24.230 on port 31339: Trying 139.162.24.230
[+] Opening connection to 139.162.24.230 on port 31339: Done
Flag: TetCTF{fr0m_0n3_b1t_fl1pp3d_t0_full_k3y_r3c0v3ry}
Thật ra là mình đã ăn may khi sử dụng plaintext = 00000000000000000000000000000000 để bruteforce. Khi phân tích kĩ trong intended solution, kết quả trả về từ server là key^plaintext. Điều này là do một số giai đoạn của mã hóa AES. Khi mình dùng plaintext là 00000000000000000000000000000000 thì khi XOR với key nó vẫn sẽ là key nên mình lấy được flag.
Chall name: flip v2
Category: Crypto
Author: ndh
Description: Changing in main() is not allowed.
Server: nc 139.162.24.230 31340
Material: main.py
main.py
# Please ensure that you solved the challenge properly at the local.
# If things do not run smoothly, you generally won't be allowed to make another attempt.
from secret.network_util import check_client, ban_client
import sys
import os
import subprocess
import tempfile
OFFSET_PLAINTEXT = 0x4010
OFFSET_KEY = 0x4020
OFFSET_MAIN_START = 0x1169
OFFSET_MAIN_END = 0x11ed
def main():
if not check_client():
return
key = os.urandom(16)
with open("encrypt", "rb") as f:
content = bytearray(f.read())
# input format: hex(plaintext) i j
try:
plaintext_hex, i_str, j_str = input().split()
pt = bytes.fromhex(plaintext_hex)
assert len(pt) == 16
i = int(i_str)
assert 0 <= i < len(content)
assert not OFFSET_MAIN_START <= i < OFFSET_MAIN_END
j = int(j_str)
assert 0 <= j < 8
except Exception as err:
print(err, file=sys.stderr)
# ban_client()
return
# update key, plaintext, and inject the fault
content[OFFSET_KEY:OFFSET_KEY + 16] = key
content[OFFSET_PLAINTEXT:OFFSET_PLAINTEXT + 16] = pt
content[i] ^= (1 << j)
tmpfile = tempfile.NamedTemporaryFile(delete=True)
with open(tmpfile.name, "wb") as f:
f.write(content)
os.chmod(tmpfile.name, 0o775)
tmpfile.file.close()
# execute the modified binary
try:
ciphertext = subprocess.check_output(tmpfile.name, timeout=1.0)
print(ciphertext.hex())
except Exception as err:
print(err, file=sys.stderr)
ban_client()
return
# please guess the AES key
if bytes.fromhex(input()) == key:
with open("secret/flag.txt") as f:
print(f.read())
from datetime import datetime
print(datetime.now(), plaintext_hex, i, j, file=sys.stderr)
main()
Recon
Bài này thì code vẫn giống y nguyên bài trước chỉ khác là có thêm 2 trường OFFSET_MAIN_START = 0x1169 và OFFSET_MAIN_END = 0x11ed. Đây là đánh dấu cho offset của hàm main. Giờ đây thì i sẽ có điều kiện là assert not OFFSET_MAIN_START <= i < OFFSET_MAIN_END, nghĩa là i không được nằm trong main, hay ta không thể tác động tới byte nào ở trong main.
Unintended Solution
Như bài trước thì mình đã tìm ra 4 payload để có thể lấy được key. Ở đây với 0x1169 <= i < 0x11ed hay 4457 <= i < 4589 thì sẽ bị Exception. Vậy chỉ cần lấy payload có i nằm ngoài khoảng đó thôi, cụ thể là mình có i = 5463 j = 1 và i = 8871 j = 2
[x] Opening connection to 139.162.24.230 on port 31340
[x] Opening connection to 139.162.24.230 on port 31340: Trying 139.162.24.230
[+] Opening connection to 139.162.24.230 on port 31340: Done
Flag: TetCTF{fr0m_0n3_b1t_fl1pp3d_t0_full_k3y_r3c0v3ry_d043a7ff4cf6285a}