На днях у меня спросили, как можно спрятать строку в исполняемом файле, чтобы "обратный инженер" не смог ее найти? Вопрос дилетантский, но так совпало, что в тот день я решал очередной челлендж на Hack The Box. Задание называется Bombs Landed
и основная его изюминка в функции, которая динамически подгружалась в память. Из-за этого Ghidra не может найти и декомпилировать код.
Решение Bombs Landed
Полное решение таска можно посмотреть на моем YouTube канале
В тот момент и родилась идея написать программу с динамически загружаемой функцией, но сделать это на максимальном уровне сложности используя только Netwide Assembler (NASM).
Какие задачи будут решены в итоге?
Мы получим частичный ответ на вопрос в начале статьи;
Ознакомимся с программой на низком уровне;
Познакомимся с синтаксисом NASM.
Как видим, практической пользы нет. Все, что мы получим в конце - программа, которая выводит в консоль flag{qwerty123}
и знания, которые обязательно пригодятся в обратной разработке.
Что такое динамически подгружаемая функция?
Как известно, функция это - подпрограмма, которая имеет набор инструкций, которые лежат на своих адресах. Функцию можно вызвать из любого места в программе если нам известен ее адрес. Вот так выглядит функция в hex.
1400122d0 : 40 55 57 48 81 ec 08 01 00 00 48 8d 6c 24 20 ... c3
1400122d0
- первый адрес нашей функции;c3
- return;
Это обычная функция, байты которой лежат в бинарном файле по адресу 1400122d0
и мы легко можем посмотреть их через r2, IDA, Ghidra в статическом режиме. Но что, если программа будет выделять область памяти, помещать туда байты функции из секретного места, после чего вызывать эту функцию. В таком случае, статический анализ файла не покажет нам эту функцию, а значит обратная разработка становиться интересней.
Вижу цель, не вижу препятствий!
Задача понятна:
Выделить место в памяти;
Узнать первый адрес выделенного пространства;
Положить туда байты функции;
Вызвать их, прочитать флаг;
Освободить памятьне сегодня.
Первым делом разберемся с секретным местом, из которого мы будем брать байты нашей функции. В рамках этой статьи я ограничусь размещением нашей функции внутри массива. На практике, в качестве секретного места, можно использовать массив + шифр цезаря, стеганографию или вообще подгружать байты из интернета. Все это мы будем делать под операционную систему Windows.
Пишем секретную функцию
Наша секретная функция будет выводить в терминал строку. Давайте напишем такую функцию на языке ассемблер.
;https://www.nasm.us/xdoc/2.11.08/html/nasmdoc6.html
NULL EQU 0
;EXTERN Импорт символов из других модулей
extern _ExitProcess@4
extern _WriteFile@20
extern _GetStdHandle@4
;ucrtbased.dll
;https://strontic.github.io/xcyclopedia/library/ucrtbase.dll-ED27C615D14DADBE15581E8CB7ABBE1C.html
extern _o_malloc
;Экспорт символов в другие модули
global Start
;инициализированные данные
section .data
Message db "flag{qwerty123}", 0Dh, 0Ah ; Объявляем строк
;неинициализированные данные
section .bss
StandardHandle resd 1
Written resd 1
;Code
section .text
; Функция выводит на экран
Print:
push edi
push ecx
push -11
call _GetStdHandle@4
mov dword [StandardHandle], eax
push NULL
push Written
;mov ecx, 15
;mov edi, Hidden+37
push ecx ;длина текста для вывода на экран
push edi ;текст для вывода на экран
push dword [StandardHandle]
call _WriteFile@20
pop ebx
pop ecx
ret
; Главная функция
Start:
mov ecx, 15 ; помещаем длину строки в eсx
mov edi, Message ; кладем переменную с текстом
call Print
; Завершение программы
exit:
push NULL
call _ExitProcess@4
Выше мы видим две функции:
Start - главная функция (точка входа);
Print - функция, которую мы в дальнейшем скроем от глаз любопытных исследователей.
Давайте скомпилируем этот код.
.\nasm.exe -fwin32 .\malloc.asm
.\GoLink.exe /entry:Start /console kernel32.dll user32.dll ucrtbased.dll malloc.obj
На выходе получится файл malloc.exe. Если запустить файл в терминале, мы увидим выхлоп в консоль: flag{qwerty123}
.
Откроем данный exe файл в x32dbg.
На скриншоте выше, красным цветом, я выделил функцию Start
, оранжевым цветом обвел функцию Print
. Видим место вызова функции Print
и ее первый адрес. Все, что между адресами 00401000 - 00401024 нужно поместить в наш скрытый массив.
Делаем массив в секции .data
.
;объвляем массив string 38
array dw 0x57, 0x51, 0x6a, 0xf5, 0xe8, 0xf9, 0x1f, 0x00, 0x00, 0xa3, 0x38, 0x20, 0x40, 0x00, 0x6a, 0x00, 0x68, 0x3c, 0x20, 0x40, 0x00, 0xb9, 0x0f, 0x00, 0x00, 0x00, 0xbf, 0xb3, 0x20, 0x40, 0x00, 0x51, 0x57, 0xff, 0x35, 0x38, 0x20, 0x40, 0x00, 0xe8, 0xda, 0x1f, 0x00, 0x00, 0x5b, 0x59, 0xc3, 0x66, 0x6c, 0x61, 0x67, 0x7b, 0x71, 0x77, 0x65, 0x72, 0x74, 0x79, 0x31, 0x32, 0x33, 0x7d
Здесь dw
означает,что каждый элемент массива занимает 2 байта. Если вы пролистаете массив в конец, то заметите, что он не заканчивается на c3
. После c3
я добавил нашу строку с флагом.
Заметка
Строки желательно заканчивать нулевым байтом, что я успешно проигнорировал во время написания кода.
В секции .bss
добавим еще две переменные.
;поинтер на скрытую функцию
Hidden resq 1
;переменная хранит перевернутые смещения
Reverse resd 1
В переменную Hidden
мы поместим указатель на выделенную область памяти. Переменная Reverse
понадобиться нам чуть позже.
Давайте выделим область в памяти. Для этого я буду использовать malloc
.
;malloc 1000
push 0x3e8
call _o_malloc
;кладем поинтер на выделенные адреса в переменную Hidden
mov [Hidden], eax
Теперь в переменной Hidden
у нас указатель на выделенные 1000 адресов. Напишем цикл и поместим байты в выделенное пространство.
;начинаем цикд
mov edx, 0x0
M1:
;т.к. у нас каждый элемент массива занимает 2 байта *2
mov cx, [array+edx*2]
;поинтер на маллок + номер итерации кладем байт из массива
mov byte [Hidden+edx], cl
;увеличиваем счетчик
add edx, 0x1
;сравниваем edx с 62 (длина массива)
cmp edx, 0x3e
;если edx не равно 62 (0x3e) то повторяем M1
jne M1
;вызываем скрытую функцию
call Hidden
Соберем это чудо и посмотрим в отладчик.
0040105E |
E8 21100000 |
call calloc.402084 |
Вот наш адрес, на котором мы вызываем переменную Hidden. Давайте перейдем по этому адресу и посмотрим, что мы положили в выделенную память.
Да, это наша функция Print
! Но что вот это?
00402088 |
E8 F91F0000 |
call 404086 |
|
004020AB |
E8 DA1F0000 |
call 40408A |
Почему вместо call _GetStdHandle@4
и call _WriteFile@20
мы видим вызов других адресов? Дело в том, что адреса вызова функции высчитываются по формуле:
Адрес который нужно вызвать - размер команды (call = 5) - адрес откуда вызываем
Например нам нужно вызвать адрес 0000000000459340 из 0000000000D610C8.
0000000000459340 - 5 - 0000000000D610C8 = FFFF FFFF FF6F 8273 и переворачиваем байты.
call 73826fff
Поскольку выделенные адреса всегда разные, мы не можем заранее узнать смещение, а значит нам надо изменить смещение команды call
, после команды malloc
.
Напишем для этого отдельную функцию:
;расчитываем смещение вызова
Calculation:
push ebp
;ecx - что мы хотим вызвать
mov ecx, esi
;от адреса который мы хотим вызвать отнимаем размер команды (5)
sub ecx, 5
;от результата отнимаем адресс который хотим вызвать
;ecx хранит смещение которое надо перевернуть
sub ecx, eax
;кладем в переменную перевернутые байты
;+100 - перемещаем переменную что бы она не наехала на массив array
mov [Reverse+100], ecx
;расчитаный адрес смещения кладем в edx
mov edx, [Reverse+100+0]
;eax = e8 (команда call). eax+1 первый байт адреса, кладем в него младший бит edx
mov [eax+1], dl
;Перезаписываем edx следующим расчитаным байтом
mov edx, [Reverse+100+1]
mov [eax+2], dl
mov edx, [Reverse+100+2]
mov [eax+3], dl
mov edx, [Reverse+100+3]
mov [eax+4], dl
pop ebp
ret
Вызвать эту функцию можно следующим образом:
mov eax, Hidden+4 ;откуда вызываем
mov esi, 0x0040300c ;что вызываем
call Calculation
Hidden+4
- адрес, откуда мы вызываем функцию.
0x0040300c
- адрес, который мы хотим вызвать (call _GetStdHandle@4
). Тоже самое надо проделать и с call _WriteFile@20
.
Финальный код
;https://www.nasm.us/xdoc/2.11.08/html/nasmdoc6.html
NULL EQU 0
;EXTERN Импорт символов из других модулей
extern _ExitProcess@4
extern _WriteFile@20
extern _GetStdHandle@4
;ucrtbased.dll
;https://strontic.github.io/xcyclopedia/library/ucrtbase.dll-ED27C615D14DADBE15581E8CB7ABBE1C.html
extern _o_malloc
;Экспорт символов в другие модули
global Start
;инициализированные данные
section .data
;объвляем массив string 38
array dw 0x57, 0x51, 0x6a, 0xf5, 0xe8, 0xf9, 0x1f, 0x00, 0x00, 0xa3, 0x38, 0x20, 0x40, 0x00, 0x6a, 0x00, 0x68, 0x3c, 0x20, 0x40, 0x00, 0xb9, 0x0f, 0x00, 0x00, 0x00, 0xbf, 0xb3, 0x20, 0x40, 0x00, 0x51, 0x57, 0xff, 0x35, 0x38, 0x20, 0x40, 0x00, 0xe8, 0xda, 0x1f, 0x00, 0x00, 0x5b, 0x59, 0xc3, 0x66, 0x6c, 0x61, 0x67, 0x7b, 0x71, 0x77, 0x65, 0x72, 0x74, 0x79, 0x31, 0x32, 0x33, 0x7d
;неинициализированные данные
section .bss
StandardHandle resd 1
Written resd 1
;поинтер на скрытую функцию
Hidden resq 1
;переменная хранит перевернутые смещения
Reverse resd 1
;Code
section .text
;расчитываем смещение вызова
Calculation:
push ebp
;ecx - что мы хотим вызвать
mov ecx, esi
;от адреса который мы хотим вызвать отнимаем размер команды (5)
sub ecx, 5
;от результата отнимаем адресс который хотим вызвать
;ecx хранит смещение которое надо перевернуть
sub ecx, eax
;кладем в переменную перевернутые байты
;+100 - перемещаем переменную что бы она не наехала на массив array
mov [Reverse+100], ecx
;расчитаный адрес смещения кладем в edx
mov edx, [Reverse+100+0]
;eax = e8 (команда call). eax+1 первый байт адреса, кладем в него младший бит edx
mov [eax+1], dl
;Перезаписываем edx следующим расчитаным байтом
mov edx, [Reverse+100+1]
mov [eax+2], dl
mov edx, [Reverse+100+2]
mov [eax+3], dl
mov edx, [Reverse+100+3]
mov [eax+4], dl
pop ebp
ret
; Функция выводит на экран
;Print:
; push edi
; push ecx
; push -11
; call _GetStdHandle@4
; mov dword [StandardHandle], eax
; push NULL
; push Written
; mov ecx, 15
; mov edi, Hidden+37
; push ecx ;длина текста для вывода на экран
; push edi ;текст для вывода на экран
; push dword [StandardHandle]
; call _WriteFile@20
; pop ebx
; pop ecx
; ret
; Главная функция
Start:
;malloc 1000 flhtcjd
push 0x3e8
call _o_malloc
;кладем поинтер на выделенные адреса в переменную Hidden
mov [Hidden], eax
;начинаем цикд
mov edx, 0x0
M1:
;т.к. у нас каждый элемент массива занимает 2 байта *2
mov cx, [array+edx*2]
;поинтер на маллок + номер итерации кладем байт из массива
mov byte [Hidden+edx], cl
;увеличиваем счетчик
add edx, 0x1
;сравниваем edx с 62
cmp edx, 0x3e
;если edx не равно 62 (0x3e) то повторяем M1
jne M1
mov eax, Hidden+4 ;откуда вызываем
mov esi, 0x0040300c ;что вызываем
call Calculation
;+39 адресс команды е8 из массива
mov eax, Hidden+39
mov esi, 0x00403006
call Calculation
;Вызываем динамическую функцию
call Hidden
;надо сделать free
; Завершение программы
exit:
push NULL
call _ExitProcess@4
Давайте посмотрим как это выглядит в Ghidra.
Видим, что нашей скрытой функции был присвоен идентификатор FUN_00402084
. Откроем функцию.
Получилось! Вместо осмысленного кода мы видим несвязанные команды. Протестировать можно здесь.
Примечание.
Поскольку я никак не обфусцировал наш массив, Ghidra все еще видит наш скрытый флаг в строках. Что бы избежать этого, нужно проявить фантазию и не хранить байты поочередно.
Дополнение.
В комментариях верно подметили, что malloc выделяет область в памяти и делает ее не исполняемой. За это отвечает флаг NX_COMPAT
. Компилятор выставляет его в зависимости от настроек. Так же на поведение могут повлиять настройки операционной системы. В случае с Windows: Параметры быстродействия > Предотвращение выполнения данных.
Комментарии (20)
Sam839
15.10.2022 15:07+1Я думаю, что большинство реверс-инженеров не будут использовать псевдокод.
notrobot1 Автор
15.10.2022 15:12Думаю это зависит от опыта человека. Мы как то реверсили таск по видео связи, там был парень который открыл Иду и мы все дружно смотрели как он пытается понять псевдо код.
notrobot1 Автор
15.10.2022 15:15+3Ну и по своему опыту хочу сказать, что любой реверс начинается с псевдо кода. Если ничего не понятно то двигаюсь дальше.
ktod
16.10.2022 09:26+1Глупость. Мало того, что псевдокод - это основа анализа. Так еще и для простых архитектур, типа avr, свой декомпилятор можно довести до такого уровня, что исследуемый псевдокод успешно компилируется gcc обратно в рабочий бинарь.
VelocidadAbsurda
16.10.2022 11:56Поддержу. Наверное, кто к чему привык, мне псевдокод в целом воспринимается как нечто куда менее предсказуемое по результату. На сложном асме можно просто сбавить темп и разобраться не спеша, комментариев написать себе напротив сложных моментов, перечитать ещё раз итд. Псевдокод же если вдруг выдал концентрированный нечитаемый бред - страдай и проглатывай большими кусками.
Говорю об IDA. Интересной выглядит идея нескольких уровней псевдокода в Binary Ninja, но в нынешнем его ещё зачаточном состоянии пока нормально оценить не получалось.
lumag
15.10.2022 15:54+6Как это сосуществует с битом NX? По идее же куча должна помечаться как RW, но без возможности выполнения, соответственно malloc должен вернуть не исполняемую память.
notrobot1 Автор
15.10.2022 16:35Хотел бы написать "работает не трогай" но нет. Действительно malloc выдает память без возможности выполнения. Мне подсказали, что на это могут влиять флаги в заголовке exe файла. Сейчас глубже изучу этот вопрос и дополню статью. Спасибо за полезный комментарий.
nikolayz
16.10.2022 10:20+1Поскольку речь тут про Windows, для выполняемого кода лучше выделять память через VirtualAlloc с флагом PAGE_EXECUTE (ну или поменять для уже выделенной области права доступа с помощью VirtualProtect). В POSIX для этой же цели можно использовать mmap и mprotect.
fk0
16.10.2022 15:17Ещё можно сгенерировать временный файл в виде .DLL/.SO и подгрузить его через dlopen/LoadLibary. Или вовсе сделать EXE и запустить его.
Только в какой-нибудь системе защиты и будут как раз хуки на такие операции как exec, dlopen и mprotect. Ещё в виндах замечательная функция создания треда в чужом процессе. А в линуксе -- ptrace(). Ещё проверка переменных окружения где можно напихать чего-то что обработает ld.so (LD_PRELOAD, LD_LIBRARY_PATH, в виндах ещё PATH). В виндах ещё проверка текущего каталога (входит в пути поиска библиотек).
Только для того, чтоб выполнить свои целевые задачи эти операции на самом деле не нужны скорей, они только вызывают подозрение.
Два тезиса:
1. Машинный код скорей вреден, не удобен, легко поддаётся статическому анализу, в т.ч. и моментальному, во время исполнения, особенно неудобны функции загрузки библиотек, где сразу виден список функций которые намеревается вызывать код (что неудобно, хотя можно импортировать какую-то одну функцию, а остальные вычислить по оффсетам для известных версий библиотек).
2. Лучше использовать существующие легитимные приложения, которые сами по себе заранее импортируют очень широкий список функций. Например, интерпретатор какого-либо ЯВУ. Или "жирное" приложение с миллионом функций. Вопрос интеграции своего кода в это приложение. Для интерпретаторов ЯВУ, очевидно, тривиальная задача (и там есть функция "eval" -- самомодификация кода становится тривиальной). И в этом плане особенный интерес представляет собственно "шелл" в котором работает юзер. В виндах правда есть UAC, но в линуксе попроще (начиная с того, что пароли вводят в терминал как есть, кнопочку SysRq в xterm мало кто нажимает, а любое окно может прослушивать весь ввод...)
fk0
15.10.2022 16:21+8Не понял про что статья вообще. Про мелкий трюк как обмануть дизассемблер? Ваш код посадят в виртуальную машину и запустят 100500 разными способами. И там будет видно, что оно пришло в конечном счёте куда нужно.
Почему, наконец, код нельзя написать хотя бы на C. Ассемблер нужен там, где он действительно нужен (где другое abi, обработчики прерывания, стартап, шелл-код, аудио-видео кодеки), а не абсолютно везде.
notrobot1 Автор
15.10.2022 16:42+1Разумеется если запустить программу под отладкой мы получим наш флаг. В этом и заключается суть. Статья показывает как можно запутать человека в решении таска. Ассемблер выбран с целью обучения, наглядно показать поведение программы.
fk0
15.10.2022 16:34+3Вдогонку. Наверное разумней если стоит задача что-то скрыть иметь что-то наподобии фрактальной "матрёшки" рекурсивно-вложенных интерпретаторов. Байт-кода, форт-машин. Да не принципиально. Например, мне кажестся удобным webassembly (в том смысле, что оно может скомпилировать свой собственный же интерпретатор). Чтоб размотка логики вручную уже попросту не представлялась возможной, а автоматизированно -- упиралась в объёмы памяти анализирующей системы. Здесь подразумевается, что пространство состояний такой системы быстро возрастает до астрономических величин, из-за чего её анализ становится сложной задачей. Разумеется каждый слой матрёшки может и должен иметь какие-то вариации относительно предыдущего слоя, чтоб анализатор не воспользовался фактом, что уровни тупо рекурсивно повторяются. Наверное это не сложно реализовать трансформируя исходные тексты интерпретатора и компилятора по какому-то подмножеству из набора вручную заданных правил.
Kotofay
15.10.2022 19:58+6Ассемблер на этом уровне никакой не нужен.
Пользуйтесь на здоровье готовыми функциями.
Например вот так (x32):
// memory class: global int dword = 0; <...> { // создание машинного кода динамически const size_t CODE_SIZE = 1024; int PC = 0; // memory class: auto int dword_auto = 0; unsigned char *code = new unsigned char[ CODE_SIZE ]; // memory class: heap int *dword_ptr_heap = new int( 0 ); int *dword_ptr = &dword; void( *func ) ( ) = ( void( *) ( ) ) ( code + PC ); qDebug() << "Init : " << dword << dword_auto << *dword_ptr << *dword_ptr_heap; // создаём код функции // пролог стандартной функции С (stdcall) //code[ PC++ ] = 0x55; // PUSH EBP //code[ PC++ ] = 0x8B; // MOV EBP, ESP //code[ PC++ ] = 0xEC; // // сохранение всех регистров и флагов code[ PC++ ] = 0x9C; // PUSHFD code[ PC++ ] = 0x60; // PUSHAD ////////////////////////////////////////////////////////////////////////// // +++ STACK AUTO VARIABLE // узнать адрес начала фрейма текущего стека( годен только для генерации внутри тела функции ) unsigned rEBP; __asm mov rEBP, ebp; unsigned ptr_auto = unsigned( &dword_auto ) - unsigned( rEBP ); // CODE: dword++ code[ PC++ ] = 0x8B; // MOV ECX, [ DWORD PTR ] code[ PC++ ] = 0x4D; code[ PC++ ] = ( unsigned char ) ( ptr_auto & 0xFF ); code[ PC++ ] = 0x83; // ADD ECX, 1 code[ PC++ ] = 0xC1; // code[ PC++ ] = 0x01; // code[ PC++ ] = 0x89; // MOV [ DWORD PTR ], ECX code[ PC++ ] = 0x4D; // code[ PC++ ] = ( unsigned char ) ( ptr_auto & 0xFF ); // --- ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// // +++ GLOBAL VARABLE //A1 5C 0C 0B 01 mov eax,dword ptr [dword ()] //83 C0 01 add eax,1 //A3 5C 0C 0B 01 mov dword ptr [dword ()],eax code[ PC++ ] = 0xA1; // MOV EAX, [ DWORD PTR ] *( unsigned* ) ( code + PC ) = unsigned( &dword ); PC += sizeof( &dword ); code[ PC++ ] = 0x83; // ADD EAX, 1 code[ PC++ ] = 0xC0; // code[ PC++ ] = 0x01; // code[ PC++ ] = 0xA3; // MOV [ DWORD PTR ], EAX *( unsigned* ) ( code + PC ) = unsigned( &dword ); PC += sizeof( &dword ); // --- ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// // +++ HEAP VARABLE //A1 5C 0C 0B 01 mov eax,dword ptr [dword ()] //83 C0 01 add eax,1 //A3 5C 0C 0B 01 mov dword ptr [dword ()],eax code[ PC++ ] = 0xA1; // MOV EAX, [ DWORD PTR ] *( unsigned* ) ( code + PC ) = unsigned( dword_ptr_heap ); PC += sizeof( *dword_ptr_heap ); code[ PC++ ] = 0x83; // ADD EAX, 1 code[ PC++ ] = 0xC0; // code[ PC++ ] = 0x01; // code[ PC++ ] = 0xA3; // MOV [ DWORD PTR ], EAX *( unsigned* ) ( code + PC ) = unsigned( dword_ptr_heap ); PC += sizeof( *dword_ptr_heap ); // --- ////////////////////////////////////////////////////////////////////////// // восстановление всех регистров и флагов code[ PC++ ] = 0x61; // POPAD code[ PC++ ] = 0x9D; // POPFD // стандартный эпилог С (stdcall) //code[ PC++ ] = 0x5D; // POP EBP code[ PC++ ] = 0xC3; // RET // помечаем эту страницу памяти как исполняемую+чтение+запись unsigned long old_protect; ::VirtualProtect( code, CODE_SIZE, PAGE_EXECUTE_READWRITE, &old_protect ); // выполняем машинный код сгенерированный автоматически func(); delete[] code; qDebug() << "Result : " << dword << dword_auto << *dword_ptr << *dword_ptr_heap; }
fk0
16.10.2022 14:51+1Зачем так страшно-то. Код который в
code[]
помещается можно скомпилировать просто компилятором, как функцию или асмовую вставку, не важно. И потом копировать куда нужно через memcpy...Если нужно, чтоб не было видно, что он вызывается, то его можно и не вызывать. Что вызовет проблемы с
-flto
и-Wl,-gc-sections
и-ffunction-sections
. Ну так можно скомпилировать в Makefile за отдельный заход. Или атрибут поставить__attribute((used))__
. И потом как хекс-коды включить в массив через `#include` (или в асме через.incbin
) прямо в виде файла. Да и вообще ещё и зашифровать можно.Kotofay
16.10.2022 15:29И потом копировать куда нужно через memcpy...
Абсолютные адреса в скомпилированном бинарнике придётся исправлять.
Тут принципиальное отличие от готового скомпилированного кода -- его изменяемость на лету.
Можно вставить исходники TCC в программу и им компилировать С-шный код, и сразу выполнять.
Justlexa
15.10.2022 20:16+9Помимо уже озвученной загвоздки с NX, «скрываемый» x86-код базозависим, (база 0x400000 в приведённом примере, четыре инструкции), релоки к нему не применяются ни загрузчиком (ведь компоновщик не сделает для такого «кода» таблицу релоков), ни вызывающим кодом.
В простейшем случае ASLR и свежие Mitigation Policies просто заставят конечную программу засегфолтить на попытке исполнения такого кода почти со 100% вероятностью на всём свежее WinXP/2k3.
vak0
15.10.2022 23:28+6Давным-давно, когда программы еще запускали под DOS-ом, помню, придумывал разные способы борьбы с отладчиками. Например, такой: пишем байты команды jump сразу после текущей выполняемой команды. Если мы под отладчиком, то уходим по этому только что записанному jump-у, а вот если не под отладчиком, то эти только что записанные байты игнорируются и проц выполняет то, что по этому адресу лежало раньше, поскольку чуть ранее он занес старые команды себе в кэш и выполняет именно их.
Предварительно на всякий случай запрещаем все прерывания.
PrinceKorwin
Обратные инженеры? Really?
s_f1
Если инженер находится в поле людей, то для него, если он не полный ноль, будет существовать и обратный инженер. Обозначается «инженер⁻¹».