- Note: much of these notes are taken from pwn.college youtube videos
Many things to learn and do, but I'll list a basic todo for myself for now
- Finish pwn.college youtube videos (6/13)
- Try out the LinkedList imaginaryCTF 3 levels challenge
- Get started on ROP Emporium
- Get started on ProtoStar
- Try a integer overflow/underflow challenge
- Try a House of * Challenge
- Figure out what is seccomp
- Figure out what is qemu and mips
-
Install pwninit
- Setup
- install cargo:
curl https://sh.rustup.rs -sSf | sh
- install pwninit:
cargo install pwninit
- I faced an error
warning: failed unstripping libc: failed running eu-unstrip, please install elfutils: No such file or directory (os error 2)
- install cargo:
- Usefulness:
- Helps you download a linker that will help you load a provided
libc
without segfaulting - Downloads debug symbols and unstrips the
libc
- Follow this up with
patchelf
to use the correct RPATH and interpreter for the providedlibc
- Helps you download a linker that will help you load a provided
- Setup
-
Install patchelf
sudo apt -y install patchelf
- configures the binary to use the
ld
linker file that was downloaded frompwninit
rather than the system's one - e.g.
patchelf --set-interpreter <ld file from pwninit> <binary file to use ld file>
- Once this is done, can run
LD_PRELOAD=<libc.so file> ./binary
-
Can run
readelf -a <binary>
to see the headers + segments, or just find out ELF info -
ldd <binary>
shows you which libc and which linker the binary is configured to use. Can use this to check if the binary is using the system libc or the one in your directory. -
checksec --file=<binary>
to see protectionsRELRO
: determines if Global Offset Table is writableNX
: determines if stack is executable --> can put shellcode on stackPIE
: depends if ASLR (Address Space Layout Randomization is enabled) --> different addresses each time- Note that in GDB its configured to disable ASLR all the time, see below for command to turn enable it
- ASLR must maintain the last 3 nibbles (1.5 bytes) the same based on alignment and relative offsets
-
Don't forget to create a
flag.txt
file sometimes, otherwise the binary might give a segfault if itfreads
orfopen
from that file -
Install VSCode
- sudo apt update
- sudo apt install curl gpg gnupg2 software-properties-common apt-transport-https
- curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
- sudo install -o root -g root -m 644 microsoft.gpg /etc/apt/trusted.gpg.d/
- echo "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" | sudo tee /etc/apt/sources.list.d/vscode.list
- sudo apt update
- sudo apt install code
-
Edit
PATH
to be able to use the binaries you install- check with shell you are using with
ps -p $$
- Bash --> edit
~/.bashrc
- Zsh --> edit
~/.zshrc
- Bash --> edit
export PATH="$PATH:/home/kali/.local/bin:/home/kali/.cargo/bin"
- check with shell you are using with
-
can use
strace
to view system calls -
nm -D <binary>
to see what function imports there are- shows symbols in binaries
-
rappel https:/yrp604/rappel lets you explore the effects of instructions.
- easily installable via https:/zardus/ctf-tools
- Libc Database: https://libc.blukat.me/
- CryptoCat Pwn Github: https:/Crypto-Cat/CTF/tree/main/pwn
- ROP Emporium: https://ropemporium.com/
- Exploit Education: https://exploit.education/
- Pwn College: https://pwn.college/
- ROP Gadget: https:/JonathanSalwan/ROPgadget
- Syscall Table: https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/
- amd64 opcode listing: http://ref.x86asm.net/coder64.html
x/32gx <0xaddress>
x
== examinegx
== 8 bytes values in hexadecimal
disass main
- disassemble the main function
info functions
- show list of all defined functions
info registers
- see values held in registers
set disable-randomization off
- enable ASLR
b *(0xaddress)
- set breakpoint at the particular address
heap
: examine heap (including tcache)p $rdx
: prints value of rdx registerp/x
to print in hexadecimal format
bins
to see bins lol
pwndbg> bins
tcachebins
0x90 [ 2]: 0x603890 —▸ 0x603800 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
fini
to step out of a function call, especially if you're going through a glibc function currently- press enter to repeat previous command
vis_heap_chunks
: visualize heap chunksinfo proc map
: shows where process is loaded in memory- print the next 5 instructions:
x/5i $rip
- examine:
- qwords:
(x/gx $rsp)
- dwords:
(x/2dx $rsp)
- halfwords:
(x/4hx $rsp)
- bytes:
(x/8b $rsp)
- qwords:
l
to rename variable- If you see stuff like
&DAT_00400b53
, means it is a global variable- double click, if its a string, can just
cmd + l
to change variable data type to likechar [3]
- double click, if its a string, can just
-
cos of the von neumann architecture, code can be treated the same as data, this leads to shellcode injection vulnerabilities
- so to mitigate this, they support memory permissions (usually passed to
mmap
) PROT_READ
: read memoryPROT_WRITE
: write memoryPROT_EXEC
: allows process to execute memory
- so to mitigate this, they support memory permissions (usually passed to
-
by default all code is located in
.text
segments, no need to execute code located on the stack or the heap -
to bypass this, you can de-protect memory using the
mprotect()
system call- Code reuse through ROP ,trick the program into
mprotect(PROT_EXEC)
the shellcode - then jump to the shellcode
- Code reuse through ROP ,trick the program into
-
Other places that shellcoding is still vulnerabile is in JIT compilation
- e.g. Javascript, Java, Lua, Python etc.
- need to generate code that is executed
- attacker can inject higher-level code can influence the resulting native code, including immediate values stored
- pages must be writable for code-generation
- pages must be executable for execution
- pages must be writable for code re-generation
- but syscalls are slow, but point of JIT is to be fast
- so it is common to have writable and executable pages, (which are vulnerable to shellcoding!)
- to see which processes could be vulnerable, or have a page that can be used to inject shellcode
grep -l rwx */maps | parallel "ls -l {//}/exe
- Even if JIT safely
mprotect()
s its pages, can still use JIT spraying- make constants in code that will be JIT-ed
let evil = "..." // has to be valid assembly
- JIT engine will
mpotect(PROT_WRITE)
, compile the code into memory, thenmprotect(PROT_EXEC)
, which means code is now present in exec memory - use vuln to redirect execution into that constant
- make constants in code that will be JIT-ed
-
can spray by declaring the same thing thousands of times, jump and hope you get lucky
-
typical goal is to launch
execve("/bin/sh", NULL, NULL);
.global _start
_start:
.intel_syntax noprefix
mov rax, 59 # syscall number of execve
lea rdi, [rip + binsh] #point first argument of execve to the /bin/sh string
mov rsi, 0 # set second argument, argv NULL
mov rdx, 0 # set third argument, envp, NULL
syscall #trigger syscall
binsh: #label of where the string is
.string "/bin/sh"
- by default its ATNT, but can specify intel
- assemble with
gcc -nostdlib -static shellcode.s -o shellcode-elf
- this gives ELF with shellcode as
.text
- this gives ELF with shellcode as
- extract with
objcopy --dump-section .text-shellcode-raw shellcode-elf
- contains the raw bytes of the shellcode, and inject as part of exploit
- interesting way to pipe in and pipe out
(cat shellcode.raw; cat) | ./<vulnerable-bin>
- ways to include data in code
.byte 0x48, 0x45, 0x4C, 0x4F # "HELLO"
.string "HELLO" # "HELLO\0"
mov rbx, 0x0068732f6e69622f # movve "/bin/sh\0" into rbx
push rbx # push "/bin/sh\0" onto the stack
mov rdi, rsp #point rdi at the stack
-
If you need to replicate exotic conditions that are hard to do as a preamble to shellcode, can build a shellcode loader in c
// create a function pointer, mmap a space that is executable and load the shellcode in and execute
page = mmap(0x1337000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON, 0,0)
read(0, page, 0x1000);
((void(*)())page)()
-
debug shellcode at high level using
strace
--> trace system calls -
for more analysis in
gdb
- use
si
to step one instruction, following the call instructions, nots
- use
ni
to step over call instructions, notn
- use
-
breakpoints can be hardcoded with the
int3
instruction- useful to do this right before the syscall, need to be handled either in code or in debugger
-
Shellcode for other architectures
- Compiling
- amd64:
gcc -nostdlib -static shellcode.s -o shellcode-elf
- mips:
mips-linux-gnu-gcc -nostdlib shellcode-mips.s -o shellcode-mips-elf
- amd64:
- Running
- amd64:
./shellcode
- mips:
qemu-mips-static ./shellcode-mips
- amd64:
- useful Qemu options:
-strace
: print out log of system calls-g 1234
: wait for gdb connection on port 1234. Connect withtarget remote localhost:1234
ingdb-multiarch
- Compiling
-
Note: I have no idea how to work with qemu and mips, is a long todo
- Untrusted code/data (i.e. downloaded JS, pngs, pdfs etc.) should live in a process with almost zero permissions
- Basic process
- spawn privileged parent process (that can interact with your system)
- spawn sandboxed child process
- when child needs to perform privileged action, asks parent
-
chroot()
: changes meaning of root- for filesystem isolation
- but no syscall filtering or other isolation
- usually you attach
busybox
into the sandboxed directory so that you don't have to transfer the other linux utilities sudo chroot /tmp/jail /busybox sh
- does not
cd
into that jail for you, you have to do it manually - does not close previously open resources
- use
openat
andexecveat
int openat(int dirfd, char *pathname, int flags)
int execveat(int dirfd, char *pathname, char **argv, char **envp, flags)
- execute and open relative to a particular file directory (or previously open directory)
- e.g. current working directory is implicitly open
- use
chroot
can be done more than once, and wipes out the previous chroot- does not provide other form of isolation, e.g. process id isolations, network isolations, IPC etc.
- now abandoned in favor of
cgroups
namespaces
seccomp
-
seccomp
- Kernel level sandboxing mechanism
- Chooses allow and disallowed certain system calls, even based on argument variables
- are inherited by children
- Example seccomp code
scmp_filter_ctx ctx; // allow all syscalls ctx = seccomp_init(SCMP_ACT_ALLOW); // whenever execve is called, kill the program seccomp_rule_add(Ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0); // applies the filter seccomp_load(ctx); // will be killed execl("/bin/cat", "cat", "/flag", (char *)0);
- To compile, need to install seccomp library
sudo apt install libseccomp-dev
- Compile with
gcc -o <out-binary> <file>.c -lseccomp
- In order to ensure that the sandboxed privileges are inherited, will call
prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0)
-
seccomp uses extended Berkeley packet Filters (eBPF) that run in an in-kernel "provably-safe" virtual machine
- originally used to filter network traffic (iptables)
-
Seccomp Attack Vectors
- Permissive Policies
ptrace()
can use sandboxed process to puppet a non-sandboxed process- originally used to attach debuggers etc.
sendmsg()
can transfer file descriptors between processesprctl()
got some weird stuffprocess_vm_writev()
allows direct access to other process' memory
- Syscall Confusion
- many 64 bit architectures are backwards compatible with their 32 bit ancestors
- on some systems, can switch between 32-bit and 64-bit mode in same process
- policies may fail to sandbox either 32/64-bit properly
- e.g.
exit()
is syscall 60 on amd64 and 1 on x86
- Trigger kernel vulnerabilities through the allowed syscalls
- Permissive Policies
- The process of Forward Engineering
- Starting from c code
hello.c
- run
cpp hello.c
to get preprocessed code with include statements expanded and comments tripped - run
gcc -S -masm=intel hello-preprocessed.c
to compile to intel assembly code- variable names are lost at this step
- type information is lost
strip <binary>
to remove semantic functions and reduce size
- Starting from c code
- Functions often begin with a prologue (setup stack frame) and an epilogue (tear down stack frame)
- Stack pointer (rsp): leftmost side of stack frame
- Base pointer (rbp): rightmost side of stack frame
- The stack used to store local variables and execution contexts
- When function is called, the address that the function should return is implicitly pushed onto stack
- The return address is implicitly popped when it returns
- Prologue
- save the caller's base pointer
- set current stack pointer as base pointer
- allocate space on the stack (subtract from stack pointer)
- Epilogue
- deallocate the stack
mov rsp, rvp
- but not actually removed
- restore the old base pointer
- return!
- deallocate the stack
- The stack grows backwards
- when you push to stack, rsp -= 8
- when you pop from stack, rsp += 8
- either bottom (0xFF) to top (0x00), or right (0xFF) to left (0x00)
- The stack starts out storing the environment variables and the program arguments
- Data Execution Prevention prevents injecting executable shellcode
- Stack Canaries protect against stack buffer overflow
- Address space layout randomization makes control flow hiajacking unreliable
- returned values are stored in
$rax
LANG
environment variable determines sorting of file fromls
- standard is
int main(int argc, char **argv, char **envp)
objdump -d <binary> -M intel
- use
-d
for disassemble - use
-M
for intel syntax because ATNT assembly is weird on x86
- use
du -sb <file>
: shows number of bytes in filekill -l
: lists all the signalspgrep <process>
===ps aux | grep <process>
- used to find the process Id so you can do something like
kill -9 <process>
to kill it
- used to find the process Id so you can do something like
/dev/shm
is used for shared memory mapped files- requires system calls to establish, but afterwards don't need, just pass the file descriptors around
- processes terminate only either by unhandled signals (e.g. SIG 10, segfault) or exit system call
- after termination, processes remain in zombie state until
wait()
is called by their parents on them, then their exit code will be returned to parents and be freed - if parent dies before this happens, they are re-parented to PID 1 until cleaned up
- PID 1 --> first process started by linux kernel /init process for shutting down + starting up system
- after termination, processes remain in zombie state until
getcwd
: get current working directoryecho $$
: echoes process id of current shell- use
pwn.cyclic(200, n=8)
to send cyclic pattern, check 8 bytes at a timepwn.cyclic_find(0x...,n=8)
to find offset
- To preserve variable names and all, compile using the
-g
flag, e.g.gcc -g -o hello hello.c
-fomit-frame-pointer
, allowsrbp
to be used as a general purpose register as modern compiler can omit frame pointers in most cases