테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

리버스 엔지니어링

소스 없이 실행 파일만 보고 프로그램이 무엇을 하는지 알아내는 일을 방어하는 쪽의 눈으로 정리한다. 실행 파일의 구조, 어셈블리와 호출 규약, 정적·동적 분석의 원리, 난독화와 패킹이 무엇인지, 악성 코드를 분석해 탐지 규칙으로 바꾸는 흐름, 그리고 내 소프트웨어를 지키는 현실적인 방법까지 다룬다.

목차 (10)
  1. 리버스 엔지니어링이란
  2. 법과 윤리
  3. 실행 파일 형식: ELF와 PE
  4. 기계어와 어셈블리
  5. 정적 분석: 실행하지 않고 읽기
  6. 동적 분석: 실행하며 지켜보기
  7. 난독화와 패킹
  8. 악성 코드 분석의 흐름
  9. 내 소프트웨어 지키기
  10. 핵심 자료

이 노트는 분석과 방어의 원리만 다룬다. 특정 제품의 보호를 푸는 방법, 라이선스 확인을 건너뛰는 방법, 게임 치트 방지나 DRM을 피하는 방법, 동작하는 공격 코드는 싣지 않는다. 메모리 안전 버그와 완화 기법, 코드 서명은 보안 › 시스템 노트에, 실제 원격 코드 실행 사례는 원격 코드 실행 노트에, 소스가 기계어가 되는 과정은 컴파일러와 언어 노트에, 프로세스와 메모리는 운영체제 노트에, 디스크·메모리 이미지를 뜨고 지운 파일을 되찾는 일은 디지털 포렌식 노트에 있다.

리버스 엔지니어링이란

빌드는 사람이 읽는 소스를 기계가 실행하는 바이트로 바꾸면서 많은 정보를 버린다. 리버스 엔지니어링은 그 결과물에서 거꾸로 설계와 동작을 짐작해 내는 일이다. 소프트웨어뿐 아니라 하드웨어, 통신 프로토콜, 파일 형식에도 쓰는 말이지만, 이 노트는 실행 파일을 중심으로 본다.

위쪽은 소스 코드가 컴파일과 링크를 거쳐 실행 파일이 되는 빌드 과정, 아래쪽은 실행 파일을 디스어셈블하고 디컴파일해 동작을 이해하는 리버스 엔지니어링 과정. 빌드에서 사라진 정보 목록이 가운데에 있다빌드(정보가 사라진다)소스 코드컴파일링크실행 파일사라지는 것:변수·함수 이름주석타입과 구조체원래의 제어 구조남는 것: 기계어, 상수와 문자열, 가져다 쓰는 라이브러리 함수 이름, (지우지 않았다면) 디버그 기호분석(거꾸로 짐작해 간다)동작 이해디컴파일디스어셈블실행 파일정적 분석은 파일을 읽어서, 동적 분석은 실행을 지켜보며 이 화살표를 거슬러 올라간다.디컴파일 결과는 원래 소스가 아니라 같은 동작을 하는 '추정' 코드다.
빌드는 정보를 버리는 한 방향 과정이다. 분석은 남은 단서(기계어, 문자열, 가져다 쓰는 함수 이름)로 버려진 정보를 짐작해 채운다. 그래서 결과는 늘 '추정'이다.

정당한 쓰임은 생각보다 넓다.

  • 상호 운용: 문서가 없는 파일 형식이나 프로토콜을 읽어 다른 프로그램과 함께 동작하게 한다. 오래된 워드 문서를 여는 오픈 소스 오피스, 다른 운영체제의 파일 시스템 드라이버가 이렇게 만들어졌다.
  • 악성 코드 분석: 침입에 쓰인 프로그램이 무엇을 했는지, 무엇을 노리는지, 어떻게 찾아내 막을지를 알아낸다. 보안 관제와 사고 대응의 핵심 업무다.
  • 취약점 연구: 소스가 공개되지 않은 제품의 약점을 찾아 제조사에 알리고 고치게 한다. 패치 전후를 비교해 무엇이 고쳐졌는지 확인하는 것도 여기에 든다.
  • 레거시와 내 코드: 소스를 잃어버린 사내 프로그램을 유지 보수하거나, 컴파일러가 내 코드를 어떻게 바꿨는지 보고 성능 문제를 찾는다.

법과 윤리

