Patriot CTF 2024 MISC 부문 문제 풀이

사용자 이름 점수
LamentTyphon 922
Dragonkeep 846
Jerrythepro123 500
sanmu 1312
6s6 200
p3cd0wn 100

우리 팀 웹 부문 풀이: https://dragonkeeep.top/category/PatriotCTF-WEB-WP/

팀워크 덕분에 나는 웹 초보자로서 유연하게 MISC 문제를 풀 수 있게 되었다(아니라고)

MISC

  • 이모지 스택
  • 팬케이크 만들기
  • RTL 웜업
  • 에코만
  • RTL 이지
  • 이모지 스택 V2

MISC 문제 풀이

이모지 스�택

난이도: 쉬움, 나에게 난이도: 쉬움

이모지 스택에 오신 것을 환영합니다! 이것은 새로운 스택 기반 이모지 언어입니다. 다른 스택 기반 튜링 머신들은 + - [] 같이 읽기 어렵고 도전적인 문자를 사용하지만, 이모지 스택은 우리의 독점적인 특허 출원 중인 이모지 시스템을 사용합니다.

구현 세부 사항은 다음과 같습니다:

👉: 스택 포인터를 한 셀 오른쪽으로 이동
👈: 스택 포인터를 한 셀 왼쪽으로 이동
👍: 현재 셀 값을 1 증가, 255로 제한
👎: 현재 셀 값을 1 감소, 0으로 제한
💬: 현재 셀의 ASCII 값 출력
🔁##: 이전 명령을 0x##번 반복
이모지 스택은 256 셀 길이이며, 각 셀은 0-255 값 지원

핵심: 뇌fuck, 바로 Ctrl+F 치환으로 해결

유일한 차이점은 🔁가 []를 대체한다는 점이며, 루프 끝에 ]를 추가해야 해서 번거롭다

직접 인터프리터 구현:

interpreter.py

program = '' # 문제 첨부 파일 내용으로 대체
instructions = []

# 프로그램 전처리
for i in range(len(program)):
    if program[i].isnumeric():
        continue
    elif program[i] == "🔁":
        instructions.pop()
        instructions.append(program[i-1] + program[i] + program[i+1] + program[i+2])
    else:
        instructions.append(program[i])

# 스택 및 포인터 초기화
memory = [0 for _ in range(256)]
pointer = 0

# 명령 실행
for i in range(len(instructions)):
    current = instructions[i]
    
    if current == "👉":
        pointer = (pointer + 1) % 256
    elif current == "👈":
        pointer = (pointer - 1) % 256
    elif current == "👍":
        memory[pointer] = (memory[pointer] + 1) % 256
    elif current == "👎":
        memory[pointer] = (memory[pointer] - 1) % 256
    elif current == "💬":
        print(chr(memory[pointer]), end="")
    elif len(current) == 4:  # 반복 명령 처리
        repeat_count = int(current[2:4], 16)
        for _ in range(repeat_count + 1):
            repeat_command = current[0]
            if repeat_command == "👉":
                pointer = (pointer + 1) % 256
            elif repeat_command == "👈":
                pointer = (pointer - 1) % 256
            elif repeat_command == "👍":
                memory[pointer] = (memory[pointer] + 1) % 256

CACI{TUR!NG_!5_R011!NG_!N_H!5_GR@V3}

팬케이크 만들기

난이도: 쉬움, 나에게 난이도: 초보자

정말 간단한 문제인데 왜 푼 사람이 적을까?

nc 연결 후 챌린지 내용 확인:

팬케이크 샵에 오신 것을 환영합니다!
팬케이크는 여러 층으로 구성되어 있으며, 모든 층을 통과해야 비밀 팬케이크 믹스 레시피를 얻을 수 있습니다.
이 서버에서는 1000개의 챌린지-응답을 완료해야 합니다.
응답은 다음과 같이 생성할 수 있습니다:
1. 챌린지를 base64로 한 번 디코딩 (출력: (encoded|n))
2. 챌린지를 n번 더 디코딩
3. (decoded|현재 챌린지 반복 횟수) 전송
예시: 485/1000 챌린지에 대한 응답: e9208047e544312e6eac685e4e1f7e20|485
행운을 빕니다!

