무엇을 바꾸면 성공하는지 먼저 읽기

이번 실습은 다운로드 ZIP의 examples/ctf_gate.c를 사용합니다. 실제 대회 문제의 정답을 외우는 대신, 프로세스 메모리에 있는 정수와 문자열을 찾아 바꾸고 그 결과를 확인하는 연습입니다. gate는 처음에 0이고 tag의 네 바이트는 NOPE입니다. checkpoint 함수는 gate가 0x1337이며 tag의 첫 네 바이트가 PASS일 때만 참을 반환합니다. main은 그 반환값에 따라 CTF demo solved 또는 CTF demo locked를 출력합니다. 반환값과 비교 조건이 작아서 어느 단계에서 실수했는지 확인하기 쉽습니다. 파일을 수정하는 실습이 아니라 실행 중인 프로세스의 상태를 바꾸는 실습이라는 점을 기억하세요. 프로그램을 새로 실행하면 전역 변수는 초기값으로 돌아갑니다.

예제에서 통과해야 하는 조건
volatile unsigned int gate = 0;
volatile unsigned char tag[4] = {'N', 'O', 'P', 'E'};

__attribute__((noinline)) int checkpoint(void) {
    return gate == 0x1337 && memcmp((const void *)tag, "PASS", 4) == 0;
}

대상 운영체제에서 빌드하고 중단점 걸기

Linux ELF는 Linux 또는 WSL 안의 GCC와 GDB로 실행합니다. Windows PE는 MSYS2 UCRT64의 GCC로 EXE를 만들고 Windows용 GDB에 연결합니다. Linux용 GDB에서 Windows 프로세스를 실행하거나 macOS에서 두 형식을 바로 실행하는 호환 계층은 이 도구에 포함하지 않습니다. 아래 명령은 ZIP을 푼 폴더에서 실행합니다. Windows에서는 출력 이름을 examples/ctf_gate.exe로 바꾸세요. -g는 심볼을 넣고 -O0는 최적화를 끄므로 변수 주소와 함수 이름을 확인하기 쉽습니다. 로컬 서버를 실행해 열린 디버거에서 생성한 파일을 선택하고, 중단점 위치에 checkpoint를 입력해 추가한 뒤 실행합니다. 파일을 열었다는 표시와 실제로 중단점에 정지했다는 표시를 구별하세요. 값 쓰기는 정지 상태에서만 사용할 수 있습니다.

Linux 빌드 명령 · Windows는 출력 이름에 .exe 추가
gcc -g -O0 examples/ctf_gate.c -o examples/ctf_gate

gate와 tag의 실제 주소에 바이트 쓰기

CTF 도구의 심볼 주소 조회에 gate를 넣고 주소 조회를 누릅니다. 결과는 현재 프로세스에서 확인한 실제 주소이며 메모리 쓰기 입력란에 자동으로 들어갑니다. 다른 PC나 이전 실행에서 본 주소를 그대로 쓰면 안 됩니다. PIE와 ASLR이 적용되면 같은 파일도 실행할 때 배치가 달라질 수 있습니다. 쓸 바이트에 37 13 00 00을 입력하고 메모리 쓰기를 누르세요. 이 실습의 x86/x86-64 환경은 little endian이므로 낮은 주소에 0x37, 그다음에 0x13을 넣어 정수 0x1337을 만듭니다. 13 37로 쓰면 같은 값이 되지 않습니다. 도구는 쓰기 직후 같은 영역을 다시 읽어 전달한 바이트와 일치하는지 검사합니다.

이어서 tag의 주소를 조회하고 50 41 53 53을 씁니다. 이는 ASCII PASS의 네 바이트입니다. tag는 길이 4의 배열이고 비교 함수도 길이 4만 읽으므로 여기서는 마지막에 00을 추가하지 않습니다. 문자열을 쓸 때 무조건 NUL을 덧붙이는 습관은 배열의 다음 바이트를 덮어쓸 수 있습니다. gate만 바꾼 상태에서는 tag 조건이 거짓이라 성공하지 않습니다. 두 영역을 모두 수정했는지 확인한 뒤 다음 단계로 넘어갑니다. 메모리 창에 해당 주소를 넣으면 실제 바이트를 직접 확인할 수 있으며 파일 오프셋이나 헤더 진입점 주소와 혼동하지 않아야 합니다.

x86/x86-64에서 기록할 바이트
gate: 00 00 00 00  ->  37 13 00 00
tag:  4e 4f 50 45  ->  50 41 53 53

반환값으로 수정 결과 확인하기

checkpoint에서 Step out을 누르면 함수가 끝나고 호출한 main 쪽에 정지합니다. 이 x86/x86-64 예제에서 정수 반환값은 eax로 확인할 수 있습니다. 값이 1이면 두 조건을 모두 통과한 것입니다. 이후 계속 실행하면 성공 문구를 출력하는 분기로 진행합니다. 반환값이 0이면 주소가 이번 실행의 주소인지, gate의 바이트 순서가 맞는지, tag에 정확히 네 바이트를 썼는지 차례대로 확인하세요. 중단점을 잘못 잡아 비교가 끝난 뒤 메모리를 바꾼 경우도 구별해야 합니다. 레지스터 쓰기 기능을 별도로 연습하려면 호출 프레임 0에서 rax를 0x2a로 바꾼 뒤 현재 값을 확인할 수 있습니다. 다만 함수가 나중에 같은 레지스터를 덮어쓸 수 있으므로 레지스터에 값을 넣었다는 사실과 최종 조건을 통과했다는 사실을 같은 것으로 보지는 마세요.

성공 조건을 만족했을 때의 예상 출력
CTF demo solved

실제로 확인한 범위와 남는 제한

2026년 10월 18일 공개 저장소의 Native debugger checks 실행 39103791405에서 이 흐름을 Windows MSYS2 UCRT64와 Ubuntu의 실제 GDB로 검사했습니다. 자동 검사는 checkpoint에 정지하고 gate와 tag의 주소를 조회해 바이트를 쓴 뒤, 레지스터를 바꾸고 읽어 확인하며 원래 값으로 복구합니다. 이어서 함수 반환값이 1인지 확인하고 프로그램 종료까지 진행합니다. 아래 이름은 해당 검사 로그의 테스트 이름입니다. 본문의 출력 블록은 소스에서 예상되는 출력을 정리한 것이며 터미널 화면을 캡처한 이미지는 아닙니다.

같은 실행에서 Linux는 Python 검사 54개를 모두 통과했고 Windows는 54개 중 POSIX 전용 2개를 제외한 검사를 통과했습니다. 별도 검사로 심볼을 제거한 파일의 첫 명령어 정지와 Linux PTY의 41 00 42 0a 입력 전달도 확인했습니다. 이 결과는 포함된 작은 예제의 검증 범위입니다. 임의의 대회 문제, 다른 아키텍처, 보호된 프로세스나 모든 GDB 버전에서의 성공을 뜻하지 않습니다. 실제 문제에 적용할 때도 먼저 입력과 중단 위치를 고정하고, 한 번에 하나의 변경을 가해 원래 실행과 비교하세요. 바꾼 메모리는 실행 파일에 저장되지 않으므로 재실행 후에는 주소 조회와 쓰기를 다시 수행해야 합니다.

연결된 CI 실행에서 통과한 실제 검사
test_native_pe_or_elf_memory_register_edits_solve_gate ... ok
test_stripped_binary_stops_without_main_symbol ... ok
Linux: Ran 54 tests / OK
Windows: Ran 54 tests / OK (skipped=2)