제한된 문자 우회를 위한 Shellcode 작성 방법과 전략 - 1

R&D

•

2024.07.18

☑️ 개요

최근, 국내외 차량용 블랙박스 9개의 보안 분석을 진행하면서 놀라운 발견을 했습니다. 이 장비들 중 7개에 Buffer Overflow 취약점이 존재했으며, 그중 4개의 장비에서는 Buffer Overflow에 대한 어떠한 보호 기법도 적용되어 있지 않았습니다. 이는 악의적인 공격자가 관리자 권한을 획득하여 시스템을 제어할 수 있는 장비가 시중에 많이 판매되고 있다는 것을 의미합니다.


Buffer Overflow를 이용한 공격 중 위험도가 가장 높은 방식은 Shellcode 삽입을 통해 장비 내에 설계되지 않은 코드를 실행하여 시스템 제어를 빼앗는 것입니다. 이 Shellcode는 공격자 입장에서 많이 사용되지만 이를 사전에 탐지하고 분석하기 위해서는 보안 전문가들도 이에 대한 지식이 있어야 하며 제작 방법을 이해해야 합니다.


이 글에서는 Shellcode의 기본 개념을 이해하고, 제한된 문자를 우회하는 전략과 기법들을 알아보겠습니다.



☑️ Shellcode란 무엇일까?

Shellcode란 공격자가 특정한 목적을 위해 시스템의 Shell을 실행시키기 위해 작성한 일련의 명령어를 의미합니다. 보통 악성 코드의 일종으로 사용되며, 취약한 소프트웨어를 이용해 시스템을 장악하거나 추가적인 악성 행위를 수행하는 데 사용됩니다.


Shellcode는 보통 Assembler로 작성되며, Buffer Overflow와 같은 취약점을 통해 실행됩니다. 이 명칭은 초기 목적이 Shell을 실행시키는 데서 유래했지만, 현재는 더 다양한 목적을 위한 코드도 포함하는 범용적인 용어로 사용됩니다.




☑️ Shellcode를 사용하기 위한 최소 조건은?

  1. Buffer Overflow나 Format String 등 Return Address를 공격자가 원하는 값으로 변경할 수 있어야 합니다. Shellcode를 실행하기 위해서는 기본적으로 Return Address를 변경해서 PC 레지스터의 값을 Shellcode가 저장된 주소로 변경할 수 있어야 합니다.


    아래 ARM 아키텍처를 사용하는 IoT 장비의 Buffer Overflow 취약점을 활용해서 R4 - R12 레지스터를 덮어 PC 레지스터를 변조하는 예시입니다.




  2. 실행 파일이 관리자 권한이나 root 계정으로 실행되어야 합니다. 관리자 권한이 없더라도 Shell은 획득할 수 있지만 시스템 접근에 제한이 걸리기 때문에 최대한 권한이 높은 계정의 실행 파일이 공격자 입장에서는 많은 제어권을 가져오는데 유리합니다. IoT와 같은 임베디드 장비들은 대부분 root 계정 하나만 사용하는 경우가 많고, 계정을 여러 개로 장비를 관리하더라도 주요 기능을 담당하는 실행 파일은 관리자 권한으로 실행되기 때문에 크게 문제가 되지는 않습니다.


    아래 예시를 보면 root 계정 외에 admin을 통해 계정을 관리하지만 해당 장비의 주요 기능들은 admin 권한으로 충분히 실행할 수 있기 때문에 꼭 root 권한을 획득할 필요는 없습니다.




  1. 쓰기 권한과 읽기 권한이 모두 할당된 영역이 필요합니다.

    정확히는 NX 보호 기법이 걸려 있지 않을 때, Stack 영역이나 Heap 영역에 데이터를 쓰고 해당 데이터 영역으로 Return Address를 변경하여 Shellcode를 실행할 수 있습니다.


    NX 보호 기법이 걸려있지 않은 실행 파일을 분석해 보면 아래와 같이 쓰기 권한(w)와 실행 권한(x)가 같이 있는 영역들이 보입니다. 해당 영역에 Shellcode를 삽입하고 Return Address를 변경하면 해당 Shellcode를 실행하여 이상 동작을 발생시킬 수 있습니다.



    만약 NX 보호 기법이 활성화되어 있다면 아래처럼 쓰기 권한이 있는 영역에 실행 권한을 지우기 때문에 Shellcode를 삽입한다고 해도 실행이 되지 않게 막을 수 있습니다.


    이 경우에는 Shellcode 삽입 공격이 아닌 RTL(Return to Library)나 ROP(Return Oriented Programming)을 통해 공격을 진행해야 합니다.



    위의 3가지 조건을 만족한다면 해당 실행 파일을 분석해서 Shellcode를 실행하고 이상 동작을 시킬 수 있습니다.



☑️ MIPS, ARM의 Shellcode 제작 시 x86_64와 주요 차이점은 무엇일까?


CPU 설계 방식의 차이

