리버싱(Reverse Engineering)은 소스 코드가 없는 프로그램을 분석하여 내부 동작 방식과 구조를 파악하는 기술이다. 보안 연구, 악성코드 분석, CTF, 취약점 분석, 호환성 연구, 디버깅 등 다양한 분야에서 사용된다. 하지만 리버싱은 반드시 허가된 대상과 합법적인 목적 안에서 수행해야 한다. 따라서 필자는 이번 글에서 리버싱의 기본 개념부터 정적 분석, 동적 분석, 어셈블리 해석, 패치 원리, 분석자가 가져야 할 방어적 관점까지 설명하도록 하겠다.
1. 리버싱의 기본 개념
리버싱의 핵심은 실행 파일을 관찰하여 다음 질문에 답하는 것이다.
- 이 프로그램은 어떤 입력을 받는가?
- 입력값은 어디에서 검증되는가?
- 중요한 분기문은 어디에 존재하는가?
- 어떤 라이브러리 함수나 시스템 호출을 사용하는가?
- 메모리와 레지스터 값은 실행 중 어떻게 변하는가?
일반적인 리버싱 분석 흐름은 다음과 같다.
| 단계 | 목적 |
|---|---|
| 파일 식별 | 파일 형식, 아키텍처, 컴파일러 정보 확인 |
| 정적 분석 | 실행하지 않고 문자열, 함수, 분기 구조 분석 |
| 동적 분석 | 디버거로 실행 흐름과 메모리 변화 관찰 |
| 핵심 로직 파악 | 검증 루틴, 암호화 루틴, 조건 분기 확인 |
| 문서화 | 함수 이름, 구조체, 흐름도, 분석 결과 정리 |
리버싱은 단순히 어셈블리를 읽는 작업이 아니라 프로그램이 가진 전체 흐름을 복원하는 과정에 가깝다.
2. 실행 파일과 메모리 구조
리버싱을 하기 위해서는 실행 파일이 메모리에 어떻게 올라가는지 이해해야 한다.
2.1 ELF 기준 메모리 구조
Linux ELF 실행 파일 기준으로 주요 메모리 영역은 다음과 같다.
| 영역 | 설명 |
|---|---|
| .text | 실행 가능한 코드가 저장되는 영역 |
| .rodata | 읽기 전용 문자열, 상수 데이터 저장 |
| .data | 초기화된 전역 변수 저장 |
| .bss | 초기화되지 않은 전역 변수 저장 |
| heap | malloc 등으로 동적 할당되는 영역 |
| stack | 지역 변수, 함수 호출 정보, 리턴 주소 저장 |
예를 들어 문자열 비교 로직을 분석한다면 .rodata에 저장된 문자열과 .text에 존재하는 비교 루틴을 함께 확인해야 한다.
2.2 x86_64 주요 레지스터
x86_64 환경에서 자주 보는 레지스터는 다음과 같다.
| 레지스터 | 역할 |
|---|---|
| RIP | 다음에 실행할 명령어 주소 |
| RSP | 현재 스택의 최상단 주소 |
| RBP | 스택 프레임 기준 주소 |
| RAX | 함수 반환값 또는 계산 결과 |
| RDI | 첫 번째 함수 인자 |
| RSI | 두 번째 함수 인자 |
| RDX | 세 번째 함수 인자 |
| RCX | 네 번째 함수 인자 |
System V AMD64 ABI 기준으로 Linux x86_64 함수 호출 시 인자는 보통 RDI, RSI, RDX, RCX, R8, R9 순서로 전달된다.
3. 정적 분석
정적 분석은 프로그램을 실행하지 않고 파일 자체를 분석하는 방식이다. 실행 위험이 있는 파일을 다룰 때 가장 먼저 수행하는 분석 단계이기도 하다.
3.1 파일 정보 확인
먼저 파일의 형식과 아키텍처를 확인한다.
file ./targetchecksec ./targetreadelf -h ./targetfile 명령어는 실행 파일 형식과 아키텍처를 확인하는 데 사용된다. checksec은 NX, Canary, PIE, RELRO 같은 보호 기법 적용 여부를 확인할 때 유용하다. readelf는 ELF 헤더, 섹션, 심볼 정보를 분석할 수 있다.
3.2 문자열 분석
실행 파일 안에 포함된 문자열은 프로그램의 기능을 추측하는 단서가 된다.
strings -a ./targetstrings -a ./target | grep -i pass예를 들어 다음과 같은 문자열이 발견되었다고 가정하자.
Input password:Correct!Wrong password!이 경우 프로그램 내부에 패스워드 검증 루틴이 존재할 가능성이 높다. 분석자는 해당 문자열이 참조되는 위치를 찾아 검증 로직으로 이동할 수 있다.
3.3 디스어셈블과 디컴파일
정적 분석 도구는 바이너리 코드를 사람이 읽을 수 있는 형태로 변환한다.
| 도구 | 용도 |
|---|---|
| Ghidra | 무료 디컴파일러, 함수 구조 복원 |
| IDA Free | 디스어셈블 및 함수 분석 |
| radare2 | CLI 기반 바이너리 분석 |
| objdump | 간단한 디스어셈블 확인 |
| Binary Ninja | 상용 리버싱 분석 도구 |
예시 C 코드는 다음과 같다.
#include <stdio.h>#include <string.h>
int main() { char input[32];
printf("Input password: "); scanf("%31s", input);
if (strcmp(input, "rev_basic_2026") == 0) { puts("Correct!"); } else { puts("Wrong password!"); }
return 0;}컴파일된 바이너리에서는 위 코드가 다음과 유사한 어셈블리 흐름으로 보일 수 있다.
lea rdi, [rbp-0x20]lea rsi, [rip+0x2004] ; "rev_basic_2026"call strcmptest eax, eaxjne wronglea rdi, [rip+0x2015] ; "Correct!"call putsjmp endwrong:lea rdi, [rip+0x2020] ; "Wrong password!"call putsstrcmp()는 두 문자열이 같으면 0을 반환한다. 따라서 test eax, eax 이후 jne wrong 분기가 존재한다면, eax가 0이 아닐 때 실패 루틴으로 이동한다는 의미이다.
4. 동적 분석
동적 분석은 프로그램을 직접 실행하면서 레지스터, 메모리, 분기 흐름을 관찰하는 방식이다.
4.1 디버거 실행
Linux 환경에서는 gdb나 pwndbg, gef 같은 플러그인을 자주 사용한다.
gdb ./target기본적인 디버깅 명령어는 다음과 같다.
| 명령어 | 설명 |
|---|---|
| break main | main 함수에 브레이크포인트 설정 |
| run | 프로그램 실행 |
| ni | 다음 명령어 실행, 함수 내부로 진입하지 않음 |
| si | 다음 명령어 실행, 함수 내부로 진입 |
| info registers | 레지스터 값 확인 |
| x/s 주소 | 해당 주소의 문자열 확인 |
| x/gx 주소 | 해당 주소의 8바이트 값 확인 |
| disassemble main | main 함수 디스어셈블 |
4.2 브레이크포인트 설정
문자열 비교 함수에 브레이크포인트를 걸면 입력값과 정답 문자열의 위치를 확인할 수 있다.
break strcmpruninfo registersx/s $rdix/s $rsix86_64 Linux 환경에서 strcmp(a, b)가 호출될 때 첫 번째 인자 a는 RDI, 두 번째 인자 b는 RSI에 들어간다. 따라서 x/s $rdi, x/s $rsi를 사용하면 비교 대상 문자열을 확인할 수 있다.
4.3 실행 흐름 추적
조건 분기는 리버싱에서 매우 중요한 단서이다.
| 명령어 | 의미 |
|---|---|
| cmp a, b | 두 값을 비교 |
| test a, a | 값이 0인지 확인할 때 자주 사용 |
| je / jz | 같으면 점프 |
| jne / jnz | 같지 않으면 점프 |
| jg / ja | 크거나 초과하면 점프 |
| jl / jb | 작거나 미만이면 점프 |
| call | 함수 호출 |
| ret | 함수 복귀 |
특히 cmp, test 이후 나오는 조건 점프를 보면 성공 루틴과 실패 루틴을 구분할 수 있다.
5. 패턴 기반 분석
리버싱에서는 자주 반복되는 코드 패턴을 빠르게 식별하는 능력이 중요하다.
5.1 비밀번호 검증 패턴
비밀번호 검증 로직은 보통 다음과 같은 형태를 가진다.
call strcmptest eax, eaxjne fail이 패턴은 문자열 비교 결과가 0이 아닐 경우 실패 루틴으로 이동한다는 의미이다. 따라서 분석자는 fail 라벨의 반대쪽 흐름을 따라가며 성공 조건을 확인한다.
5.2 길이 검증 패턴
입력 길이를 확인하는 코드에서는 strlen() 호출 이후 비교 명령어가 등장하는 경우가 많다.
call strlencmp rax, 0x10jne fail위 코드는 입력 문자열의 길이가 0x10, 즉 16바이트가 아니면 실패한다는 뜻이다.
5.3 반복문 패턴
문자 단위 검증에서는 반복문과 인덱스 증가가 함께 나타난다.
mov eax, 0loop_start:movzx edx, byte ptr [rbp+rax-0x30]xor edx, 0x23cmp dl, byte ptr [rip+rax+table]jne failadd rax, 1cmp rax, 0x10jne loop_start이러한 코드는 입력값을 한 글자씩 가져와 연산한 뒤 테이블 값과 비교하는 구조이다. 이 경우 분석자는 반복문의 종료 조건, 연산 방식, 비교 테이블을 함께 복원해야 한다.
6. 패치와 후킹의 원리
패치는 바이너리의 일부 명령어를 수정하여 실행 흐름을 바꾸는 것이다. 단, 상용 소프트웨어의 인증 우회나 무단 변조는 불법이 될 수 있으므로 반드시 자신이 만든 프로그램, CTF 문제, 허가된 테스트 환경에서만 수행해야 한다.
6.1 조건 분기 패치
예를 들어 다음과 같은 코드가 있다고 가정한다.
test eax, eaxjne failjne fail은 비교 결과가 0이 아닐 때 실패 루틴으로 이동한다. 실습용 바이너리에서는 이 조건 분기를 반대로 바꾸거나 NOP 처리하여 실행 흐름이 어떻게 변하는지 관찰할 수 있다.
| 패치 방식 | 의미 |
|---|---|
| JNE -> JE | 점프 조건을 반대로 변경 |
| JNE -> JMP | 항상 점프하도록 변경 |
| JNE -> NOP | 조건 분기를 제거 |
| CALL 제거 | 특정 함수 호출을 건너뜀 |
이 과정의 목적은 무단 우회가 아니라 어셈블리 명령어와 프로그램 흐름이 어떻게 연결되는지 이해하는 것이다.
6.2 후킹의 개념
후킹은 특정 함수 호출을 가로채서 인자나 반환값을 관찰하거나 바꾸는 기법이다. 보안 분석에서는 프로그램이 어떤 API를 호출하는지 추적하기 위해 사용된다.
예를 들어 분석자는 다음과 같은 질문을 던질 수 있다.
- 어떤 파일을 열려고 하는가?
- 어떤 네트워크 주소에 접근하려 하는가?
- 어떤 입력값을 비교 함수에 전달하는가?
- 암호화 함수에 들어가는 평문과 키는 무엇인가?
후킹은 매우 강력한 분석 방법이지만 실제 서비스나 타인의 프로그램에 무단으로 적용해서는 안 된다.
7. 안티 리버싱과 분석자 관점
프로그램은 분석을 어렵게 만들기 위해 여러 기법을 사용할 수 있다.
7.1 대표적인 안티 리버싱 기법
| 기법 | 설명 |
|---|---|
| 심볼 제거 | 함수 이름과 변수 이름 제거 |
| 난독화 | 제어 흐름과 문자열을 복잡하게 변형 |
| 패킹 | 실행 시점에 실제 코드를 복원 |
| 안티 디버깅 | 디버거 실행 여부를 탐지 |
| 무결성 검사 | 코드 영역이 변조되었는지 확인 |
이러한 기법은 분석 난이도를 높이지만 분석을 불가능하게 만들지는 않는다. 분석자는 먼저 표면적인 실행 흐름을 파악하고, 점진적으로 복원 범위를 넓혀야 한다.
7.2 안전한 분석 환경
알 수 없는 바이너리를 분석할 때는 격리된 환경을 사용하는 것이 좋다.
- 가상 머신 사용
- 네트워크 격리
- 스냅샷 생성
- 분석 로그 기록
- 해시값 저장
- 원본 파일 보존
특히 출처가 불분명한 파일은 절대 메인 환경에서 실행하지 않아야 한다.
8. 리버싱 실습 체크리스트
리버싱을 처음 시작할 때는 다음 순서로 분석하면 좋다.
8.1 기본 정보 확인
file ./targetsha256sum ./targetchecksec ./target8.2 문자열 확인
strings -a ./target | less8.3 함수 구조 확인
objdump -d ./target | less또는 Ghidra, IDA Free 같은 도구로 함수 목록과 참조 관계를 확인한다.
8.4 디버깅
gdb ./targetbreak mainrundisassemble main8.5 핵심 로직 정리
분석 중에는 다음 내용을 꾸준히 기록해야 한다.
| 항목 | 기록 내용 |
|---|---|
| 입력 위치 | 사용자 입력이 저장되는 버퍼 또는 변수 |
| 검증 함수 | strcmp, memcmp, custom check 함수 |
| 성공 조건 | 성공 루틴으로 이동하는 조건 |
| 실패 조건 | 실패 루틴으로 이동하는 조건 |
| 중요 문자열 | 에러 메시지, 성공 메시지, 힌트 문자열 |
| 중요 주소 | 분기문, 함수 시작 주소, 테이블 주소 |
9. 리버싱 학습 방향
리버싱 실력을 키우려면 어셈블리, 운영체제, 컴파일러, 디버거 사용법을 함께 공부해야 한다.
9.1 추천 학습 순서
- C언어 포인터와 메모리 구조 이해
- x86_64 어셈블리 기본 명령어 학습
- gdb로 간단한 C 프로그램 디버깅
- Ghidra로 함수와 문자열 참조 분석
- CTF Reversing 문제 풀이
- ELF, PE 파일 구조 학습
- 난독화와 패킹 개념 이해
리버싱은 처음에는 어셈블리 때문에 어렵게 느껴지지만, 반복되는 패턴을 익히면 점점 프로그램의 구조가 눈에 들어오기 시작한다.
오늘은 리버싱의 기본 개념과 정적 분석, 동적 분석, 어셈블리 해석, 패치와 후킹의 원리, 안전한 분석 환경까지 알아보았다.