Skip to content

Commit

Permalink
Reject BPF program if uninit stack is accessed
Browse files Browse the repository at this point in the history
Reject programs if registers are used before intialized

Signed-off-by: Alan Jowett <alanjo@microsoft.com>
  • Loading branch information
Alan-Jowett committed May 14, 2024
1 parent 3fb3da0 commit 9d46db6
Show file tree
Hide file tree
Showing 9 changed files with 547 additions and 6 deletions.
94 changes: 94 additions & 0 deletions libfuzzer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# ubpf_fuzzer

This is a libfuzzer based fuzzer.

To build, run:
```
cmake \
-G Ninja \
-S . \
-B build \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DUBPF_ENABLE_LIBFUZZER=1 \
-DCMAKE_BUILD_TYPE=Debug
cmake --build build
```

To run:
Create folder for the corpus and artifacts for any crashes found, then run the fuzzer.

```
mkdir corpus
mkdir artifacts
build/bin/ubpf_fuzzer corpus -artifact_prefix=artifacts/
```

Optionally, add the "-jobs=100" to gather 100 crashes at a time.

This will produce a lot of output that looks like:
```
#529745 REDUCE cov: 516 ft: 932 corp: 442/22Kb lim: 2875 exec/s: 264872 rss: 429Mb L: 50/188 MS: 3 CrossOver-ChangeBit-EraseBytes-
#529814 REDUCE cov: 516 ft: 932 corp: 442/22Kb lim: 2875 exec/s: 264907 rss: 429Mb L: 45/188 MS: 4 ChangeBit-ShuffleBytes-PersAutoDict-EraseBytes- DE: "\005\000\000\000\000\000\000\000"-
#530202 REDUCE cov: 516 ft: 932 corp: 442/22Kb lim: 2875 exec/s: 265101 rss: 429Mb L: 52/188 MS: 3 ChangeByte-ChangeASCIIInt-EraseBytes-
#531224 REDUCE cov: 518 ft: 934 corp: 443/22Kb lim: 2875 exec/s: 265612 rss: 429Mb L: 73/188 MS: 2 CopyPart-PersAutoDict- DE: "\001\000\000\000"-
#531750 REDUCE cov: 518 ft: 934 corp: 443/22Kb lim: 2875 exec/s: 265875 rss: 429Mb L: 45/188 MS: 1 EraseBytes-
#532127 REDUCE cov: 519 ft: 935 corp: 444/22Kb lim: 2875 exec/s: 266063 rss: 429Mb L: 46/188 MS: 2 ChangeBinInt-ChangeByte-
#532246 REDUCE cov: 519 ft: 935 corp: 444/22Kb lim: 2875 exec/s: 266123 rss: 429Mb L: 66/188 MS: 4 ChangeBit-CrossOver-ShuffleBytes-EraseBytes-
#532357 NEW cov: 520 ft: 936 corp: 445/22Kb lim: 2875 exec/s: 266178 rss: 429Mb L: 55/188 MS: 1 ChangeBinInt-
#532404 REDUCE cov: 520 ft: 936 corp: 445/22Kb lim: 2875 exec/s: 266202 rss: 429Mb L: 57/188 MS: 2 ChangeBit-EraseBytes-
#532486 REDUCE cov: 520 ft: 936 corp: 445/22Kb lim: 2875 exec/s: 266243 rss: 429Mb L: 44/188 MS: 2 EraseByte
```