같은 기술이라도 무엇을, 누구의 허락으로, 어떤 목적으로 하느냐에 따라 합법과 불법이 갈린다. 나라마다 규정이 다르고 자주 바뀌므로 아래는 방향만 잡는 개요이고 법률 조언이 아니다. 한국 저작권법은 호환에 필요한 정보를 얻기 위한 프로그램코드역분석을 조건부로 허용한다(저작권법 제101조의4). EU의 소프트웨어 지침도 상호 운용을 위한 디컴파일을 비슷한 조건으로 허용한다. 미국은 기술적 보호 조치의 우회를 금지하는 DMCA 1201조에 선의의 보안 연구 같은 예외를 3년마다 정한다(미국 저작권청). 예외는 대부분 '필요한 범위 안에서', '얻은 정보를 다른 목적에 쓰지 않을 것' 같은 조건이 붙는다.

리버스 엔지니어링을 시작하기 전에 확인할 다섯 칸: 분석 권한, 라이선스와 약관, 나라별 법의 예외, 다루는 데이터, 결과를 조율해 공개하는 계획1. 권한내 것이거나허락받은 대상인가2. 약관라이선스·약관이분석을 막는가3. 법나라마다 다른예외 조항4. 데이터개인정보·비밀이섞여 있지 않은가5. 공개찾은 취약점을조율해 알릴 계획하나라도 확실하지 않으면 멈추고 묻는다. 회사 일이라면 법무·보안 담당과,연구라면 소속 기관과 먼저 정한다. 이 그림은 법률 조언이 아니다.
분석을 시작하기 전에 확인할 다섯 가지. 법이 허용해도 계약(라이선스·약관)으로 금지했을 수 있고, 회사 장비와 데이터에는 회사 정책이 먼저다.

취약점을 찾았다면 바로 공개하지 않고 제조사에 먼저 알려 고칠 시간을 준다. 이것이 조율된 취약점 공개(CVD)다(CISA). 연락처는 제조사의 보안 창구나 security.txt, 버그 바운티 규칙에서 찾고, 규칙이 정한 범위 밖은 건드리지 않는다. 공개 절차와 보고서 쓰는 법은 보안 › 사람과 운영 노트에 있다.

실행 파일 형식: ELF와 PE

실행 파일은 기계어만 담은 덩어리가 아니라, 운영체제의 로더가 읽는 '적재 설명서'다. 리눅스와 안드로이드는 ELF, 윈도는 PE, macOS와 iOS는 Mach-O를 쓴다. 생김새는 달라도 구성은 비슷하다. 맨 앞 헤더가 어떤 CPU용인지와 첫 명령의 위치(진입점)를 알리고, 표가 코드·데이터 구역의 위치와 권한을 적는다.

ELF와 PE 실행 파일 형식의 구역을 위에서 아래로 나란히 놓은 그림. 두 형식 모두 맨 앞 헤더에 기계 종류와 진입점이 있고, 코드 섹션, 상수와 데이터 섹션, 기호나 리소스 같은 부가 정보 섹션이 뒤따른다ELF (리눅스, BSD, 안드로이드)ELF 헤더7f 45 4c 46, 기계, 진입점프로그램 헤더 표메모리에 올릴 세그먼트.text기계어 코드.plt / .got공유 라이브러리 호출.rodata상수·문자열.data / .bss전역 변수.dynsym가져오고 내보내는 기호.symtab전체 기호(strip하면 없음)섹션 헤더 표섹션의 위치와 크기PE (윈도 .exe, .dll)DOS 헤더'MZ'와 PE 헤더 위치PE 서명 + COFF'PE\0\0', 기계, 섹션 수옵셔널 헤더진입점, 이미지 베이스섹션 표섹션마다 이름·위치·권한.text기계어 코드.rdata상수, 가져오기 표(IAT).data전역 변수.rsrc아이콘·버전 정보.reloc주소 재배치 정보색칠한 칸이 헤더다. 섹션 이름은 관례일 뿐이라, 분석 도구는 이름보다헤더에 적힌 권한(읽기·쓰기·실행)과 위치·크기를 믿는다.
ELF와 PE의 주요 구역. 분석은 보통 헤더부터 읽는다. 어떤 CPU용인지, 진입점이 어디인지, 어떤 라이브러리 함수를 가져다 쓰는지만 봐도 프로그램의 성격이 꽤 드러난다.
  • 가져오는 함수(PE의 가져오기 표, ELF의 .dynsym): 프로그램이 부르는 라이브러리 함수 목록이다. 네트워크·암호·프로세스 생성 함수가 보이면 무엇을 하려는지 짐작할 수 있다.
  • 기호: 함수와 변수 이름표다. 배포용 빌드는 보통 지워(strip) 두므로 이름 없는 함수 수천 개를 마주하는 것이 정적 분석의 일상이다. 디버그 정보(DWARF, PDB)가 있으면 이름과 타입까지 되살아난다.
  • 섹션 권한: 코드 구역은 읽기·실행, 데이터 구역은 읽기·쓰기로 나뉜다. 쓰기와 실행이 함께 허용된 구역은 정상 프로그램에서 드물어서 그 자체로 의심할 단서다.

