Пишем упаковщик по шагам. Шаг шестой. TLS.

Предыдущий шаг здесь.

Появилась новая версия библиотеки для работы с PE-файлами (0.1.5). Перекачайте и пересоберите ее.

Пришло время заняться обработкой такой важной вещи, как Thread Local Storage (TLS) - локальной памяти потока. Что она из себя представляет? Это небольшая структура, которая говорит загрузчику PE-файлов о том, где находятся данные, которые должны быть выделены в памяти для каждого потока. Загрузчиком также производится вызов функции TlsAlloc, и значение, возвращенное ей, записывается по адресу, также указанному в этой структуре (называется это индексом). Кроме того, эта же структура может содержать адрес массива, хранящего набор коллбэков (адресов функций), которые будут вызваны загрузчиком при загрузке файла в память или при создании нового потока в процессе.

С TLS, признаться честно, все будет несколько хардкорнее, чем с остальным, так что приготовьтесь и напрягите мозг. Мой прошлый упаковщик TLS-коллбэки не поддерживал, трусливо выдавая сообщение о том, что они есть, но не обрабатываются. В принципе, поведение разумное, так как TLS-коллбеки имеют в основном всякие странные файлы, использующие эту вещь как антиотладочный прием. Ни один штатный линкер, вроде линкера от Майкрософт или Борланд, не поддерживают создание TLS-коллбэков. Тем не менее, для создания годного упаковщика мы их поддержку запилим.

Но начнем по порядку. Как обычно, будем править структуру packed_file_info (файл structures.h проекта simple_pe_packer). В нее на этот раз добавятся четыре поля:

//Сюда загрузчик будет записывать TLS-индекс
  DWORD tls_index;//Относительный адрес индекса TLS в оригинальном файле
  DWORD original_tls_index_rva;//Оригинальный адрес массива TLS-коллбэков в оригинальном файле
  DWORD original_rva_of_tls_callbacks;//Относительный адрес массива TLS-коллбэков в файле//после нашей модификации
  DWORD new_rva_of_tls_callbacks;

В этих полях мы будем хранить необходимые для распаковщика значения, связанные с TLS. Отдельно поясню про коллбэки. Поле AddressOfCallBacks структуры IMAGE_TLS_DIRECTORY указывает на массив абсолютных виртуальных адресов (т.е. на адреса, идущие друг за другом), которые, в свою очередь, указывают на функции, являющиеся коллбэками. Завершается этот массив нулевым элементом. Загрузчик по очереди дергает все функции в этом массиве при следующих событиях: создание процесса, создание потока, завершение потока, завершение процесса. Первый раз они вызываются даже до того, как процесс получит управление. Чтобы дать понять загрузчику, что в нашем упакованном файле есть TLS-коллбеки (разумеется, если они были в оригинальном), мы сделаем следующее: поле AddressOfCallBacks обнулять не будем, а запишем туда адрес массива, содержащий один-единственный пустой коллбэк (не нули, а реальный коллбэк, который ничего не делает). При загрузке упакованного образа в память этот коллбэк будет выполнен и загрузчик с этого момента будет знать, что у файла есть TLS-коллбэки. Если бы мы записали в поле AddressOfCallbacks ноль или указатель на пустой массив, то загрузчика впоследствии уже не смогли бы убедить в том, что коллбэки есть. А вот сам массив коллбэков можно будет впоследствии уже менять, так как загрузчик перечитывает его каждый раз, когда он ему становится нужен.

Индекс TLS и данные, которыми инициализируется память потока, мы переместим в свою секцию (которую мы назвали kaimi.ru, помните?), дабы они не перетерлись после распаковки, а непосредственно в распаковщике уже имеющийся от загрузчика индекс запишем туда, где он должен быть в оригинальном файле. Саму структуру IMAGE_TLS_DIRECTORY (точнее, IMAGE_TLS_DIRECTORY32, мы ведь пакуем 32-разрядные бинарники) мы также запишем в свою секцию. Туда же запишем и массив наших подложных коллбэков из единственного пустого, если они есть в оригинальном файле.

Переходим к написанию кода. После этого блока кода:

//Проверим, что файл реально стал меньшеif(out_buf.size()>= src_length){
      std::cout<<"File is incompressible!"<< std::endl;return-1;}

допишем следующее:

//Если файл имеет TLS, получим информацию о нем
    std::auto_ptr<pe_base::tls_info> tls;if(image.has_tls()){
      std::cout<<"Reading TLS..."<< std::endl;
      tls.reset(new pe_base::tls_info(image.get_tls_info()));}

Теперь добавим немного кода после места, где мы пересобираем ресурсы:

//Если у файла был TLSif(tls.get()){//Указатель на нашу структуру с информацией//для распаковщика//Эта структура в самом начале свежедобавленной секции,//мы ее туда добавили чуть раньше
        packed_file_info* info =reinterpret_cast<packed_file_info*>(&added_section.get_raw_data()[0]);
 
        //Запишем относительный виртуальный адрес//оригинального TLS
        info->original_tls_index_rva = tls->get_index_rva();
 
        //Если у нас были TLS-коллбэки, запишем в структуру//относительный виртуальный адрес их массива в оригинальном файлеif(!tls->get_tls_callbacks().empty())
          info->original_rva_of_tls_callbacks = tls->get_callbacks_rva();
 
        //Теперь относительный виртуальный адрес индекса TLS//будет другим - мы заставим загрузчик записать его в поле tls_index//структуры packed_file_info
        tls->set_index_rva(pe_base::rva_from_section_offset(added_section, offsetof(packed_file_info, tls_index)));}

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

Далее работаем с секцией "kaimi.ru", в которую мы раньше записывали только тело распаковщика. Добавим ей, во-первых, атрибут доступа на запись, заменив строку

      unpacker_section.readable(true).executable(true);

на

//Доступна на запись, чтение и исполнение
      unpacker_section.readable(true).executable(true).writeable(true);

Заменим также строку

const pe_base::section& unpacker_added_section = image.add_section(unpacker_section);

на

    pe_base::section& unpacker_added_section = image.add_section(unpacker_section);

потому что с этой секцией мы теперь будем работать.

Теперь на некоторое время перейдем к проекту распаковщика (unpacker). Опишу детально, как мы будем обрабатывать TLS, имеющий коллбеки. Мы сохраняем все оригинальные адреса TLS (это мы уже сделали). Далее мы создаем свой массив коллбэков, имеющий всего один коллбэк, да и тот пустой, чтобы просто дать понять загрузчику, что коллбэки есть. Мы в новом TLS указываем на этот массив. Далее, сразу после распаковки файла мы вручную выполняем все коллбэки оригинального файла, потому что загрузчик по понятным причинам этого не сделает - у него всего один пустой коллбэк. После этого мы правим массив коллбэков, созданный нами, записывая туда все адреса уже оригинальных функций, и с этого момента управление TLS-коллбэками переходит во власть загрузчика, нам больше делать ничего не надо. Таким образом, наша текущая задача - сделать в распаковщике пустой TLS-коллбэк. Дабы не плодить дополнительных функций, просто модифицируем пролог функции unpacker_main:

//Пролог вручную
  __asm
  {
    jmp next;
    ret 0xC;
next:
 
    push ebp;
    mov ebp, esp;
    sub esp, 256;}

Распаковщик, таким образом, начнет исполняться с инструкции jmp next, сразу перепрыгнув на свое основное тело. А вот тот самый пустой коллбэк, который нам нужен, выглядит как "ret 0xC", и на эту инструкцию мы сделаем указатель в массиве коллбэков. Эта инструкция просто отдает управление, предварительно убрав из стека 0xC = 12 байтов. Если кто-то не в курсе, прототип TLS-коллбэка выглядит так:

typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK)(
    PVOID DllHandle,
    DWORD Reason,
    PVOID Reserved
    );

и использует он соглашение вызовов stdcall и три четырехбайтовых параметра. Итого, мы должны убрать из стека 3 * 4 = 12 байтов. Коллбэк не возвращает значения, поэтому модифицировать регистр eax в его теле необязательно.

Теперь заменим все эти строки:

//Относительный виртуальный адрес директории импорта
  DWORD original_import_directory_rva;//Виртуальный размер директории импорта
  DWORD original_import_directory_size;//Относительный виртуальный адрес директории ресурсов
  DWORD original_resource_directory_rva;//Виртуальный размер директории ресурсов
  DWORD original_resource_directory_size;//Оригинальная точка входа
  DWORD original_entry_point;//Общий размер всех секций файла
  DWORD total_virtual_size_of_sections;//Количество секций в оригинальном файле
  BYTE number_of_sections;
 
  //Копируем эти значения из структуры packed_file_info,//которую для нас записал упаковщик
  original_import_directory_rva = info->original_import_directory_rva;
  original_import_directory_size = info->original_import_directory_size;
  original_resource_directory_rva = info->original_resource_directory_rva;
  original_resource_directory_size = info->original_resource_directory_size;
  total_virtual_size_of_sections = info->total_virtual_size_of_sections;
  number_of_sections = info->number_of_sections;