Eventually it will probably crash and produce a message like:
```
=================================================================
==376403==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x000000000000 bp 0x7ffca9d3cda0 sp 0x7ffca9d3cb98 T0)
==376403==Hint: pc points to the zero page.
==376403==The signal is caused by a READ memory access.
==376403==Hint: address points to the zero page.
#0 0x0 (<unknown module>)
#1 0x50400001a48f (<unknown module>)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (<unknown module>)
==376403==ABORTING
MS: 1 ChangeByte-; base unit: cea14e5e2ecdc723b9beb640471a18b4ea529f75
0x28,0x0,0x0,0x0,0xb4,0x50,0x10,0x6a,0x6a,0x4a,0x6a,0x2d,0x2e,0x1,0x0,0x0,0x0,0x0,0x0,0x0,0x4,0x21,0x0,0x0,0x0,0x0,0x95,0x95,0x26,0x21,0xfc,0xff,0xff,0xff,0x95,0x95,0x95,0x95,0x97,0xb7,0x97,0x97,0x0,0x8e,0x0,0x24,
(\000\000\000\264P\020jjJj-.\001\000\000\000\000\000\000\004!\000\000\000\000\225\225&!\374\377\377\377\225\225\225\225\227\267\227\227\000\216\000$
artifact_prefix='artifacts/'; Test unit written to artifacts/crash-7036cbef2b568fa0b6e458a9c8062571a65144e1
Base64: KAAAALRQEGpqSmotLgEAAAAAAAAEIQAAAACVlSYh/P///5WVlZWXt5eXAI4AJA==
```

To triage the crash, the crash can be post processed using:
```
libfuzzer/split.sh artifacts/crash-7036cbef2b568fa0b6e458a9c8062571a65144e1
Extracting program-7036cbef2b568fa0b6e458a9c8062571a65144e1...
Extracting memory-7036cbef2b568fa0b6e458a9c8062571a65144e1...
Disassembling program-7036cbef2b568fa0b6e458a9c8062571a65144e1...
Program size: 40
Memory size: 2
Disassembled program:
mov32 %r0, 0x2d6a4a6a
jgt32 %r1, %r0, +0
add32 %r1, 0x95950000
jgt32 %r1, 0x9595ffff, -4
exit
Memory contents:
00000000: 0024 .$
```

To repro the crash, you can run:
```
build/bin/ubpf_fuzzer artifacts/crash-7036cbef2b568fa0b6e458a9c8062571a65144e1
```

Or you can repro it using ubpf_test:
```
build/bin/ubpf-test --mem artifacts/memory-7036cbef2b568fa0b6e458a9c8062571a65144e1 artifacts/program-7036cbef2b568fa0b6e458a9c8062571a65144e1 --jit
```

115 changes: 113 additions & 2 deletions libfuzzer/libfuzz_harness.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,23 @@ int LLVMFuzzerTestOneInput(const uint8_t* data, std::size_t size)
return -1;
}

if ((program_length % sizeof(ebpf_inst)) != 0) {
// The program length needs to be a multiple of sizeof(ebpf_inst_t).
// This is not interesting, as the fuzzer input is invalid.
// Do not add it to the corpus.
return -1;
}

for (std::size_t i = 0; i < program_length / sizeof(ebpf_inst); i++) {
ebpf_inst inst = reinterpret_cast<const ebpf_inst*>(program_start)[i];
if (inst.opcode == EBPF_OP_CALL && inst.src == 1) {
// Until local calls are fixed, reject local calls.
// This is not interesting, as the fuzzer input is invalid.
// Do not add it to the corpus.
return -1;
}
}

// Copy any input memory into a writable buffer.
if (memory_length > 0) {
memory.resize(memory_length);
Expand Down Expand Up @@ -133,15 +150,109 @@ int LLVMFuzzerTestOneInput(const uint8_t* data, std::size_t size)
return -1;
}

uint64_t result = 0;
uint64_t jit_result = 0;
uint64_t interpreter_result = 0;

// Reserve 3 pages for the stack.
const std::size_t helper_function_stack_space = 3*4096; // 3 pages
const std::size_t prolog_size = 64; // Account for extra space needed for the prolog of the jitted function.
uint8_t ubpf_stack[UBPF_STACK_SIZE + helper_function_stack_space];
memset(ubpf_stack, 0, sizeof(ubpf_stack));

// Tell the interpreter where the stack is.
ubpf_set_stack(vm.get(), (uint8_t*)ubpf_stack + sizeof(ubpf_stack) - UBPF_STACK_SIZE - prolog_size);