평: 어리석은 문제

한 번 base64 디코딩 후 base64 문자열과 디코딩 횟수를 읽어온다. 디코딩이 완료되면 챌린지 횟수를 붙여서 전송

스크립트 작성 후 플래그 획득

from base64 import b64decode as decode
import warnings
from tqdm import tqdm
warnings.filterwarnings("ignore")
from pwn import *
conn = remote('chal.pctf.competitivecyber.club', 9001)
conn.recvuntil('Good luck!')
for iteration in tqdm(range(1002)): # 두 개의 \n이 있어서 빈 루프 두 번 실행
    actual_iteration = iteration - 2
    data = conn.recvline().decode()
    if data == '\n':
        continue
    data = data.split(': ')[1]
    decoded_data = decode(data).decode()
    base_string, times = decoded_data.split('|')
    
    for _ in range(int(times)):
        base_string = decode(base_string)
        
    conn.sendline(base_string + '|' + str(actual_iteration).encode())
conn.interactive()

출력:

(999/1000) >> Wow you did it, you've earned our formula!
DO NOT SHARE:
pctf{store_bought_pancake_batter_fa82370}

pctf{store_bought_pancake_batter_fa82370}

RTL 웜업

난이도: 초보자, 나에게 난이도: 초보자

'b'로 시작하는 모든 내용 추출 (이진수 내용)

b01010000
b01000011
b01010100
b01000110
b01111011
b01010010
b01010100
b01001100
b01011111
b01101001
b00100100
b01011111
b01000100
b01000000
b01000100
b01011111
b00110000
b01000110
b01011111
b01001000
b01000000
b01110010
b01100100
b01110111
b01000000
b01110010
b00110011
b01111101

8비트 ASCII 코드로 변환

def binary_to_ascii(binary_str):
    # 'b' 접두사 제거하고 이진수를 10진수로 변환
    decimal_value = int(binary_str, 2)
    # 10진수를 ASCII 문자로 변환
    return chr(decimal_value)

binary_values = [
    "01010000",  # P
    "01000011",  # C
    "01010100",  # T
    "01000110",  # F
    "01111011",  # {
    "01010010",  # R
    "01010100",  # T
    "01001100",  # L
    "01011111",  # _
    "01101001",  # i
    "00100100",  # $
    "01011111",  # _
    "01000100",  # D
    "01000000",  # @
    "01000100",  # D
    "01011111",  # _
    "00110000",  # 0
    "01000110",  # F
    "01011111",  # _
    "01001000",  # H
    "01000000",  # @
    "01110010",  # r
    "01100100",  # d
    "01110111",  # w
    "01000000",  # @
    "01110010",  # r
    "00110011",  # 3
    "01111101"   # }
]

ascii_characters = [binary_to_ascii(bv) for bv in binary_values]

print("".join(ascii_characters)) 

PCTF{RTL_i$_D@D_0F_H@rdw@r3}

에코만

난이도: 쉬움, 나에게 난이도: 초보자

중국 CTF러가 가장 좋아하는 Linux RCE

외국인에게는 이 문제가 쉬울 수 있지만, 이런 것들을 매일 다루는 중국 CTF러들에게는 너무 기초적이다

#!/usr/bin/python3

import os, pwd, re
import socketserver, signal
import subprocess

listen_port = 3333

blacklist = os.popen("ls /bin").read().split("\n")
blacklist.remove("echo")

def filter_check(input_command):
    user_input = input_command
    parsed = input_command.split()
    # echo로 시작해야 함
    if "echo" not in parsed:
        return False
    else:
        if ">" in parsed:
            # "HEY! No moving things around."
            req.sendall(b"HEY! No moving things around.\n\n")
            return False
        else:
            parsed = input_command.replace("$", " ").replace("(", " ").replace(")", " ").replace("|"," ").replace("&", " ").replace(";"," ").replace("<"," ").replace(">"," ").replace("`"," ").split()
            for i in range(len(parsed)):
                if parsed[i] in blacklist:
                    return False
            return True