기계어와 어셈블리

CPU가 실행하는 것은 바이트로 된 명령이고, 어셈블리는 그 바이트를 사람이 읽을 이름으로 적은 것이다. 분석에서 가장 많이 만나는 x86-64는 범용 레지스터 16개를 쓰고, 함수가 인자를 어디로 받고 무엇을 보존할지는 호출 규약이 정한다. 리눅스·macOS는 System V 규약, 윈도는 x64 규약을 따른다. 이것을 알아야 어셈블리에서 '이 함수는 인자 둘을 받아 하나를 돌려준다'를 읽어 낼 수 있다.

위쪽은 64비트 rax 레지스터의 아래 32비트가 eax, 16비트가 ax, 8비트가 al로 겹쳐 있는 막대. 아래쪽은 System V와 Windows x64 호출 규약에서 인자, 반환값, 보존 레지스터, 스택 규칙을 비교한 표범용 레지스터 16개(rax~r15) 중 하나: 아래쪽 비트에 이름이 겹쳐 있다rax64비트eax32비트ax16비트al8비트비트 63032비트 값을 eax에 쓰면 위 32비트는 0이 된다호출 규약System V (리눅스·macOS)Windows x64정수 인자rdi, rsi, rdx, rcx, r8, r9rcx, rdx, r8, r9반환값raxrax보존(호출된 쪽)rbx, rbp, r12–r15rbx, rbp, rdi, rsi, r12–r15스택16B 정렬, red zone 128B16B 정렬, 그림자 공간 32B그 밖에 rsp(스택 꼭대기), rip(다음 명령 주소), rflags(비교 결과 플래그)가 있다.
x86-64 레지스터. 위는 한 레지스터 안에 크기별 이름이 겹친 모습, 아래는 두 호출 규약의 차이다. 같은 함수도 윈도용과 리눅스용 빌드에서 인자를 받는 레지스터가 다르다.

아래 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
    ret
