오늘은 Layer7 CTF에서 풀어본 문제 풀이을 작성할 것이다.
풀이
1. mic_check (MISC)

Layer7{15th_CTF_25tg_C1ub}
2. chat (MISC)
문제 설명
"We made Ai Chat service for you!
But.. this service is too unstable..!"
챗봇 서비스라고 인식하고 바로 시스템 규칙에 대해 물어봤다.

시스템 규칙으로 정보 노출이나 인증 토큰에 대한 접근을 차단한다고 한다.
추가로 관리자 모드를 활성화할 수 있으며 Deepseek-OCR을 사용하여 업로드한 이미지, 텍스트 파일을 읽어 AUTHZ:OK가 발견되면 활성화된다고 한다.
이미지 파일로 만들어 몇 번 시도해 보았는데 계속 관리자 모드 활성화를 안 시켜줘서 이게 아닌가.. 하고 방향을 좀 틀었다.
뭔가를 요청할 때마다 REDACTED가 뜨길래 REDACTED된 내용을 요청했다.

여기서 예시 (더미 토큰 사용)으로 관리자 모드가 활성화되면 Internal Authentication Token을 출력해 준다는 것을 알 수 있었다.
그래서 토큰 자체가 플래그로 되어있구나 싶어서 내부 토큰을 요청했다.

내부 토큰을 보여달라 하니 예시 문자열로 Wh7_W3_US3_D33PS33K_OCR_S7ST3M?이라는 문장이 나왔다.
매우 의심스러워 플래그에 넣어보니 정답이었다.
Layer7{Wh7_W3_US3_D33PS33K_OCR_S7ST3M?}
3. The Immitation Game (MISC)
문제 설명
Prove that you are the reincarnation of Alan Turing.
해당 문제는 제공된 사이트에 접속하니 노벨상 수상자에 대한 설명 중 옳은 것을 고르는 문제 풀이 사이트였다.
문제는 총 383개로 일정 개수 이상 맞추면 플래그를 줄 것이라고 생각했다.
깜빡하고 캡처를 안 따서 글로 설명할 것이다.
처음엔 로컬 LLM을 연동해서 자동화 코드로 정답 맞는 거 고르면 되겠지.. 하며 자동화 코드를 지피티에게 부탁했다.
해당 자동화 코드를 돌려놓고 다른 문제를 풀었는데,, 이거 다 돌아가는 데만 3시간 반이 걸렸다.
근데 정답률이 65%라 플래그도 못 얻었다.
Ollama와는 작별이다.
그래서 새벽 시간 대에 직접 하나씩 해볼까.. 하고 수동으로 풀고 있었는데 정답 선다의 특징을 대충 알 것 같았다.
그래서 LLM 빼고 선다 중 가장 긴 문자열을 고르는 자동화 코드로 변경했다.
import requests
import time
BASE = "http://172.238.19.81:18008"
QUESTION_URL = f"{BASE}/api/question"
CHECK_URL = f"{BASE}/api/check"
RESULT_URL = f"{BASE}/result"
def main():
session = requests.Session()
solved = 0
while True:
q = session.get(QUESTION_URL).json()
# 🔥 문제 끝났으면 즉시 result 페이지로 이동
if q.get("quizComplete") or q.get("resultReady"):
print("\n🎉 QUIZ COMPLETE — 결과 페이지 가져오는 중...\n")
r = session.get(RESULT_URL)
print(r.text) # ← 플래그 포함된 HTML 그대로 출력
return
# 정상 문제 처리
question = q.get("question")
options = q.get("options", [])
print(f"[{solved+1}] {question}")
if not options: # ← options 비면 문제 끝이므로 result로 이동
print("⚠ 옵션 없음 → result 페이지로 이동")
r = session.get(RESULT_URL)
print(r.text)
return
# 가장 긴 보기 선택
idx = max(range(len(options)), key=lambda i: len(options[i]))
print("선택:", idx)
# 정답 제출
session.post(
CHECK_URL,
json={
"questionId": q.get("id"),
"selectedIndex": idx
}
)
solved += 1
time.sleep(0.03)
if __name__ == "__main__":
main()
해당 코드는 돌아가는데 1분도 걸리지 않았고 정답률은 93%로 플래그가 추출되었다..

Layer7{8264bef1682a6157b3291a16}
대회 끝난 후 알아보니 정답은 인물의 이름이 담긴 선다가 정답이었다고 한다.
4. hell-em (WEB)

웹 문제를 풀 때 항상 일단 하나씩 눌러보는 편인데

