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

R&D

•

2024.08.08

☑️ 입력 불가 문자가 있을 때 우회 방법은?

Shellcode 작성 시 특정 바이트(0x00, 0x0a 등)가 입력 불가한 경우, 이를 우회하기 위해 다양한 방법이 존재합니다.



Opcode 변경을 통한 우회

ARM, MIPS 등 대부분의 아키텍처에는 유사한 기능을 수행하지만 서로 다른 Opcode를 가진 명령어들이 많이 있습니다. 아래에서 ARM과 MIPS의 덧셈 연산에 대한 서로 다른 Opcode 예시를 살펴보겠습니다.


ARM

ARM에는 ADD와 ADDS 2가지의 다른 명령어가 있습니다. ADD는 0x81, ADDS는 0x91의 Opcode를 가지며 비슷한 기능을 합니다.




ADD 명령어는 두 레지스터의 값을 더하거나 레지스터와 즉값을 더하여 결과를 저장합니다. 이 명령어는 조건 플래그를 수정하지 않습니다.


ADD R0, R1, #1 ; R0 = R1 + 1



ADDS 명령어는 ADD 명령어와 동일하게 덧셈을 수행하지만, 추가적으로 조건 플래그(CPSR, Current Program Status Register의 조건 플래그)를 업데이트합니다. 이는 조건부 분기나 조건부 명령어 실행에 유용합니다.


ADDS R0, R1, #1 ; R0 = R1 + 1, CPSR의 N, Z, C, V 플래그가 업데이트됨



ADDS 명령어가 영향을 미치는 주요 플래그는 다음과 같습니다:

  • N (Negative flag): 결과가 음수일 때 설정됩니다.

  • Z (Zero flag): 결과가 0일 때 설정됩니다.

  • C (Carry flag): 덧셈에서 자릿수가 넘칠 때 설정됩니다.

  • V (Overflow flag): 덧셈에서 오버플로가 발생할 때 설정됩니다.



MIPS

MIPS에서는 ARM보다는 많은 수의 ADD 연산을 가지고 있습니다. 각 연산은 산술 오버플로(Arithmetic Overflow)를 검사하는지의 유무(U)와 상수 연산을 진행하는지(I) 유무를 통해 4가지 형태로 나뉘어 있습니다.



ADD 명령어는 두 레지스터의 값을 더한 결과를 저장합니다. 이 명령어는 산술 오버플로를 검사하며, 오버플로가 발생하면 예외를 발생시킵니다.


ADD $t0, $t1, $t2 ; $t0 = $t1 + $t2, 오버플로 검사



ADDI 명령어는 레지스터와 즉값(immediate value)을 더한 결과를 저장합니다. 이 명령어도 산술 오버플로를 검사하며, 오버플로가 발생하면 예외를 발생시킵니다.

ADDI $t0, $t1, 10 ; $t0 = $t1 + 10, 오버플로 검사



ADDIU 명령어는 ADDI와 유사하게 동작하지만, 산술 오버플로를 검사하지 않습니다. 즉, 결과 값이 오버플로가 발생해도 예외가 발생하지 않습니다.

ADDIU $t0, $t1, 10 ; $t0 = $t1 + 10, 오버플로 검사 안 함



ADDU 명령어는 ADD와 유사하게 동작하지만, 산술 오버플로를 검사하지 않습니다. 즉, 오버플로가 발생해도 예외가 발생하지 않습니다.

ADDU $t0, $t1, $t2 ; $t0 = $t1 + $t2, 오버플로 검사 안 함



위의 내용에서 볼 수 있듯이, 덧셈뿐만 아니라 대부분의 연산에는 유사한 기능을 하는 다른 Opcode가 존재합니다. 따라서 Shellcode를 작성할 때 Opcode의 Hex 데이터가 입력이 불가능 하다면 비슷한 동작을 하는 다른 Opcode를 찾아 실행할 수 있습니다.



연산을 통한 우회

일반적으로 덧셈, 뺄셈 등의 기본 연산을 통해 우회하는 방법이 많이 사용되지만, XOR을 사용하거나 시프트 연산(LSL 등)을 사용하여 우회할 수 있습니다


산술 연산

덧셈과 뺄셈 연산을 이용하여 입력 불가능한 문자를 우회하는 방법은 일반적으로 가장 많이 사용되는 간단한 방법입니다.

공격자가 0x0a(\n) 값을 Shellcode로 넘길 수 없을 때, 공격자는 덧셈 뺄셈을 이용해서 이를 우회할 수 있습니다.

ADD reg, 0x0a ; 레지스터 값에 0x0a 더함 -> Shellcode에 0x0a가 들어감


ADD reg, 0x10 ; 레지스터 값에 0x10 더함
SUB reg, 0x06 ; 레지스터 값에 0x06 뺌



비트 연산

