2026-06-04·1분 읽기·
rev-basic-0이 strcmp 한 방이었다면, 1번은 입력을 한 글자씩 cmp로 비교한다. 검증 함수에 흩어진 비교 상수 21개를 주소순으로 모아 ASCII로 디코드하면 정답이 나온다. objdump로 cmp 체인을 뽑고 wine으로 Correct를 확인한 과정.
이 글이 도움이 됐나요?
문제: DreamHack — rev-basic-1 (Reversing Basic Challenge #1) 분류: Reversing 난이도: 🌱 새싹 FLAG:
DH{Compar3_the_ch4ract3r}
| 항목 | 내용 |
|---|---|
| 문제명 | rev-basic-1 |
| 난이도 | 🌱 새싹 |
| 분류 | Reversing |
| 제공 파일 | chall1.exe (PE32+ x86-64, Windows 콘솔) |
| 핵심 기법 | 입력을 한 글자씩 cmp — 코드에 박힌 비교 상수를 순서대로 모으면 정답 |
rev-basic-0과 똑같이 입력을 받아 Correct/Wrong을 출력한다. 다만 0번은 검증이 strcmp 한 번이라 비교 문자열이 .rdata에 통째로 박혀 있었는데, 1번은 그 문자열을 한 글자씩 따로 비교한다. 정답 문자열이 코드 곳곳의 cmp 상수로 쪼개져 들어가 있다.
file chall1.exe
file과 strings는 0번과 판박이다. Input : , %256s, Correct, Wrong — scanf("%256s")로 입력받아 검증 후 분기한다. main은 입력 버퍼를 들고 0x140001000을 호출하고, 그 반환값으로 갈린다.
strings chall1.exe | grep -iE 'correct|wrong|input|%256'
0x140001000(check)에 들어가면 0번과 결이 다르다. strcmp 호출 하나가 아니라, 같은 모양의 블록이 글자 수만큼 반복된다. 한 블록은 이렇게 생겼다.
; i 번째 글자 검사 (블록이 인덱스만 바꿔 21번 반복)
mov eax, 1
imul rax, rax, i ; rax = i (인덱스)
mov rcx, [rsp+8] ; rcx = 입력 버퍼
movzx eax, BYTE PTR [rcx+rax] ; eax = input[i]
cmp eax, 0xXX ; 정해진 상수와 비교
je .next ; 같으면 다음 글자로
xor eax, eax ; 하나라도 다르면
jmp .return ; → return 0 (Wrong)입력 input[i]를 고정 상수 0xXX와 비교해서, 같으면 다음 글자로 넘어가고 다르면 그 자리에서 0을 반환한다. 21개 블록을 모두 통과하면 마지막에 1을 반환해 Correct가 출력된다.
objdump -d -M intel chall1.exe | awk '/140001000:/{p=1} p; /^$/{if(p)exit}'
Ghidra 디컴파일러(analyzeHeadless)로 보면 이 반복 블록이 글자별 if (param_1[i] == 'C') 중첩으로 그대로 펼쳐진다 — 비교 상수가 곧 정답 글자다.
![클릭하여 확대 Ghidra headless 디컴파일 — check()가 param_1[0]=='C', [1]=='o', … 글자마다 비교하는 중첩 if 체인](/images/blog/dreamhack-rev-basic-1-writeup/tool_ghidra_check.png)
그러니 각 블록의 cmp eax, 0xXX에서 상수만 주소순으로 뽑으면 정답 문자열이 그대로 복원된다. objdump로 cmp eax, 줄만 추리면 21개가 한눈에 보인다.
objdump -d -M intel chall1.exe | grep -iE 'cmp +eax,0x' | head -25

objdump는 따로 설치할 게 없고 PE도 잘 떠준다. 디스어셈블 결과에서 check 함수 범위(0x140001000~0x14000128e)의 cmp eax, 0xXX만 정규식으로 긁어 바이트로 모은 뒤 ASCII로 디코드하면 끝이다.
#!/usr/bin/env python3
import re
CHECK_START, CHECK_END = 0x140001000, 0x14000128e
constants = []
for line in open("disasm.txt"): # objdump -d -M intel chall1.exe > disasm.txt
m = re.match(r"\s*([0-9a-f]+):\s+[0-9a-f ]+\s+cmp\s+eax,0x([0-9a-f]+)", line)
if m and CHECK_START <= int(m.group(1), 16) < CHECK_END:
constants.append(int(m.group(2), 16))
answer
[*] 비교 상수 21개: ['0x43','0x6f','0x6d','0x70','0x61','0x72','0x33','0x5f','0x74','0x68','0x65','0x5f','0x63','0x68','0x34','0x72','0x61','0x63','0x74','0x33','0x72']
[+] 입력값 : Compar3_the_ch4ract3r
[+] FLAG : DH{Compar3_the_ch4ract3r}
43 6f 6d 70 ...을 글자로 옮기면 Compar3_the_ch4ract3r. 실제 바이너리에 넣어 확인한다. 리눅스라 wine으로 돌렸다.
# verify.py
import os, subprocess
env = dict(os.environ, WINEPREFIX=os.path.expanduser("~/.wine_revbasic1"), WINEDEBUG="-all")
r = subprocess.run(["wine", "chall1.exe"], input="Compar3_the_ch4ract3r\n",
capture_output=True, text=True, env=env, timeout=60)
print((r.stdout + r.stderr).strip())Input : CorrectCorrect. 플래그는 DH{Compar3_the_ch4ract3r}.

비교를 쪼개도 상수는 그대로 코드에 남는다.
0번의 strcmp를 한 글자씩 cmp로 펼쳤을 뿐, 비교 대상은 여전히 평문이다. 단지 .rdata의 한 줄에서 코드 곳곳의 immediate로 흩어졌을 뿐이라, 주소순으로 다시 모으면 같은 문자열이 나온다.
반복되는 패턴을 찾으면 길이도 답도 같이 나온다.
movzx → cmp → je 블록이 21번 반복된다는 것 자체가 정답이 21글자라는 뜻이고, 각 블록의 상수가 그 자리의 글자다. 디스어셈블에서 한 종류의 명령(cmp eax,)만 필터링하면 사람이 한 줄씩 읽지 않아도 답이 모인다. 다음 번호들은 이 cmp 자리에 XOR이나 산술 연산이 끼어들 뿐, 상수를 모아 역산하는 흐름은 같다.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…