Step 178. CTF 맛보기 3: Reversing — crackme 2개 + Ghidra — 소스 없이 읽는 법
Level 3 — CTF 실전과 공격 스킬 심화 | 난이도 ★★★★☆ | 예상 소요 시간 6시간
전제: Step 177(Pwn 입문), Step 62~65(C와 메모리). WSL 우분투의 gcc와 gdb를 쓸 수 있습니다.
⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. crackme 연습 바이너리는 풀라고 만들어진 합법 학습 자료입니다.
- 준비물: WSL 우분투 (실측: Ubuntu 24.04, gcc 13.3.0, gdb 15.1). Ghidra는 별도 설치가 필요한 도구라 이 챕터에서는 화면 예시로 안내합니다.
- 주의: 오늘 만드는 crackme는 여러분이 직접 컴파일한 연습용입니다. 리버싱 기법을 남의 소프트웨어에 함부로 적용하면 라이선스·법률 문제가 될 수 있습니다.
Reversing(리버싱)은 소스 코드 없이 실행 파일만 받아 "이게 뭘 하는 프로그램인지" 해부하는 종목입니다. 어제 Pwn이 "허점을 찾아 공격"이라면, 리버싱은 "동작을 이해하는 것" 자체가 목표입니다. 오늘의 연습 과제는 crackme — 비밀번호를 맞히면 "correct"를 출력하는 연습용 바이너리입니다. 소스를 보지 않고 비밀번호를 찾아내는 것이 게임의 규칙입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 정적 분석(실행하지 않고 읽기)과 동적 분석(실행하며 관찰)의 차이를 설명한다
strings로 바이너리 안의 문자열 단서를 찾는다objdump의 디스어셈블리에서 수상한 연산(비교·XOR)을 찾는다- gdb 브레이크포인트로 "비교되는 두 문자열"을 실행 중에 관찰한다
- 디컴파일러(Ghidra)가 무엇을 보여 주는 도구인지 안다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | C(crackme 제작용) + WSL 우분투 bash (실측: gcc 13.3.0, gdb 15.1, x86-64) |
| 오늘의 명령어 | strings, objdump -d, gdb -batch -x 명령파일, gdb의 break / x/s / continue |
| 필요한 개념 | 정적 vs 동적 분석, 디컴파일러, XOR 감춤, 함수 인자 레지스터(rdi·rsi) |
| 오늘의 산출물 | crackme 2개의 풀이 기록 + "무엇으로 찾았나" 정리 |
2-1. 정적 분석 vs 동적 분석
리버싱의 도구는 두 갈래입니다. 정적 분석(static analysis)은 프로그램을 실행하지 않고 파일 자체를 읽습니다 — 문자열 목록, 디스어셈블리, 디컴파일 결과. 동적 분석(dynamic analysis)은 실행해 가며 관찰합니다 — 디버거로 멈추고, 메모리와 레지스터를 들여다봅니다.
실전에서는 둘을 번갈아 씁니다. 정적으로 "수상한 자리"를 찾고, 동적으로 그 자리에서 실제 값을 확인하는 식입니다. 오늘 두 번째 crackme가 바로 그 순서를 요구합니다.
2-2. strings — 가장 싼 첫 도구
컴파일된 바이너리 안에는 문자열 상수(프롬프트, 에러 메시지, 그리고 때때로 비밀번호)가 그대로 들어 있습니다. strings는 바이너리에서 "사람이 읽을 수 있는 글자열"만 건져 줍니다. 리버싱의 첫 동작은 언제나 이것입니다 — 안 나오면 그때 무거운 도구를 꺼냅니다.
2-3. 디컴파일러와 Ghidra
디컴파일러(decompiler)는 기계어를 거슬러 의사 C 코드로 복원해 보여 주는 도구입니다. Ghidra는 NSA가 공개한 무료 디컴파일러로, CTF 리버싱의 표준 도구입니다. 바이너리를 가져오면(Import → 분석) CodeBrowser 창에서 함수 목록(Symbol Tree)과 의사 코드(Decompile 창)를 나란히 보여 줍니다.
이 환경에는 설치돼 있지 않으므로 화면은 예시로 안내하고(3-5), 같은 일을 하는 기본 도구(objdump, gdb)로 원리를 실측합니다. 어셈블리가 나오더라도 겁내지 마세요 — 오늘 필요한 것은 cmp(비교), call(호출), xor(배타적 OR) 세 글자뿐입니다 (Step 177 벽 5의 약속).
2-4. XOR 감춤 — 한 바이트짜리 자물쇠
비밀번호를 평문으로 저장하면 strings에 바로 걸립니다. 그래서 출제자는 비밀번호를 XOR로 감춥니다 — 각 바이트에 같은 값(키)을 XOR해 저장하고, 프로그램이 실행될 때 다시 XOR해 복원합니다. XOR는 같은 키로 두 번 하면 원래대로 돌아오는 성질(A ⊕ K ⊕ K = A)이 있어 "감추기와 복원"에 딱입니다 (Step 104의 Natas에서 만났던 바로 그 연산입니다).
strings로는 안 보이지만, 실행 중 복원된 순간은 보입니다 — 정적 분석의 한계를 동적 분석이 메우는 전형입니다.
2-5. 함수 인자는 레지스터로 간다
x86-64 리눅스에서 함수를 호출할 때 인자는 정해진 레지스터에 실립니다. 첫 번째 인자는 rdi, 두 번째는 rsi. strcmp(a, b)가 호출되는 순간 rdi에는 a의 주소, rsi에는 b의 주소가 있습니다. gdb의 x/s $rdi는 "rdi가 가리키는 곳을 문자열로(x/s) 보여 달라"는 뜻 — 이 두 줄이 오늘의 핵심 관찰 도구입니다.
3. 따라 하기
3-1. 실험실 — crackme 두 개 만들기
출제자가 되어 봅니다. 아는 구조를 만들고, 그다음 플레이어가 되어 부숩니다. ~/lab178 폴더를 만들어 진행하세요.
입력 (crackme1.c — 평문 비밀번호)
#include <stdio.h>
#include <string.h>
int main(void){
char pw[64];
printf("password: ");
scanf("%63s", pw);
if(strcmp(pw, "sup3r-s3cr3t-pw") == 0)
puts("correct! FLAG{str1ngs_t0ld_m3_th3_pw}");
else
puts("wrong.");
return 0;
}
입력 (crackme2.c — XOR로 감춘 비밀번호)
#include <stdio.h>
#include <string.h>
int main(void){
char pw[64];
/* "원래 비밀번호"를 0x5A로 XOR해 감춘 배열 — 평문은 어디에도 없다 */
char secret[] = {0x37, 0x6b, 0x3e, 0x34, 0x6b, 0x3d, 0x32, 0x2e,
0x05, 0x28, 0x6e, 0x38, 0x38, 0x6b, 0x2e, 0x00};
char decoded[64];
int i;
for(i = 0; secret[i]; i++) decoded[i] = secret[i] ^ 0x5A;
decoded[i] = 0;
printf("password: ");
scanf("%63s", pw);
if(strcmp(pw, decoded) == 0)
puts("correct! FLAG{x0r_c4nnot_h1de_fr0m_gdb}");
else
puts("wrong.");
return 0;
}
컴파일
mkdir -p ~/lab178 && cd ~/lab178
gcc -O1 -o crackme1 crackme1.c
gcc -O1 -o crackme2 crackme2.c
(2026-09-09 실측. scanf의 반환값 경고가 뜰 수 있지만 실습에는 지장이 없습니다.)
이제부터는 소스를 잊으세요. 여러분은 방금 받은 낯선 바이너리 두 개를 푸는 플레이어입니다.
3-2. crackme 1 풀이 — strings의 승리
첫 도구는 가장 싼 것, strings입니다:
strings ./crackme1
password:
sup3r-s3cr3t-pw
wrong.
correct! FLAG{str1ngs_t0ld_m3_th3_pw}
(2026-09-09 실측. 실제로는 다른 시스템 문자열들 사이에 이 줄들이 섞여 나옵니다 — 수상한 것만 골라 적었습니다. strings ./crackme1 | grep -iE "pw|secret|flag"처럼 걸러 보세요.)
출력 읽는 법: password:는 프롬프트, wrong.과 correct!는 판정 메시지. 그리고 그 사이에 어색하게 끼어 있는 sup3r-s3cr3t-pw — 프롬프트도 메시지도 아닌 문자열은 "비교 대상"일 가능성이 높습니다. 확인해 봅니다:
echo sup3r-s3cr3t-pw | ./crackme1
password: correct! FLAG{str1ngs_t0ld_m3_th3_pw}
(2026-09-09 실측.)
정리: 소스도, 어셈블리도 필요 없었습니다. 비밀번호가 평문으로 박힌 바이너리는 strings 한 줄에 열립니다. 실제 crackme 입문 문제의 절반이 이 수준이고, 그래서 리버싱의 첫 동작은 언제나 strings입니다.
3-3. crackme 2 풀이 1단계 — strings가 침묵하다
같은 도구를 두 번째 바이너리에 들이댑니다:
strings ./crackme2
password:
wrong.
correct! FLAG{x0r_c4nnot_h1de_fr0m_gdb}
(2026-09-09 실측. 비밀번호 후보가 보이지 않습니다 — strings ./crackme2 | grep -iE "m1dn1ght|r4bb1t"은 아무것도 반환하지 않음을 실측으로 확인했습니다.)
출력 읽는 법: 프롬프트와 판정 메시지는 있는데 비교 대상이 없습니다. 비밀번호가 파일 안에 평문으로 존재하지 않는다는 뜻입니다 — 감춰져 있습니다. 정적 분석의 첫 도구가 여기서 한계를 만났습니다.
그래도 포기하기 전에 한 가지 더, 디스어셈블리에서 "감춤의 방식"을 찾습니다:
objdump -d ./crackme2 | grep "0x5a"
1219: 83 f2 5a xor $0x5a,%edx
(2026-09-09 실측.)
출력 읽는 법: xor $0x5a — 뭔가에 0x5A를 XOR하고 있습니다. XOR는 "감추기와 복원"의 연산(2-4)입니다. 즉 이 프로그램은 실행 중에 무언가를 복원합니다. 복원된 값이 보고 싶다면, 복원이 끝난 순간에 실행을 멈추면 됩니다. 동적 분석의 출발입니다.
3-4. crackme 2 풀이 2단계 — gdb로 비교의 순간을 포착
복원된 비밀번호는 strcmp를 호출하는 바로 그 순간에 메모리에 있습니다. 그 순간에 멈춰 봅시다. 먼저 main 안에서 strcmp 호출 위치를 찾습니다:
gdb -batch -ex "disassemble main" ./crackme2 | grep strcmp
0x000000000000126d <+164>: call 0x10b0 <strcmp@plt>
(2026-09-09 실측.)
<+164> — main 시작점에서 164바이트 지점에 호출이 있습니다. 거기에 브레이크포인트를 겁니다. gdb 명령을 파일로 준비합니다 (cmds.gdb):
break *main+164
run < in.txt
x/s $rdi
x/s $rsi
$rdi/$rsi는 비교되는 두 문자열의 주소(2-5)입니다. 아무 입력 파일을 만들고 실행합니다:
echo AAAA > in.txt
gdb -batch -x cmds.gdb ./crackme2
Breakpoint 1 at 0x126d
Breakpoint 1, 0x000055555555526d in main ()
0x7fffffffe5e0: "AAAA"
0x7fffffffe620: "m1dn1ght_r4bb1t"
(2026-09-09 실측. 주소값은 실행마다 다릅니다.)
출력 읽는 법: 결정적 장면입니다. rdi에는 우리가 넣은 "AAAA", rsi에는 프로그램이 방금 복원한 "m1dn1ght_r4bb1t"가 나란히 서 있습니다. strings가 못 찾은 비밀번호를, 실행 중의 한순간을 얼려서 꺼냈습니다. 확인:
echo m1dn1ght_r4bb1t | ./crackme2
password: correct! FLAG{x0r_c4nnot_h1de_fr0m_gdb}
(2026-09-09 실측.)
왜: XOR 감춤은 "파일 안"에서는 안전하지만 "실행 중"에는 무방비입니다 — 비교하려면 어쨌든 복원해야 하니까요. 이것이 정적 분석과 동적 분석의 분업입니다. 파일이 말하지 않으면, 실행에게 묻는다.
참고:
break strcmp처럼 함수 이름에 바로 거는 방법도 있습니다. 다만 프로그램 시작 준비 단계(동적 로더)에서도 strcmp가 여러 번 호출돼, 실측에서 연속 네 번이 로더의 호출이었습니다.continue로 몇 번 넘기거나, 오늘처럼 main 안의 호출 주소에 정확히 거는 방법이 빠릅니다.
3-5. 같은 일을 Ghidra로 — 화면 예시
이 환경에는 Ghidra가 없어 화면 예시로 안내합니다. 실제로 해볼 분은 ghidra-sre.org에서 받아 설치하세요 (Java가 필요합니다). 절차는 이렇습니다:
# 화면 예시 — Ghidra 사용 흐름
1. Ghidra 실행 → File > New Project → 프로젝트 생성
2. crackme1 파일을 드래그해 Import → 더블클릭 → 분석(Analyze) Yes
3. CodeBrowser 왼쪽 Symbol Tree > Functions > main 더블클릭
4. 오른쪽 Decompile 창에 의사 C 코드가 표시됨:
undefined8 main(void)
{
char local_48 [64];
printf("password: ");
__isoc99_scanf("%63s", local_48);
iVar1 = strcmp(local_48, "sup3r-s3cr3t-pw");
if (iVar1 == 0) { puts("correct! ..."); }
else { puts("wrong."); }
return 0;
}
읽는 법: 어셈블리를 몰라도 흐름이 읽힙니다 — 입력을 받아 strcmp로 비교하고, 비교 대상 문자열이 디컴파일 결과에 그대로 보입니다. 변수명이 local_48, iVar1처럼 난해한 것은 정상입니다. 기계가 지은 이름을 읽으며 "아, 이건 비밀번호 버퍼구나" 추론하는 것이 리버싱의 일상입니다.
crackme2를 Ghidra에 넣으면 XOR 복원 루프가 의사 코드로 보이고, 키(0x5A)가 눈에 들어옵니다 — 그러면 3-3의 objdump 발견과 같은 결론에 디컴파일러로 도달하는 셈입니다. 도구가 달라도 사고는 같습니다: 비교 대상을 찾아라.
3-6. 풀이 기록 정리
| 문제 | strings | objdump | gdb | 열쇠 |
|---|---|---|---|---|
| crackme1 | 비밀번호 평문 노출 | (안 봄) | (필요 없음) | strings 한 줄 |
| crackme2 | 침묵 (XOR 감춤) | xor $0x5a 발견 |
strcmp 순간에 평문 관찰 | 정적→동적 연계 |
이 표의 마지막 칸이 리버싱의 문법입니다: 가벼운 도구부터, 안 되면 한 단계 무거운 도구로.
4. 미션과 연습문제
미션 — 나만의 crackme 만들고 친구(또는 미래의 나)에게 내주기
- crackme2를 변형한 crackme3.c를 만듭니다 — XOR 키를 0x5A가 아닌 다른 한 바이트(예: 0x37)로 바꾸고, 비밀번호도 바꿉니다 (감춘 배열은 파이썬으로 계산:
bytes(b ^ 0x37 for b in pw.encode())) - 컴파일한 뒤, 소스를 보지 않고 오늘의 세 도구 순서(strings → objdump → gdb)로 다시 풉니다
- 풀이 기록을 남깁니다: "어느 도구에서 무엇이 보였는가"
연습문제
문제 1. 정적 분석과 동적 분석의 차이를, 오늘 crackme2 풀이의 어느 장면이 보여 주는지 들어 설명하세요.
문제 2. crackme1의 비밀번호는 왜 strings에 그대로 걸렸나요? C 소스의 문자열 상수가 컴파일 후 어디에 가는지 관점에서 답하세요.
문제 3. crackme2에서 xor $0x5a,%edx 한 줄을 발견했을 때 "이 프로그램이 실행 중에 뭔가를 복원한다"고 추론할 수 있는 이유를 XOR의 성질로 설명하세요.
문제 4. gdb에서 x/s $rsi가 비밀번호를 보여 줬습니다. 왜 하필 rdi와 rsi였나요?
5. 모범 답안과 완료 기준
미션 모범 답안
키 0x37, 비밀번호 blu3-m00n으로 만들었다면:
[제작] enc = bytes(b ^ 0x37 for b in "blu3-m00n".encode())
→ secret[] = {0x55, 0x5b, 0x42, 0x04, 0x5a, ...}
[풀이 1: strings] ./crackme3 → 비밀번호 안 보임 (감춤 성공 확인)
[풀이 2: objdump] grep "0x37" → xor $0x37 발견 → "XOR 감춤이구나"
[풀이 3: gdb] break *main+164 (디스어셈블리로 strcmp 위치 먼저 확인)
→ rsi에 "blu3-m00n" 관찰 → echo로 확인 → correct!
검증하는 법: ① 감춘 비밀번호가 strings에 안 걸리는가(걸리면 감춤 실패 — 배열에 평문이 섞인 것), ② objdump에서 새 키가 발견되는가, ③ gdb 없이 키와 배열만으로 수동 복원도 할 수 있는가(파이썬 한 줄: bytes(b ^ 0x37 for b in enc)) — 셋이 다 되면 "만드는 쪽과 부수는 쪽"을 둘 다 이해한 것입니다.
연습문제 해답
문제 1 해답. 정적 분석은 파일을 실행하지 않고 읽는 것이고, 동적 분석은 실행하며 관찰하는 것입니다. crackme2에서 strings와 objdump(정적)는 "감춰져 있다, XOR이다"까지는 알려 줬지만 비밀번호 자체는 몰랐습니다. gdb로 strcmp 호출 순간에 멈춰(동적) 복원된 평문 m1dn1ght_r4bb1t를 관찰해 풀었습니다. 파일이 침묵할 때 실행에게 묻는 것 — 이 연계가 리버싱의 기본 리듬입니다.
문제 2 해답. C 소스의 문자열 상수(리터럴)는 컴파일 후 바이너리의 데이터 영역에 평문 그대로 저장되기 때문입니다. strcmp(pw, "sup3r-s3cr3t-pw")의 비교 대상은 코드가 아니라 데이터라 기계어로 변환되지 않습니다. strings는 이 데이터 영역에서 읽히는 글자열을 건져 내므로, 평문 상수는 반드시 걸립니다.
문제 3 해답. XOR는 같은 키로 두 번 적용하면 원래 값으로 돌아오는 성질(A ⊕ K ⊕ K = A)이 있습니다. 그래서 "키로 XOR된 데이터 + 실행 중 다시 XOR하는 코드"의 조합은 곧 "감춰 둔 값을 실행 중에 복원한다"는 뜻입니다. 반대로 XOR가 아니라 단순 덧셈 루프나 상수 비교만 보였다면 다른 추론을 했을 것입니다 — 디스어셈블리에서 연산의 종류를 읽는 이유입니다.
문제 4 해답. x86-64 리눅스의 함수 호출 규약에서 인자는 정해진 레지스터로 전달되기 때문입니다 — 첫 번째 인자는 rdi, 두 번째는 rsi. strcmp(입력, 비밀번호)의 두 인자가 그 두 레지스터에 실리므로, 호출 직전에 멈춰 x/s로 읽으면 비교되는 두 문자열이 보입니다. 규약을 알면 "비교의 순간"이 어디에 실려 있는지 예측할 수 있습니다.
완료 기준 체크리스트
- [ ] 정적 분석과 동적 분석의 차이를 한 문장씩 말할 수 있다
- [ ]
strings로 crackme1의 비밀번호를 찾아 풀었다 - [ ] crackme2에서 strings가 침묵하는 것을 확인하고 이유를 안다
- [ ]
objdump -d에서xor $0x5a를 찾아 "감춤"을 추론했다 - [ ] gdb 브레이크포인트로 strcmp 순간의 두 문자열을 관찰해 crackme2를 풀었다
- [ ] Ghidra의 화면 구성(Symbol Tree, Decompile 창)과 절차를 안다
- [ ] 미션: crackme3를 만들고 세 도구 순서로 다시 풀었다
6. 흔한 실수와 해결
벽 1. strings 출력이 너무 많아 뭐가 단서인지 모르겠다
증상: 수백 줄이 나와서 눈이 둥둥 뜁니다.
원인: 시스템 라이브러리 문자열까지 다 섞여 나옵니다. 정상입니다.
해결: 걸러서 보세요 — strings ./crackme1 | grep -iE "pass|flag|secret|wrong|correct". 판정 메시지("wrong", "correct") 주변의 문자열이 비교 대상일 확률이 높습니다. 또 길이 필터 strings -n 6도 잡음을 줄여 줍니다.
벽 2. gdb에서 $rdi가 변수 확장으로 사라진다
증상: x/s $rdi를 치면 Argument required (starting display address). 오류가 납니다 — $rdi가 통째로 사라집니다. 이 챕터를 실측하는 동안 실제로 겪은 문제입니다 (쉘을 거쳐 gdb에 전달할 때 $가 쉘 변수 확장으로 먹힘).
원인: 쉘이 $rdi를 환경 변수로 해석해 빈 문자열로 바꿔 버립니다.
해결: 대화형 gdb 안에서 직접 치면 문제없고, 스크립트로 넘길 때는 명령 파일(-x cmds.gdb)을 쓰되 파일을 만들 때 $가 확장되지 않게 주의하세요 (작성자는 파이썬의 chr(36)로 우회했습니다). 같은 이유로 bash에서 echo $rdi도 빈 줄이 나옵니다.
벽 3. break strcmp에 걸었는데 이상한 문자열들만 나온다
증상: /lib64/ld-linux-x86-64.so.2, __vdso_clock_gettime 같은 문자열이 보입니다.
원인: 프로그램 시작 준비를 하는 동적 로더도 strcmp를 씁니다. main이 실행되기 전에 여러 번 호출됩니다 (2026-09-09 실측에서 연속 네 번).
해결: continue로 몇 번 넘기거나, 3-4처럼 disassemble main으로 main 안의 호출 위치를 찾아 break *main+오프셋에 정확히 거세요. 후자가 재현 가능한 방법입니다.
벽 4. 주소가 책과 다르다
증상: 책은 <+164>인데 내 환경은 다른 오프셋입니다.
원인: 컴파일러 버전과 옵션(-O1 여부)에 따라 기계어 배치가 달라집니다. 정상입니다.
해결: 오프셋을 외우지 말고 절차를 외우세요 — disassemble main → call ... strcmp 줄 찾기 → 그 줄의 오프셋에 break. 절차는 어떤 바이너리에서도 같습니다.
벽 5. Ghidra 분석이 끝나지 않는다 / 변수명이 이상하다
증상: Analyze가 오래 걸리고, 의사 코드에 iVar1, local_48 같은 이름만 보입니다.
원인: 둘 다 정상입니다. 분석은 파일 크기에 따라 수 분 걸릴 수 있고, 디컴파일러의 변수명은 기계가 임시로 지은 것입니다.
해결: 분석은 기다리면 됩니다. 변수명은 읽다 보면 역할이 보입니다 — 비교에 쓰이면 "비밀번호 버퍼", 반환값이면 "판정 결과". 추론하며 읽는 것 자체가 리버싱 실력입니다. 나중에는 L키로 이름을 바꿔 가며 읽게 됩니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| Reversing | 소스 없이 실행 파일을 해부해 동작을 이해하는 종목 |
| crackme | 비밀번호를 맞히는 연습용 바이너리 — 리버싱의 입문 문제 |
| 정적 분석 | 실행하지 않고 파일을 읽기 (strings, objdump, 디컴파일) |
| 동적 분석 | 실행하며 관찰하기 (gdb 브레이크포인트, 레지스터 관찰) |
| XOR 감춤 | 같은 키로 두 번 XOR하면 원래대로 — 감추기와 복원에 사용 |
| 디컴파일러(Ghidra) | 기계어를 의사 C 코드로 복원해 보여 주는 도구 |
| 호출 규약 | 함수 인자는 rdi, rsi, … 순서의 레지스터로 전달된다 |
오늘의 명령어
| 명령 | 하는 일 |
|---|---|
strings ./바이너리 |
바이너리에서 읽을 수 있는 문자열 건지기 — 리버싱의 첫 동작 |
strings -n 6 ./바이너리 | grep -iE "패턴" |
길이 필터 + 패턴으로 잡음 줄이기 |
objdump -d ./바이너리 | grep xor |
디스어셈블리에서 수상한 연산 찾기 |
gdb -batch -ex "disassemble main" ./바이너리 |
main의 기계어 배치 보기 (호출 위치 찾기) |
break *main+오프셋 → x/s $rdi / x/s $rsi |
비교 순간에 멈춰 두 인자 문자열 관찰 |
명령어보다 중요한 감각
오늘의 두 crackme가 보여 준 것은 리버싱의 문법 전체입니다. 가벼운 도구부터 — strings가 안 되면 objdump, 파일이 침묵하면 실행에게 묻는다. 그리고 거꾸로, "감추고 싶은 것은 실행 중에는 반드시 드러난다"는 것도 봤습니다. 비교하려면 복원해야 하고, 복원된 순간은 관찰 가능하니까요.
이 감각은 crackme를 넘어 갑니다. 악성코드 분석도, 취약점 분석도, "이 프로그램이 진짜 하는 일이 뭔가"라는 모든 질문도 결국 같은 도구 상자에서 시작합니다. 웹, Pwn에 이어 리버싱까지 — 세 종목의 맛을 봤습니다. 어느 도구를 쥘 때 손이 편했는지, 그 기억이 Step 181에서 주력 분야를 고르는 재료가 됩니다.
전부 체크되면 Step 178 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.