TetCTF 2024

TetCTF 2024

CRYPTOGRAPHY WRITEUP

Author:

  • Pham Quoc Trung

Used Language:

  • Python3

Problem Solving:

flip

Chall name: flip Category: Crypto Author: ndh Description: You are allowed to inject a software fault. Server: nc 139.162.24.230 31339 Material: flip.zip

encrypt.c

// To compile:
// git clone https://github.com/kokke/tiny-AES-c
// gcc encrypt.c tiny-AES-c/aes.c
#include "tiny-AES-c/aes.h"
#include <unistd.h>

uint8_t plaintext[16] = {0x20, 0x24};
uint8_t key[16] = {0x20, 0x24};

int main() {
    struct AES_ctx ctx;
    AES_init_ctx(&ctx, key);
    AES_ECB_encrypt(&ctx, plaintext);
    write(STDOUT_FILENO, plaintext, 16);
    return 0;
}

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


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 checkj chạy từ 0 tới 7. Hệ thống sẽ tiến hành ghi plaintextkey (đượ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ị ij, 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 keyplaintext 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

docker build -t flip .
docker run -p 31339:31339 --name flip flip

Với payload đầu

Key:  417c6136abdc1613544eae683d789618
00000000000000000000000000000000 4539 2
Output:  8a1e8d6fcda9afb20cfc64a044934c54417c6136abdc1613544eae683d789618000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Payload thứ hai

Key:  acc71f8fc25dca86c4d1a973e38693b1
00000000000000000000000000000000 4551 5
Output:  acc71f8fc25dca86c4d1a973e38693b1

Payload thứ ba

Key:  5ead98e1634d9f0dd793eeb1c0431b81
00000000000000000000000000000000 5463 1
Output:  5ead98e1634d9f0dd793eeb1c0431b81

Payload cuối

Key:  dcd2cd3f4a6f29039fcc3863f2bf6566
00000000000000000000000000000000 8871 2
Output:  dcd2cd3f4a6f29039fcc3863f2bf6566

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

from pwn import *

conn = remote('139.162.24.230' ,31339)

pt = '0' * 32
i = 4551
j = 5

payload = pt + " " + str(i) + " " + str(j)

conn.sendline(payload.encode())

key = conn.recvline().decode().strip() 

conn.sendline(key.encode())

print("Flag:" ,conn.recvline().decode())
[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}

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.

Faster bruteforce and some explain: Link

flip v2

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 = 0x1169OFFSET_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 = 1i = 8871 j = 2

Code lấy flag:

from pwn import *

conn = remote('139.162.24.230' ,31340)

pt = '0' * 32
i = 5463
j = 1

payload = pt + " " + str(i) + " " + str(j)

conn.sendline(payload.encode())

key = conn.recvline().decode().strip() 

# If use another plaintext
# key = conn.recvline().decode()
# key = xor(bytes.fromhex(key), bytes.fromhex(pt))
# key = key.hex()

conn.sendline(key.encode())

print("Flag:" ,conn.recvline().decode())
[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}

Flag: TetCTF{fr0m_0n3_b1t_fl1pp3d_t0_full_k3y_r3c0v3ry_d043a7ff4cf6285a}

Last updated