함수 호출 단계 그림: 왼쪽은 호출하는 쪽 두 명령과 add_scaled의 13개 명령 목록, 오른쪽은 스택 칸에 돌아갈 주소, 저장한 rbp, 지역 변수 a, b, c가 쌓이는 모습과 rsp, rbp가 가리키는 위치, 아래는 레지스터 값명령mov edi, 3▶mov edi, 3mov esi, 5▶mov esi, 5call add_scaled▶call add_scaledpush rbp▶push rbpmov rbp, rsp▶mov rbp, rspmov dword ptr [rbp - 0x4], edi▶mov dword ptr [rbp - 0x4], edimov dword ptr [rbp - 0x8], esi▶mov dword ptr [rbp - 0x8], esimov eax, dword ptr [rbp - 0x4]▶mov eax, dword ptr [rbp - 0x4]mov ecx, dword ptr [rbp - 0x8]▶mov ecx, dword ptr [rbp - 0x8]shl ecx, 0x2▶shl ecx, 0x2add eax, ecx▶add eax, ecxmov dword ptr [rbp - 0xc], eax▶mov dword ptr [rbp - 0xc], eaxmov eax, dword ptr [rbp - 0xc]▶mov eax, dword ptr [rbp - 0xc]pop rbp▶pop rbpret▶ret스택(높은 주소가 위)0x7fe00x7fd80x7fd00x7fcc0x7fc80x7fc4호출한 쪽의 스택← rsprbp = 0x8010 (호출한 쪽)edi = 3 esi = 0eax = 0 ecx = 0기계어: bf 03 00 00 00← rsprbp = 0x8010 (호출한 쪽)edi = 3 esi = 5eax = 0 ecx = 0기계어: be 05 00 00 00돌아갈 주소 = 0x401136← rsprbp = 0x8010 (호출한 쪽)edi = 3 esi = 5eax = 0 ecx = 0기계어: e8 (상대 주소 4바이트)돌아갈 주소 = 0x401136저장한 rbp = 0x8010← rsprbp = 0x8010 (호출한 쪽)edi = 3 esi = 5eax = 0 ecx = 0기계어: 55돌아갈 주소 = 0x401136저장한 rbp = 0x8010← rsp← rbpedi = 3 esi = 5eax = 0 ecx = 0기계어: 48 89 e5돌아갈 주소 = 0x401136저장한 rbp = 0x8010a = 3← rsp← rbpedi = 3 esi = 5eax = 0 ecx = 0기계어: 89 7d fc돌아갈 주소 = 0x401136저장한 rbp = 0x8010a = 3b = 5← rsp← rbpedi = 3 esi = 5eax = 0 ecx = 0기계어: 89 75 f8돌아갈 주소 = 0x401136저장한 rbp = 0x8010a = 3b = 5← rsp← rbpedi = 3 esi = 5eax = 3 ecx = 0기계어: 8b 45 fc돌아갈 주소 = 0x401136저장한 rbp = 0x8010a = 3b = 5← rsp← rbpedi = 3 esi = 5eax = 3 ecx = 5기계어: 8b 4d f8돌아갈 주소 = 0x401136저장한 rbp = 0x8010a = 3b = 5← rsp← rbpedi = 3 esi = 5eax = 3 ecx = 20기계어: c1 e1 02돌아갈 주소 = 0x401136저장한 rbp = 0x8010a = 3b = 5← rsp← rbpedi = 3 esi = 5eax = 23 ecx = 20기계어: 01 c8돌아갈 주소 = 0x401136저장한 rbp = 0x8010a = 3b = 5c = 23← rsp← rbpedi = 3 esi = 5eax = 23 ecx = 20기계어: 89 45 f4돌아갈 주소 = 0x401136저장한 rbp = 0x8010a = 3b = 5c = 23← rsp← rbpedi = 3 esi = 5eax = 23 ecx = 20기계어: 8b 45 f4돌아갈 주소 = 0x401136저장한 rbp = 0x8010a = 3b = 5c = 23← rsprbp = 0x8010 (호출한 쪽)edi = 3 esi = 5eax = 23 ecx = 20기계어: 5d돌아갈 주소 = 0x401136저장한 rbp = 0x8010a = 3b = 5c = 23← rsprbp = 0x8010 (호출한 쪽)edi = 3 esi = 5eax = 23 ecx = 20기계어: c3a·b·c는 rsp 아래 red zone에 놓인다
1 / 15

mov edi, 3 — 호출하는 쪽이 첫 인자 a = 3를 edi에 넣는다(System V 규약: 정수 인자는 rdi, rsi, rdx, rcx, r8, r9 순)

add_scaled(3, 5) 한 번의 호출을 명령 하나씩 따라간다. 명령은 clang -O0이 x86-64 리눅스용으로 낸 것이다. 스택은 아래(낮은 주소)로 자라고, 주소는 설명용 예시다(실제로는 실행마다 ASLR로 바뀐다).

호출이 끝나면 eax에 23이 남고 스택은 처음 높이로 돌아온다. 최적화 없는 코드는 모든 변수를 스택에 두어 소스와 거의 한 줄씩 대응하므로 읽기 쉽다. 같은 함수를 -O2로 컴파일하면 두 명령만 남는다.

add_scaled:
    lea   eax, [rdi + 4*rsi]         ; 8d 04 b7
    ret                              ; c3

lea는 원래 주소 계산 명령이지만 '더하기와 2·4·8배'를 한 번에 해 주어 산수에 자주 쓰인다. 스택 프레임도, 지역 변수 c도 사라졌다. 실제 분석 대상은 대부분 최적화된 코드라, 소스의 변수와 줄이 어셈블리에 그대로 보이지 않는다는 것을 늘 염두에 둔다. 내 코드가 무엇이 되는지는 Compiler Explorer로 바로 확인할 수 있다.

정적 분석: 실행하지 않고 읽기

정적 분석은 파일을 실행하지 않고 읽는다. 디스어셈블러는 바이트를 명령으로 풀어 적는다. x86은 명령 길이가 1~15바이트로 제각각이라 어디서부터 읽느냐에 따라 해석이 달라지므로, 진입점에서 출발해 점프를 따라가며 코드와 데이터를 가르는 '재귀 하강' 방식을 주로 쓴다. 그다음 명령을 기본 블록(중간에 들어오거나 나가는 곳이 없는 명령 묶음)으로 나누고 점프로 이어 제어 흐름 그래프(CFG)를 만든다.

