취약점분석/Pwnable

[Pwnable] FSOP - 1 (_IO_FILE)

poiri3r 2025. 11. 25. 21:23

오늘 포스팅에서는 FSOP의 기초에 대해 알아보겠습니다.

 

FSOP는 File Stream Oriented Programming의 약자로 file struct 구조를 이용한 방식입니다.

FSOP에 대한 자료가 너무 방대하고 익스플로잇 방법이 너무 많아서 첫번째 포스팅에서는 전체적인 틀을 잡고 가도록 하겠습니다.

 

파일 스트림이란 리눅스의 glib에서 파일을 다룰 때 사용하는 _IO_FILE 객체를 의미합니다.

저희가 C언어에서 파일을 열때

FILE *fp=fopen("./test.txt", 'r')

 

과 같은 코드를 사용합니다.

이때 fopen함수를 통해 반환되는 포인터가 IO_FILE 포인터가 됩니다. 이때 fopen()으로 연 파일들은 내부적으로 malloc(sizeof(_IO_FILE_plus))를 호출하여 동적 할당합니다.

또한 stdin,stdout,stderr와 같은 파일들은 프로그램이 켜질때 glibc가 로딩되면서 자동으로 Libc의 데이터 영역에 생성됩니다.

해당 파일들은 오프셋이 고정이므로 Libc의 베이스 주소를 알면 공격이 가능합니다.

 

_IO_FILE의 구조체의 핵심 멤버는 다음과 같습니다.

1. 상태 플래그(_flags) - Offset 0x00

2. 버퍼 포인터 6개

3. 체인과 디스크립터

4. vtable (_IO_FILE_plus)

 

// test.c
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 1. stdout 사용 (printf)
    printf("Checking stdout...\n");

    // 2. 파일 열기 (Heap에 생성됨)
    FILE *fp = fopen("/dev/null", "w");
   
    // 3. 파일에 쓰기 (버퍼 포인터 이동시키기 위함)
    fprintf(fp, "A");

    // 여기서 멈춰서 디버깅 할 예정
    getchar();
   
    fclose(fp);
    return 0;
}

 

디버깅을 위해 다음과 같은 코드를 작성하고 컴파일하겠습니다.

디버깅에서 변수 이름과 구조체 정보를 보기 위해서 -g 옵션을 넣어야합니다.

gcc -o test test.c -g

 

gdb를 실행해보겠습니다.

 

gdb -q ./test

 

로 실행했습니다.

 

b main으로 메인에 브레이크포인트를 걸고 시작했습니다.

그 뒤 다음 명령어를 입력해줍니다.

p _IO_2_1_stdout_ 
혹은
p *stdout

stdout 구조체
flag값 16진수로 출력

해당 출력 값들을 살펴보겠습니다.

1. flags

_flags = 0xfbad2084에서 fbad는 magic number로 정상 파일 객체임을 나타냅니다.

해당 매직 넘버는 _IO_str_overflow 같은 함수를 쓸 때 0xfbad 등으로 조작해서 보안 검사를 통과해야 합니다.

 

2. Buffer Pointer

_IO_read_base = 0x0
_IO_read_ptr  = 0x0  // 현재 읽는 위치
_IO_read_end  = 0x0

위의 세 줄은 읽기용으로 정보 유출시 사용합니다.

 

_IO_write_base = 0x0
_IO_write_ptr  = 0x0  // 현재 쓰는 위치
_IO_write_end  = 0x0  // 버퍼의 끝

위의 세 줄은 FSOP 실행 트리거로, write_ptr은 현재 쓰는 위치, write_end는 버퍼의 끝을 나타냅니다. write_ptr로 현재 쓰는 위치를 write_end와 같거나 넘치게 조작하면 overflow함수를 호출하게 되는데 이 또한 공격 포인트가 될 수 있습니다.

 

_IO_buf_base = 0x0    // 실제 힙 메모리 시작 주소
_IO_buf_end  = 0x0

해당 값은 0x0으로 채워져있지만 printf가 한 번 실행되면 힙 주소로 채워집니다.

3. 체인과 디스크립터

