2026-06-04·1분 읽기·
rev-basic-1이 글자마다 cmp를 펼친 코드였다면, 2번은 루프 하나로 정답 배열과 비교한다. 비교 대상이 byte가 아니라 .data의 4바이트 정수 배열이라 한 칸이 4바이트씩이다. 각 원소의 저바이트만 모으면 정답. objdump로 배열을 떠내 디코드하고 wine으로 Correct를 확인했다.
이 글이 도움이 됐나요?
문제: DreamHack — rev-basic-2 (Reversing Basic Challenge #2) 분류: Reversing 난이도: 🌱 새싹 FLAG:
DH{Comp4re_the_arr4y}
| 항목 | 내용 |
|---|---|
| 문제명 | rev-basic-2 |
| 난이도 | 🌱 새싹 |
| 분류 | Reversing |
| 제공 파일 | chall2.exe (PE32+ x86-64, Windows 콘솔) |
| 핵심 기법 | 정답을 4바이트 정수 배열로 두고 루프로 비교 — 각 원소의 저바이트가 글자 |
rev-basic-1은 글자마다 cmp를 펼쳐 둔 코드였다. 2번은 그걸 루프 하나로 깔끔하게 접었다. 대신 비교 대상이 한 바이트짜리 문자열이 아니라 4바이트 정수 배열이라, 인덱스가 한 칸 늘 때 메모리는 4바이트씩 건너뛴다. 이 폭만 맞춰 읽으면 정답이 나온다.
file chall2.exe
file·strings는 시리즈 내내 같다. Input : , %256s, Correct, Wrong — 입력받아 0x140001000(check)으로 검증하고 분기한다.
strings chall2.exe | grep -iE 'correct|wrong|input|%256'
check 함수는 인덱스 i를 0부터 올리는 루프다.
; check(input) @ 0x140001000
mov [rsp], 0 ; i = 0
loop:
movsxd rax, [rsp] ; rax = i
cmp rax, 0x12 ; i < 18 ?
jae .ok ; 다 돌았으면 → return 1
lea rcx, [0x140003000] ; 정답 배열 베이스
mov r8, [rsp+0x20] ; r8 = 입력 버퍼
movzx edx, BYTE [r8+i] ; edx = input[i]
cmp [rcx+rax*4], edx ; ★ 배열[i] (4바이트) == input[i] ?
je .next
xor eax, eax ; 하나라도 다르면 return 0 (Wrong)
jmp .ret
.next:
i++ → loop
.ok:
mov eax, 1 ; 전부 통과 → return 1 (Correct)
.ret:
ret핵심은 cmp [rcx+rax*4], edx다. rax*4 — 인덱스에 4를 곱한다. 비교 대상이 바이트 배열이면 *1이었을 텐데 *4라는 건 **원소 하나가 4바이트(DWORD)**라는 뜻이다. input[i]는 movzx로 0~255 범위라, 배열의 각 DWORD도 결국 저바이트에 ASCII 값을 담은 정수다.
루프 상한이 0x12(18)이니 18칸을 비교한다.
objdump -d -M intel chall2.exe | awk '/140001000:/{p=1} p; /^$/{if(p)exit}'
Ghidra 디컴파일러(analyzeHeadless)로 보면 루프가 명확하다 — DAT_140003000의 4바이트 정수 배열을 인덱스마다([i*4]) 입력 바이트와 비교하다 어긋나면 0을 반환한다.


0x140003000을 .data에서 떠보면 4바이트 정수 18개가 늘어서 있다.
objdump -s -j .data chall2.exe | grep -E '14000300|14000301|14000302|14000303|14000304'140003000 43000000 6f000000 6d000000 70000000 C...o...m...p...
140003010 34000000 72000000 65000000 5f000000 4...r...e..._...
140003020 74000000 68000000 65000000 5f000000 t...h...e..._...
140003030 61000000 72000000 72000000 34000000 a...r...r...4...
140003040 79000000 00000000 ... y.......리틀엔디언이라 각 DWORD의 첫 바이트가 값이다. 43 6f 6d 70 34 72 65 5f 74 68 65 5f 61 72 72 34 79 — 17글자 Comp4re_the_arr4y. 18번째 칸은 0x00000000인데, 입력의 널 종단('\0')과 맞아떨어져 루프를 통과한다.

스크립트로는 PE 섹션 테이블을 파싱해 0x140003000의 파일 오프셋을 구하고, DWORD 18개를 읽어 저바이트만 모은 뒤 트레일링 널을 떼면 된다.
#!/usr/bin/env python3
import struct
IMAGE_BASE, TABLE_VA, COUNT = 0x140000000, 0x140003000, 0x12
raw = open("chall2.exe", "rb").read()
pe = struct.unpack_from("<I", raw, 0x3c)[0]
nsec = struct.unpack_from("<H", raw, pe + 6)[0]
sect = pe + 24 + struct.unpack_from("<H", raw, pe + 20)[0]
rva = TABLE_VA - IMAGE_BASE
[*] 테이블 @ 0x140003000 DWORD 18개 (저바이트): ['0x43','0x6f','0x6d','0x70','0x34','0x72','0x65','0x5f','0x74','0x68','0x65','0x5f','0x61','0x72','0x72','0x34','0x79','0x0']
[+] 입력값 : Comp4re_the_arr4y (17글자)
[+] FLAG : DH{Comp4re_the_arr4y}
실제 바이너리에 넣어 확인한다. 리눅스라 wine으로 돌렸다.
# verify.py
import os, subprocess
env = dict(os.environ, WINEPREFIX=os.path.expanduser("~/.wine_revbasic2"), WINEDEBUG="-all")
r = subprocess.run(["wine", "chall2.exe"], input="Comp4re_the_arr4y\n",
capture_output=True, text=True, env=env, timeout=60)
print((r.stdout + r.stderr).strip())Input : CorrectCorrect. 플래그는 DH{Comp4re_the_arr4y}.

비교의 단위(스케일)를 먼저 본다.
[rcx+rax*4]의 *4 하나가 "원소가 4바이트"라는 정보를 다 담고 있다. 바이트 배열로 착각해 한 칸씩 읽으면 43 00 00 00 6f 00 ...처럼 널이 끼어 깨진다. 인덱싱 스케일(*1/*4/*8)을 보고 자료형 폭을 맞추는 게 배열 리버싱의 첫 단추다.
상수는 코드에서 데이터로 옮겨갔을 뿐이다.
0번은 .rdata의 문자열, 1번은 코드 속 immediate, 2번은 .data의 정수 배열 — 형태만 바뀌었지 정답은 여전히 평문으로 들어 있다. 어디에 담겨 있든 비교 직전에 메모리로 올라오니, 그 자리를 떠내면 된다. 다음 번호들은 이 비교 직전에 XOR·산술 같은 변환이 끼어들어, 그때부터는 역산이 필요해진다.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…