sum_to 함수의 제어 흐름 그래프. 초기화 블록에서 비교 블록으로 내려가고, 비교 블록은 조건이 참이면 반환 블록으로 점프하고 거짓이면 본문 블록으로 내려가며, 본문 블록은 비교 블록으로 되돌아가 고리를 이룬다B0 sum_toxor eax, eaxmov ecx, 1B1 .loopcmp ecx, edijg .doneB2add eax, ecxinc ecxjmp .loopB3 .doneret점프(참): i > n그대로 내려감: i ≤ n되돌아감짐작한 원래 코드int sum_to(int n) {  int s = 0;  for (int i = 1; i <= n; i++)    s += i;  return s;}B2 → B1로 돌아가는 화살표가 곧 반복문이다
1부터 n까지 더하는 함수를 손으로 줄인 어셈블리. 점프 대상과 점프 바로 다음 명령에서 블록을 끊으면 블록 4개와 화살표 4개가 나온다. 되돌아가는 화살표가 반복문, 갈라지는 화살표가 조건문이다.

디컴파일러는 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가 대표적이다.

중단점 그림: 위쪽은 기계어 8d 04 b7 c3의 첫 바이트가 cc로 바뀐 모습, 아래쪽은 int3 실행, 운영체제 통지, 원래 바이트 복원과 조사, 한 명령 실행 뒤 다시 설치하는 순환원래 코드8d04b7c3중단점 설치 뒤cc04b7c3lea eax, [rdi+4*rsi]; retint3 — 여기서 멈춘다멈춘 뒤의 순환0xCC 실행예외 발생운영체제가디버거에 알림원래 바이트 8d 복원레지스터·메모리 조사한 명령만 실행다시 0xCC 설치코드를 고치지 않는 하드웨어 중단점(디버그 레지스터 DR0~DR3)은 4개까지 걸 수 있다.
소프트웨어 중단점은 코드를 잠시 고쳐서 건다. 멈춘 프로그램 입장에서는 한 바이트가 바뀐 것이므로, 자기 코드의 무결성을 검사하는 프로그램은 중단점을 알아챌 수 있다. 이것이 분석 방해 기법이 노리는 지점이기도 하다.
  • 추적: 멈추지 않고 기록만 한다. 시스템 호출 추적(strace, 윈도의 이벤트 추적), 라이브러리 호출 추적, 실행을 그대로 기록했다가 거꾸로 돌려 보는 rr 같은 기록·재생 디버거가 있다.
  • 샌드박스와 가상 머신: 모르는 프로그램은 분석 전용 가상 머신(QEMU 등)에서만 돌린다. 스냅숏으로 언제든 깨끗한 상태로 되돌리고, 네트워크는 끊거나 가짜 서비스로 흉내 낸다. 자동 샌드박스는 파일·레지스트리·네트워크 활동을 보고서로 만들어 준다.
  • 환경을 알아채는 표본: 악성 코드 중에는 가상 머신이나 디버거를 감지하면 얌전히 굴거나, 몇 시간 기다렸다 동작하는 것이 있다. 그래서 동적 분석 결과에 아무것도 없다고 무해하다는 뜻은 아니고, 정적 분석과 함께 본다.

난독화와 패킹

난독화는 동작은 그대로 두고 읽기만 어렵게 바꾸는 변환이다. 이름을 지우거나 의미 없는 이름으로 바꾸기, 문자열을 암호화해 두었다가 쓸 때 풀기, 제어 흐름을 한 개의 큰 분기 표로 납작하게 만들기(control-flow flattening), 늘 참인데 그렇게 보이지 않는 조건(opaque predicate) 넣기, 원래 코드를 자체 가상 머신의 바이트코드로 바꾸기 같은 기법이 있다. 패킹은 실행 파일 전체를 압축하거나 암호화하고 실행할 때 메모리에서 푸는 작은 코드를 앞에 붙이는 것이다. 크기를 줄이려는 정상적인 실행 파일 압축기(UPX 등)도 같은 원리다.

