[PWN] FSOP

Magnolia·2026년 4월 1일

FSOP (File Stream Oriented Programming)

FSOP (File Stream Oriented Programming)FILE 구조체를 조작하여 원하는 코드 실행을 유도하는 libc 내부 객체 기반 익스플로잇 기법이다.

glibc에서 printf, puts, fread, fwrite와 같은 함수들은 단순히 syscall을 호출하는 것이 아니라 FILE 구조체를 통해 동작된다.

예를 들어 stdout은 내부적으로 다음과 같은 구조체이다.

stdout -> FILE 구조체 -> _IO_file_jumps (vtable)

FILE 구조체 안에는 다음이 있다.

FILE
	buffer pointers
    flags
    lock
    vtable -> 함수 포인터 테이블

따라서 공격자가 FILE 구조체를 조작하거나 vtable을 조작하면 libc 내부 함수 호출 과정에서 임의 함수 호출이 발생한다.

이것이 FSOP이다.


FILE 구조체

FILE 구조체란

C에서 사용하는 파일 입출력 함수들은 내부적으로 FILE 구조체를 통해 동작한다.

예를 들어 다음 코드가 있다고 하자.

printf("hello");

이 함수는 바로 write() syscall을 호출하는 것이 아니라 stdout에 해당하는 FILE 객체를 통해 출력된다.

즉 내부적으로는 다음과 같은 흐름이 된다.

printf -> stdout (FILE 구조체) -> buffer -> write syscall

따라서 FILE 구조체는 libc에서 파일 입출력 상태를 관리하는 객체라고 볼 수 있다.


glibc에서의 FILE 구조

glibc에서는 FILE이 다음 두 구조로 구성된다.

struct _IO_FILE
struct _IO_FILE_plus

실제로 프로그램에서 사용되는 것은 IO_FILE_plus이다.

struct _IO_FILE_plus
{
	struct _IO_FILE file;
    const struct _IO_jump_t *vtable;
};

즉 메모리 구조는 다음과 같다.

_IO_FILE 구조체 | vtable pointer

FILE 구조체 주요 필드

_IO_FILE 구조체에는 많은 필드가 있지만 입출력 동작을 이해하는 데 중요한 것은 다음 버퍼 관련 필드들이다.

읽기 버퍼

_IO_read_base
_IO_read_ptr
_IO_read_end

구조

read_base ---- read_ptr ---- read_end
  • read_base : 읽기 버퍼 시작
  • read_ptr : 현재 읽는 위치
  • read_end : 읽기 버퍼 끝

즉 프로그램이 파일을 읽을 때 데이터는 먼저 버퍼에 들어오고 read_ptr이 이동하면서 데이터를 소비한다.

쓰기 버퍼

_IO_write_base
_IO_write_ptr
_IO_write_end

구조

write_base ---- write_ptr ---- write_end
  • write_base : 쓰기 버퍼 시작
  • write_ptr : 현재 쓰는 위치
  • write_end : 버퍼 끝

예를 들어

printf("hello");

를 호출하면

buffer: hello____
             ^
          write_ptr

처럼 버퍼에 먼저 저장된다.
버퍼가 가득 차거나 flush가 발생하면 실제 syscall이 수행된다.


vtable

vtable이란

vtable은 virtual function table의 약자로 함수 포인터 테이블이다.

FILE 구조체 뒤에는 다음 구조가 존재한다.

struct _IO_jump_t

이 구조에는 FILE 동작을 위한 함수들이 들어 있다.


vtable 구조

대표적인 항목들은 다음과 같다.

struct _IO_jump_t
{
    void (*__finish)(_IO_FILE *, int);
    void (*__overflow)(_IO_FILE *, int);
    void (*__underflow)(_IO_FILE *);
    size_t (*__xsputn)(_IO_FILE *, const void *, size_t);
    size_t (*__xsgetn)(_IO_FILE *, void *, size_t);
    int (*__seekoff)(_IO_FILE *, long, int, int);
    int (*__close)(_IO_FILE *);
};

즉 FILE 관련 동작들이 모두 함수 포인터로 정의되어 있다.

  • __overflow : 버퍼가 가득 찼을 때 호출
  • __underflow : 읽기 버퍼가 비었을 때 호출
  • __xsputn : 여러 바이트 쓰기
  • __xsgetn : 여러 바이트 읽기

vtable 호출 방식

glibc 내부 코드에서는 다음과 같이 호출된다.

예를 들어 _IO_OVERFLOW 매크로는 다음과 같다.

fp->vtable->__overflow(fp)

즉 실제 동작은 call [vtable + offset]과 같다.


실제 메모리 구조

stdout을 예로 들면 다음과 같다.

