| 사용자 이름 | 점수 |
|---|---|
| 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!}