Tech News
← Back to articles

Linux Internals: How /proc/self/mem writes to unwritable memory (2021)

read original related products more articles

Introduction

An obscure quirk of the /proc/*/mem pseudofile is its “punch through” semantics. Writes performed through this file will succeed even if the destination virtual memory is marked unwritable. In fact, this behavior is intentional and actively used by projects such as the Julia JIT compiler and rr debugger.

This behavior raises some questions: Is privileged code subject to virtual memory permissions? In general, to what degree can the hardware inhibit kernel memory access?

By exploring these questions , this article will shed light on the nuanced relationship between an operating system and the hardware it runs on. We’ll examine the constraints the CPU can impose on the kernel, and how the kernel can bypass these constraints.

Patching libc with /proc/self/mem

So what do these punch-through semantics look like?

Consider this code:

#include #include #include /* Write @len bytes at @ptr to @addr in this address space using * /proc/self/mem. */ void memwrite(void *addr, char *ptr, size_t len) { std::ofstream ff("/proc/self/mem"); ff.seekp(reinterpret_cast(addr)); ff.write(ptr, len); ff.flush(); } int main(int argc, char **argv) { // Map an unwritable page. (read-only) auto mymap = (int *)mmap(NULL, 0x9000, PROT_READ, // <<<<<<<<<<<<<<<<<<<<< READ ONLY <<<<<<<< MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (mymap == MAP_FAILED) { std::cout << "FAILED

"; return 1; } std::cout << "Allocated PROT_READ only memory: " << mymap << "

"; getchar(); // Try to write to the unwritable page. memwrite(mymap, "\x40\x41\x41\x41", 4); std::cout << "did mymap[0] = 0x41414140 via proc self mem.."; getchar(); std::cout << "mymap[0] = 0x" << std::hex << mymap[0] << "

... continue reading