stdout
	FILE 구조체
	buffer 정보
    vtable pointer
       ↓
   _IO_file_jumps
       ↓
   함수 포인터 테이블

즉 FILE 객체는 버퍼 상태와 함수 포인터 테이블을 함께 가지고 있는 구조체이다.


FSOP의 기본 원리

glibc는 출력을 처리할 때 FILE 객체 내부 상태를 보고 그 FILE 객체에 연결된 vtable을 따라가서 적절한 함수를 호출한다.

따라서 FILE 구조체가 공격자에 의해 조작된 상태라면 glibc는 공격자가 설정한 vtable을 사용하게 되고 그 결과 의도하지 않은 함수 호출이 발생할 수 있다.

이러한 이유로 FILE 구조체는 단순한 데이터 구조가 아니라 제어 흐름에 영향을 줄 수 있는 객체가 된다.


_IO_list_all

glibc는 열린 모든 FILE 객체를 연결 리스트 형태로 관리한다.

이 리스트의 시작점이 되는 전역 변수가 바로 _IO_list_all이다.

개념적으로 보면 다음과 같은 구조이다.

_IO_list_all -> stdout -> stderr -> file1 -> file2

각 FILE 구조체에는 _chain 이라는 포인터가 존재하며, 이를 통해 다음 FILE 객체를 가리킨다. 즉 FILE 구조체 내부에는 다음과 같은 연결 구조가 존재한다.

FILE
	buffer pointers
	flags
	lock
    _chain -> 다음 FILE
    vtable

glibc는 파일 관련 작업을 수행할 때 _IO_list_all을 시작점으로 하여 모든 FILE 객체를 순회한다.


_IO_flush_all_lockp

프로그램이 종료되거나 fflush와 같은 동작이 발생하면 glibc는 내부적으로 모든 FILE 객체를 순회하며 flush 작업을 수행한다.

이때 호출되는 함수가 _IO_flush_all_lockp()이다.

개념적으로 보면 다음과 같은 코드와 유사하다.

for (fp = _IO_list_all; fp; fp = fp->_chain) {
    if (fp has pending output)
        _IO_OVERFLOW(fp, EOF);
}

_IO_list_all을 시작으로 연결된 모든 FILE 객체를 순회하며 출력 버퍼가 남아 있는 FILE에 대해 flush를 수행한다.

이 과정에서 _IO_OVERFLOW()가 호출된다.


FSOP 공격 흐름

지금까지 본 내용들은 정리하면 다음과 같다.
1. FILE 구조체에는 함수 포인터 테이블(vtable)이 존재한다.
2. glibc는 FILE 객체를 _IO_list_all을 통해 관리한다.
3. 프로그램 종료 시 _IO_flush_all_lockp()가 실행되며 모든 FILE 객체를 순회한다.
4. flush 과정에서 _IO_OVERFLOW()가 호출된다.

따라서 공격자가 다음 조건을 만족시키면 코드 실행이 가능해진다.

  • fake FILE 구조체를 만들 수 있다
  • _IO_list_all을 fake FILE로 덮을 수 있다
  • fake FILE의 vtable을 공격자가 제어할 수 있다

이 상태에서 프로그램이 종료되거나 flush가 발생하면 다음과 같은 흐름이 만들어진다.

exit
_IO_flush_all_lockp
fp = _IO_list_all
_IO_OVERFLOW(fp)
fp->vtable->__overflow
attacker controlled function

즉 glibc는 공격자가 만든 fake FILE 구조체를 정상적인 FILE 객체라고 생각하고 처리하게 된다.

그 결과 vtable에 설정된 함수 포인터가 호출되면서 공격자가 원하는 함수가 실행된다.


Fake FILE 구조체

FSOP 공격에서는 공격자가 직접 FILE 구조체와 유사한 구조를 만들어야 한다.

예를 들어 다음과 같은 구조를 만들 수 있다.

fake_FILE
flags
buffer pointers
...
vtable pointer -> fake vtable

그리고 fake vtable에는 공격자가 원하는 함수 주소를 넣는다.

fake_vtable
__overflow -> system

이 상태에서 _IO_OVERFLOW(fp)가 호출되면 다음과 같은 상황이 발생한다.

system(fp)

따라서 FILE 구조체 내부 값들을 적절히 조작하면 원하는 인자를 전달하는 것도 가능하다.


정리

FSOP 공격의 핵심은 다음과 같다.
1. fake FILE 구조체 생성
2. _IO_list_all을 fake FILE로 overwrite
3. fake vtable 생성
4. _IO_OVERFLOW() 호출 유도
5. 임의 함수 실행
즉 FILE 구조체를 이용해 glibc 내부 함수 호출 흐름을 hijack하는 공격이다.

0개의 댓글