사례 연구 · DownUnderCTF 2023

CTF 바이너리에서
플래그를 찾으세요

REA로 작은 플래그 검사기를 분석하고 규칙을 방정식으로 바꿔, 원본 프로그램이 Correct!을 출력하는 입력을 찾아봅니다.

masked squares 플래그 검사기 · 쉬움 · Linux x86-64

원본 문제 보기

바이너리에서 검증된 답까지

  1. 01 · 검사 코드 찾기문자열 → 검사 함수REA는 프롬프트와 그 참조, 입력을 허용하거나 거부하는 코드를 찾습니다.
  2. 02 · 규칙 추출문자 36개 · 합 26개REA는 각 검사에 쓰이는 명령어와 데이터를 반환합니다. 에이전트는 이를 방정식으로 바꿉니다.
  3. 03 · 풀이와 실행복원한 플래그 → Correct!Python 스크립트가 방정식을 풉니다. REA는 원본 프로그램이 결과를 받아들이는 모습을 캡처합니다.
joseph가 만든 문제이며 DownUnderCTF 공식 저장소에 공개돼 있습니다.

검사 코드 찾기

제공 파일은 15 KB 실행 파일입니다. 플래그를 입력받고 Correct! 또는 Incorrect!을 출력합니다. 에이전트에게 간단한 요청을 해보세요.

코딩 에이전트

REA로 ms_flag_checker를 분석하고 플래그를 찾으세요. 검사 동작을 설명한 뒤 원래 프로그램으로 답을 검증하세요.

문제 파일을 로컬에 다운로드하고 네이티브 분석 도구를 연결한 상태에서 사용하는 예제 프롬프트입니다.

  1. 입력 프롬프트에서 해당 함수로 추적

    에이전트 → REA
    open_binary
    {"path": "ms_flag_checker"}
    
    search_strings
    {"pattern": "flag|correct|wrong|input",
     "mode": "regex", "case_sensitive": false}
    
    xrefs
    {"address": "0x102004"}
    
    resolve_containing_procedure
    {"address": "0x10126d"}
    REA → 에이전트 · 결과 발췌
    0x102004  "What is the flag? "
    0x10201c  "Incorrect!"
    0x102027  "Correct!"
    
    Prompt referenced at: 0x10126d
    Containing function: 0x10124d

    원래 함수 이름이 제거된 실행 파일에서도 이 참조를 통해 조사를 시작할 위치를 찾을 수 있습니다.

  2. 검사기와 보조 함수 두 개 읽기

    에이전트 → REA
    analyze_function
    {"procedure": "0x10124d"}
    
    analyze_function
    {"procedure": "0x101189"}
    
    analyze_function
    {"procedure": "0x101217"}
    REA → 에이전트 · 선택한 명령어
    0x1012c1: CMP EDI, 0x24
    0x1012c6: LEA RBX, [0x1040e0]
    0x1012cd: LEA RBP, [0x104060]
    0x1012d4: LEA R13, [RBX + 0x270]
    0x1012e1: CALL 0x00101189
    0x1012ec: CALL 0x00101217
    0x1012f1: CMP dword ptr [RBP], EAX
    0x1012f4: JNZ 0x00101335
    0x1012f6: ADD RBX, 0x18

    첫 루프는 문자 코드 36개를 복사합니다. 검사 루프는 마스크를 해석해 선택한 코드를 더하고 저장된 수와 비교합니다. 0x270바이트 범위를 0x18바이트씩 이동하므로 마스크는 26개입니다.

새로 수행한 REA 분석에서 일부 결과를 골랐습니다. 로컬 대상 경로는 표시용으로 줄였고, 주소는 REA가 가져온 이미지를 기준으로 합니다.

하나의 검사로 문자 하나를 알아냅니다

검사기는 입력을 6×6 격자로 배치합니다. 마스크는 합산할 칸을 선택합니다. 압축된 인코딩에서 음수는 칸 건너뛰기, 양수는 칸 선택을 뜻합니다.

일곱 번째 마스크는 21칸을 건너뛰고 위치 21을 선택한 뒤 14칸을 건너뜁니다. 이 칸만 합계에 포함됩니다. 필요한 문자 코드는 55이므로 문자는 7입니다.
일곱 번째 검사는 한 위치만 선택합니다. 그림 열기.

바이너리에서 마스크와 목표값 읽기

에이전트 → REA · read_bytes
{"address": "0x1040e0", "length": 624}
{"address": "0x104060", "length": 144}
REA → 에이전트 · 일곱 번째 항목
Mask at 0x104170:
eb 01 f2 00  →  -21, +1, -14, stop

Target at 0x104078:
37 00 00 00  →  55

명령어 MOVSX은 마스크 바이트를 부호 있는 값으로 읽습니다. 그래서 eb은 −21을 뜻합니다. 목표값은 리틀 엔디언 정수입니다. 이 바이트들로 code[21] = 55임을 알 수 있으므로 해당 문자는 7입니다.

