DD Oriented Programming
Prologue
It all started when I got fed up with the typical Linux process injection PoCs that relied on either ptrace() or LD_PRELOAD. I wanted something different, something a bit more intriguing—a more generic way to inject arbitrary code by using a malicious shared object library. I realized that the procfs memory-related entries with r/w permissions left an open door to quite literally pwn and own the process, so I set out to prove it. This PoC was born on an Ubuntu 22.04 x64 system, and it’s here to show just how much you can do when you get creative with procfs.
Motivation
- It’s cool and I love pwn.
- Could allow bypassing some process specific anti-debugging concepts.
Requirements & Components
This PoC relies on
procfsentries that include data that indicates the process’s current state and mappings:1 2
/proc/<pid>/maps /proc/<pid>/syscall
And the ability to r/w from/to memory via:
1
/proc/<pid>/mem
But what is the
procfsand what are the prementioned process specific entries?The PoC’s Caveats
It’s worth mentioning that since 2012 the Linux kernel added a configurable security module named ‘yama’ which could manage or prevent the access to relevant
procfsentries and the debugging of processes via theptracesyscall. However, if you’re running as therootuser you can run the following command to disable this potential system-wide restriction:1
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
Other than that, there should be no more system-wide mitigations in standard Linux systems.
The Glorious Linux procfs
Quoted from
man proc:1 2
The proc filesystem is a pseudo-filesystem which provides an interface to kernel data structures. It is commonly mounted at /proc.... Most of the files in the proc filesystem are read-only, but some files are writable, allowing kernel variables to be changed.
We mostly care about the
/proc/<pid>/subdirectories which are defined as:1
... subdirectories exposing information about the process with the corresponding process ID.
So… Simply put, files under
/proc/<pid>/are virtual files that allow us to determine a process’s state in terms of its execution and the resources that it’s currently utilizing (according to the process id that is specified at theprocfsvirtual file path). Theprocfspseudo-filesystem is great for process debugging purposes at runtime, but it can be also abused to allow process exploitation and that will become very clear later on.The Juicy Entries
When browsing the specific entries that the
procfshas to offer I had two main goals in mind:- Identifying a process’s current state
- Reading and writing to/from a process’s memory
The latter goal is fairly easy to achieve and its relevant procfs entry is commonly known as /proc/<pid>/mem, which as its name suggests, it’s an interface to a process’s memory that depending on the system’s configuration allows reading and or writing to the process’s memory. The first goal however is a bit more trickier to achieve, more specifically determining the process’s current state because for determining the process’s mapped virtual memory, we have an entry that is used often during process debugging which is /proc/<pid>/maps.
1
2
3
4
5
6
7
8
9
10
11
12
13
# Example /proc/<pid>/maps contents of a /bin/sh process:
mapped range perms symbol
6322c9120000-6322c9124000 r--p 00000000 103:02 36700809 /usr/bin/dash
...
6322c9140000-6322c9142000 rw-p 00000000 00:00 0
6322ca5ef000-6322ca610000 rw-p 00000000 00:00 0 [heap]
7acf3b800000-7acf3b828000 r--p 00000000 103:02 36700281 /usr/lib/x86_64-linux-gnu/libc.so.6
...
7acf3bb98000-7acf3bb9a000 r--p 00000000 103:02 36700260 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fff64725000-7fff64746000 rw-p 00000000 00:00 0 [stack]
7fff647f0000-7fff647f4000 r--p 00000000 00:00 0 [vvar]
7fff647f4000-7fff647f6000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
As can be seen in the example /proc/pid/maps output, we can very comfortably define the process’s memory mappings according to their size, memory permissions and associated executable/shared object path or purpose.
But how do we determine the process’s current execution state? The process’s register values, current instruction being executed, etc. are critical to actually be able to control the process’s flow and to restore the process’s execution later on. During my research I discovered the virtual file: /proc/<pid>/syscall, which was introduced in Linux 2.6.27 (2017) and as quoted in man proc it exposes the:
1
...system call number and argument registers for the system call currently being executed by the process, followed by the values of the stack pointer and program counter registers.
Which is EXACTLY what I was looking for.
1
2
# Example output from /proc/<pid>/syscall in an x64 system
0 0x3 0x749a81074000 0x20000 0x22 0x749a81073010 0x749a81073010 0x7fff6b43c3b8 0x749a80f147e2
Exploitation Flow
Now that we know all of the essential procfs entries, we need to compile the abilities that we gain from them into a complete exploit. I went for a memory corruption exploit that could be used on modern ELF binaries, meaning it’s able to bypass common modern binary exploitation mitigations:
- NX enabled (no executable stack)
- Full RELRO (GOT & PLT protection)
- ASLR (Address Space Layout Randomization, prevents the usage of hardcoded addresses)
Eventually, I went for a ROP chain that’s executed by corrupting the stack after enumerating gadgets in mapped executable virtual memory and finding the current value of the RSP register in the target process. We’re aiming to execute dlopen as an easy way to execute complex logic via the malicious so instead of a fragile ROP chain.
Exploitation Overview
- Parsing the
/proc/<pid>/syscallmaps into registers. - Finding a ‘cave’ in the mapped BSS segments (there’s always a cave due to page aligned mappings of different segments) - we use that ‘cave’ to write our malicious so path for the later
dlopencall, this phase is technically optional as the so path could be written directly onto the stack and then referred to via a stack pointer, but I thought that the BSS approach would be easier to implement. - Enumerating mapped segments with execution permissions for gadgets using
/proc/<pid>/mapsfor mappings and their permissions and/proc/<pid>/memto scan the relevant mappings for specific gadget signatures. - Finding
dlopen’s address by calculating its address usinglibc’s base address and the offset specified indlopen’s ELF symbol. - Writing the ROP chain to the stack using the
RSPobtained from/proc/<pid>/syscall, and/proc/<pid>/memfor the actual stack content manipulation. - Trigger the execution explicitly or wait for the syscall to finish its execution.
ROP Chain Gadgets (dlopen)
1
2
3
4
5
6
7
8
9
10
rop_chain = (
p64(found_gadgets["nop"]) + # aligning the stack
p64(found_gadgets["pop_rax"]) + # rax = dlopen address
p64(dlopen_addr) +
p64(found_gadgets["pop_rdi"]) + # rdi = pointer to the so_path
p64(address) +
p64(found_gadgets["pop_rsi"]) + # rsi = RTLD_LAZY / 1
p64(os.RTLD_LAZY) +
p64(found_gadgets["jmp_rax"]) + # executing dlopen with our args
)
Test Malicious SO Contents
1
2
3
4
5
6
7
8
#include <stdio.h>
#include <stdlib.h>
__attribute__((constructor)) // executed when the library is loaded into memory
void init_library() {
printf("Library loaded: Hello from the constructor!\n"); // direct output
system("date >> /tmp/win"); // blind verification
}
The PoC’s PoC
TODO
- Restoring the BSS cave with null bytes.
- Restoring the process’s execution state via the SO’s logic.
- Adding support for multiple architectures.
- Add logic that checks if a stack alignment is necessary or not (currently aligning the stack using a NOP gadget).
- Explore thread based entry points for network processes like web servers.
- Receive the malicious SO over a socket (to avoid writing it to the disk).
- Use a more accurate stack pointer to override the stack return address more precisely.
Epilogue
I hope you enjoyed this read—I’d love to hear your thoughts on what could be improved. Just for the record, I’m not responsible for any unauthorized usage of this technique. That said, feel free to dig into the source code. I’ve put in the effort to make it as readable as possible, and I hope you find it as clear as I intended it to be.