위쪽은 4096바이트 구역 세 개의 엔트로피 막대: 0으로 채운 구역은 0, 글은 약 5, 압축되거나 암호화된 구역은 8에 가깝다. 아래쪽은 패킹된 실행 파일이 헤더, 작은 풀기 코드, 엔트로피가 높은 압축 덩어리로 이뤄진 배치섀넌 엔트로피(바이트당 비트, 최대 8) · 4096바이트 표본0으로 채운 구역0.00글(영문·한글 문자열)5.33압축·암호화된 구역7.96패킹된 실행 파일의 배치(개념)헤더풀기 코드압축된 원래 프로그램실행하면 풀기 코드가 원래 프로그램을 메모리에 풀어 놓고 그리로 뛴다. 그래서 파일만 읽는정적 분석에는 풀기 코드만 보이고, 높은 엔트로피와 적은 가져오기 함수가 패킹의 단서가 된다.
바이트가 0~255에 얼마나 고르게 퍼졌는지를 재는 엔트로피. 압축·암호화된 데이터는 거의 완전히 고르게 퍼져 7.96비트에 가깝다. 실행 파일의 한 구역이 이렇게 높으면 패킹이나 숨겨 둔 데이터를 의심한다.

방어하는 쪽이 이것을 알아야 하는 이유는 악성 코드가 같은 기법을 쓰기 때문이다. 패킹하면 파일의 바이트가 매번 달라져 해시나 단순 문자열 서명에 걸리지 않는다. 그래서 탐지는 '무엇처럼 생겼나'에서 '무엇을 하나'(행위)로 옮겨 가고, 분석가는 엔트로피, 가져오는 함수가 이상하게 적은 것, 쓰기와 실행이 함께 허용된 섹션 같은 단서로 패킹을 알아본 뒤, 실행 중 메모리에 풀린 코드를 떠서 분석한다. 이론적으로도 한계가 있다. 어떤 프로그램이든 '실행 결과 말고는 아무것도 새지 않게' 숨기는 일반적인 난독화는 불가능하다는 것이 증명되어 있다(Barak 등 2001).

악성 코드 분석의 흐름

보안 팀이 수상한 파일을 받았을 때의 목표는 '완전한 이해'가 아니라 '빠른 방어'다. 무엇을 하는 파일인지, 이미 어디까지 퍼졌는지, 어떻게 찾아 막을지를 답하면 된다. 그래서 싼 단계부터 비싼 단계로, 필요한 만큼만 깊이 들어간다(NIST SP 800-83).

악성 코드 분석 단계 그림: 격리와 준비, 분류, 정적 분석, 동적 분석, 침해 지표 추출, 탐지 규칙과 공유의 여섯 칸이 차례로 강조되고 아래에 단계별 할 일이 나온다1. 격리·준비2. 분류3. 정적 분석4. 동적 분석5. IOC 추출6. 탐지·공유· 분석 전용 가상 머신, 깨끗한 스냅숏· 네트워크는 끊거나 가짜 서비스로 흉내· 표본은 압축·이름 바꿔 실수로 열리지 않게· SHA-256 해시로 이미 알려졌는지 조회· 파일 형식, 서명, 문자열, 가져오는 함수· 엔트로피로 패킹 여부 짐작· 디스어셈블·디컴파일로 핵심 함수 찾기· 설정값, 암호 상수, 명령 처리 분기· 난독화된 부분은 표시해 두고 넘어감· 샌드박스에서 실행하며 행동 기록· 프로세스·파일·레지스트리·네트워크· 풀린 코드는 메모리에서 떠서 다시 정적 분석· 해시, 도메인·IP, 파일 경로, 뮤텍스 이름· 행위를 MITRE ATT&CK 기법에 대응· 바뀌기 쉬운 지표와 어려운 지표 구분· YARA 규칙으로 같은 계열 표본 찾기· EDR·SIEM 탐지 규칙, 차단 목록· 보고서와 지표를 공유해 다른 조직도 방어
1 / 6

격리: 표본은 분석 전용 가상 머신에서만 다룬다. 깨끗한 스냅숏으로 언제든 되돌리고, 네트워크는 끊거나 가짜 서비스로 흉내 내 바깥에 피해가 가지 않게 한다.

악성 코드 분석의 일반적인 흐름. 순서는 고정이 아니라 정적·동적 분석을 오가며 되풀이한다. 모든 단계는 격리된 환경에서, 조직의 승인과 절차 안에서 한다.

찾아낸 특징은 탐지 규칙으로 바꾼다. 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학습 도구내가 쓴 코드가 어떤 어셈블리가 되는지 브라우저에서 바로 본다.