def backend_handler(request):
    request.sendall(b'This is shell made to use only the echo command.\n')
    while True:
        request.sendall(b'Please input command: ')
        user_input = request.recv(4096).strip(b'\n').decode()
        # 입력 검증
        if user_input:
            if filter_check(user_input):
                output = os.popen(user_input).read()
                request.sendall((output + '\n').encode())
            else:
                request.sendall(b"HEY! I said only echo works.\n\n")
        else:
            request.sendall(b"Where\'s the command.\n\n")

class incoming_connection(socketserver.BaseRequestHandler):
    def handle(self):
        signal.alarm(1500)
        request = self.request
        backend_handler(request)

class ReusableTCPServer(socketserver.ForkingMixIn, socketserver.TCPServer):
    pass

def main():
    user_id = pwd.getpwnam('ctf')[2]
    os.setuid(user_id)
    socketserver.TCPServer.allow_reuse_address = True
    server = ReusableTCPServer(("0.0.0.0", listen_port), incoming_connection)
    server.serve_forever()

if __name__ == '__main__':
    main()

무서워 보이지? 코드를 읽어보면:

  • 모든 리눅스 명령 사용 불가
  • 반드시 echo로 시작
  • $, (, ), ` 필터링으로 인라인 실행 불가

, < 필터링으로 리디렉션 불가

다양한 방법으로 해결 가능 (풀이 수가 내 점수보다 많음)

필터링 하나씩 대응:

  • ca''t 같은 방식으로 문자열 매칭 우회
  • echo로 시작하지만 %0a를 사용하면 echo 실행 후 다른 명령 실행 가능
  • 인라인 실행 불필요
  • 리디렉션 불필요

먼저 플래그가 현재 디렉토리에 있는지 확인

echo *

출제자가 너무 친절해서, 루트 디렉토리로 돌아갈 필요도 없다 (/, .는 필터링되지 않았음. 돌아가려면 돌아갈 수 있음)

리눅스의 줄바꿈 문자로 즉시 해결 (%0a는 ;와 유사한 기능으로 다른 명령 실행 가능)

PCTF{echo_is_such_a_versatile_command}

RTL 이지

난이도: 쉬움, 나에게 난이도: 쉬움

아, 이 문제 318점이야?

top.v



module top (
    clock,
    data_in,
    data_out
);

  // 모듈 인자
  input wire clock;
  input wire [7:0] data_in;
  output reg [7:0] data_out;

  // 스크랩 신호
  reg pulser$clock;
  reg pulser$enable;
  wire pulser$pulse;

  // 로컬 신호
  reg [9:0] temp;

  // 서브 모듈 인스턴스
  top$pulser pulser (
      .clock (pulser$clock),
      .enable(pulser$enable),
      .pulse (pulser$pulse)
  );

  // 업데이트 코드
  always @(*) begin
    pulser$enable = 1'b1;
    pulser$clock = clock;
    temp = ((data_in) & 10'h3ff) << 32'h2 ^ 32'ha;
    data_out = ((temp >> 32'h2) & 8'hff);
  end

endmodule  // top


module top$pulser (
    clock,
    enable,
    pulse
);

  // 모듈 인자
  input wire clock;
  input wire enable;
  output reg pulse;

  // 스크랩 신호
  reg  strobe$enable;
  wire strobe$strobe;
  reg  strobe$clock;
  reg  shot$trigger;
  wire shot$active;
  reg  shot$clock;
  wire shot$fired;

  // 서브 모듈 인스턴스
  top$pulser$strobe strobe (
      .enable(strobe$enable),
      .strobe(strobe$strobe),
      .clock (strobe$clock)
  );
  top$pulser$shot shot (
      .trigger(shot$trigger),
      .active (shot$active),
      .clock  (shot$clock),
      .fired  (shot$fired)
  );

  // 업데이트 코드
  always @(*) begin
    strobe$clock = clock;
    shot$clock = clock;
    strobe$enable = enable;
    shot$trigger = strobe$strobe;
    pulse = shot$active;
  end

endmodule  // top$pulser


module top$pulser$shot (
    trigger,
    active,
    clock,
    fired
);

  // 모듈 인자
  input wire trigger;
  output reg active;
  input wire clock;
  output reg fired;

  // 상수 선언
  localparam duration = 32'h17d7840;

  // 스크랩 신호
  reg [31:0] counter$d;
  wire [31:0] counter$q;
  reg counter$clock;
  reg state$d;
  wire state$q;
  reg state$clock;

  // 서브 모듈 인스턴스
  top$pulser$shot$counter counter (
      .d(counter$d),
      .q(counter$q),
      .clock(counter$clock)
  );
  top$pulser$shot$state state (
      .d(state$d),
      .q(state$q),
      .clock(state$clock)
  );

  // 업데이트 코드
  always @(*) begin
    counter$clock = clock;
    state$clock = clock;
    counter$d = counter$q;
    state$d = state$q;
    if (state$q) begin
      counter$d = counter$q + 32'h1;
    end
    fired = 1'b0;
    if (state$q && (counter$q == duration)) begin
      state$d = 1'b0;
      fired   = 1'b1;
    end
    active = state$q;
    if (trigger) begin
      state$d   = 1'b1;
      counter$d = 32'h0;
    end
  end

endmodule  // top$pulser$shot


module top$pulser$shot$counter (
    d,
    q,
    clock
);

  // 모듈 인자
  input wire [31:0] d;
  output reg [31:0] q;
  input wire clock;

  // 업데이트 코드 (커스텀)
  initial begin
    q = 32'h0;
  end

  always @(posedge clock) begin
    q <= d;
  end

endmodule  // top$pulser$shot$counter


module top$pulser$shot$state (
    d,
    q,
    clock
);

  // 모듈 인자
  input wire d;
  output reg q;
  input wire clock;

  // 업데이트 코드 (커스텀)
  initial begin
    q = 1'h0;
  end

  always @(posedge clock) begin
    q <= d;
  end

endmodule  // top$pulser$shot$state


module top$pulser$strobe (
    enable,
    strobe,
    clock
);

  // 모듈 인자
  input wire enable;
  output reg strobe;
  input wire clock;

  // 상수 선언
  localparam threshold = 32'h5f5e100;

  // 스크랩 신호
  reg [31:0] counter$d;
  wire [31:0] counter$q;
  reg counter$clock;

  // 서브 모듈 인스턴스
  top$pulser$strobe$counter counter (
      .d(counter$d),
      .q(counter$q),
      .clock(counter$clock)
  );

  // 업데이트 코드
  always @(*) begin
    counter$clock = clock;
    counter$d = counter$q;
    if (enable) begin
      counter$d = counter$q + 32'h1;
    end
    strobe = enable & (counter$q == threshold);
    if (strobe) begin
      counter$d = 32'h1;
    end
  end

endmodule  // top$pulser$strobe


module top$pulser$strobe$counter (
    d,
    q,
    clock
);

  // 모듈 인자
  input wire [31:0] d;
  output reg [31:0] q;
  input wire clock;

  // 업데이트 코드 (커스텀)
  initial begin
    q = 32'h0;
  end

  always @(posedge clock) begin
    q <= d;
  end

endmodule  // top$pulser$strobe$counter

.v 파일을 읽고 다음을 확인:

temp = ((data_in) & 10'h3ff) << 32'h2 ^ 32'ha;
data_out = ((temp >> 32'h2) & 8'hff);

.svg 파일에서 출력값 가져옴

0h52,0h41,0h56,0h44,0h79,0h4a,0h42,0h70,0h66,0h5d,0h47,0h6c,0h61,0h70,0h7b,0h72,0h76,0h6b,0h6d,0h6c,0h5d,0h6b,0h71,0h5d,0h31,0h63,0h71,0h7b

스크립트 작성 후 플래그 획득

def calculate_input(data_out_values):
    input_values = []
    
    for data_out in data_out_values:
        data_out_int = int(data_out, 16)
        temp = data_out_int << 2
        input_val = (temp ^ 0xA) >> 2
        input_values.append(input_val)

    return input_values

def input_to_ascii(input_values):
    ascii_chars = [chr(val) for val in input_values]
    return ascii_chars

data_out_values = [
    '0x52', '0x41', '0x56', '0x44', '0x79', 
    '0x4A', '0x42', '0x70', '0x66', '0x5D', 
    '0x47', '0x6C', '0x61', '0x70', '0x7B', 
    '0x72', '0x76', '0x6B', '0x6D', '0x6C', 
    '0x5D', '0x6B', '0x71', '0x5D', '0x31', 
    '0x63', '0x71', '0x7B'
]

input_values = calculate_input(data_out_values)
ascii_chars = input_to_ascii(input_values)

for data_out, input_val, ascii_char in zip(data_out_values, input_values, ascii_chars):
    print(f"출력: {data_out}, 입력: {input_val}, ASCII: '{ascii_char}'")

PCTF{H@rd_Encryption_is_3asy}

이모지 스택 V2

난이도: 중간, 나에게 난이도: 어려움

v1과 동일하지만 몇 가지 추가 기능

주의할 점은 래스터 스캔 순서를 사용한다는 것

ChatGPT로 수정하면 쉽게 해결 (스크립트는 디스코드에서 가져옴. 내가 작성한 코드에서 문제가 발생했는데, 티켓을 열어 출제자에게 질문했을 때 "No Hints! You've got it"라고 답변을 받았다. 마지막에 래스터 스캔 순서로 저장하지 않아서 발생한 문제였다)

from PIL import Image 
import cv2
import  numpy as np


class EmojiStackInterpreter:

    def __init__(self, program_code):
        self.stack = cv2.imread("initial_state.png", 0)
        self.sp_x = 0 
        self.sp_y = 0
        self.last_jz = 0
        self.instruction_pointer = 0
        self.previous_command = ""
        self.program = program_code

    def execute(self, command):
        if command == "👆":
            self.sp_y = (self.sp_y + 1) % 255
        elif command == "👇":
            self.sp_y = (self.sp_y - 1) % 255
        elif command == "👉":
            self.sp_x = (self.sp_x + 1) % 255
        elif command == "👈":
            self.sp_x = (self.sp_x - 1) % 255
        elif command == "👍":
            if self.stack[self.sp_y][self.sp_x] < 255:
                self.stack[self.sp_y, self.sp_x] =  self.stack[self.sp_y, self.sp_x] + 1
        elif command == "👎": 
            if self.stack[self.sp_y][self.sp_x] > 0:
                self.stack[self.sp_y, self.sp_x] =  self.stack[self.sp_y, self.sp_x] - 1
        elif command == '🫸':
            if self.stack[self.sp_y, self.sp_x] == 0:
                depth = 1
                while depth != 0:
                    self.instruction_pointer += 1
                    if self.program[self.instruction_pointer] == '🫸':
                        depth += 1
                    elif self.program[self.instruction_pointer] == '🫷':
                        depth -= 1
        elif command == '🫷':
            if self.stack[self.sp_y, self.sp_x] != 0:
                depth = 1
                while depth != 0:
                    self.instruction_pointer -= 1
                    if self.program[self.instruction_pointer] == '🫷':
                        depth += 1
                    elif self.program[self.instruction_pointer] == '🫸':
                        depth -= 1
        elif command == "💬":
            print(chr(self.stack[self.sp_y, self.sp_x]),end="")
        elif command == "🔁":
            base12_mapping = {'🕛': 0, '🕐': 1, '🕑': 2, '🕒': 3, '🕓': 4, '🕔': 5, '🕕': 6, '🕖': 7, '🕗': 8, '🕘': 9, '🕙': 'a', '🕚': 'b'}
            a = base12_mapping[self.program[self.instruction_pointer + 1]]
            b = base12_mapping[self.program[self.instruction_pointer + 2]]
            c = base12_mapping[self.program[self.instruction_pointer + 3]]
            repeat_times = int(str(a) + str(b) + str(c), 12)
            for _ in range(repeat_times):
                self.execute(self.previous_command)
            self.instruction_pointer += 3
        else:
            print(">>>", ord(command), "<<<")

    def run(self):
        while self.instruction_pointer < len(self.program):
            self.execute(self.program[self.instruction_pointer])
            self.previous_command = self.program[self.instruction_pointer]
            self.instruction_pointer += 1
        return -1
    

file = open("program.txt","r")
program_code = file.read()
file.close()
interpreter = EmojiStackInterpreter(program_code)

interpreter.run()

cv2.imwrite("flag.png",interpreter.stack)

출력:

정말 끔찍하군(아니라고)

PCTF{3MOJ!==G00D!}

태그: CTF MISC 이모지 스택 RTL 리버스 엔지니어링

6월 30일 16:21에 게시됨