status 페이지에 플래그가 적혀있었다.
Layer7{c4llenge_fr0m_h3ll-em}
revenge를 풀어보려 했지만,, 풀지 못했다.
5. happyware (REV)
문제 설명
Layer7의 부원 “swap”이 FLAG를 잊어버려서 힘들어하고 있었습니다.
그러다가 FLAG를 발견했으나, “행복”을 머금고 있었습니다.
“swap”은 행복을 되찾으려고 “행복”을 없애야 한다고 다짐했습니다.
본 문제는
https://dreamhack.io/wargame/challenges/512
영감을 받아 메커니즘을 변형한 문제입니다.
문제 파일을 다운 받아보니 chall 파일과 flag.png.happy 파일이 존재했다.
행복을 없애야 한다고 다짐했다길래 chall 파일을 통해 flag.png를 flag.png.happy로 암호화했구나 싶어서 복호화 코드를 짜면 되겠다고 생각했다.
IDA로 일단 chall 파일을 열어 분석했다.
해당 구간이 암호화 과정 중 가장 중요한 부분이었다.
.text:0000000000001590 loc_1590: ; CODE XREF: main+4D6↓j
.text:0000000000001590 add r13d, ebp
.text:0000000000001593 add r11d, ebx
.text:0000000000001596 add r10d, esi
.text:0000000000001599 xor ecx, r13d
.text:000000000000159C xor edx, r11d
.text:000000000000159F xor eax, r10d
.text:00000000000015A2 rol ecx, 10h
.text:00000000000015A5 rol edx, 10h
.text:00000000000015A8 add r9d, ecx
.text:00000000000015AB add edi, edx
.text:00000000000015AD rol eax, 10h
.text:00000000000015B0 xor ebp, r9d
.text:00000000000015B3 xor ebx, edi
.text:00000000000015B5 add r12d, eax
.text:00000000000015B8 rol ebp, 0Ch
.text:00000000000015BB rol ebx, 0Ch
.text:00000000000015BE xor esi, r12d
.text:00000000000015C1 add r13d, ebp
.text:00000000000015C4 add r11d, ebx
.text:00000000000015C7 rol esi, 0Ch
.text:00000000000015CA xor ecx, r13d
.text:00000000000015CD xor edx, r11d
.text:00000000000015D0 add r10d, esi
.text:00000000000015D3 rol ecx, 8
.text:00000000000015D6 rol edx, 8
.text:00000000000015D9 xor eax, r10d
.text:00000000000015DC add r9d, ecx
.text:00000000000015DF add edi, edx
.text:00000000000015E1 xor ebp, r9d
.text:00000000000015E4 xor ebx, edi
.text:00000000000015E6 mov [rsp+248h+var_234], edi
.text:00000000000015EA rol ebp, 7
.text:00000000000015ED rol ebx, 7
.text:00000000000015F0 mov [rsp+248h+var_238], r9d
.text:00000000000015F5 rol eax, 8
.text:00000000000015F8 add r8d, r15d
.text:00000000000015FB add r13d, ebx
.text:00000000000015FE xor r14d, r8d
.text:0000000000001601 add r12d, eax
.text:0000000000001604 mov edi, r14d
.text:0000000000001607 mov r14d, dword ptr [rsp+248h+var_240]
.text:000000000000160C xor esi, r12d
.text:000000000000160F rol edi, 10h
.text:0000000000001612 rol esi, 7
.text:0000000000001615 add r14d, edi
.text:0000000000001618 add r11d, esi
.text:000000000000161B xor r15d, r14d
.text:000000000000161E mov r9d, r14d
.text:0000000000001621 xor ecx, r11d
.text:0000000000001624 rol r15d, 0Ch
.text:0000000000001628 rol ecx, 10h
.text:000000000000162B add r8d, r15d
.text:000000000000162E xor edi, r8d
.text:0000000000001631 rol edi, 8
.text:0000000000001634 add r9d, edi
.text:0000000000001637 xor edi, r13d
.text:000000000000163A rol edi, 10h
.text:000000000000163D xor r15d, r9d
.text:0000000000001640 add r9d, ecx
.text:0000000000001643 add r12d, edi
.text:0000000000001646 rol r15d, 7
.text:000000000000164A xor ebx, r12d
.text:000000000000164D rol ebx, 0Ch
.text:0000000000001650 add r13d, ebx
.text:0000000000001653 xor edi, r13d
.text:0000000000001656 mov r14d, edi
.text:0000000000001659 rol r14d, 8
.text:000000000000165D add r12d, r14d
.text:0000000000001660 xor ebx, r12d
.text:0000000000001663 rol ebx, 7
.text:0000000000001666 xor esi, r9d
.text:0000000000001669 add r10d, r15d
.text:000000000000166C add r8d, ebp
.text:000000000000166F rol esi, 0Ch
.text:0000000000001672 xor edx, r10d
.text:0000000000001675 xor eax, r8d
.text:0000000000001678 rol edx, 10h
.text:000000000000167B add r11d, esi
.text:000000000000167E rol eax, 10h
.text:0000000000001681 xor ecx, r11d
.text:0000000000001684 rol ecx, 8
.text:0000000000001687 lea edi, [r9+rcx]
.text:000000000000168B mov r9d, [rsp+248h+var_238]
.text:0000000000001690 xor esi, edi
.text:0000000000001692 mov dword ptr [rsp+248h+var_240], edi
.text:0000000000001696 mov edi, [rsp+248h+var_234]
.text:000000000000169A add r9d, edx
.text:000000000000169D rol esi, 7
.text:00000000000016A0 add edi, eax
.text:00000000000016A2 xor r15d, r9d
.text:00000000000016A5 xor ebp, edi
.text:00000000000016A7 rol r15d, 0Ch
.text:00000000000016AB rol ebp, 0Ch
.text:00000000000016AE add r10d, r15d
.text:00000000000016B1 add r8d, ebp
.text:00000000000016B4 xor edx, r10d
.text:00000000000016B7 xor eax, r8d
.text:00000000000016BA rol edx, 8
.text:00000000000016BD rol eax, 8
.text:00000000000016C0 add r9d, edx
.text:00000000000016C3 add edi, eax
.text:00000000000016C5 xor r15d, r9d
.text:00000000000016C8 xor ebp, edi
.text:00000000000016CA rol r15d, 7
.text:00000000000016CE rol ebp, 7
.text:00000000000016D1 sub [rsp+248h+var_220], 1
.text:00000000000016D6 jnz loc_1590
수많은 add, xor, rol을 반복하여 키스트림을 생성한다.
키스트림은 입력 파일의 첫 바이트, 마지막 바이트, 파일 크기만으로 결정된다는 것을 알게 되었다.
즉, 동일한 첫 `바이트, 마지막 바이트, 파일 크기를 가진 파일을 넣으면 동일한 키스트림이 생성된다.
flag.png.happy 파일은 원본 flag.png의 첫 바이트, 마지막 바이트, 파일 크기가 동일하기에 동일한 dummy 파일을 만들어 암호화를 시킨 후 역연산을 진행했다.
png 시그니처와 IEND 청크 구조로 첫 바이트 0x89, 마지막 바이트는 0x82 고정으로 두고 원본 파일 크기는 .happy 헤더에 기록되어 있었다.
동일한 조건의 dummy 파일을 만들어 chall에 넣어 원본과 같은 키스트림을 구해 역연산을 수행했다.
#!/usr/bin/env python3
import struct
import subprocess
from pathlib import Path
CHALL_PATH = "./chall" # 바이너리 경로
FLAG_HAPPY = "flag.png.happy" # 주어진 암호 파일
DUMMY_PLAIN = "dummy.bin" # 우리가 만들 평문
DUMMY_HAPPY = "dummy.bin.happy" # chall가 만들어줄 암호 파일
OUTPUT_FLAG = "flag.png" # 최종 복호 PNG
MAGIC = b"HPPY"
def read_happy(path: str):
p = Path(path)
data = p.read_bytes()
if data[:4] != MAGIC:
raise ValueError(f"{path}: MAGIC HPPY 아님")
(size,) = struct.unpack("<I", data[4:8])
body = data[8:8+size]
return size, body
def make_dummy(size: int, path: str):
# PNG 시그니처를 맞추기 위해:
# 첫 바이트 = 0x89, 마지막 바이트 = 0x82, 나머지 0
buf = bytearray(size)
buf[0] = 0x89
buf[-1] = 0x82
Path(path).write_bytes(buf)
print(f"[+] dummy 생성: {path} (size={size})")
def run_chall(input_path: str):
print(f"[+] {CHALL_PATH} {input_path} 실행")
subprocess.run([CHALL_PATH, input_path], check=True)
def main():
# 1. flag.png.happy 읽어서 길이, 암호 데이터 추출
size_flag, c_flag = read_happy(FLAG_HAPPY)
print(f"[+] {FLAG_HAPPY}: size={size_flag}, cipher length={len(c_flag)}")
# 2. dummy 평문 생성 (같은 길이, 같은 첫/마지막 바이트)
make_dummy(size_flag, DUMMY_PLAIN)
# 3. chall로 dummy 암호화
run_chall(DUMMY_PLAIN)
# 4. dummy.bin.happy 읽기
size_dummy, c_dummy = read_happy(DUMMY_HAPPY)
if size_dummy != size_flag:
raise ValueError("dummy와 flag의 size 불일치")
# 5. 우리가 만든 dummy 평문 다시 읽기
q_dummy = bytearray(Path(DUMMY_PLAIN).read_bytes())
if len(q_dummy) != size_flag:
raise ValueError("dummy 평문 길이 이상함")
# 6. keystream = C_dummy XOR Q_dummy
keystream = bytes(cd ^ qd for cd, qd in zip(c_dummy, q_dummy))
# 7. flag 평문 = C_flag XOR keystream
plain_flag = bytes(cf ^ k for cf, k in zip(c_flag, keystream))
# 8. PNG 파일로 저장
Path(OUTPUT_FLAG).write_bytes(plain_flag)
print(f"[+] 복호 완료: {OUTPUT_FLAG}")
if __name__ == "__main__":
main()
ubuntu를 열어 해당 파일을 실행했다.

flag.png가 생성되었다.

Layer7{h4ve_a_n1ce_d4y}
6. JumpGame (REV)
문제 설명
Jump!! Jump!!
제공된 파일을 다운 받으니 apk 파일 하나가 존재했다.
unzip으로 apk 압축을 풀고 lib/x86/libnative-lib.so 파일을 IDA로 열어보았다.
int __cdecl JNI_OnLoad(int a1)
{
int v1; // ecx
int result; // eax
int v3; // ecx
int v4; // [esp+8h] [ebp-10h] BYREF
unsigned int v5; // [esp+Ch] [ebp-Ch]
v5 = __readgsdword(0x14u);
v1 = (*(int (__cdecl **)(int, int *, int))(*(_DWORD *)a1 + 24))(a1, &v4, 65542);
result = -1;
if ( !v1 )
{
v3 = (*(int (__cdecl **)(int, const char *))(*(_DWORD *)v4 + 24))(v4, "com/example/jumpgame/MainActivity");
result = -1;
if ( v3 )
result = ((*(int (__cdecl **)(int, int, char **, int))(*(_DWORD *)v4 + 860))(v4, v3, off_304C, 1) >> 31) | 0x10006;
}
if ( __readgsdword(0x14u) != v5 )
JUMPOUT(0x8C3);
return result;
}
JNI_OnLoad 함수 (디컴파일)
여기서 off_304C를 확인해 보았다.
.data:0000304C off_304C dd offset aCheckcode ; DATA XREF: LOAD:0000009C↑o
.data:0000304C ; JNI_OnLoad+6E↑o
.data:0000304C ; "checkCode"
.data:00003050 dd offset aLjavaLangStrin_0 ; "(Ljava/lang/String;)Z"
.data:00003054 dd offset sub_8D0
.data:00003054 _data ends
여기서 sub_8D0 함수를 확인해 보았다.
char __cdecl sub_8D0(int a1, int a2, int a3)
{
int v3; // edi
int v4; // eax
int v5; // edi
int v6; // esi
int v7; // eax
_BYTE *v8; // edi
unsigned __int8 v9; // al
int v10; // ecx
unsigned __int8 *v11; // edx
int v12; // edx
unsigned __int8 v13; // cl
char *v14; // eax
unsigned int v15; // eax
int v16; // edx
int v17; // ecx
char result; // al
unsigned int v19; // eax
int v20; // eax
unsigned int v21; // edx
int v22; // edx
unsigned int v23; // eax
__m128i *v24; // eax
__m128i v25; // xmm0
__m128i v26; // xmm1
__m128i v27; // xmm2
char v28; // dl
__m128i v29; // xmm3
__m128i v30; // xmm0
__m128i v31; // xmm1
__m128i v32; // xmm4
__m128i v33; // xmm4
__m128i v34; // xmm2
__m128i v35; // xmm3
int v36; // [esp+4h] [ebp-10A8h]
unsigned int v37; // [esp+4h] [ebp-10A8h]
__m128i *v38; // [esp+4h] [ebp-10A8h]
unsigned __int8 v39; // [esp+8h] [ebp-10A4h]
int v40; // [esp+Ch] [ebp-10A0h]
unsigned __int8 v41; // [esp+13h] [ebp-1099h]
unsigned __int8 v42; // [esp+14h] [ebp-1098h]
char v43; // [esp+15h] [ebp-1097h]
unsigned __int8 v44; // [esp+16h] [ebp-1096h]
unsigned __int8 v45; // [esp+17h] [ebp-1095h]
unsigned __int8 v46; // [esp+18h] [ebp-1094h] BYREF
_BYTE v47[127]; // [esp+19h] [ebp-1093h] BYREF
char v48[4096]; // [esp+98h] [ebp-1014h] BYREF
unsigned int v49; // [esp+1098h] [ebp-14h]
v3 = a3;
v49 = __readgsdword(0x14u);
if ( !a3 )
goto LABEL_25;
v4 = __open_2("/proc/self/maps", 0);
if ( v4 >= 0 )
{
v5 = v4;
v6 = read(v4, v48, 4095);
close(v5);
if ( v6 <= 0 )
{
v3 = a3;
}
else
{
v48[v6] = 0;
if ( strstr(v48, "frida") )
goto LABEL_25;
if ( strstr(v48, "gum") )
goto LABEL_25;
v3 = a3;
if ( strstr(v48, "frida-agent") )
goto LABEL_25;
}
}
v7 = (*(int (__cdecl **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, v3, 0);
if ( !v7 )
{
LABEL_25:
result = 0;
goto LABEL_26;
}
v8 = (_BYTE *)v7;
if ( strlen(v7) != 6 )
goto LABEL_24;
if ( (unsigned __int8)(*v8 - 58) < 0xF6u )
goto LABEL_24;
if ( (unsigned __int8)(v8[1] - 58) < 0xF6u )
goto LABEL_24;
if ( (unsigned __int8)(v8[2] - 58) < 0xF6u )
goto LABEL_24;
if ( (unsigned __int8)(v8[3] - 58) < 0xF6u )
goto LABEL_24;
if ( (unsigned __int8)(v8[4] - 58) < 0xF6u )
goto LABEL_24;
if ( (unsigned __int8)(v8[5] - 58) < 0xF6u )
goto LABEL_24;
v36 = strlen(v8);
__memcpy_chk(&v46, v8, v36, 128);
*(_DWORD *)&v47[v36 - 1] = 929837171;
v47[v36 + 3] = 0;
v9 = v46;
if ( !v46 )
goto LABEL_24;
v10 = -2128831035;
v11 = v47;
do
{
v10 = 16777619 * (v10 ^ v9);
v9 = *v11++;
}
while ( v9 );
if ( v10 != -1075226523 )
{
LABEL_24:
(*(void (__cdecl **)(int, int, _BYTE *))(*(_DWORD *)a1 + 680))(a1, a3, v8);
goto LABEL_25;
}
strcpy(v48, "FLAG_SEED_2025s0l7");
v12 = -2128831035;
v13 = 70;
v14 = &v48[1];
do
{
v12 = 16777619 * (v12 ^ v13);
v13 = *v14++;
}
while ( v13 );
v15 = __ROL4__(v12, 13) ^ 0x89CB1B76;
v16 = v12 & 3 ^ 1;
v17 = 0;
v40 = v16;
do
{
if ( v16 == 2 )
{
v15 = 1664525 * v15 + 1013904223;
goto LABEL_36;
}
if ( v16 == 1 )
{
v37 = v15 - 1640531527;
v19 = -2048144789 * ((v15 - 1640531527) ^ ((v15 - 1640531527) >> 16));
v20 = -1028477387 * (v19 ^ (v19 >> 13));
v21 = v20;
LABEL_29:
v22 = v20 ^ HIWORD(v21);
v15 = v37;
goto LABEL_30;
}
if ( v16 )
{
v23 = v15 ^ (v15 << 13) ^ ((v15 ^ (v15 << 13)) >> 17) ^ (32 * (v15 ^ (v15 << 13) ^ ((v15 ^ (v15 << 13)) >> 17)));
v37 = v23 - 1640531527;
v21 = -1028477387
* ((-2048144789 * ((v23 - 1640531527) ^ ((v23 - 1640531527) >> 16))) ^ ((-2048144789
* ((v23 - 1640531527) ^ ((v23 - 1640531527) >> 16))) >> 13));
v20 = v21 ^ v23;
goto LABEL_29;
}
v15 ^= (v15 << 13) ^ ((v15 ^ (v15 << 13)) >> 17) ^ (32 * (v15 ^ (v15 << 13) ^ ((v15 ^ (v15 << 13)) >> 17)));
LABEL_36:
LOBYTE(v22) = v15;
LABEL_30:
*(&v41 + v17++) = v22;
v16 = v40;
}
while ( v17 != 5 );
v24 = (__m128i *)malloc(30);
if ( v24 )
{
v38 = v24;
v39 = __ROL1__(v43, 1);
v25 = _mm_cvtsi32_si128(v44 ^ 0xAAu);
v26 = _mm_cvtsi32_si128(v39);
v27 = _mm_cvtsi32_si128(v41);
v28 = v45 ^ ((v39 ^ ((v41 ^ 0x4B) - v42)) - (v44 ^ 0xAA));
v29 = _mm_cvtsi32_si128(v45);
v30 = _mm_shufflelo_epi16(_mm_unpacklo_epi8(v25, v25), 0);
v31 = _mm_shufflelo_epi16(_mm_unpacklo_epi8(v26, v26), 0);
v32 = _mm_cvtsi32_si128(v42);
v33 = _mm_shufflelo_epi16(_mm_unpacklo_epi8(v32, v32), 0);
v34 = _mm_shufflelo_epi16(_mm_unpacklo_epi8(v27, v27), 0);
v35 = _mm_xor_si128(_mm_shufflelo_epi16(_mm_unpacklo_epi8(v29, v29), 0), (__m128i)xmmword_530);
*v24 = _mm_unpacklo_epi64(
_mm_unpacklo_epi32(
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(
_mm_sub_epi8(_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_4E0), v34), v33),
v31),
v30),
v35),
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(
_mm_sub_epi8(_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_510), v34), v33),
v31),
v30),
v35)),
_mm_unpacklo_epi32(
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(
_mm_sub_epi8(_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_520), v34), v33),
v31),
v30),
v35),
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(
_mm_sub_epi8(_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_540), v34), v33),
v31),
v30),
v35)));
v24[1].m128i_i32[0] = _mm_cvtsi128_si32(
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(
_mm_sub_epi8(_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_4F0), v34), v33),
v31),
v30),
v35));
v24[1].m128i_i32[1] = _mm_cvtsi128_si32(
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(
_mm_sub_epi8(_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_500), v34), v33),
v31),
v30),
v35));
v24[1].m128i_i32[2] = _mm_cvtsi128_si32(
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(_mm_sub_epi8(_mm_xor_si128(v34, (__m128i)xmmword_550), v33), v31),
v30),
v35));
v24[1].m128i_i8[12] = v28 ^ 0x6C;
v24[1].m128i_i8[13] = 0;
sub_DE0(v24);
free(v38);
}
(*(void (__cdecl **)(int, int, _BYTE *))(*(_DWORD *)a1 + 680))(a1, a3, v8);
result = 1;
LABEL_26:
if ( __readgsdword(0x14u) != v49 )
JUMPOUT(0xDD5);
return result;
}
sub_8D0 함수 (디컴파일)
함수에선 frida, gum, firda-agent를 감지하고 발견되면 거부한다.
v7 = (*(int (__cdecl **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, v3, 0);
if ( !v7 )
{
LABEL_25:
result = 0;
goto LABEL_26;
}
v8 = (_BYTE *)v7;
if ( strlen(v7) != 6 )
goto LABEL_24;
if ( (unsigned __int8)(*v8 - 58) < 0xF6u )
goto LABEL_24;
if ( (unsigned __int8)(v8[1] - 58) < 0xF6u )
goto LABEL_24;
if ( (unsigned __int8)(v8[2] - 58) < 0xF6u )
goto LABEL_24;
if ( (unsigned __int8)(v8[3] - 58) < 0xF6u )
goto LABEL_24;
if ( (unsigned __int8)(v8[4] - 58) < 0xF6u )
goto LABEL_24;
if ( (unsigned __int8)(v8[5] - 58) < 0xF6u )
goto LABEL_24;
v36 = strlen(v8);
__memcpy_chk(&v46, v8, v36, 128);
*(_DWORD *)&v47[v36 - 1] = 929837171;
v47[v36 + 3] = 0;
v9 = v46;
if ( !v46 )
goto LABEL_24;
v10 = -2128831035; # 0x811C9DC5
v11 = v47;
do
{
v10 = 16777619 * (v10 ^ v9);
v9 = *v11++;
}
while ( v9 );
if ( v10 != -1075226523 ) # 0xBFE95865
{
그리고 6글자 input을 받아 "s0l7"을 붙여 FNV-1 해시를 검사한다.
만약 결과가 -1075226523이 아니면 실패한다.
해당 코드는 입력을 검증하는 코드이다.
strcpy(v48, "FLAG_SEED_2025s0l7");
v12 = -2128831035;
v13 = 70;
v14 = &v48[1];
do
{
v12 = 16777619 * (v12 ^ v13);
v13 = *v14++;
}
while ( v13 );
v15 = __ROL4__(v12, 13) ^ 0x89CB1B76;
v16 = v12 & 3 ^ 1;
v17 = 0;
v40 = v16;
do
{
if ( v16 == 2 )
{
v15 = 1664525 * v15 + 1013904223;
goto LABEL_36;
}
if ( v16 == 1 )
{
v37 = v15 - 1640531527;
v19 = -2048144789 * ((v15 - 1640531527) ^ ((v15 - 1640531527) >> 16));
v20 = -1028477387 * (v19 ^ (v19 >> 13));
v21 = v20;
LABEL_29:
v22 = v20 ^ HIWORD(v21);
v15 = v37;
goto LABEL_30;
}
if ( v16 )
{
v23 = v15 ^ (v15 << 13) ^ ((v15 ^ (v15 << 13)) >> 17) ^ (32 * (v15 ^ (v15 << 13) ^ ((v15 ^ (v15 << 13)) >> 17)));
v37 = v23 - 1640531527;
v21 = -1028477387
* ((-2048144789 * ((v23 - 1640531527) ^ ((v23 - 1640531527) >> 16))) ^ ((-2048144789
* ((v23 - 1640531527) ^ ((v23 - 1640531527) >> 16))) >> 13));
v20 = v21 ^ v23;
goto LABEL_29;
}
v15 ^= (v15 << 13) ^ ((v15 ^ (v15 << 13)) >> 17) ^ (32 * (v15 ^ (v15 << 13) ^ ((v15 ^ (v15 << 13)) >> 17)));
LABEL_36:
LOBYTE(v22) = v15;
LABEL_30:
*(&v41 + v17++) = v22;
v16 = v40;
}
while ( v17 != 5 );
해당 코드에서는 "FLAG_SEED_2025s0l7"을 통해 5바이트 복호화 키를 생성하며 v41, v42, v43, v44, v45에 저장된다.
v24 = (__m128i *)malloc(30);
if ( v24 )
{
v38 = v24;
v39 = __ROL1__(v43, 1);
v25 = _mm_cvtsi32_si128(v44 ^ 0xAAu);
v26 = _mm_cvtsi32_si128(v39);
v27 = _mm_cvtsi32_si128(v41);
v28 = v45 ^ ((v39 ^ ((v41 ^ 0x4B) - v42)) - (v44 ^ 0xAA));
v29 = _mm_cvtsi32_si128(v45);
v30 = _mm_shufflelo_epi16(_mm_unpacklo_epi8(v25, v25), 0);
v31 = _mm_shufflelo_epi16(_mm_unpacklo_epi8(v26, v26), 0);
v32 = _mm_cvtsi32_si128(v42);
v33 = _mm_shufflelo_epi16(_mm_unpacklo_epi8(v32, v32), 0);
v34 = _mm_shufflelo_epi16(_mm_unpacklo_epi8(v27, v27), 0);
v35 = _mm_xor_si128(_mm_shufflelo_epi16(_mm_unpacklo_epi8(v29, v29), 0), (__m128i)xmmword_530);
v24는 30바이트의 플래그 버퍼이며 해당 버퍼에 값을 연산하여 flag를 생성한다.
*v24 = _mm_unpacklo_epi64(
_mm_unpacklo_epi32(
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(
_mm_sub_epi8(_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_4E0), v34), v33),
v31),
v30),
v35),
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(
_mm_sub_epi8(_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_510), v34), v33),
v31),
v30),
v35)),
_mm_unpacklo_epi32(
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(
_mm_sub_epi8(_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_520), v34), v33),
v31),
v30),
v35),
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(
_mm_sub_epi8(_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_540), v34), v33),
v31),
v30),
v35)));
v24[1].m128i_i32[0] = _mm_cvtsi128_si32(
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(
_mm_sub_epi8(_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_4F0), v34), v33),
v31),
v30),
v35));
v24[1].m128i_i32[1] = _mm_cvtsi128_si32(
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(
_mm_sub_epi8(_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_500), v34), v33),
v31),
v30),
v35));
v24[1].m128i_i32[2] = _mm_cvtsi128_si32(
_mm_xor_si128(
_mm_sub_epi8(
_mm_xor_si128(_mm_sub_epi8(_mm_xor_si128(v34, (__m128i)xmmword_550), v33), v31),
v30),
v35));
v24[1].m128i_i8[12] = v28 ^ 0x6C;
v24[1].m128i_i8[13] = 0;
sub_DE0(v24);
해당 코드에서 암호화된 테이블의 값을 가져와 SSE 연산을 진행하여 flag를 생성한다.
각 xmmword_~~~ 테이블에 있는 값을 가져와 xor, sub 등 연산을 진행한다.
.rodata:000004E0 xmmword_4E0 xmmword 234F273Ah ; DATA XREF: sub_8D0+3D2↓r
.rodata:000004F0 xmmword_4F0 xmmword 5E255396h ; DATA XREF: sub_8D0+45E↓r
.rodata:00000500 xmmword_500 xmmword 35E9129h ; DATA XREF: sub_8D0+47F↓r
.rodata:00000510 xmmword_510 xmmword 3C4D9154h ; DATA XREF: sub_8D0+432↓r
.rodata:00000520 xmmword_520 xmmword 29565B53h ; DATA XREF: sub_8D0+3F6↓r
.rodata:00000530 xmmword_530 xmmword 6C6C6C6Ch ; DATA XREF: sub_8D0+3E6↓r
.rodata:00000540 xmmword_540 xmmword 32299632h ; DATA XREF: sub_8D0+412↓r
.rodata:00000550 xmmword_550 xmmword 4F3D9329h ; DATA XREF: sub_8D0+4A0↓r
각 테이블에 저장되어 있는 암호화된 값이다.
이제 구조를 파악했으니 역연산 코드를 통해 복호화된 값들을 구할 것이다.
def dword_to_xmm(d):
# 32비트 값을 little-endian 4바이트 + 나머지 12바이트 0 으로 확장
return [
d & 0xff,
(d >> 8) & 0xff,
(d >> 16) & 0xff,
(d >> 24) & 0xff,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
]
# 네가 준 .rodata 값들
xmm_4E0 = dword_to_xmm(0x234F273A)
xmm_4F0 = dword_to_xmm(0x5E255396)
xmm_500 = dword_to_xmm(0x035E9129)
xmm_510 = dword_to_xmm(0x3C4D9154)
xmm_520 = dword_to_xmm(0x29565B53)
xmm_530 = dword_to_xmm(0x6C6C6C6C)
xmm_540 = dword_to_xmm(0x32299632)
xmm_550 = dword_to_xmm(0x4F3D9329)
def rol32(x, n):
return ((x << n) | (x >> (32 - n))) & 0xffffffff
def rol1(b):
return ((b << 1) & 0xff) | (b >> 7)
def broadcast(byte):
return [byte] * 16
def block(base, v30, v31, v33, v34, v35):
t = [(base[i] ^ v34[i]) & 0xff for i in range(16)]
t = [(t[i] - v33[i]) & 0xff for i in range(16)]
t = [(t[i] ^ v31[i]) & 0xff for i in range(16)]
t = [(t[i] - v30[i]) & 0xff for i in range(16)]
t = [(t[i] ^ v35[i]) & 0xff for i in range(16)]
return t
def unpacklo_epi32(a, b):
# 4바이트씩 끊어서: [a0,b0,a1,b1]
al = [a[0:4], a[4:8], a[8:12], a[12:16]]
bl = [b[0:4], b[4:8], b[8:12], b[12:16]]
new_lanes = [al[0], bl[0], al[1], bl[1]]
out = []
for lane in new_lanes:
out += lane
return out
def unpacklo_epi64(a, b):
# 8바이트씩 끊어서: [a0,b0]
al = [a[0:8], a[8:16]]
bl = [b[0:8], b[8:16]]
new_lanes = [al[0], bl[0]]
out = []
for lane in new_lanes:
out += lane
return out
def main():
# 1) FNV-1 해시로 v41~v45 키 만들기
seed = b"FLAG_SEED_2025s0l7"
fnv = 0x811C9DC5
for c in seed:
fnv ^= c
fnv = (fnv * 0x1000193) & 0xffffffff
v12 = fnv
v15 = rol32(v12, 13) ^ 0x89CB1B76
v16 = (v12 & 3) ^ 1
# 이 문제에선 v16이 2로 떨어짐
assert v16 == 2
keys = []
for _ in range(5):
v15 = (1664525 * v15 + 1013904223) & 0xffffffff
keys.append(v15 & 0xff)
v41, v42, v43, v44, v45 = keys
# 2) SSE에서 쓰는 브로드캐스트 벡터들
v39 = rol1(v43)
v44_x = v44 ^ 0xAA
v30 = broadcast(v44_x)
v31 = broadcast(v39)
v33 = broadcast(v42)
v34 = broadcast(v41)
v35 = broadcast(v45)
v35 = [(v35[i] ^ xmm_530[i]) & 0xff for i in range(16)]
# 3) 각 테이블 블록 디코딩
t4E0 = block(xmm_4E0, v30, v31, v33, v34, v35)
t510 = block(xmm_510, v30, v31, v33, v34, v35)
t520 = block(xmm_520, v30, v31, v33, v34, v35)
t540 = block(xmm_540, v30, v31, v33, v34, v35)
u0 = unpacklo_epi32(t4E0, t510)
u1 = unpacklo_epi32(t520, t540)
first16 = unpacklo_epi64(u0, u1) # 0~15 바이트
t4F0 = block(xmm_4F0, v30, v31, v33, v34, v35)
t500 = block(xmm_500, v30, v31, v33, v34, v35)
t550 = block(xmm_550, v30, v31, v33, v34, v35)
rest12 = t4F0[0:4] + t500[0:4] + t550[0:4] # 16~27
# 마지막 한 바이트 (C 코드의 v28 ^ 0x6C)
v28 = v45 ^ ((v39 ^ ((v41 ^ 0x4B) - v42)) - v44_x)
final_28 = (v28 ^ 0x6C) & 0xff
flag_bytes = first16 + rest12 + [final_28, 0x00]
print("hex :", " ".join(f"{b:02X}" for b in flag_bytes))
print("str :", bytes(flag_bytes[:-1]).decode("ascii"))
if __name__ == "__main__":
main()
❯ python3 ss.py
hex : 4C 61 79 65 72 37 7B 4A 75 6D 70 5F 54 30 5F 54 30 75 63 68 5F 37 68 45 5F 35 4B 79 7D 00
str : Layer7{Jump_T0_T0uch_7hE_5Ky}
Layer7{Jump_T0_T0uch_7hE_5ky}
후기
개인전 CTF는 처음이라 좀 두려웠는데, 일단 끝까지 열심히 참여한 것 같아 좋았다.
아쉬운 부분은 WEB 문제를 제대로 푼 게 없어서 다음엔 더 WEB에 집중적으로 공부해야겠다.
재밌는 문제가 많아서 좋았지만 다 풀어보지 못해 아쉽다.
풍선 다 터트리고 싶었고 마트료시카 다 부숴서라도 플래그를 보고 싶었다.
'Knights Frontier_1st > CTF | 워게임' 카테고리의 다른 글
| [ Knights Frontier ] ROP Emporium 올클리어 (0) | 2026.04.02 |
|---|---|
| [ Knights Frontier ] Space Alone 올클리어 (2) | 2026.03.29 |
| [ Knights Frontier ] BombLab 올클리어 & 바이너리 패치 수행 (0) | 2026.03.27 |
| [ RED RACCOON ] TI 실전 추적 챌린지 (0) | 2026.01.23 |
| [ ACDC ] 2025 ACDC write-up (0) | 2025.11.06 |