XOR 연산은 입력 불가능한 문자를 우회하는데에 매우 유용합니다. XOR 연산을 사용하면 입력 불가능한 문자와 XOR 연산을 하여 새로운 값으로 변환할 수 있습니다. 또한, XOR 연산은 원본 값을 복원할 수 있는 특성을 가지고 있습니다.


예를 들어, ASCII 코드 0x00을 우회하기 위해 0x01로 변환하는 XOR 연산은 다음과 같습니다:

XOR reg, 0x01 ; 레지스터 값을 0x01과 XOR 연산하여 변환



시프트 연산

LSL 연산은 비트를 왼쪽으로 이동시키는 연산입니다. 입력 불가능한 문자를 우회하기 위해 사용될 수 있습니다. 특히, 특정 비트 위치의 값을 이동시켜 원하는 ASCII 범위 내의 값으로 변환할 수 있습니다.

예를 들어, 0xf02을 만드는 과정을 3단계로 분리하고 LSL 연산자를 사용하여 아래처럼 작성할 수 있습니다.

MOV reg, 0x0f ; 레지스터에 0xf 넣음
LSL reg, 8 ; 레지스터를 0xf00 으로 시프트 연산
ADD reg, 0x10 ; 레지스터 값 0xf10



ARM용 Thumb 모드

Thumb 모드란?

  • ARM 모드

    32비트 명령어 세트를 사용하여, 각 명령어가 32비트 크기를 가집니다. 이 모드는 복잡한 명령어를 포함할 수 있으며, 많은 기능을 제공합니다.


  • Thumb 모드

    16비트 명령어 세트를 사용하여, 각 명령어가 16비트 크기를 가집니다. 이 모드는 ARM 모드에 비해 코드 크기를 줄이고 메모리 사용을 효율적으로 만듭니다. 일부 기능이 제한되지만 명령어 길이가 짧아지는 장점이 있습니다.



Thumb 모드의 특징

  • 명령어 크기

    Thumb 모드에서는 명령어가 16비트 길이로 고정되어 있어, ARM 모드의 32비트 명령어에 비해 더 작은 크기를 가집니다. 이로 인해 코드 밀도가 높아져, 더 작은 메모리 공간에 더 많은 코드를 저장할 수 있습니다.


  • 명령어 집합

    Thumb 명령어 집합은 ARM 명령어 집합의 부분 집합으로, 일부 복잡한 명령어가 생략되거나 단순화되었습니다. 하지만, 대부분의 일반적인 연산을 수행할 수 있는 충분한 명령어가 포함되어 있습니다.


  • 성능

    Thumb 모드는 코드 밀도를 높이는데 중점을 두지만, 16비트 명령어의 제약으로 인해 일부 연산이 더 많은 사이클을 필요로 할 수 있습니다. 그러나, 캐시 효율성 향상 및 메모리 대역폭 절감으로 인해 실제 성능은 ARM 모드와 유사하거나 더 나을 수 있습니다.


  • 전환

    ARM 프로세서는 ARM 모드와 Thumb 모드 간의 전환을 지원합니다. BX 명령어를 사용하여 두 모드 간에 전환할 수 있습니다. 예를 들어, ARM 모드에서 Thumb 모드로 전환하려면 다음과 같이 할 수 있습니다.


BX R0 ; R0의 하위 비트가 1이면 Thumb 모드로 전환, 0이면 ARM 모드 유지



Thumb 모드의 장점

  • 메모리 절약

    16비트 명령어 사용으로 인해 코드 크기가 줄어들어, 동일한 메모리 공간에 더 많은 코드를 저장할 수 있습니다.


  • 전력 소비 감소

    작은 코드 크기와 효율적인 메모리 사용은 전력 소비를 줄이는 데 도움이 됩니다.


  • 캐시 효율성 증가

    더 작은 명령어 크기는 캐시 히트 비율을 증가시켜 성능을 향상시킵니다.



Thumb 모드를 사용한 우회

Thumb 모드를 사용하면 Opcode와 데이터 부분의 Hex 데이터가 변경되므로, 특정 문자를 우회해야 할 경우 매우 유용하게 활용할 수 있습니다.


특히, Null(0x00) 문자를 우회하기에 Thumb 모드가 매우 편리합니다. 그 이유는 위에서 설명한 명령어의 크기로 인해 생기는 장점인데 16비트로 명령어를 표현해야 하기 때문에 ARM 모드일 때보다 메모리의 여유 공간이 없어 0x00이 들어가기 어렵습니다.


다음은 ARM 모드와 Thumb 모드에서 같은 작업을 수행하는 명령어의 인코딩을 비교한 예시입니다.

  • ARM 모드: movs r0, #1 → 0x0100B0E3 (32비트)

  • Thumb 모드: movs r0, #1 → 0x0120 (16비트)