명령어에서 읽기 쉬운 검사 코드로

단계를 선택해 합산 보조 함수와 호출 함수가 C 코드의 같은 규칙에 어떻게 대응하는지 살펴보세요.

REA · 원본 명령어 발췌
0x10121c: MOV ECX, 0x0

0x10122c: CMP dword ptr [RSI + RAX*0x1], 0x0
0x101230: JZ 0x00101223

0x101232: ADD ECX, dword ptr [RDI + RAX*0x1]

0x101223: ADD RAX, 0x4

0x1012f1: CMP dword ptr [RBP], EAX
0x1012f4: JNZ 0x00101335
읽기 쉬운 C 코드 · 검사 하나의 요약
int check_one_mask(const int codes[36],
                   const int mask[36],
                   int target) {
    int total = 0;
    for (int i = 0; i < 36; ++i) {
        if (mask[i]) {
            total += codes[i];
        }
    }
    return total == target;
}

01 · 0에서 시작합니다. ECX에는 누적 합이 저장됩니다.

명령어는 합산 보조 함수와 호출 함수에서 가져왔습니다. C 코드는 설명용 요약으로, 의미 있는 이름과 36개 칸을 한 번에 순회하는 루프를 사용했습니다.

방정식 26개 풀기

대부분의 마스크는 여러 칸을 선택합니다. 첫 검사는 문자 코드 16개를 더해 합이 1441인지 확인합니다. 선택 영역들이 겹치므로 작은 Python 스크립트로 함께 풀 수 있는 연립방정식이 됩니다.

파이썬 · 솔버 발췌
from z3 import Int, Or, Solver, Sum, sat

codes = [Int(f"code_{i}") for i in range(36)]
solver = Solver()
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}"
for code in codes:
    solver.add(Or(*[code == ord(c) for c in alphabet]))
for code, char in zip(codes, "DUCTF{"):
    solver.add(code == ord(char))
solver.add(codes[-1] == ord("}"))

for selected, target in checks:
    solver.add(Sum([codes[i] for i in selected]) == target)

assert solver.check() == sat
model = solver.model()
print("".join(chr(model.eval(code).as_long()) for code in codes))

Z3는 이 방정식들을 만족하는 값을 찾는 솔버입니다. 발췌 코드는 추출한 검사를 사용하며, 영문자·숫자·밑줄·중괄호로 이루어진 DUCTF{…} 플래그를 가정합니다. 전체 다운로드에는 마스크 26개와 목표값이 모두 들어 있습니다.

전체 풀이 코드 다운로드

원본 프로그램으로 정답 검증

REA의 프로세스 캡처로 원본 실행 파일이 구한 플래그를 받아들이는 것을 기록했습니다. 선택된 문자 하나를 z에서 y로 바꾸면 첫 합계가 1441에서 1440이 되어 프로그램이 거부합니다.

입력 관찰된 출력 종료 코드
솔버가 구한 플래그 Correct! 0
한 문자 변경 Incorrect! 255
복구된 플래그 및 캡처된 출력 표시
REA · 원본 프로그램 출력
What is the flag? DUCTF{ezzzpzzz_07bcda7bfe81faf43caa}
Correct!

복구한 값은 주최 측이 공개한 문제 메타데이터의 플래그와도 일치합니다. 이 대조는 바이너리를 푼 뒤에 수행했습니다.

출처, 대상 식별 정보, 분석 세부 사항

2026년 10월 8일 Linux x64에서 REA 4.1.0과 Ghidra 12.1.4를 사용해 새로 분석하고 프로세스 실행을 두 차례 캡처했습니다. 바이너리는 심볼 정보가 제거된 x86-64 ELF이며, 크기는 15,248바이트입니다.

원본 실행 파일 · SHA-256
dcd3bec4f608e11f3a12ce461100aaf5f621851ee63a1f870d7c90d4e3dd51ae

주최 측의 소스나 풀이를 읽기 전에 문자열 참조, 함수, 상수를 분석했습니다. Python 풀이 코드와 그림은 이 REA 결과로 만들었습니다.

주최 측 소스와 비교 · 문제 메타데이터와 플래그 · REA 증거 기록

직접 해보기

원본 문제와 풀이 코드를 한 폴더에 다운로드하세요. 위 프롬프트로 에이전트와 함께 조사하거나, 제공된 스크립트를 실행하세요.

터미널 · 해결 및 실행
python3 -m venv .venv
.venv/bin/python -m pip install z3-solver
.venv/bin/python solve.py

chmod +x ms_flag_checker
./ms_flag_checker

스크립트가 출력하는 후보 플래그를 프로그램 프롬프트에 붙여넣으세요. 풀이에는 Python과 z3-solver가 필요합니다. 원본 실행 파일은 Linux x86-64에서 실행해야 합니다.

코딩 에이전트와 REA 설정하기

상단