가장 큰 차이점은 cpu의 설계 방식이 RISC와 CISC 차이가 있습니다.

  • RISC (Reduced Instruction Set Computing)

    단순하고 빠른 명령어를 사용하여 효율성을 극대화하는 설계 철학을 따릅니다. 각 명령어가 동일한 시간 내에 실행되도록 설계되어 있습니다. RISC의 주요 목표는 각 명령어를 간단하게 유지하고 명령어 수를 줄여서 실행 속도를 높이는 것입니다.


  • CISC (Complex Instruction Set Computing)

    복잡하고 다양한 명령어를 사용하여 다양한 작업을 처리하는 설계 철학을 따릅니다. 단일 명령어로 복잡한 작업을 수행할 수 있게 하여, 프로그래머가 작성해야 하는 코드 양을 줄이는 것이 목표입니다. 명령어가 복잡하고, 실행 시간이 명령어마다 다를 수 있습니다.



RISC (ARM & MIPS)

MIPS와 ARM은 RISC 아키텍처를 따르는 대표적인 프로세서들입니다. RISC 아키텍처의 특징은 다음과 같습니다.

  • 고정된 명령어 크기

    대부분의 명령어가 동일한 크기(일반적으로 4바이트)로 되어 있어 명령어 디코딩이 간단합니다.


  • 단순한 명령어 형식

    명령어 형식이 단순하고 명령어 수가 적습니다. 아키텍처 특성상 메모리 접근을 최소화하고, 대부분의 연산이 레지스터 간에 이루어집니다.


  • 하드웨어 복잡성 감소

    복잡한 명령어를 피하고, 단순한 명령어를 사용함으로써 하드웨어 복잡성을 줄이고, 전력 소모와 발열을 감소시킵니다.


ADD r0, r1, r2 ; r0 = r1 + r2



CISC (x86)

x86은 CISC 아키텍처를 따르는 대표적인 프로세서입니다. CISC 아키텍처의 특징은 다음과 같습니다.

  • 가변 길이 명령어

    명령어 길이가 가변적이며, 1바이트에서 수십 바이트까지 다양합니다. 이는 명령어 디코딩을 복잡하게 만듭니다.


  • 복잡한 명령어 형식

    다양한 명령어 형식과 주소 지정 모드를 지원하여, 단일 명령어로 복잡한 작업을 수행할 수 있습니다.


  • 하드웨어 복잡성 증가

    복잡한 명령어를 디코딩하고 실행하기 위해 더 복잡한 하드웨어가 필요합니다. 이는 전력 소모와 발열을 증가시킵니다.


    ADD EAX, EBX ; EAX = EAX + EBX
    ADD EAX, ECX ; EAX = EAX + ECX



RISC 아키텍처를 사용하면 CISC 보다 복잡성이 줄어들 수 있다고 보면 됩니다.



사용 레지스터

ARM

16개의 범용 레지스터 (R0-R15), 스택 포인터(R13), 링크 레지스터(R14), 프로그램 카운터(R15)


MIPS

32개의 범용 레지스터 ($0-$31), 각 레지스터는 고유한 용도로 사용


x86

8개의 주요 범용 레지스터 (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP), 32비트 크기


x86_64

16개의 범용 레지스터 (RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8-R15), 64비트 크기, 하위 32비트, 16비트, 8비트 접근 가능



시스템 콜

ARM

ARM 아키텍처에서 시스템 콜을 수행하려면 소프트웨어 인터럽트 명령어 (svc, 구버전 swi)를 사용합니다. 시스템 콜 번호와 인자는 레지스터를 통해 전달됩니다.

  • 시스템 콜 번호: r7

  • 인자: r0 - r6

  • 반환 값: r0


    MOV r0, #1 ; exit code 1

    MOV r7, #1 ; syscall number for exit

    SVC 0 ; make the syscall



MIPS

MIPS 아키텍처에서 시스템 콜은 syscall 명령어를 사용합니다. 시스템 콜 번호와 인자는 레지스터를 통해 전달됩니다.

  • 시스템 콜 번호: $v0

  • 인자: $a0 - $a3

  • 반환 값: $v0


    li $v0, 10 ; syscall number for exit

    syscall ; make the syscall



x86

x86 아키텍처에서는 시스템 콜을 수행하기 위해 주로 int 0x80 인터럽트를 사용합니다. 시스템 콜 번호와 인자는 레지스터를 통해 전달됩니다.

  • 시스템 콜 번호: eax

  • 인자: ebx, ecx, edx, esi, edi

  • 반환 값: eax


    mov eax, 1 ; syscall number for exit

    mov ebx, 1 ; exit code 1

    int 0x80 ; make the syscall



x86_64

x86_64 아키텍처에서는 syscall 명령어를 사용하여 시스템 콜을 수행합니다. 시스템 콜 번호와 인자는 레지스터를 통해 전달됩니다.

  • 시스템 콜 번호: rax

  • 인자: rdi, rsi, rdx, r8, r9, r10

  • 반환 값: rax


    mov rax, 60 ; syscall number for exit

    mov rdi, 1 ; exit code 1

    syscall ; make the syscall


Solutions

Services

Tech Blog

R&D

ZIEN

Contact Us

대표자명

조영민

Address

서울특별시 금천구 서부샛길 606 대성디폴리스 A동 1606, 1607호

Telephone

070-4571-9854

E-mail

support@zi-en.io

Ⓒ ZIEN Inc. All rights reserved.