Thumb 모드를 사용하면 더 짧은 Hex 데이터로 Shellcode를 작성하여 문자를 우회할 수 있습니다. 그러나 구버전(ARMv4 이전)의 ARM에서는 지원되지 않으므로, 대상 시스템의 ARM 버전을 반드시 확인해야 합니다.



☑️ Shellcode에서 사용하는 문자열은 어디에 저장해야 할까?


Data Segment 사용

Data Segment는 초기화된 전역 변수와 정적 변수가 저장되는 Data 영역과 초기화되지 않은 전역 변수와 정적 변수가 저장되는 BSS(Block Started by Symbol) 영역으로 구성됩니다. 이 영역은 일반적으로 프로그램 실행 중 동적으로 할당된 메모리 외의 전역 데이터가 저장되는 곳입니다.



BSS 영역은 프로그램이 시작될 때 자동으로 0으로 초기화됩니다. Shellcode에서 BSS 영역을 통한 임의의 데이터 저장은 가장 많이 사용하는 방법으로, 아래와 같은 장점이 있습니다.

  • 자동 초기화

    BSS 영역의 변수들은 프로그램 시작 시 0으로 초기화되므로, 별도의 초기화 코드가 필요 없습니다.


  • 자유로운 접근

    BSS 영역에 저장된 변수들은 프로그램 전체에서 접근 가능하므로, Shellcode의 여러 부분에서 쉽게 사용할 수 있습니다.


그러나 BSS 영역에 접근하려면 정확한 주소를 알아야 하기 때문에 주소 계산에 대한 추가 코드가 필요할 수 있습니다. 또한, BSS 영역에 접근하더라도 write 함수와 같은 쓰기 함수를 활용해 데이터를 추가로 입력할 수 있는 과정이 필요하므로, 일반적으로 추가 입력이 어려운 임베디드 장치에서는 사용하기 어려울 수 있습니다.


Data 영역은 초기화된 데이터가 들어있습니다. 이 영역에 문자열을 저장하는 것은 다음과 같은 장점이 있습니다.

  • 편리성

    설정 데이터나 고정 값이 들어가 있기 때문에 Shellcode에서 사용하기 편리합니다.


  • 안정성

    프로그램 시작 시 이미 할당되고 초기화되어 있기 때문에, 코드 실행 중에 다른 데이터에 의해 덮어 씌어질 위험이 적습니다.


그러나 Data 영역에 원하는 문자열을 저장하는 로직이 존재해야 하며, 설정 값에 길이 제한이 걸려 있는 경우가 많아 원하는 문자열을 모두 입력하는 것에 어려움이 있을 수 있습니다.



Shellcode 뒤에 저장

Shellcode에서 문자열을 저장하는 또 다른 방법은 PC(Program Counter) 레지스터를 기준으로 Shellcode 뒤에 저장하는 것입니다. 이는 다음과 같은 장점을 제공합니다:

  • 간편한 주소 계산

    PC 레지스터를 기준으로 하면, 상대적인 주소를 이용해 문자열에 접근할 수 있으므로 주소 계산이 간편합니다.


  • 실시간 문자열 변경

    Shellcode 실행 중에 실시간으로 문자열을 변경할 수 있으므로, 더 유연한 Shellcode 작성이 가능합니다.


이 방법의 단점은 Shellcode가 실행되는 환경에 따라 주소가 달라질 수 있다는 것입니다. 즉, 다른 환경에서 동일한 Shellcode를 실행할 때 문자열의 위치가 달라질 수 있으므로, 상대 주소 계산이 정확하지 않을 수 있습니다.




☑️ 마무리

Shellcode 작성은 시스템 보안과 해킹 기법의 중요한 부분입니다. 다양한 제약 조건 속에서 안전하고 효율적인 코드를 작성하는 것은 어렵겠지만 매우 중요한 기술입니다. 이번 글에서는 shellcode의 정의와 사용 조건, 기본적인 작성 방법, 그리고 Null 바이트와 같은 금지 문자를 우회하는 다양한 기법에 대해 알아보았습니다.


특히, ARM, MIPS, x86_64 아키텍처에서 각각의 특성을 고려한 우회 방법을 살펴보며, 실제로 적용할 수 있는 전략들을 배웠습니다. 이러한 기술은 보안 전문가에게 보안 취약점을 파악하고 이를 방어하는 데 큰 도움이 될 것이라고 기대합니다.


해당 내용에 관심이 많았다면 앞으로의 학습 방향으로는 더 복잡한 shellcode 작성법과 보호 기법 우회 등을 공부하는 것을 추천합니다. 예를 들어, 최신 운영 체제의 보안 메커니즘을 우회하는 방법이나, 다양한 네트워크 프로토콜을 이용한 원격 코드 실행 기법 등이 있습니다.


이 글이 보안 전문가들에게 shellcode 작성에 대한 이해를 높이고, 임베디드 개발자가 컴파일 시 보호 기법에 대해 다시 한번 생각해 볼 수 있으면 좋겠습니다.


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.