[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-80563":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":15,"stars7d":15,"stars30d":16,"stars90d":14,"forks30d":14,"starsTrendScore":17,"compositeScore":18,"rankGlobal":9,"rankLanguage":9,"license":9,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":19,"hasPages":19,"topics":21,"createdAt":9,"pushedAt":9,"updatedAt":22,"readmeContent":23,"aiSummary":24,"trendingCount":14,"starSnapshotCount":14,"syncStatus":16,"lastSyncTime":25,"discoverSource":26},80563,"page_inject","sgkdev\u002Fpage_inject","sgkdev","CVE-2026-31431-killed page-cache exploit — code exec into containers sharing the same image layer",null,"C",63,11,62,0,1,2,3,42.94,false,"main",[],"2026-06-12 04:01:29","# page_inject - AF_ALG aead cross-container escape\n\n**AF_ALG aead vulnerability cross-container exploit -- pivot from one compromised container into every sibling container that shares the same `libc.so.6` image layer.**\n\nThis is an **escape primitive**: it runs from\n**inside** an unprivileged container that the attacker has already\ncompromised, and uses the AF_ALG `authencesn` ESN-rotation 4-byte\narbitrary-write bug (CVE-2026-31431) to plant a persistent `read()`\nhook in the page-cache pages of `libc.so.6`. Because Docker \u002F\ncontainerd back overlayfs lower-layer files with **shared inodes**,\nthose pages are visible to every sibling container instantiated from\nthe same image -- the hook fires in their processes too, and the\nattacker gets command execution inside each one.\n\n## Threat model\n\n* Attacker has shell access to a single container (call it `victim`)\n  on a host that runs other containers (`siblings`) from the **same\n  image** as `victim`.\n* `victim` runs with default Docker\u002Fk8s posture: unprivileged uid\n  inside the container's user namespace, default seccomp profile,\n  default AppArmor profile, no special capabilities, no host bind\n  mounts.\n* `victim` has **only**:\n  * read access to its own libc (`\u002Fusr\u002Flib\u002Fx86_64-linux-gnu\u002Flibc.so.6`\n    or wherever the distro installs it)\n  * the standard `socket(AF_ALG, ...)` syscall family\n  * the standard `splice` \u002F `vmsplice` syscalls\n  * write access to a directory it can `chmod +x` (e.g. `\u002Ftmp`)\n* The kernel must be vulnerable to CVE-2026-31431 (any `algif_aead +\n  authencesn` build prior to the upstream revert fix).\n\nThat is all. No special `CAP_*`, no host filesystem\naccess. The attacker drops a self-contained statically-linked binary\ninside the container, runs it, and the page-cache corruption -- and\ntherefore the hook -- becomes visible to every sibling.\n\n## How the exploit chains together\n\n1. **Page-cache page identity.** Inside an overlayfs container,\n   `\u002Fusr\u002Flib\u002F...\u002Flibc.so.6` is served by the lower image layer's\n   ext4 inode. Every container started from the same image shares\n   that backing inode, and the kernel's page cache is keyed by the\n   underlying inode -- not by the overlay or by the namespace. So a\n   single 4-byte write into a page-cache page is visible to all\n   sibling containers' processes that have that page mmap'd.\n\n2. **AF_ALG aead vuln turns one such write into many.** `algif_aead`\n   chains the user RX iovec with the trailing `authsize` bytes of the\n   spliced TX SGL, and `authencesn`'s ESN rotation parks 4 bytes of\n   the AAD's `seq_high` field at `dst[assoclen + cryptlen]` -- which\n   is the first byte of that chained foreign tail. The spliced page\n   is a page-cache page of a file the attacker only has read access\n   to, but the cipher copies bytes into it anyway, with no dirty\n   bookkeeping. (See `crypto\u002Falgif_aead.c` and `crypto\u002Fauthencesn.c`\n   for the underlying mechanics.)\n\n3. **Bootstrapping a callable primitive.** The first thing\n   `page_inject` does is bootstrap **Zone A** -- an asm-encoded\n   re-implementation of the same AF_ALG dance (`write_cache.asm`),\n   placed inside libc's `.text` cave. This makes the 4-byte write a\n   regular `call` from inside any future hook payload, no per-call\n   socket setup needed.\n\n4. **Installing the hook.** The injector then writes **Zone C**\n   (`zone_c.asm`) into libc's `.text` cave and patches the first\n   7-12 bytes of `read()` with an `E9 disp32` jump to it. The\n   prologue's displaced bytes are emulated faithfully in Zone C's\n   fast-path (three different glibc prologues are recognised --\n   see \"Prologue handling\" below). The hook is now live in libc's\n   page cache.\n\n5. **Hook propagation.** Every sibling container runs processes that\n   call `read()` constantly (logging daemons, healthchecks, `cat\n   \u002Fetc\u002Fhostname`, anything). On the first such call inside a sibling\n   container, the hijacked prologue jumps into Zone C, which:\n   * `stat(\"\u002F\")`s the container's root inode (a stable per-namespace\n     ID), uses it as the container's slot key,\n   * scans the slot table for an existing entry with that key,\n   * if absent, registers the key and `fork()`s a long-lived\n     **command-loop child** that polls the CMD area for orders,\n   * returns to `read()+N` so the caller is none the wiser.\n   The original sibling process keeps running. From now on the\n   attacker has a daemon inside that container.\n\n6. **Command channel.** The attacker uses the same `page_inject`\n   binary in `--shell` mode to write commands into the slot region's\n   CMD area. Each registered sibling container's hook child polls,\n   forks `\u002Fbin\u002Fsh -c \u003Ccmd>`, captures stdout\u002Fstderr into the OUTPUT\n   area, signals completion, and goes back to polling. The shell\n   shows the output. Because every CMD\u002FOUTPUT write also goes through\n   the vuln primitive, no special privilege is needed.\n\n7. **Unhook.** When done, `unhook` restores `read()`'s original\n   prologue bytes and zeros the slot table; hook children see an\n   empty slot on their next iteration and self-terminate. The\n   page-cache modifications themselves are clean (the kernel never\n   marked the modified pages dirty), so once every container that\n   has libc mmap'd is stopped, a `drop_caches` reverts the cache\n   fully -- no on-disk artefact remains.\n\n## Building\n\nThe injector is built **outside** the victim container -- typically\non the attacker's own development machine -- because most production\ncontainer images don't ship a compiler. A standard Linux x86_64 dev\nenvironment with `gcc` (with `-static`-link support) and `nasm` is\nenough.\n\n```bash\nmake            # assembles .asm sources via gen_arrays.sh, links static page_inject\nmake shellcode  # also produces inspectable .bin flat binaries\nmake clean      # removes generated files and the binary\n```\n\nThe output is a single statically-linked ELF (`.\u002Fpage_inject`) that\nruns on any modern x86_64 Linux kernel.\n\n## Delivery and usage (from the victim container)\n\nOnce the attacker has shell on `victim`, they upload the binary to a\nwritable directory (typically `\u002Ftmp`):\n\n```bash\n# inside the compromised container, attacker session\nvictim$ .\u002Fpage_inject\n```\n\nWith no arguments, `page_inject` defaults to\n`\u002Fusr\u002Flib\u002Fx86_64-linux-gnu\u002Flibc.so.6` (the post-merge Debian\u002FUbuntu\nlocation). For other distros the libc is at a different path; either\npass it explicitly or use `--root \u002F` to scan the built-in lookup\ntable from the container's root:\n\n```bash\n# Fedora \u002F Rocky \u002F CentOS\nvictim$ .\u002Fpage_inject \u002Fusr\u002Flib64\u002Flibc.so.6\n\n# Arch\nvictim$ .\u002Fpage_inject \u002Fusr\u002Flib\u002Flibc.so.6\n\n# Auto-detect, regardless of distro:\nvictim$ .\u002Fpage_inject --root \u002F\n```\n\nEither invocation does the same thing: ELF-parse the in-container\nlibc, install the hook in its page cache, monitor the slot table for\n~30 s while siblings register, and run a one-shot `id` against the\nfirst sibling that registered as a sanity check.\n\nAfter bootstrap, drop into the command shell to drive any registered\nsibling:\n\n```\nvictim$ .\u002Fpage_inject --shell --no-bootstrap\n=== page-cache shell ===\nContainers (3):\n  [0] 0x0018598d  \u003C- target\n  [1] 0x001859ab\n  [2] 0x001859cd\n\ninject:0018598d> exec id\nuid=0(root) gid=0(root) groups=0(root)\ninject:0018598d> target 0x001859ab\ninject:001859ab> exec hostname\n1ccd66abee9d\ninject:001859ab> exec cat \u002Fetc\u002Fshadow\nroot:$6$.....\ninject:001859ab> unhook\n... read() prologue restored, slot table zeroed ...\n```\n\n`unhook` cleans the hook out of every sibling container in one shot\nand lets the hook children self-terminate.\n\n```\nUsage: page_inject [OPTIONS] [LIBC_PATH]\n\nOptions:\n  --root \u003Cprefix>   Auto-resolve libc.so.6 under \u003Cprefix> using the\n                    built-in fixed-path lookup table. Inside the\n                    victim container that's normally --root \u002F .\n  --shell [0xKEY]   Drop into interactive command shell after\n                    injection. Optional KEY pre-selects the target.\n  --no-bootstrap    Skip injection (shell-only; hook must already\n                    be live in the page cache).\n  --timeout SEC     Slot monitoring timeout in --shell mode\n                    (default 30 s).\n  --help, -h        Show help.\n\nDefault libc (when no --root and no LIBC_PATH given):\n  \u002Fusr\u002Flib\u002Fx86_64-linux-gnu\u002Flibc.so.6\n```\n\n## Dual injection path\n\nDifferent glibc builds leave different amounts of `.text` cave space\nbetween the executable LOAD segment and the next read-only LOAD.\n`page_inject` selects between two layouts at inject time:\n\n* **Path A -- libc-only (default).** Both Zone C and Zone A live in\n  libc's `.text` cave. The slot table + CMD + OUTPUT areas live in\n  libc's `.hash` section -- legacy SysV hash data that ld.so doesn't\n  read at runtime since it uses `.gnu.hash` instead. When `.hash`\n  is absent (Arch's modern toolchain), `page_inject` carves the slot\n  region out of the tail of `.eh_frame_hdr` instead, after first\n  shrinking the `fde_count` field so the unwinder no longer\n  considers the freed bytes part of the FDE binary-search index\n  (the unwinder transparently falls through to a linear scan of\n  `.eh_frame` for any IP whose FDE used to be in the truncated range\n  -- LSB-mandated behaviour).\n\n* **Path B -- libc trampoline + ld.so payload.** Some glibc builds\n  shrink the libc cave below the size needed for the full Zone C +\n  Zone A payload (Ubuntu 24.04 \u002F glibc 2.39 ships an 711 B cave).\n  In that case `page_inject` writes a 36 B trampoline into libc's\n  cave -- it does the fast-path `.bss`-key gate intra-libc -- and\n  on the slow path it computes ld.so's runtime base from libc's\n  GOT slot for `_rtld_global` (an ld.so-side symbol every glibc\n  imports privately) and jumps into a base-register variant of\n  Zone C in **ld.so**'s `.text` cave. The slot table + CMD + OUTPUT\n  + `.bss` key all stay in libc; the ld.so-side Zone C reaches them\n  through `rbp + offset` after the trampoline seeds `rbp =\n  libc_base`.\n\nIf neither layout fits, `page_inject` refuses cleanly without\nwriting anything to libc or ld.so on disk or in the page cache.\n\n## `read()` prologue handling\n\nDifferent glibc versions emit different opening sequences in\n`read()`. The injector recognises each one, reads back the bytes\nthat the hook displaces, and emulates them in Zone C's fast-path so\nsingle-threaded `read()` resumes correctly at `read+N`:\n\n| Glibc range | Prologue (after optional `endbr64`)             | Notes |\n|-------------|--------------------------------------------------|-------|\n| 2.36 \u002F 2.39 | `cmpb $0x0, __libc_single_threaded(%rip)`        | 7 bytes; emulated cmpb sets ZF for the original `jne .Lthreaded`. |\n| 2.43        | `push rbp; movsxd rdi,edi; xor r9d,r9d`          | 7 bytes; emulated byte-for-byte. |\n| 2.31 \u002F 2.35 | `mov eax, fs:[0x18]`                              | 8 bytes; emulated byte-for-byte (FS-prefixed `[disp32]` is absolute, not RIP-relative, so byte-copy is faithful). |\n\nThe fast-path emulation slot in Zone C is sized for the longest\nknown prologue (8 bytes) plus the 5-byte `rel32` jmp; shorter\nprologues fill the trailing slot byte with a NOP filler so the\ntotal slot length is constant.\n\n## File layout\n\n```\npage_inject\u002F\n  page_inject.c           Main injector: ELF parsing, vuln primitive,\n                          dual-path layout selection, inject + unhook.\n  zone_c.asm              Path-A hook dispatcher shellcode.\n  zone_c_ld.asm           Path-B hook dispatcher (rbp-base variant).\n  trampoline.asm          Path-B 36-byte libc-side stub.\n  write_cache.asm         Zone A (vuln write primitive shellcode).\n  gen_arrays.sh           Assemble .asm -> asm_bytecode.c.\n  asm_bytecode.c          [generated] shellcode byte arrays.\n  Makefile                Build system.\n```\n\n## Tested-and-supported matrix\n\nThe exploit has been verified end-to-end on the following container \nsnap distros. Each entry has had `page_inject` injected from inside \none container and seen its hook fire in a sibling container started \nfrom the same image; commands executed correctly via the page-cache \nchannel; and unhook restored the libc page state cleanly.\n\n| Image             | glibc | Inject path | Read() prologue | Slot region                    |\n|-------------------|-------|-------------|-----------------|--------------------------------|\n| `debian:bookworm` | 2.36  | A           | cmpb            | `.hash`                         |\n| `ubuntu:24.04`    | 2.39  | B           | cmpb            | `.hash` (libc-side, addressed via `rbp` from ld.so) |\n| `ubuntu:22.04`    | 2.35  | A           | TLS-fs          | `.hash`                         |\n| `fedora:40`       | 2.39  | A           | cmpb            | `.hash`                         |\n| `archlinux:latest`| 2.43  | A           | push-rbp        | `.eh_frame_hdr` (truncated tail) |\n\n## Operational notes\n\n* `page_inject` is statically linked deliberately so the attacker's\n  own process is unaffected by the hook it installs.\n* `page_inject` recognises an \"already hooked\" libc (E9 + nops at\n  `read()`'s prologue) and refuses to re-inject. If you are on a \n  test env and your page cache is stuck in that state, stop all \n  containers using the image and `drop_caches` to reset.\n","page_inject 是一个利用 CVE-2026-31431 漏洞实现跨容器逃逸的工具。它通过在已攻陷的无特权容器内部运行，利用 AF_ALG `authencesn` ESN 旋转漏洞（CVE-2026-31431）在共享 `libc.so.6` 镜像层的所有兄弟容器中植入持久化的 `read()` 钩子，从而实现代码执行。该项目使用 C 语言编写，适用于攻击者已经获取了单个容器访问权限，并希望通过该容器进一步渗透到同一镜像启动的其他容器中的场景。无需特殊权限或主机文件系统访问，仅需读取自身 libc、标准 socket 和 splice 系统调用即可完成攻击。","2026-06-11 04:01:15","CREATED_QUERY"]