// Execute the program using the input memory.
if (ubpf_exec(vm.get(), memory.data(), memory.size(), &result) != 0) {
if (ubpf_exec(vm.get(), memory.data(), memory.size(), &interpreter_result) != 0) {
// The program passed validation during load, but failed during execution.
// due to a runtime error. Add it to the corpus as it may be interesting.
return 0;
}

auto fn = ubpf_compile(vm.get(), &error_message);
if (fn == nullptr) {
// The program failed to compile.
// This is not interesting, as the fuzzer input is invalid.
// Do not add it to the corpus.
free(error_message);
return -1;
}

memset(ubpf_stack, 0, sizeof(ubpf_stack));

// Setup the stack for the function call.
uintptr_t new_rsp = (uintptr_t)ubpf_stack + sizeof(ubpf_stack);
uintptr_t* rsp;
uintptr_t* old_rsp_ptr;
uintptr_t old_rdi;
uintptr_t old_rsi;
uintptr_t old_rax;
new_rsp &= ~0xf;
new_rsp -= sizeof(uintptr_t);

rsp = (uintptr_t*)new_rsp;

// Save space for the old value of rsp
*(--rsp) = 0;
old_rsp_ptr = rsp;

// Store the function address.
*(--rsp) = (uintptr_t)fn;

// Store the memory address.
*(--rsp) = (uintptr_t)memory.data();

// Store the memory size.
*(--rsp) = (uintptr_t)memory.size();

// Copy the current value of rsp into the reserved space.
__asm__ __volatile__("movq %%rsp, %0" : "=r"(*old_rsp_ptr));

// Copy the current value of rcx into the reserved space.
__asm__ __volatile__("movq %%rdi, %0" : "=r"(old_rdi));

// Copy the current value of rdx into the reserved space.
__asm__ __volatile__("movq %%rsi, %0" : "=r"(old_rsi));

// Copy the current value of rax into the reserved space.
__asm__ __volatile__("movq %%rax, %0" : "=r"(old_rax));

// Set the new value of rsp.
__asm__ __volatile__("movq %0, %%rsp" : : "r"(rsp));

// Pop arguments into registers.
__asm__ __volatile__("pop %rsi");
__asm__ __volatile__("pop %rdi");

// Pop the function address into rax.
__asm__ __volatile__("pop %rax");

// Call the function.
__asm__ __volatile__("call *%rax");

// Pop the old value of rsp.
__asm__ __volatile__("pop %rsp");

// Copy rax into jit result.
__asm__ __volatile__("movq %%rax, %0" : "=r"(jit_result));

// Put back the old value of rax.
__asm__ __volatile__("movq %0, %%rax" : : "r"(old_rax));

// Put back the old value of rdx.
__asm__ __volatile__("movq %0, %%rsi" : : "r"(old_rsi));

// Put back the old value of rcx.
__asm__ __volatile__("movq %0, %%rdi" : : "r"(old_rdi));


// If interpreter_result is not equal to jit_result, raise a fatal signal
if (interpreter_result != jit_result) {
printf("interpreter_result: %lx\n", interpreter_result);
printf("jit_result: %lx\n", jit_result);
throw std::runtime_error("interpreter_result != jit_result");
}

// Program executed successfully.
// Add it to the corpus as it may be interesting.
return 0;
Expand Down
37 changes: 37 additions & 0 deletions libfuzzer/split.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash

# Split the file name into path and base name
path=$(dirname $1)
base=$(basename $1)

# Get the first 4 bytes from the file (which is the length of the program)
input="$(xxd -p -l 4 $1)"
# Convert from little endian
input="${input:6:2}${input:4:2}${input:2:2}${input:0:2}"

# Convert input from hex string to value
length=$((16#$input))

# Extract the hash part from the file name
hash=$(echo $base | cut -d'-' -f2-)

# Copy the program to a file named program-$hash
echo "Extracting program-$hash..."
dd if=$1 of=$path/program-$hash bs=1 skip=4 count=$length 2> /dev/null

echo "Extracting memory-$hash..."
# Copy the rest to a file named memory-$hash
dd if=$1 of=$path/memory-$hash bs=1 skip=$((4 + $length)) 2> /dev/null

echo "Disassembling program-$hash..."
# Unassembly program using bin/ubpf-disassembler
bin/ubpf-disassembler $path/program-$hash > $path/program-$hash.asm

echo "Program size: $(stat -c %s $path/program-$hash)"
echo "Memory size: $(stat -c %s $path/memory-$hash)"

echo "Disassembled program:"
cat $path/program-$hash.asm

echo "Memory contents:"
xxd $path/memory-$hash
4 changes: 4 additions & 0 deletions ubpf/disassembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ def disassemble_one(data, offset):
if opcode_name == "exit":
return opcode_name
elif opcode_name == "call":
if src_reg == 1:
opcode_name += " local"
return "%s %s" % (opcode_name, I(imm))
elif opcode_name == "ja":
return "%s %s" % (opcode_name, O(off))
Expand All @@ -143,6 +145,8 @@ def disassemble_one(data, offset):
if opcode_name == "exit":
return opcode_name
elif opcode_name == "call":
if src_reg == 1:
opcode_name += " local"
return "%s %s" % (opcode_name, I(imm))
elif opcode_name == "ja":
return "%s %s" % (opcode_name, O(off))
Expand Down
18 changes: 18 additions & 0 deletions vm/inc/ubpf.h
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,24 @@ extern "C"
uint64_t*
ubpf_get_registers(const struct ubpf_vm* vm);

/**
* @brief Override the storage location for the BPF stack in the VM.
*
* @param[in] vm The VM to set the stack storage in.
* @param[in] stack The stack storage.
*/
void
ubpf_set_stack(struct ubpf_vm* vm, uint8_t stack[UBPF_STACK_SIZE]);

/**
* @brief Retrieve the storage location for the BPF stack in the VM.
*
* @param[in] vm The VM to get the stack storage from.
* @return uint8_t* A pointer to the stack storage.
*/
uint8_t*
ubpf_get_stack(const struct ubpf_vm* vm);

/**
* @brief Optional secret to improve ROP protection.
*
Expand Down
1 change: 1 addition & 0 deletions vm/ubpf_int.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ struct ubpf_vm
int instruction_limit;
#ifdef DEBUG
uint64_t* regs;
uintptr_t stack;
#endif
};

Expand Down
9 changes: 8 additions & 1 deletion vm/ubpf_jit_x86_64.c
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,6 @@ translate(struct ubpf_vm* vm, struct jit_state* state, char** errmsg)
}

struct ebpf_inst inst = ubpf_fetch_instruction(vm, i);
state->pc_locs[i] = state->offset;

int dst = map_register(inst.dst);
int src = map_register(inst.src);
Expand All @@ -314,6 +313,8 @@ translate(struct ubpf_vm* vm, struct jit_state* state, char** errmsg)
emit_alu64_imm32(state, 0x81, 5, RSP, 8);
}

state->pc_locs[i] = state->offset;

switch (inst.opcode) {
case EBPF_OP_ADD_IMM:
emit_alu32_imm32(state, 0x81, 0, dst, inst.imm);
Expand Down Expand Up @@ -723,6 +724,12 @@ translate(struct ubpf_vm* vm, struct jit_state* state, char** errmsg)
state->jit_status = UnknownInstruction;
*errmsg = ubpf_error("Unknown instruction at PC %d: opcode %02x", i, inst.opcode);
}

// If this is a ALU32 instruction, truncate the target register to 32 bits.
if (((inst.opcode & EBPF_CLS_MASK) == EBPF_CLS_ALU) &&
(inst.opcode & EBPF_ALU_OP_MASK) != 0xd0) {
emit_truncate_u32(state, dst);
}
}

if (state->jit_status != NoError) {
Expand Down
6 changes: 6 additions & 0 deletions vm/ubpf_jit_x86_64.h
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ emit_alu32_imm8(struct jit_state* state, int op, int src, int dst, int8_t imm)
emit1(state, imm);
}

static inline void
emit_truncate_u32(struct jit_state* state, int destination)
{
emit_alu32_imm32(state, 0x81, 4, destination, UINT32_MAX);
}

/* REX.W prefix and ModRM byte */
/* We use the MR encoding when there is a choice */
/* 'src' is often used as an opcode extension */
Expand Down

0 comments on commit 9d46db6

Please sign in to comment.