리버스 엔지니어링
소스 없이 실행 파일만 보고 프로그램이 무엇을 하는지 알아내는 일을 방어하는 쪽의 눈으로 정리한다. 실행 파일의 구조, 어셈블리와 호출 규약, 정적·동적 분석의 원리, 난독화와 패킹이 무엇인지, 악성 코드를 분석해 탐지 규칙으로 바꾸는 흐름, 그리고 내 소프트웨어를 지키는 현실적인 방법까지 다룬다.
목차 (10)
이 노트는 분석과 방어의 원리만 다룬다. 특정 제품의 보호를 푸는 방법, 라이선스 확인을 건너뛰는 방법, 게임 치트 방지나 DRM을 피하는 방법, 동작하는 공격 코드는 싣지 않는다. 메모리 안전 버그와 완화 기법, 코드 서명은 보안 › 시스템 노트에, 실제 원격 코드 실행 사례는 원격 코드 실행 노트에, 소스가 기계어가 되는 과정은 컴파일러와 언어 노트에, 프로세스와 메모리는 운영체제 노트에, 디스크·메모리 이미지를 뜨고 지운 파일을 되찾는 일은 디지털 포렌식 노트에 있다.
리버스 엔지니어링이란
빌드는 사람이 읽는 소스를 기계가 실행하는 바이트로 바꾸면서 많은 정보를 버린다. 리버스 엔지니어링은 그 결과물에서 거꾸로 설계와 동작을 짐작해 내는 일이다. 소프트웨어뿐 아니라 하드웨어, 통신 프로토콜, 파일 형식에도 쓰는 말이지만, 이 노트는 실행 파일을 중심으로 본다.
정당한 쓰임은 생각보다 넓다.
- 상호 운용: 문서가 없는 파일 형식이나 프로토콜을 읽어 다른 프로그램과 함께 동작하게 한다. 오래된 워드 문서를 여는 오픈 소스 오피스, 다른 운영체제의 파일 시스템 드라이버가 이렇게 만들어졌다.
- 악성 코드 분석: 침입에 쓰인 프로그램이 무엇을 했는지, 무엇을 노리는지, 어떻게 찾아내 막을지를 알아낸다. 보안 관제와 사고 대응의 핵심 업무다.
- 취약점 연구: 소스가 공개되지 않은 제품의 약점을 찾아 제조사에 알리고 고치게 한다. 패치 전후를 비교해 무엇이 고쳐졌는지 확인하는 것도 여기에 든다.
- 레거시와 내 코드: 소스를 잃어버린 사내 프로그램을 유지 보수하거나, 컴파일러가 내 코드를 어떻게 바꿨는지 보고 성능 문제를 찾는다.
법과 윤리
같은 기술이라도 무엇을, 누구의 허락으로, 어떤 목적으로 하느냐에 따라 합법과 불법이 갈린다. 나라마다 규정이 다르고 자주 바뀌므로 아래는 방향만 잡는 개요이고 법률 조언이 아니다. 한국 저작권법은 호환에 필요한 정보를 얻기 위한 프로그램코드역분석을 조건부로 허용한다(저작권법 제101조의4). EU의 소프트웨어 지침도 상호 운용을 위한 디컴파일을 비슷한 조건으로 허용한다. 미국은 기술적 보호 조치의 우회를 금지하는 DMCA 1201조에 선의의 보안 연구 같은 예외를 3년마다 정한다(미국 저작권청). 예외는 대부분 '필요한 범위 안에서', '얻은 정보를 다른 목적에 쓰지 않을 것' 같은 조건이 붙는다.
취약점을 찾았다면 바로 공개하지 않고 제조사에 먼저 알려 고칠 시간을 준다. 이것이 조율된 취약점 공개(CVD)다(CISA). 연락처는 제조사의 보안 창구나 security.txt, 버그 바운티 규칙에서 찾고, 규칙이 정한 범위 밖은 건드리지 않는다. 공개 절차와 보고서 쓰는 법은 보안 › 사람과 운영 노트에 있다.
실행 파일 형식: ELF와 PE
실행 파일은 기계어만 담은 덩어리가 아니라, 운영체제의 로더가 읽는 '적재 설명서'다. 리눅스와 안드로이드는 ELF, 윈도는 PE, macOS와 iOS는 Mach-O를 쓴다. 생김새는 달라도 구성은 비슷하다. 맨 앞 헤더가 어떤 CPU용인지와 첫 명령의 위치(진입점)를 알리고, 표가 코드·데이터 구역의 위치와 권한을 적는다.
- 가져오는 함수(PE의 가져오기 표, ELF의
.dynsym): 프로그램이 부르는 라이브러리 함수 목록이다. 네트워크·암호·프로세스 생성 함수가 보이면 무엇을 하려는지 짐작할 수 있다. - 기호: 함수와 변수 이름표다. 배포용 빌드는 보통 지워(strip) 두므로 이름 없는 함수 수천 개를 마주하는 것이 정적 분석의 일상이다. 디버그 정보(DWARF, PDB)가 있으면 이름과 타입까지 되살아난다.
- 섹션 권한: 코드 구역은 읽기·실행, 데이터 구역은 읽기·쓰기로 나뉜다. 쓰기와 실행이 함께 허용된 구역은 정상 프로그램에서 드물어서 그 자체로 의심할 단서다.
기계어와 어셈블리
CPU가 실행하는 것은 바이트로 된 명령이고, 어셈블리는 그 바이트를 사람이 읽을 이름으로 적은 것이다. 분석에서 가장 많이 만나는 x86-64는 범용 레지스터 16개를 쓰고, 함수가 인자를 어디로 받고 무엇을 보존할지는 호출 규약이 정한다. 리눅스·macOS는 System V 규약, 윈도는 x64 규약을 따른다. 이것을 알아야 어셈블리에서 '이 함수는 인자 둘을 받아 하나를 돌려준다'를 읽어 낼 수 있다.
아래 C 함수를 clang으로 최적화 없이(-O0) 컴파일하면 다음 어셈블리가 된다(인텔 문법, x86-64 리눅스).
int add_scaled(int a, int b) {
int c = a + b * 4;
return c;
}add_scaled:
push rbp ; 호출한 쪽의 rbp 저장
mov rbp, rsp ; 이 함수의 기준점
mov dword ptr [rbp - 0x4], edi ; a
mov dword ptr [rbp - 0x8], esi ; b
mov eax, dword ptr [rbp - 0x4]
mov ecx, dword ptr [rbp - 0x8]
shl ecx, 0x2 ; b * 4
add eax, ecx
mov dword ptr [rbp - 0xc], eax ; c
mov eax, dword ptr [rbp - 0xc] ; 반환값은 eax
pop rbp
retmov edi, 3 — 호출하는 쪽이 첫 인자 a = 3를 edi에 넣는다(System V 규약: 정수 인자는 rdi, rsi, rdx, rcx, r8, r9 순)
호출이 끝나면 eax에 23이 남고 스택은 처음 높이로 돌아온다. 최적화 없는 코드는 모든 변수를 스택에 두어 소스와 거의 한 줄씩 대응하므로 읽기 쉽다. 같은 함수를 -O2로 컴파일하면 두 명령만 남는다.
add_scaled:
lea eax, [rdi + 4*rsi] ; 8d 04 b7
ret ; c3lea는 원래 주소 계산 명령이지만 '더하기와 2·4·8배'를 한 번에 해 주어 산수에 자주 쓰인다. 스택 프레임도, 지역 변수 c도 사라졌다. 실제 분석 대상은 대부분 최적화된 코드라, 소스의 변수와 줄이 어셈블리에 그대로 보이지 않는다는 것을 늘 염두에 둔다. 내 코드가 무엇이 되는지는 Compiler Explorer로 바로 확인할 수 있다.
정적 분석: 실행하지 않고 읽기
정적 분석은 파일을 실행하지 않고 읽는다. 디스어셈블러는 바이트를 명령으로 풀어 적는다. x86은 명령 길이가 1~15바이트로 제각각이라 어디서부터 읽느냐에 따라 해석이 달라지므로, 진입점에서 출발해 점프를 따라가며 코드와 데이터를 가르는 '재귀 하강' 방식을 주로 쓴다. 그다음 명령을 기본 블록(중간에 들어오거나 나가는 곳이 없는 명령 묶음)으로 나누고 점프로 이어 제어 흐름 그래프(CFG)를 만든다.
디컴파일러는 CFG에서 반복문과 조건문의 모양을 찾아 C 비슷한 코드로 다시 쓰고, 레지스터와 스택 칸을 변수로 묶어 타입을 추론한다. 결과는 원래 소스가 아니라 같은 동작을 하는 추정 코드라서 이름은 local_c, param_1 같은 것이 붙고, 분석가가 의미를 알아낼 때마다 이름과 타입을 고쳐 가며 읽는다. 컴파일러가 바꿔 놓은 모양을 그대로 보여 주기도 한다. 위 함수를 clang -O1로 컴파일하면 반복문이 사라지고 n(n+1)/2를 계산하는 산수 몇 줄만 남는다.
도구로는 NSA가 오픈 소스로 공개한 Ghidra, 오래된 상용 도구 IDA Pro, 중간 표현과 스크립트를 앞세운 Binary Ninja, 명령줄 중심의 오픈 소스 radare2와 그 갈래 Rizin이 널리 쓰인다. 기능은 서로 겹치고, 차이는 지원하는 아키텍처, 디컴파일 품질, 자동화 API, 가격에 있다. 가볍게 볼 때는 binutils의 objdump로도 충분하다.
동적 분석: 실행하며 지켜보기
동적 분석은 프로그램을 실제로 돌리며 관찰한다. 정적 분석으로는 알기 어려운 실행 중의 값(복호화된 문자열, 계산된 주소, 서버에서 받은 설정)을 직접 볼 수 있다. 가장 기본 도구는 디버거다. 원하는 명령에서 멈추고(중단점), 한 명령씩 실행하고, 레지스터와 메모리를 읽는다. 리눅스는 GDB와 LLDB, 윈도는 WinDbg와 x64dbg가 대표적이다.
- 추적: 멈추지 않고 기록만 한다. 시스템 호출 추적(
strace, 윈도의 이벤트 추적), 라이브러리 호출 추적, 실행을 그대로 기록했다가 거꾸로 돌려 보는 rr 같은 기록·재생 디버거가 있다. - 샌드박스와 가상 머신: 모르는 프로그램은 분석 전용 가상 머신(QEMU 등)에서만 돌린다. 스냅숏으로 언제든 깨끗한 상태로 되돌리고, 네트워크는 끊거나 가짜 서비스로 흉내 낸다. 자동 샌드박스는 파일·레지스트리·네트워크 활동을 보고서로 만들어 준다.
- 환경을 알아채는 표본: 악성 코드 중에는 가상 머신이나 디버거를 감지하면 얌전히 굴거나, 몇 시간 기다렸다 동작하는 것이 있다. 그래서 동적 분석 결과에 아무것도 없다고 무해하다는 뜻은 아니고, 정적 분석과 함께 본다.
난독화와 패킹
난독화는 동작은 그대로 두고 읽기만 어렵게 바꾸는 변환이다. 이름을 지우거나 의미 없는 이름으로 바꾸기, 문자열을 암호화해 두었다가 쓸 때 풀기, 제어 흐름을 한 개의 큰 분기 표로 납작하게 만들기(control-flow flattening), 늘 참인데 그렇게 보이지 않는 조건(opaque predicate) 넣기, 원래 코드를 자체 가상 머신의 바이트코드로 바꾸기 같은 기법이 있다. 패킹은 실행 파일 전체를 압축하거나 암호화하고 실행할 때 메모리에서 푸는 작은 코드를 앞에 붙이는 것이다. 크기를 줄이려는 정상적인 실행 파일 압축기(UPX 등)도 같은 원리다.
방어하는 쪽이 이것을 알아야 하는 이유는 악성 코드가 같은 기법을 쓰기 때문이다. 패킹하면 파일의 바이트가 매번 달라져 해시나 단순 문자열 서명에 걸리지 않는다. 그래서 탐지는 '무엇처럼 생겼나'에서 '무엇을 하나'(행위)로 옮겨 가고, 분석가는 엔트로피, 가져오는 함수가 이상하게 적은 것, 쓰기와 실행이 함께 허용된 섹션 같은 단서로 패킹을 알아본 뒤, 실행 중 메모리에 풀린 코드를 떠서 분석한다. 이론적으로도 한계가 있다. 어떤 프로그램이든 '실행 결과 말고는 아무것도 새지 않게' 숨기는 일반적인 난독화는 불가능하다는 것이 증명되어 있다(Barak 등 2001).
악성 코드 분석의 흐름
보안 팀이 수상한 파일을 받았을 때의 목표는 '완전한 이해'가 아니라 '빠른 방어'다. 무엇을 하는 파일인지, 이미 어디까지 퍼졌는지, 어떻게 찾아 막을지를 답하면 된다. 그래서 싼 단계부터 비싼 단계로, 필요한 만큼만 깊이 들어간다(NIST SP 800-83).
격리: 표본은 분석 전용 가상 머신에서만 다룬다. 깨끗한 스냅숏으로 언제든 되돌리고, 네트워크는 끊거나 가짜 서비스로 흉내 내 바깥에 피해가 가지 않게 한다.
찾아낸 특징은 탐지 규칙으로 바꾼다. YARA는 '이 문자열이나 바이트 패턴이 있고, 이 조건을 만족하면 이 이름을 붙인다'를 적는 규칙 언어로, 같은 계열의 표본을 모으거나 저장소와 메모리를 훑는 데 널리 쓴다. 아래는 구조만 보이려는 교육용 예로, 시험용 표식 문자열이 든 PE 파일(맨 앞 두 바이트가 'MZ')을 찾는다.
rule Training_Marker_Example
{
meta:
description = "교육용 예: 시험용 표식 문자열이 든 PE 파일을 찾는다"
strings:
$marker = "KILLTERM-YARA-TRAINING" ascii wide
condition:
uint16(0) == 0x5A4D and $marker
}좋은 규칙은 한 표본의 우연한 특징(빌드 경로 하나, 해시 하나)보다 계열 전체가 공유하는 특징을 잡고, 정상 파일에 잘못 걸리지 않는지 깨끗한 파일 묶음에 돌려 본 뒤에 배포한다. 행위는 MITRE ATT&CK의 기법 번호로 적어 두면 다른 팀과 같은 말로 이야기할 수 있다. 이렇게 만든 규칙과 침해 지표는 EDR·SIEM 같은 보안 관제 도구에 넣어 다른 기기에서도 같은 것을 찾게 한다.
내 소프트웨어 지키기
반대편에서 보면 결론은 단순하다. 사용자 기기에서 도는 코드는 결국 읽힌다. 난독화와 분석 방해는 비용을 올릴 뿐이고, 앱 안에 넣은 API 비밀이나 '클라이언트가 확인한 결제'는 언젠가 드러난다고 보고 설계해야 한다. 모바일 앱의 분석 저항을 어디까지 기대할지는 OWASP MASVS의 RESILIENCE 항목이 정리하고 있다.
- 코드 서명: 배포 파일에 서명해 누가 만들었고 중간에 바뀌지 않았음을 운영체제와 사용자가 확인하게 한다(윈도의 Authenticode, 오픈 소스 패키지의 Sigstore). 원리는 보안 › 시스템 노트의 서명된 업데이트 그림에 있다.
- 무결성 검사: 실행 중에 자기 코드의 해시를 확인하면 단순한 변조는 막지만, 검사하는 코드도 같은 기기에 있으므로 완전하지 않다. 서버가 판단할 수 있는 것은 서버가 판단한다.
- 안전한 개발 과정: 분석당하더라도 약점이 적도록 메모리 안전 언어, 의존성 관리, 빠른 업데이트 경로를 갖춘다(NIST SSDF). 외부 연구자가 찾은 문제를 받을 창구(
security.txt, 공개 정책)를 열어 두는 것도 방어다.
핵심 자료
공식 명세와 도구 문서, 입문서를 모았다. 요지는 직접 정리한 한 줄이다.
| 자료 | 종류 | 한 줄 요지 |
|---|---|---|
| ELF 형식(man 5 elf) · System V gABI | 형식 명세 | ELF 헤더·프로그램 헤더·섹션의 필드를 하나씩 정의한다. |
| PE Format (Microsoft Learn) | 형식 명세 | 윈도 실행 파일의 헤더, 섹션, 가져오기·내보내기 표를 설명하는 공식 문서다. |
| System V x86-64 psABI | 호출 규약 | 리눅스·macOS의 인자 전달, 스택 정렬, red zone을 정한 규약 원문이다. |
| x64 calling convention (Microsoft Learn) | 호출 규약 | 윈도 x64의 레지스터 인자 4개와 그림자 공간 규칙이다. |
| Ghidra | 정적 분석 도구 | NSA가 2019년 오픈 소스로 공개한 디스어셈블러·디컴파일러. 무료이고 여러 아키텍처를 지원한다. |
| IDA Pro | 정적 분석 도구 | Hex-Rays의 상용 디스어셈블러와 디컴파일러. 오래 쓰인 업계 표준 중 하나다. |
| Binary Ninja | 정적 분석 도구 | 중간 표현(IL)과 스크립트 API를 앞세운 상용 분석 도구다. |
| radare2 · Rizin | 정적 분석 도구 | 명령줄 중심의 오픈 소스 분석 프레임워크와 그 갈래다. |
| GDB · LLDB · WinDbg · x64dbg | 디버거 | 중단점, 한 걸음 실행, 메모리 조사를 하는 대표 디버거들이다. |
| YARA 문서 | 탐지 규칙 | 문자열과 조건으로 파일을 분류하는 규칙 언어의 공식 문서다. |
| MITRE ATT&CK | 위협 지식 | 공격자의 행위를 전술·기법으로 정리한 공개 지식 베이스다. |
| NIST SP 800-83 Rev. 1 | 대응 지침 | 악성 코드 사고의 예방과 대응을 다룬 지침이다. |
| Practical Malware Analysis (Sikorski, Honig) | 책 | 격리 환경 구축부터 정적·동적 분석까지 실습으로 익히는 입문서다. |
| Reverse Engineering for Beginners (Yurichev) | 무료 책 | 같은 C 코드가 여러 컴파일러·아키텍처에서 어떤 어셈블리가 되는지 보여 준다. |
| Compiler Explorer | 학습 도구 | 내가 쓴 코드가 어떤 어셈블리가 되는지 브라우저에서 바로 본다. |
- ELF 형식(man 5 elf) · System V gABI — 형식 명세
- PE Format (Microsoft Learn) — 형식 명세
- System V x86-64 psABI — 호출 규약
- x64 calling convention (Microsoft Learn) — 호출 규약
- Ghidra — 정적 분석 도구
- IDA Pro — 정적 분석 도구
- Binary Ninja — 정적 분석 도구
- radare2 · Rizin — 정적 분석 도구
- GDB · LLDB · WinDbg · x64dbg — 디버거
- YARA 문서 — 탐지 규칙
- MITRE ATT&CK — 위협 지식
- NIST SP 800-83 Rev. 1 — 대응 지침
- Practical Malware Analysis (Sikorski, Honig) — 책
- Reverse Engineering for Beginners (Yurichev) — 무료 책
- Compiler Explorer — 학습 도구
- ELF 명세 PDF (Tool Interface Standard)
- Rizin
- LLDB
- WinDbg 문서 (Microsoft Learn)
- x64dbg
- rr — 기록·재생 디버거
- QEMU
- GNU objdump 문서
- UPX — 실행 파일 압축기
- On the (Im)possibility of Obfuscating Programs (Barak 등, 2001)
- YARA 프로젝트
- 미국 저작권청 — DMCA 1201조 예외 규정
- 국가법령정보센터 — 저작권법(제101조의4 프로그램코드역분석)
- CISA — 조율된 취약점 공개(CVD) 프로그램
- Authenticode (Microsoft Learn)
- Sigstore
- NIST SP 800-218 — 안전한 소프트웨어 개발 프레임워크(SSDF)
- OWASP MASVS — 모바일 앱 보안 검증 표준(RESILIENCE 포함)