From 447ac96a4426f3719fa4c3bab53bb103f92ccbf4 Mon Sep 17 00:00:00 2001 From: Peace-Maker Date: Wed, 12 Feb 2025 17:24:52 +0100 Subject: [PATCH 1/2] Decode `_IO_*` flags in FileStructure member The `_flags` and `_flags2` bitmasks are decoded to a readable string when printing the struct. Next to setting the flags as an integer like before, you can set individual bits now and check set bits. ``` fileStr = FileStructure(null=0xdeadbeeef) fileStr.flags = 0xfbad1807 fileStr.flags = IO_flags._IO_MAGIC | IO_flags._IO_USER_BUF | IO_flags._IO_UNBUFFERED fileStr.flags._IO_USER_BUF = 1 ``` Printing the FileStructure shows the flags in human readable form too ``` { flags: 0xfbad0401 (_IO_USER_BUF | _IO_UNBUFFERED | _IO_MAGIC) _IO_read_ptr: 0x0 ... ``` --- docs/source/filepointer.rst | 1 + pwnlib/filepointer.py | 171 +++++++++++++++++++++++++++++++----- 2 files changed, 149 insertions(+), 23 deletions(-) diff --git a/docs/source/filepointer.rst b/docs/source/filepointer.rst index 10ac45712..dde3d49c8 100644 --- a/docs/source/filepointer.rst +++ b/docs/source/filepointer.rst @@ -1,6 +1,7 @@ .. testsetup:: * from pwnlib.filepointer import * + from pwnlib.filepointer import _update_var :mod:`pwnlib.filepointer` --- `FILE*` structure exploitation ============================================================ diff --git a/pwnlib/filepointer.py b/pwnlib/filepointer.py index 8b05781e3..b0ebd9476 100644 --- a/pwnlib/filepointer.py +++ b/pwnlib/filepointer.py @@ -25,10 +25,12 @@ from __future__ import absolute_import from __future__ import division +import ctypes + 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.util.packing import pack, unpack log = getLogger(__name__) @@ -62,14 +64,18 @@ 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} + 25:{name:'_freeres_list',size:length}, + 26:{name:'_freeres_buf',size:length}, + 27:{name:'_pad5',size:length}, + 28:{name:'_mode',size:4}, + 29:{name:'_unused2',size:length}, + 30:{name:'vtable',size:length} } del name, size, length -def update_var(l): +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. @@ -82,8 +88,8 @@ def update_var(l): 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} + >>> _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, '_freeres_list': 8, '_freeres_buf': 8, '_pad5': 8, '_mode': 4, '_unused2': 20, 'vtable': 8} """ var={} for i in variables: @@ -92,11 +98,109 @@ def update_var(l): if var[i]<=0: var[i]+=l if l==4: - var['unknown2']=56 + var['_unused2']=40 else: - var['unknown2']=48 + var['_unused2']=20 return var +class IO_flags: + _IO_MAGIC = 0xFBAD0000 # Magic number + _IO_MAGIC_MASK = 0xFFFF0000 + _IO_USER_BUF = 0x0001 # Don't deallocate buffer on close. + _IO_UNBUFFERED = 0x0002 + _IO_NO_READS = 0x0004 # Reading not allowed. + _IO_NO_WRITES = 0x0008 # Writing not allowed. + _IO_EOF_SEEN = 0x0010 + _IO_ERR_SEEN = 0x0020 + _IO_DELETE_DONT_CLOSE = 0x0040 # Don't call close(_fileno) on close. + _IO_LINKED = 0x0080 # In the list of all open files. + _IO_IN_BACKUP = 0x0100 + _IO_LINE_BUF = 0x0200 + _IO_TIED_PUT_GET = 0x0400 # Put and get pointer move in unison. + _IO_CURRENTLY_PUTTING = 0x0800 + _IO_IS_APPENDING = 0x1000 + _IO_IS_FILEBUF = 0x2000 + _IO_USER_LOCK = 0x8000 + +class IO_flags2: + _IO_FLAGS2_MMAP = 1 + _IO_FLAGS2_NOTCANCEL = 2 + _IO_FLAGS2_USER_WBUF = 8 + _IO_FLAGS2_NOCLOSE = 32 + _IO_FLAGS2_CLOEXEC = 64 + _IO_FLAGS2_NEED_LOCK = 128 + +class _FlagsUnionBase(ctypes.Union): + def __getattr__(self, name): + if any(name == field[0] for field in self._flags_bits._fields_): + return getattr(self._flags_bits, name) + return super().__getattr__(name) + + def __setattr__(self, name, value): + if any(name == field[0] for field in self._flags_bits._fields_): + setattr(self._flags_bits, name, value) + return super().__setattr__(name, value) + + def __int__(self): + return int(self._flags) + + def __str__(self): + return "{:#x} ({})".format(self._flags, self._flags_bits) + +# https://elixir.bootlin.com/glibc/glibc-2.41/source/libio/libio.h#L66 +class _IOFileFlags_bits(ctypes.LittleEndianStructure): + _pack_ = 1 + _fields_ = [ + ("_IO_USER_BUF", ctypes.c_uint8, 1), # Don't deallocate buffer on close. + ("_IO_UNBUFFERED", ctypes.c_uint8, 1), + ("_IO_NO_READS", ctypes.c_uint8, 1), # Reading not allowed. + ("_IO_NO_WRITES", ctypes.c_uint8, 1), # Writing not allowed. + ("_IO_EOF_SEEN", ctypes.c_uint8, 1), + ("_IO_ERR_SEEN", ctypes.c_uint8, 1), + ("_IO_DELETE_DONT_CLOSE", ctypes.c_uint8, 1), # Don't call close(_fileno) on close. + ("_IO_LINKED", ctypes.c_uint8, 1), # In the list of all open files. + ("_IO_IN_BACKUP", ctypes.c_uint8, 1), + ("_IO_LINE_BUF", ctypes.c_uint8, 1), + ("_IO_TIED_PUT_GET", ctypes.c_uint8, 1), # Put and get pointer move in unison. + ("_IO_CURRENTLY_PUTTING", ctypes.c_uint8, 1), + ("_IO_IS_APPENDING", ctypes.c_uint8, 1), + ("_IO_IS_FILEBUF", ctypes.c_uint8, 1), + ("_IO_BAD_SEEN__UNUSED", ctypes.c_uint8, 1), # No longer used, reserved for compat. + ("_IO_USER_LOCK", ctypes.c_uint8, 1), + ("_IO_MAGIC", ctypes.c_uint16, 16), # Magic number 0xFBAD0000. + ] + + def __str__(self): + return " | ".join(name for name, _, _ in self._fields_ if getattr(self, name)) + +class _IOFileFlags(_FlagsUnionBase): + _fields_ = [ + ("_flags", ctypes.c_uint64), + ("_flags_bits", _IOFileFlags_bits), + ] + + +# https://elixir.bootlin.com/glibc/glibc-2.41/source/libio/libio.h#L85 +class _IOFileFlags2_bits(ctypes.LittleEndianStructure): + _pack_ = 1 + _fields_ = [ + ("_IO_FLAGS2_MMAP", ctypes.c_uint8, 1), + ("_IO_FLAGS2_NOTCANCEL", ctypes.c_uint8, 1), + ("_IO_FLAGS2_USER_WBUF", ctypes.c_uint8, 1), + ("_IO_FLAGS2_NOCLOSE", ctypes.c_uint8, 1), + ("_IO_FLAGS2_CLOEXEC", ctypes.c_uint8, 1), + ("_IO_FLAGS2_NEED_LOCK", ctypes.c_uint8, 1), + ] + + def __str__(self): + return " | ".join(name for name, _, _ in self._fields_ if getattr(self, name)) + +class _IOFileFlags2(_FlagsUnionBase): + _fields_ = [ + ("_flags", ctypes.c_uint64), + ("_flags_bits", _IOFileFlags2_bits), + ] + @python_2_bytes_compatible class FileStructure(object): @@ -113,7 +217,8 @@ class FileStructure(object): >>> context.clear(arch='amd64') >>> fileStr = FileStructure(null=0xdeadbeeef) - >>> fileStr.flags = 0xfbad1807 + >>> fileStr.flags = 0xfbad1807 # or use flags by name: + >>> fileStr.flags = IO_flags._IO_MAGIC | IO_flags._IO_USER_BUF | IO_flags._IO_UNBUFFERED | IO_flags._IO_NO_READS | IO_flags._IO_CURRENTLY_PUTTING | IO_flags._IO_IS_APPENDING >>> fileStr._IO_buf_base = 0xcafebabe >>> fileStr._IO_buf_end = 0xfacef00d >>> payload = bytes(fileStr) @@ -128,8 +233,10 @@ class FileStructure(object): 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 = IO_flags._IO_MAGIC | IO_flags._IO_USER_BUF + >>> q.flags._IO_TIED_PUT_GET = 1 >>> q - { flags: 0x0 + { flags: 0xfbad0401 (_IO_USER_BUF | _IO_TIED_PUT_GET | _IO_MAGIC) _IO_read_ptr: 0x0 _IO_read_end: 0x0 _IO_read_base: 0x0 @@ -144,7 +251,7 @@ class FileStructure(object): markers: 0x0 chain: 0x0 fileno: 0x0 - _flags2: 0x0 + _flags2: 0x0 () _old_offset: 0xffffffffffffffff _cur_column: 0x0 _vtable_offset: 0x0 @@ -154,7 +261,11 @@ class FileStructure(object): _offset: 0xffffffffffffffff _codecvt: 0x0 _wide_data: 0xdeadbeef - unknown2: 0x0 + _freeres_list: 0x0 + _freeres_buf: 0x0 + _pad5: 0x0 + _mode: 0x0 + _unused2: 0x0 vtable: 0x0} """ @@ -164,19 +275,29 @@ class FileStructure(object): 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.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) + if hasattr(self, item) and isinstance(getattr(self, item), _FlagsUnionBase): + if isinstance(value, (bytes, bytearray)): + getattr(self, item)._flags = unpack(value.ljust(context.bytes, b'\x00')) + else: + getattr(self, item)._flags = value + else: + 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))) + val = getattr(self, i) + if isinstance(val, int): + structure.append(" %s: %#x" % (i, val)) + else: + structure.append(" %s: %s" % (i, val)) return "{"+ "\n".join(structure)+"}" def __len__(self): @@ -189,7 +310,7 @@ def __bytes__(self): structure += getattr(self, val).ljust(context.bytes, b'\x00') else: if self.length[val] > 0: - structure += pack(getattr(self, val), self.length[val]*8) + structure += pack(int(getattr(self, val)), self.length[val]*8) return structure def struntil(self,v): @@ -217,13 +338,13 @@ def struntil(self,v): 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) + structure += pack(int(getattr(self, val)), self.length[val]*8) if val == v: break return structure[:-1] def setdefault(self,null): - self.flags=0 + self.flags=_IOFileFlags() self._IO_read_ptr=0 self._IO_read_end=0 self._IO_read_base=0 @@ -238,7 +359,7 @@ def setdefault(self,null): self.markers=0 self.chain=0 self.fileno=0 - self._flags2=0 + self._flags2=_IOFileFlags2() self._old_offset=0 self._cur_column=0 self._vtable_offset=0 @@ -248,7 +369,11 @@ def setdefault(self,null): self._offset=0xffffffffffffffff self._codecvt=0 self._wide_data=null - self.unknown2=0 + self._freeres_list=0 + self._freeres_buf=0 + self._pad5=0 + self._mode=0 + self._unused2=0 self.vtable=0 def write(self,addr=0,size=0): @@ -271,8 +396,8 @@ def write(self,addr=0,size=0): >>> 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.flags._IO_NO_WRITES = 0 + self.flags._IO_CURRENTLY_PUTTING = 1 self._IO_write_base = addr self._IO_write_ptr = addr+size self._IO_read_end = addr @@ -299,7 +424,7 @@ def read(self,addr=0,size=0): >>> 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.flags._IO_NO_READS = 0 self._IO_read_base = 0 self._IO_read_ptr = 0 self._IO_buf_base = addr From a7274b5c5453d2408556f8bd87dc9aa528bed7ba Mon Sep 17 00:00:00 2001 From: Peace-Maker Date: Wed, 12 Feb 2025 18:33:43 +0100 Subject: [PATCH 2/2] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f63c1ddc..027242f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ The table below shows which release corresponds to each branch, and what date th - [#2527][2527] Allow setting debugger path via `context.gdb_binary` - [#2530][2530] Do NOT error when passing directory arguments in `checksec` commandline tool. - [#2529][2529] Add LoongArch64 support +- [#2542][2542] Decode `_IO_*` flags in `FileStructure` member [2519]: https://github.com/Gallopsled/pwntools/pull/2519 [2507]: https://github.com/Gallopsled/pwntools/pull/2507 @@ -93,6 +94,7 @@ The table below shows which release corresponds to each branch, and what date th [2527]: https://github.com/Gallopsled/pwntools/pull/2527 [2530]: https://github.com/Gallopsled/pwntools/pull/2530 [2529]: https://github.com/Gallopsled/pwntools/pull/2529 +[2542]: https://github.com/Gallopsled/pwntools/pull/2542 ## 4.15.0 (`beta`)