jfs_probe: a diagnostic kernel module for slab-adjacency exploit dev
companion to the
JFS jfs_readdir OOB-write
post. while weaponizing that primitive against current Resolute the thing
that kept stalling me wasn't the crafter or the trigger - it was not knowing
what slab class lives in the page next to the OOB write target. KASAN will
tell you "slot was X bytes from allocated region of size Y in cache Z", but
only after the fact, after you've already crafted bad bytes and crashed
something. for spray work you want to see the page layout before you
fire the bug.
so i built jfs_probe.ko. ~650 lines of GPL'd kernel module plus
a ~265-line userspace ctl tool. root-only, CAP_SYS_ADMIN-gated on every ioctl.
it exposes kernel internals userland normally can't see. i'm publishing it
because the next person doing slab-OOB work on a current kernel will need
exactly this, and rebuilding it from scratch is a couple of evenings nobody
should have to spend.
what it does
| ioctl | function |
|---|---|
KVA_PFN | kernel virtual address → pfn (linear / vmalloc / direct-map handled) |
PAGE_INFO | for a pfn: order, migratetype, slab cache name, slab page state, refcount, mapping flags |
ADJACENCY | same metadata for pfn-1 and pfn+1. the two pages physically adjacent to a chosen one in the buddy partition |
MY_CRED | kva and pfn of the calling task's struct cred. canonical "where is my cred?" lookup |
DUMP_CACHE | dump a kmem_cache's layout: object size, slab page order, object count per slab, alignment, flags. useful for figuring out which cache your spray is actually hitting under SLAB_MERGE_DEFAULT / RANDOM_KMALLOC_CACHES |
WALK_FD_MAP | walk the current process's fd_array reporting kva/pfn of each struct file |
WALK_IPIMAP | walk JFS imap inodes (specific to the JFS work; less generally useful) |
VICTIMS_ALLOC | kernel-side spray helper: allocate N objects of size S in a chosen cache, optionally pinning them in memory |
VICTIMS_CHECK | read back the sprayed objects' first N bytes; useful to detect whether OOB-write content landed in any of them |
VICTIMS_FREE | free a chosen subset of sprayed objects to create holes |
VICTIMS_PUNCH | free a single sprayed object (used to manufacture a free slot at a known kva for the OOB to target) |
READ_BYTES | read N bytes of kernel memory at a chosen kva. the obvious diagnostic primitive |
all of these are trivial-to-write rootkit primitives. they don't cross any new
privilege boundary - they're a strict subset of what insmod already
hands any user with init_root. the value is having them packaged behind a clean
ioctl interface so you can diff state between attempts while you're
weaponizing, instead of guessing from dmesg alone.
the kallsyms_lookup_name trick
since 5.7, kallsyms_lookup_name is no longer exported to modules.
deliberate hardening to make rootkit dev a little less convenient.
jfs_probe resolves it at module init with a kprobe:
static int resolve_unexported_symbols(void)
{
struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" };
kallsyms_lookup_name_t kln;
int r = register_kprobe(&kp);
if (r < 0) return r;
kln = (kallsyms_lookup_name_t)kp.addr;
unregister_kprobe(&kp);
if (!kln) return -ENOENT;
resolved_get_pfnblock_migratetype =
(get_pfnblock_migratetype_t)kln("get_pfnblock_migratetype");
return 0;
}
register_kprobe with a symbol name resolves the address internally
and stores it in kp.addr. unregister, cast, call. it's the standard
out-of-tree workaround, documented in dozens of kernel-research posts. the point
is that EXPORT_SYMBOL_GPL removal was never a security boundary
against in-tree kprobes from a CAP_SYS_ADMIN-loaded module, and was never meant
to be one.
once kallsyms_lookup_name is in hand, anything else unexported is
one more lookup away (here that's get_pfnblock_migratetype, for the
buddy migrate-type adjacency reports).
how we actually used it during the JFS work
before each weaponization attempt:
jfs_probe_ctl mycred. record the test process's cred kva/pfn.jfs_probe_ctl pageinfo <cred-pfn>. confirm the cred lives in:A-0000192(the merged kmalloc-cg-192 cache afterSLAB_HWCACHE_ALIGN | SLAB_ACCOUNTmerge).- set up the JFS image and stage the mount.
jfs_probe_ctl dumpcache jfs_dirent. sanity-check the dirent buffer cache.- spray a chosen victim cache via
VICTIMS_ALLOC; record the kvas. - punch a hole at a strategically-chosen kva via
VICTIMS_PUNCH. this is the slot we want the freshly-allocated__get_free_page(GFP_KERNEL)dirent buffer to land adjacent to. - fire the trigger (
ls /mnt/poc). jfs_probe_ctl adj <dirent-pfn>. what page actually landed next to the dirent buffer? KASAN tells you after the fact; this tells you whether the grooming worked before you risk the crash.jfs_probe_ctl victims_check. if the OOB landed in a sprayed victim, the bytes will show up here.
prep, observe, fire, observe, diff. without the module you're guessing which slab class is adjacent; with it you know going in. didn't stop me from failing to finish the JFS LPE chain, but at least i knew exactly what was adjacent when i failed.
not a weapon
jfs_probe needs init_user_ns
CAP_SYS_ADMIN just to insmod in the first place.
kernel.modules_disabled=1 blocks it, an unprivileged user can't load
it, and an aa-rootns userns root can't load it either because module loading is
gated against init_user_ns. once it's loaded every ioctl re-checks
capable(CAP_SYS_ADMIN) and returns -EPERM otherwise.
none of it is reachable from unprivileged userspace. an attacker who can
insmod already owns the kernel and doesn't need this module.
the use case is the lab kernel and nothing else. you have root on a VM, you have the module source, you build it against a kernel you control, you load it, and you watch what your would-be-unprivileged primitive is doing under the hood. nothing about this module makes any kernel weaker for real users.
the alternative is a kernel-debugger setup, gdb against qemu-kvm with
-s -S and the kernel vmlinux. more powerful, slower to
iterate on. jfs_probe is the cheap version i actually used.
what's missing
- no slab-allocator state walker.
DUMP_CACHEreports the cache's metadata but does not iterate the slabs. would be nice for finding the active partial slab to predict the next allocation site. - no support for
SLAB_VIRTUALkernels. resolute is SLUB; if your target is a kernel withCONFIG_SLAB_VIRTUAL=ythe kva/pfn paths will need rework. WALK_IPIMAPis JFS-specific and useless on any other workload. left in because removing it requires also pulling some JFS-internal headers.kproberegister failure is fatal at module init. if your target kernel hasCONFIG_KPROBES=n(rare on distro builds, possible on embedded), the module won't load. cleanest replacement is to scan/proc/kallsymsfrom userspace and pass the address in via insmod parameter; not implemented yet.
build and use
# on a kernel matching the running uname -r:
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# load
sudo insmod jfs_probe.ko
# build the userland tool
gcc -O2 -o jfs_probe_ctl jfs_probe_ctl.c
# inspect your own cred
sudo ./jfs_probe_ctl mycred
# adjacency for a chosen pfn
sudo ./jfs_probe_ctl adj 0x1234567
# dump a cache layout
sudo ./jfs_probe_ctl dumpcache cred_jar
# unload
sudo rmmod jfs_probe
it refuses to load on a kernel different from the one it was built against, so rebuild against the target kernel's headers each time.
downloads
- jfs_probe.c: the kernel module, 652 lines, GPL.
- jfs_probe_ctl.c: userspace ctl tool, 265 lines.
- Makefile: standard out-of-tree build.
SHA-256:
b68a1d0f0920ee7d256737f1139cc72c42b26bf7d34b2483de5bed0a7d218d13 jfs_probe.c
5d6ead1d243b3483af408b9e4fa9d779ffcbebcc3decda3ad5aa9b2fb6605c8d jfs_probe_ctl.c
0ae9a9137c4db11f4bc36ca6c4eb700c0fb54d6138eccf659e9b451807567b73 Makefile
credit
_SiCk <sick@afflicted.sh> (https://afflicted.sh)
no warranty. no support. no apologies.
. _SiCk · afflicted.sh