_chain  = 0x7ffff7fa4aa0  // 다음 파일(stdin 등)을 가리킴
_fileno = 0x1             // 1번 = stdout (표준 출력)

 

_chain은 프로그램 종료 시 호출되는 공격 루트고, _fileno는 vtable 1번으로 stdout을 가르킵니다.

 

stdin이랑 stderr도 확인해보겠습니다.

stdin은 _chain이 0x0이 들어가있는 점 빼면 큰 차이 없이 비슷합니다.

 

n을 넣어서 printf를 실행시키고, 버퍼 포인터 변화를 확인해보겠습니다.

일단 먼저 _IO_... 포인터들의 값에 힙 주소가 할당된 것을 확인할 수 있습니다.

다음은 _mode값입니다. _mode값이 -1인 경우 char(1바이트) 단위 문자열을 다룬다는 뜻인데, 최신 공격 기법인 House of Apple의 경우 _mode를 강제로 1로 바꾼뒤 _wide_data 구조체를 호출하게 유도한 뒤 익스플로잇을 합니다.

 

이번엔 vtable의 포인터 값을 바꾼 뒤 결과를 확인해보겠습니다.

set ((struct _IO_FILE_plus *) &_IO_2_1_stdout_)->vtable = 0x4141414141414141
p /x ((struct _IO_FILE_plus *) &_IO_2_1_stdout_)->vtable

이건 프로그램이 vtable check에 걸려 정상 vtable 주소가 아닌 걸 확인하고, abort()함수를 호출해 프로그램을 강제 종료했습니다.

이건 glibc가 최신 버전(<2.23)에서는 vtable 바꿔치기를 방지하는 보안 기법이 적용되었기 때문에 발생합니다.

 

이번엔 vtable에 414141414141이 아닌 정상적인 vtable 주소를 찾아서 넣어보겠습니다.

_IO_wfile_jumps라는 vtable 주소를 사용해서 넣어주었습니다.

   // 여기서 멈춰서 디버깅 할 예정
    getchar();

해당 포인트에서 입력이 멈췄고 사용자가 입력을 하니 정상적으로 프로그램이 종료되었습니다.

 

성공적으로 방어기법을 우회했습니다. 여기서 쉘을 따려면 _wide_data도 덮어쓰고 좀 더 복잡한 과정을 거쳐야하기 때문에 다음 포스팅때 써보도록 하겠습니다.

 

헷갈릴만한 포인트를 적어보자면 _IO_FILE이랑 _IO_FILE_plus랑은 다른 구조체입니다.

파일 구조체 할당을 요청하면 힙에서 _IO_FILE에 vtable 포인터를 추가로 붙인 _IO_FILE_plus를 할당합니다.

저희가 아까 출력해서 본 구조체는 정확하게는 _IO_FILE_plus 구조체입니다

struct _IO_FILE_plus {
    struct _IO_FILE file;            
    const struct _IO_jump_t *vtable; // <-여기에 vtable 포인터가 추가됨
};

 

다음으로 glibc 버전에 따른 공격 기법의 차이를 분류해보겠습니다.

먼저 구버전인 glibc버전에서는 vtable을 아예 저희가 만든 table로 바꿨습니다.

하지만 해당 2.23 이후의 버전에서는 vtable의 주소가 유효한지 검사하는 보안 로직(vtable check)이 추가되었기 때문에 이런식의 공격이 불가능합니다.

해당 vtable check은 포인터가 libc의 읽기 전용 섹션(__libc_IO_vtables) 안에 있는지 검색합니다.

따라서 최근 버전에서는 vtable을 검증을 통과하는 정품 vtable 주소를 넣은 뒤 (_IO_wfile_jumps) 그 주소가 참조하는 데이터(_wide_data)를 변경하여 익스플로잇을 시도합니다.

 

여기까지 FSOP의 기초인 _IO_FILE에 대해 살펴봤습니다.

다음 포스팅에서는 FSOP의 응용과 실제 공격 기법들을 libc버전에 따라 깊게 파헤쳐보도록 하겠습니다.

읽어주셔서 감사합니다~