Step 217. crackme 10개 (난이도 하) — 유형을 간파하는 속도를 기른다
Level 3 — Reversing 트랙 | 난이도 ★★★☆☆ | 예상 소요 시간 6시간
전제: Step 178(리버싱 맛보기), Step 215(Ghidra 심화), Step 216(x64dbg 심화)을 마쳤다. strings → objdump → gdb의 3단 도구 순서가 몸에 있다.
⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. crackme는 "풀라고 만들어진" 합법 학습 자료이고, 오늘의 연습 바이너리는 직접 컴파일한 것입니다.
- 준비물: WSL 우분투 (실측: Ubuntu 24.04, gcc 13.3.0, gdb 15.1). 외부 플랫폼 crackmes.one의 화면은 예시로 안내하고, 풀이 훈련은 직접 만든 crackme 3종으로 실측합니다.
- 주의: 내려받은 바이너리는 반드시 랩·VM 안에서만 실행하세요. "crackme니까 안전하겠지"가 아니라, 출처를 신뢰할 수 없는 실행 파일은 원칙적으로 격리된 환경에서만 다룹니다.
지금까지 리버싱 기법을 하나씩 배웠습니다. 오늘부터 이틀은 반복입니다 — 같은 기법을 낯선 문제에 얼마나 빨리 적용하는가가 실력입니다. 난이도 하 crackme는 사실 세 가지 유형의 반복이고, 유형을 30초 안에 간파하는 것이 오늘의 목표입니다. 마라톤 선수가 지형을 읽듯, 바이너리를 열자마자 "아, 이 유형"이 나오게 만드는 날입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 난이도 하 crackme의 세 유형(직접 비교 / 변환 비교 / 글자별 분기)을 구분한다
- 각 유형에 맞는 첫 도구를 바로 고른다
- 30분 제한 안에 "유형 분류 → 접근법 결정"을 끝내는 리듬을 몸에 익힌다
- 유형별 소요 시간을 기록해 자신의 약점을 데이터로 파악한다
- crackmes.one에서 문제를 고르고 안전하게 실행하는 절차를 안다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | C(문제 제작용) + WSL 우분투 bash (실측: gcc 13.3.0, gdb 15.1), crackmes.one은 화면 예시 |
| 오늘의 명령어 | strings | grep -iE 패턴, objdump -d | grep -E "cmp|xor|sub|add", gdb break *main+오프셋 + x/s $rdi / $rsi |
| 필요한 개념 | 정적/동적 분석의 분업(Step 178), XOR·덧셈 감춤, 한 글자씩 비교(cmpb), 키젠의 기초 |
| 오늘의 산출물 | 유형 분류표 + 유형별 평균 풀이 시간 기록 |
2-1. 난이도 하의 세 유형
수백 개의 입문 crackme를 걸러 보면 검증 방식은 셋 중 하나입니다.
- 직접 비교형 — 정답 문자열이 바이너리에 평문으로 있고
strcmp로 비교.strings한 줄에 끝납니다. - 변환 비교형 — 정답을 XOR나 덧셈으로 감춰 두고 실행 중에 복원해 비교. strings는 침묵하고, 디스어셈블리에 변환 연산이 보입니다.
- 글자별 분기형 — 정답 문자열을 통째로 저장하지 않고,
pw[0] == 'g'같은 비교를 글자마다 나열. strings에 정답이 없고, 디스어셈블리에cmpb $0x??가 반복됩니다.
2-2. 유형 간파 — 첫 3분의 리듬
바이너리를 받으면 정해진 순서가 있습니다.
1. 실행해 본다 (랩 안에서!) → 프롬프트와 판정 메시지 확인
2. strings → 정답 후보가 보이나?
보이면 → 유형 1, 사실상 종료
안 보이면 → 유형 2 또는 3
3. objdump -d | grep -E "xor|sub|add" → 변환 연산이 있나? → 유형 2
objdump -d | grep cmpb → 글자별 비교 반복? → 유형 3
"돌려보기"가 먼저인 이유: 입력 프롬프트와 에러 메시지가 분석의 시작점을 알려 줍니다. 그리고 실행은 반드시 랩에서 — 이것이 2-4의 안전 절차와 이어집니다.
2-3. crackmes.one 사용법 — 화면 예시
외부 플랫폼에서 문제를 고르는 절차입니다. 이 환경은 외부 네트워크를 쓰지 않으므로 화면 예시로 안내합니다:
# 화면 예시 — crackmes.one 탐색
1. https://crackmes.one 접속 → 상단 메뉴에서 crackmes 목록
2. 필터: Difficulty 1~2, Platform: 어느 OS든 (WSL이면 ELF/Linux, Windows VM이면 PE)
3. 목록 각 행: 이름 / 작성자 / 난이도 / 평점 — 평점이 높고 댓글(solutions)이 있는 것부터
4. 내려받은 zip의 압축을 랩 폴더에서 풀고 실행
고르는 요령: 난이도 1부터 하나씩, 풀릴 때까지 같은 난이도. 막히면 solutions 탭의 풀이를 "읽고 재현"하는 것도 정석 학습법입니다 — 다만 읽기 전에 최소 20분은 혼자 붙잡아 보세요.
2-4. 내려받은 바이너리의 안전 수칙
crackme는 합법 학습 자료지만, 인터넷에서 받은 실행 파일이라는 사실은 변하지 않습니다. 세 가지를 지킵니다: ① 격리된 랩/VM에서만 실행, ② 실행 전 file과 strings로 대략의 정체 확인, ③ 이상한 네트워크·파일 접근이 의심되면 실행 대신 정적 분석만. 실제 악성코드 분석가의 첫 수칙과 같습니다.
2-5. 시간 기록의 이유
오늘의 완료 기준에는 "유형별 평균 소요 시간"이 있습니다. 감으로 "변환형이 약하다"고 느끼는 것과, 기록으로 "변환형만 평균 22분, 나머지는 8분"을 아는 것은 다릅니다. 약점이 숫자가 되면 다음 연습의 표적이 됩니다.
3. 따라 하기
3-1. 훈련장 — 유형별 대표 crackme 3종
외부 문제를 풀기 전에, 세 유형의 "교과서 표본"을 직접 만들어 풀이 사이클을 몸에 새깁니다. 이 3종은 제작 과정을 여러분이 알고 있지만, 소스를 보지 않고 푸는 것이 규칙입니다.
입력 (e1_direct.c — 유형 1: 직접 비교)
#include <stdio.h>
#include <string.h>
int main(void){
char pw[64];
printf("key: ");
scanf("%63s", pw);
if(strcmp(pw, "c0ffee-t1me") == 0)
puts("correct! FLAG{e1_strcmp_1s_0ver}");
else
puts("wrong.");
return 0;
}
입력 (e2_add.c — 유형 2: 변환 비교)
#include <stdio.h>
#include <string.h>
int main(void){
char pw[64];
/* 정답 각 글자에 +3을 더해 저장 — 평문은 없다 */
char enc[] = {0x70, 0x36, 0x66, 0x75, 0x36, 0x77, 0x30,
0x73, 0x37, 0x77, 0x6b, 0x00};
char decoded[64];
int i;
for(i = 0; enc[i]; i++) decoded[i] = enc[i] - 3;
decoded[i] = 0;
printf("key: ");
scanf("%63s", pw);
if(strcmp(pw, decoded) == 0)
puts("correct! FLAG{e2_m1nus_thr33}");
else
puts("wrong.");
return 0;
}
입력 (e3_chars.c — 유형 3: 글자별 분기)
#include <stdio.h>
int main(void){
char pw[64];
printf("key: ");
scanf("%63s", pw);
if(pw[0] != 0x67) goto fail;
if(pw[1] != 0x6f) goto fail;
if(pw[2] != 0x6c) goto fail;
if(pw[3] != 0x64) goto fail;
if(pw[4] != 0x33) goto fail;
if(pw[5] != 0x6e) goto fail;
if(pw[6] != 0x00) goto fail;
puts("correct! FLAG{e3_cmp_by_cmp}");
return 0;
fail:
puts("wrong.");
return 0;
}
cd ~/lab214_218
gcc -O1 -o e1_direct e1_direct.c
gcc -O1 -o e2_add e2_add.c
gcc -O1 -o e3_chars e3_chars.c
(2026-09-09 실측으로 빌드·동작 확인.)
지금부터 여러분은 이 세 바이너리를 처음 받은 플레이어입니다. 소스는 덮어 두세요.
3-2. e1 풀이 — strings의 압승 (유형 1)
strings ./e1_direct | grep -iE "key|wrong|correct|0ffee"
key:
c0ffee-t1me
wrong.
correct! FLAG{e1_strcmp_1s_0ver}
(2026-09-09 실측.)
출력 읽는 법: 프롬프트(key:)와 판정 메시지 사이에 끼어 있는 c0ffee-t1me — 비교 대상이 평문으로 들어 있습니다. 확인:
echo c0ffee-t1me | ./e1_direct
key: correct! FLAG{e1_strcmp_1s_0ver}
(2026-09-09 실측.)
소요 시간: 1분. 유형 1은 "풀이"가 아니라 "발견"입니다. 실제 플랫폼의 난이도 1 문제 상당수가 이것이고, 그래서 첫 도구는 언제나 strings입니다.
3-3. e2 풀이 — strings 침묵, 연산 발견, gdb 포착 (유형 2)
strings ./e2_add | grep -iE "key|wrong|correct|m3cr3t"
key:
correct! FLAG{e2_m1nus_thr33}
wrong.
(2026-09-09 실측. 정답 후보가 없습니다 — 감춰져 있습니다.)
유형 2 또는 3입니다. 변환 연산을 찾습니다:
objdump -d ./e2_add | grep -E "sub.*0x3"
1216: 83 e8 03 sub $0x3,%eax
(2026-09-09 실측.)
출력 읽는 법: 무언가에서 3을 빼는 루프 — "실행 중에 복원한다"는 신호입니다. 복원된 값을 보는 방법은 두 가지. ① 정적 역산: enc 배열을 바이너리에서 찾아 각각 −3. ② 동적 포착: 비교 순간에 gdb로 읽기. 후자가 빠릅니다:
gdb -batch -ex "disassemble main" ./e2_add | grep strcmp
0x000000000000126a <+161>: call 0x10b0 <strcmp@plt>
(2026-09-09 실측.) main+161에 브레이크를 걸고 두 인자를 읽습니다:
(gdb) break *main+161
(gdb) run < in2.txt ← in2.txt에는 아무 글자(AAAA)
(gdb) x/s $rdi
(gdb) x/s $rsi
Breakpoint 1, 0x000055555555526a in main ()
0x7fffffffe5e0: "AAAA"
0x7fffffffe620: "m3cr3t-p4th"
(2026-09-09 실측. gdb 명령은 Step 178처럼 파일로 넘기되 $ 소실에 주의하세요 — 스크립트는 파이썬 chr(36)으로 만들었습니다.)
복원된 정답 m3cr3t-p4th를 그대로 넣어 correct!를 확인했습니다 (2026-09-09 실측). 유형 2의 정석: 감춤은 파일 안에서만 유효하고, 비교하려면 실행 중에 드러난다.
3-4. e3 풀이 — cmpb 사슬에서 글자 모으기 (유형 3)
strings는 침묵하고 변환 연산도 안 보인다면, 글자별 비교를 찾습니다:
objdump -d ./e3_chars | grep cmpb
11e6: 80 3c 24 67 cmpb $0x67,(%rsp)
11ec: 80 7c 24 01 6f cmpb $0x6f,0x1(%rsp)
11f3: 80 7c 24 02 6c cmpb $0x6c,0x2(%rsp)
11fa: 80 7c 24 03 64 cmpb $0x64,0x3(%rsp)
1201: 80 7c 24 04 33 cmpb $0x33,0x4(%rsp)
1208: 80 7c 24 05 6e cmpb $0x6e,0x5(%rsp)
120f: 80 7c 24 06 00 cmpb $0x0,0x6(%rsp)
(2026-09-09 실측.)
출력 읽는 법: 버퍼의 0번째는 0x67, 1번째는 0x6f… — 정답이 한 글자씩 흩어져 있습니다. 파이썬으로 모읍니다:
python3 -c "print(''.join(chr(v) for v in [0x67,0x6f,0x6c,0x64,0x33,0x6e]))"
gold3n
(2026-09-09 실측. echo gold3n | ./e3_chars → correct! FLAG{e3_cmp_by_cmp} 확인.)
유형 3은 gdb조차 필요 없는 경우가 많습니다 — 디스어셈블리가 곧 정답지입니다. 다만 글자가 수십 개로 길어지면 파이썬으로 상수를 긁어 모으는 편이 정확합니다.
3-5. 유형 분류표 — 3종 결산
| 문제 | strings | 결정적 단서 | 풀이 도구 | 유형 |
|---|---|---|---|---|
| e1 | 정답 노출 | (strings 자체가 단서) | strings | 직접 비교 |
| e2 | 침묵 | sub $0x3 변환 루프 |
objdump → gdb | 변환 비교 |
| e3 | 침묵 | cmpb 반복 |
objdump + 파이썬 | 글자별 분기 |
이 표의 흐름 — strings → 연산 검색 → (필요하면) gdb — 이 난이도 하의 만능 순서입니다. 이 순서가 몸에 배면 10개 연습은 반복 훈련이 됩니다.
3-6. 본 훈련 — 플랫폼에서 10개
crackmes.one에서 난이도 1~2를 10개 고릅니다 (2-3의 화면 예시 참조). 규칙:
- 각 문제에 30분 제한 — 타이머를 켭니다
- 첫 3분은 유형 간파에 씁니다 (2-2의 리듬)
- 30분 안에 못 풀면 "미해결"로 기록하고 다음으로 — 하나에 하루를 쓰지 않습니다
- 전부 끝나면 유형별 평균 시간을 냅니다
외부 플랫폼 접속이 안 되는 환경이라면 3-1의 소스를 변형(정답·변환 키·글자 배열 변경)해 스스로 문제를 만들어 푸는 것으로 대체할 수 있습니다 — 다만 미래의 내가 만든 문제는 단서의 위치를 기억하므로, 만들고 며칠 뒤에 푸는 것이 훈련이 됩니다.
4. 미션과 연습문제
미션 — 10개 풀이 로그와 유형 분류표
- crackmes.one 난이도 1~2에서 10개를 고릅니다 (접속이 안 되면 변형 자작으로 대체)
- 각 문제마다 기록합니다: 유형 판정(몇 분 걸렸는지), 사용한 도구 순서, 총 소요 시간, 성공/미해결
- 유형별 평균 소요 시간을 계산합니다
- 가장 느렸던 유형에 대해 "왜 느렸는가"를 두 줄로 씁니다 — 그것이 다음 연습의 표적입니다
연습문제
문제 1. e2에서 정답이 strings에 걸리지 않은 이유를, C 문자열 리터럴과 배열 초기화의 차이 관점에서 설명하세요.
문제 2. 유형 3(글자별 분기)에서 출제자가 정답을 통째로 저장하지 않고 한 글자씩 비교하는 이유는 무엇인가요? 그리고 그 방어가 왜 objdump 앞에서는 무력한가요?
문제 3. e2를 gdb 없이, objdump 정보만으로 풀려면 무엇을 더 찾아야 하나요? 절차를 써 보세요.
문제 4. "30분 제한"과 "미해결 기록"이 학습법으로서 왜 중요한가요? 하나에 오래 매달리는 것과 비교해 답하세요.
5. 모범 답안과 완료 기준
미션 모범 답안
풀이 로그의 예 (문제별 한 줄):
#01 easy_crack | 유형1 직접비교 (판정 1분) | strings | 3분 | 성공
#02 xor_baby | 유형2 변환 (판정 3분) | objdump→gdb | 11분 | 성공
#03 charbychar | 유형3 글자별 (판정 4분) | objdump+python | 14분 | 성공
#04 mysterybox | 판정 실패 — 변환처럼 보였으나 해시 | — | 30분 | 미해결
...
유형별 평균: 직접비교 4분 / 변환 12분 / 글자별 13분
가장 느린 유형: 변환 — objdump에서 연산을 찾은 뒤 "역산"과 "gdb 포착" 중
선택을 망설여 시간을 씀. 다음엔 gdb 포착을 먼저 시도하기로 규칙을 정함.
검증하는 법: ① 10행이 전부 있는가 (미해결도 행이다). ② 각 행에 유형과 도구 순서가 있는가. ③ 평균 계산과 "왜 느렸나" 문장이 있는가. 로그의 완결성이 곧 채점 기준입니다 — 10개 전부 성공일 필요는 없습니다.
연습문제 해답
문제 1 해답. e1의 정답은 문자열 리터럴이라 바이너리 데이터 영역에 평문 그대로 저장되어 strings에 걸립니다. 반면 e2는 정답을 +3 한 바이트 배열로 저장해, 파일 안에는 변환된 값만 있습니다. strings가 건지는 것은 "읽을 수 있는 글자의 연속"이므로, 변환된 바이트들은 사람이 읽는 문자열로 보이지 않아 걸리지 않습니다. 감춤의 핵심은 "파일 안에 평문을 두지 않는 것"입니다.
문제 2 해답. 정답 문자열을 통째로 저장하면 strings에 노출되므로, 비교 상수를 글자별로 흩어 놓아 문자열 탐색을 무력화하려는 의도입니다. 하지만 그 상수들은 결국 기계어의 비교 명령 안에 그대로 들어가므로, objdump -d | grep cmpb로 상수를 순서대로 긁어 모으면 정답이 복원됩니다. 파일 탐색은 피했지만 디스어셈블리에는 노출되는, 반쪽짜리 감춤입니다.
문제 3 해답. 변환 전 배열(enc)의 바이트들을 바이너리에서 찾아야 합니다. 절차: ① objdump로 sub $0x3 루프를 찾는다. ② 루프가 읽는 배열의 주소를 디스어셈블리에서 찾는다(lea나 데이터 참조). ③ 그 주소의 바이트들을 objdump -s(데이터 덤프)나 gdb의 x/12bx로 읽는다. ④ 각 바이트에서 3을 빼 파이썬으로 복원한다. gdb 포착이 빠르지만, 정적 역산은 "실행할 수 없는 바이너리"에서 유일한 길이라 둘 다 익혀야 합니다.
문제 4 해답. crackme 10개의 목적은 개별 문제의 정답이 아니라 유형 간파 속도와 도구 선택의 자동화이기 때문입니다. 한 문제에 두 시간을 쓰면 그 문제 하나만 배우지만, 30분으로 잘라 여섯 문제를 보면 여섯 가지 변형을 경험합니다. 그리고 미해결을 기록해 두면 그것이 정확히 내 약점의 목록이 됩니다 — "못 푼 문제"는 실패가 아니라 다음 훈련의 커리큘럼입니다.
완료 기준 체크리스트
- [ ] 난이도 하의 세 유형과 각 유형의 첫 도구를 말할 수 있다
- [ ] e1을 strings로 풀었다
- [ ] e2를 objdump(연산 발견) → gdb(비교 순간 포착)로 풀었다
- [ ] e3을 cmpb 사슬에서 파이썬으로 글자를 모아 풀었다
- [ ] crackmes.one의 난이도 필터와 안전 실행 절차를 안다
- [ ] 10개 풀이 로그(미해결 포함)와 유형별 평균 시간을 작성했다
- [ ] 가장 느린 유형과 그 이유를 기록했다
6. 흔한 실수와 해결
벽 1. strings가 너무 많이 나와서 단서를 못 찾겠다
증상: 수백 줄이 출력됩니다.
원인: 시스템 라이브러리 문자열이 섞여 나옵니다. 정상입니다.
해결: 판정 메시지 주변만 보세요 — strings ./문제 | grep -iE "key|pass|wrong|correct|flag|try". 그래도 많으면 strings -n 6으로 길이를 걸러 잡음을 줄입니다.
벽 2. 유형 2인 줄 알았는데 gdb에서 strcmp에 안 멈춘다
증상: break strcmp에 걸었는데 로더의 호출만 잡히거나, 아예 안 멈춥니다.
원인: 전자는 Step 178의 벽 3과 같고(로더도 strcmp를 씀), 후자는 그 바이너리가 strcmp를 안 쓰는 경우입니다 — 글자별 비교(유형 3)나 자체 비교 루프.
해결: 함수 이름에 걸지 말고 disassemble main으로 비교 호출 위치를 찾아 break *main+오프셋에 거세요. strcmp 호출 자체가 없으면 유형 3으로 전환해 cmpb를 찾습니다.
벽 3. 내려받은 crackme가 실행되지 않는다
증상: Permission denied 또는 cannot execute binary file.
원인: 각각 실행 권한 없음 / 다른 아키텍처·OS용 바이너리입니다.
해결: 전자는 chmod +x. 후자는 file ./문제로 확인하세요 — PE(Windows용)이면 WSL에서는 못 돌립니다. 플랫폼을 보고 고르는 이유입니다 (2-3).
벽 4. 변환 연산을 찾았는데 역산한 값이 이상하다
증상: −3을 했는데 깨진 글자가 나옵니다.
원인: 변환의 방향을 반대로 읽었거나, 루프가 배열 전체가 아니라 일부에만 적용되는 경우입니다.
해결: 디스어셈블리를 다시 읽어 "저장된 값에 뭘 하는 연산인지" 확인하세요. 헷갈리면 정적 역산을 포기하고 gdb 포착으로 — 실행 중의 진실 한 번이 추측 열 번을 이깁니다.
벽 5. 30분이 지났는데 놓기가 아쉽다
증상: "조금만 더 하면 될 것 같은데"라며 한 문제에 하루를 씁니다.
원인: 거의 푼 것 같은 감각은 착각인 경우가 많고, 설령 맞아도 오늘의 목표(속도)와 어긋납니다.
해결: 규칙을 지키세요 — 미해결로 기록하고 넘어가는 것이 오늘의 정답입니다. 그 문제는 내일 Step 218(난이도 중)의 도구를 배운 뒤 다시 보면 시야가 달라져 있습니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 직접 비교형 | 정답 평문 저장 + strcmp — strings에 걸림 |
| 변환 비교형 | XOR/덧셈으로 감춤 → 실행 중 복원 — gdb 포착이 정석 |
| 글자별 분기형 | cmpb 상수 사슬 — objdump가 곧 정답지 |
| 유형 간파 | 바이너리를 열고 첫 3분에 "어느 유형인가"를 정하는 리듬 |
| 풀이 로그 | 유형·도구·시간의 기록 — 약점을 숫자로 만드는 도구 |
| crackmes.one | 난이도별 crackme 저장소 — 격리 환경 실행이 철칙 |
오늘의 명령어
| 명령 | 하는 일 |
|---|---|
strings ./문제 | grep -iE "패턴" |
유형 1 판정 겸 풀이 |
objdump -d ./문제 | grep -E "xor|sub|add" |
유형 2 판정 — 변환 연산 찾기 |
objdump -d ./문제 | grep cmpb |
유형 3 판정 — 글자별 비교 사슬 |
gdb -batch -ex "disassemble main" |
비교 호출 위치 찾기 |
break *main+오프셋 → x/s $rdi x/s $rsi |
비교 순간의 두 문자열 포착 |
file ./문제 |
내려받은 바이너리의 플랫폼 확인 |
명령어보다 중요한 감각
난이도 하 10개의 진짜 보상은 플래그 열 개가 아니라 첫 3분의 자동화입니다. 실행해 보고, strings를 돌리고, 연산을 검색한다 — 이 순서가 생각 없이 나올 때까지 반복하는 것이 오늘의 전부입니다.
그리고 로그를 남기는 이유를 잊지 마세요. "변환형에서 망설인다"는 기록 한 줄이 있으면, 내일의 나는 무엇을 연습할지 알고 일어납니다. 리버싱 실력은 재능이 아니라 기록된 반복의 합계입니다.
전부 체크되면 Step 217 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.