на одно копирование структуры, а то слишком много лишнего кода получается:

//Копируем все поля структуры packed_file_info, так как они нам будут//нужны далее, но структуру по указателю info мы скоро затрем
  packed_file_info info_copy;memcpy(&info_copy, info, sizeof(info_copy));

Заменим теперь все обращения к вышеперечисленным переменным соответствующим образом, меняя, например, original_import_directory_rva на info_copy.original_import_directory_rva.

Поправим файлик parameters.h, у нас изменились необходимые для упаковщика смещения, кроме того, добавилось еще одно:

#pragma once
 
staticconstunsignedint original_image_base_offset =0x11;staticconstunsignedint rva_of_first_section_offset =0x1B;staticconstunsignedint empty_tls_callback_offset =0x2;

Последнее смещение в коде распаковщика (empty_tls_callback_offset) - это как раз смещение на инструкцию ret, осуществляющую выход из TLS-коллбэка.

Идем дальше. В отличие от директории импортов и директории ресурсов, директорию TLS мы восстанавливать не будем - это бессмысленно. Загрузчик все равно ее не будет перечитывать. Перейдем к обработке TLS. Код будем размещать в распаковщике после той части, в которой фиксим импорты. Для начала скопируем индекс, который нам предоставил загрузчик, в ячейку памяти, где он должен лежать:

//Скопируем TLS-индексif(info_copy.original_tls_index_rva)*reinterpret_cast<DWORD*>(info_copy.original_tls_index_rva+ original_image_base)= info_copy.tls_index;

Далее - более сложная часть. Обработаем TLS-коллбэки:

if(info_copy.original_rva_of_tls_callbacks){//Если TLS имеет коллбэки
    PIMAGE_TLS_CALLBACK* tls_callback_address;//Указатель на первый коллбэк оригинального массива
    tls_callback_address =reinterpret_cast<PIMAGE_TLS_CALLBACK*>(info_copy.original_rva_of_tls_callbacks+ original_image_base);while(true){//Если коллбэк нулевой - это конец массиваif(!*tls_callback_address)break;
 
      //Скопируем в наш массив коллбэков//адрес оригинального*reinterpret_cast<PIMAGE_TLS_CALLBACK*>(info_copy.new_rva_of_tls_callbacks+ original_image_base)=*tls_callback_address;
 
      //Перейдем к следующему коллбэку++tls_callback_address;}
 
    //Вернемся на начало уже нового массива
    tls_callback_address =reinterpret_cast<PIMAGE_TLS_CALLBACK*>(info_copy.new_rva_of_tls_callbacks+ original_image_base);while(true){//Если коллбэк нулевой - это конец массиваif(!*tls_callback_address)break;
 
      //Вызовем коллбэк(*tls_callback_address)(reinterpret_cast<PVOID>(original_image_base), DLL_PROCESS_ATTACH, 0);
 
      //Перейдем к следующему коллбэку++tls_callback_address;}}

Сначала мы перечисляем все адреса коллбэков в оригинальном массиве и копируем их в наш массив TLS-коллбэков, чтобы загрузчик их подхватил, когда они понадобятся в следующий раз. Однако, при создании процесса загрузчик дернул только наш единственный пустой коллбэк, а PE-файл ожидает, что будут вызваны его коллбэки с параметром DLL_PROCESS_ATTACH. Поэтому нам нужен второй цикл, в котором мы вызываем все коллбэки из оригинального массива, передав в них базовый адрес загрузки образа в первом параметре и DLL_PROCESS_ATTACH (=1) во втором. Третий параметр не используется, смотрите прототип выше. Можно было бы, конечно, скопировать адреса коллбэков и вызвать их и в одном цикле, но вдруг в коллбэках бинарник модифицирует сам себя или ожидает, чтобы массив коллбэков был сразу заполнен? В любом случае, два цикла - тоже не панацея, но это более надежно.

С распаковщиком все, и теперь мы возвращаемся к упаковщику. Необходимо разместить директорию TLS в секции "kaimi.ru", а также скопировать туда файловые данные, использующиеся для инициализации локальных данных новых потоков.

//Если у файла есть TLSif(tls.get()){
        std::cout<<"Rebuilding TLS..."<< std::endl;
 
        //Ссылка на сырые данные секции распаковщика//Сейчас там есть только тело распаковщика
        std::string& data = unpacker_added_section.get_raw_data();
 
        //Изменим размер данных секции распаковщика ровно//по количеству байтов в теле распаковщика//(на случай, если нулевые байты с конца были обрезаны//библиотекой для работы с PE)
        data.resize(sizeof(unpacker_data));
 
        //Вычислим позицию, в которую запишем структуру IMAGE_TLS_DIRECTORY32
        DWORD directory_pos = data.size();//Выделим место под эту структуру//запас sizeof(DWORD) нужен для выравнивания, так как//IMAGE_TLS_DIRECTORY32 должна быть выровнена 4-байтовую на границу
        data.resize(data.size()+sizeof(IMAGE_TLS_DIRECTORY32)+sizeof(DWORD));
 
        //Если у TLS есть коллбэки...if(!tls->get_tls_callbacks().empty()){//Необходимо зарезервировать место//под оригинальные TLS-коллбэки//Плюс 1 ячейка под нулевой DWORD
          DWORD first_callback_offset = data.size();
          data.resize(data.size()+sizeof(DWORD)*(tls->get_tls_callbacks().size()+1));
 
          //Первый коллбэк будет нашим пустым (ret 0xC),//запишем его адрес*reinterpret_cast<DWORD*>(&data[first_callback_offset])=
            image.rva_to_va_32(pe_base::rva_from_section_offset(unpacker_added_section, empty_tls_callback_offset));
 
          //Запишем относительный виртуальный адрес//новой таблицы TLS-коллбэков
          tls->set_callbacks_rva(pe_base::rva_from_section_offset(unpacker_added_section, first_callback_offset));
 
          //Теперь запишем в структуру packed_file_info, которую мы//записали в самое начало первой секции,//относительный адрес новой таблицы коллбэковreinterpret_cast<packed_file_info*>(&image.get_image_sections().at(0).get_raw_data()[0])->new_rva_of_tls_callbacks = tls->get_callbacks_rva();}else{//Если нет коллбэков, на всякий случай обнулим адрес
          tls->set_callbacks_rva(0);}
 
        //Очистим массив коллбэков, они нам больше не нужны//Мы их сделали вручную
        tls->clear_tls_callbacks();
 
        //Установим новый относительный адрес//данных для инициализации локальной памяти потока
        tls->set_raw_data_start_rva(pe_base::rva_from_section_offset(unpacker_added_section, data.size()));//Пересчитываем адрес конца этих данных
        tls->recalc_raw_data_end_rva();
 
        //Пересобираем TLS//Указываем пересборщику, что не нужно писать данные и коллбэки//Мы сделаем это вручную (коллбэки уже записали, куда надо)//Также указываем, что не нужно обрезать нулевые байты в конце секции
        image.rebuild_tls(*tls, unpacker_added_section, directory_pos, false, false, pe_base::tls_data_expand_raw, true, false);
 
        //Дополняем секцию данными для инициализации//локальной памяти потока
        unpacker_added_section.get_raw_data()+= tls->get_raw_data();//Теперь установим виртуальный размер секции "kaimi.ru"//с учетом SizeOfZeroFill поля TLS
        image.set_section_virtual_size(unpacker_added_section, data.size()+ tls->get_size_of_zero_fill());//Наконец, обрежем уже ненужные нулевые байты с конца секции
        pe_base::strip_nullbytes(unpacker_added_section.get_raw_data());//и пересчитаем ее размеры (физический и виртуальный)
        image.prepare_section(unpacker_added_section);}

Опишу этот внушительный кусок кода. Сначала мы зарезервировали место под структуру IMAGE_TLS_DIRECTORY32 в последней секции с распаковщиком ("kaimi.ru") сразу после его кода, затем выделели место под массив TLS-коллбэков по их оригинальному количеству (каждый из них занимает 4 байта, плюс последний элемент - нулевой). В новый массив коллбэков записали указатель на код в распаковщике, который ничего не делает, кроме выравнивания стека (ret 0xC). Это даст понять загрузчику, что коллбэки у файла есть. Далее пересчитали указатели на данные, которые загрузчик будет использовать для инициализации локальных данных потоков. Мы размещаем эти данные после структуры IMAGE_TLS_DIRECTORY32 и массива TLS-коллбэков. Потом мы пересобираем TLS с помощью библиотеки для работы с PE (по сути, она просто записывает структуру IMAGE_TLS_DIRECTORY32 куда надо и заполняет ее, автоматическую запись коллбэков и данных мы отключили). Наконец, мы пересчитываем виртуальный размер секции с учетом значения поля SizeOfZeroFill в TLS оригинального файла (мы это значение не меняем). Я точно не могу сказать, как это поле обрабатывается (и описаний толковых в интернете, увы, не нашел) - зануляет ли загрузчик данные после EndAddressOfRawData прямо внутри секции, либо же после инициализации локальной памяти потока, но лучше перестраховаться и выделить место прямо в секции. На размер упакованного файла это не влияет, так как мы увеличиваем виртуальный размер секции, а не физический. После всего этого мы наконец-то обрезаем с конца секции ненужные нулевые байты (она последняя, и мы можем так сделать, об этом я уже как-то писал) и пересчитываем виртуальный и физический размеры секции (реально может поменяться только физический, так как виртуальный мы сами назначили, и он больше или равен физическому).

Уберем теперь добавленную раньше строку:

    image.remove_directory(IMAGE_DIRECTORY_ENTRY_TLS);

Остается протестировать работоспособность. Обработку TLS без коллбэков можно проверить на любой скомпилированной Борландом программе. Можно также собрать программу с помощью Microsoft Visual Studio любой версии, используя в коде __declspec(thread). TLS с коллбэками сделать не так просто, я пользовался примером отсюда, компилируя его в Visual C++ 6.0, хотя можно было бы собрать TLS с коллбэками руками и на MASM32. После небольшой проверки я удостоверился, что все работает, как надо!

Честно сказать, я заметил одну особенность, которая присуща всем упаковщикам, которые я опробовал - они все не меняют значение адреса индекса TLS. Не могу пока что сказать, почему это так, но вполне вероятно, что причина такого поведения есть. В комментариях в исходных кодах UPX сказано, что массив TLS-коллбэков должен быть также, как и структура IMAGE_TLS_DIRECTORY32, выровнен на границу DWORD'а, однако я не стал этого делать, так как даже на XP PE-файл с невыровненным массивом отлично работал.

Есть еще замечание по старому коду. Внезапно выяснилось, что Win XP плохеет, если урезаны директории данных в PE-заголовке (Data Directory), и его explorer.exe перестает отображать иконки у файлов. Поэтому придется закомментировать строку

//image.strip_data_directories(16 - 4); //Закомментировали из-за непереносимости в WinXP

для сохранения совместимости.

Полный солюшен для этого шага: Own PE Packer, step 6

Также рекомендую почитать

Пишем упаковщик по шагам. Шаг шестой. TLS. Обсудить на форуме


Источник: http://feedproxy.google.com/~r/kaimi/dev/~3/Ge43--WMNxA/

Читать комменты и комментировать

Добавить комментарий / отзыв



Защитный код
Обновить

Пишем упаковщик по шагам. Шаг шестой. TLS. | | 2012-09-21 23:39:00 | | Блоги и всяко-разно | | Предыдущий шаг здесь.Появилась новая версия библиотеки для работы с PE-файлами (0.1.5). Перекачайте и пересоберите ее.Пришло время заняться обработкой такой важной вещи, как Thread Local Storage (TLS) - локальной памяти потока. Что она из себя представляет? Эт | РэдЛайн, создание сайта, заказать сайт, разработка сайтов, реклама в Интернете, продвижение, маркетинговые исследования, дизайн студия, веб дизайн, раскрутка сайта, создать сайт компании, сделать сайт, создание сайтов, изготовление сайта, обслуживание сайтов, изготовление сайтов, заказать интернет сайт, создать сайт, изготовить сайт, разработка сайта, web студия, создание веб сайта, поддержка сайта, сайт на заказ, сопровождение сайта, дизайн сайта, сайт под ключ, заказ сайта, реклама сайта, хостинг, регистрация доменов, хабаровск, краснодар, москва, комсомольск |
 
Поделиться с друзьями: