На днях у меня спросили, как можно спрятать строку в исполняемом файле, чтобы "обратный инженер" не смог ее найти? Вопрос дилетантский, но так совпало, что в тот день я решал очередной челлендж на Hack The Box. Задание называется Bombs Landed и основная его изюминка в функции, которая динамически подгружалась в память. Из-за этого Ghidra не может найти и декомпилировать код.

Решение Bombs Landed

Полное решение таска можно посмотреть на моем YouTube канале

В тот момент и родилась идея написать программу с динамически загружаемой функцией, но сделать это на максимальном уровне сложности используя только Netwide Assembler (NASM).

Какие задачи будут решены в итоге?

  1. Мы получим частичный ответ на вопрос в начале статьи;

  2. Ознакомимся с программой на низком уровне;

  3. Познакомимся с синтаксисом 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 в статическом режиме. Но что, если программа будет выделять область памяти, помещать туда байты функции из секретного места, после чего вызывать эту функцию. В таком случае, статический анализ файла не покажет нам эту функцию, а значит обратная разработка становиться интересней.

Вижу цель, не вижу препятствий!

Задача понятна:

  1. Выделить место в памяти;

  2. Узнать первый адрес выделенного пространства;

  3. Положить туда байты функции;

  4. Вызвать их, прочитать флаг;

  5. Освободить память не сегодня.

Первым делом разберемся с секретным местом, из которого мы будем брать байты нашей функции. В рамках этой статьи я ограничусь размещением нашей функции внутри массива. На практике, в качестве секретного места, можно использовать массив + шифр цезаря, стеганографию или вообще подгружать байты из интернета. Все это мы будем делать под операционную систему 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)


  1. PrinceKorwin
    15.10.2022 14:51
    +19

    Обратные инженеры? Really?


    1. s_f1
      15.10.2022 20:27
      +32

      Если инженер находится в поле людей, то для него, если он не полный ноль, будет существовать и обратный инженер. Обозначается «инженер⁻¹».


  1. Sam839
    15.10.2022 15:07
    +1

    Я думаю, что большинство реверс-инженеров не будут использовать псевдокод.


    1. notrobot1 Автор
      15.10.2022 15:12

      Думаю это зависит от опыта человека. Мы как то реверсили таск по видео связи, там был парень который открыл Иду и мы все дружно смотрели как он пытается понять псевдо код.


    1. notrobot1 Автор
      15.10.2022 15:15
      +3

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


    1. ktod
      16.10.2022 09:26
      +1

      Глупость. Мало того, что псевдокод - это основа анализа. Так еще и для простых архитектур, типа avr, свой декомпилятор можно довести до такого уровня, что исследуемый псевдокод успешно компилируется gcc обратно в рабочий бинарь.


    1. VelocidadAbsurda
      16.10.2022 11:56

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

      Говорю об IDA. Интересной выглядит идея нескольких уровней псевдокода в Binary Ninja, но в нынешнем его ещё зачаточном состоянии пока нормально оценить не получалось.


  1. lumag
    15.10.2022 15:54
    +6

    Как это сосуществует с битом NX? По идее же куча должна помечаться как RW, но без возможности выполнения, соответственно malloc должен вернуть не исполняемую память.


    1. notrobot1 Автор
      15.10.2022 16:35

      Хотел бы написать "работает не трогай" но нет. Действительно malloc выдает память без возможности выполнения. Мне подсказали, что на это могут влиять флаги в заголовке exe файла. Сейчас глубже изучу этот вопрос и дополню статью. Спасибо за полезный комментарий.


      1. nikolayz
        16.10.2022 10:20
        +1

        Поскольку речь тут про Windows, для выполняемого кода лучше выделять память через VirtualAlloc с флагом PAGE_EXECUTE (ну или поменять для уже выделенной области права доступа с помощью VirtualProtect). В POSIX для этой же цели можно использовать mmap и mprotect.


        1. 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 мало кто нажимает, а любое окно может прослушивать весь ввод...)


  1. fk0
    15.10.2022 16:21
    +8

    Не понял про что статья вообще. Про мелкий трюк как обмануть дизассемблер? Ваш код посадят в виртуальную машину и запустят 100500 разными способами. И там будет видно, что оно пришло в конечном счёте куда нужно.

    Почему, наконец, код нельзя написать хотя бы на C. Ассемблер нужен там, где он действительно нужен (где другое abi, обработчики прерывания, стартап, шелл-код, аудио-видео кодеки), а не абсолютно везде.


    1. notrobot1 Автор
      15.10.2022 16:42
      +1

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


  1. fk0
    15.10.2022 16:34
    +3

    Вдогонку. Наверное разумней если стоит задача что-то скрыть иметь что-то наподобии фрактальной "матрёшки" рекурсивно-вложенных интерпретаторов. Байт-кода, форт-машин. Да не принципиально. Например, мне кажестся удобным webassembly (в том смысле, что оно может скомпилировать свой собственный же интерпретатор). Чтоб размотка логики вручную уже попросту не представлялась возможной, а автоматизированно -- упиралась в объёмы памяти анализирующей системы. Здесь подразумевается, что пространство состояний такой системы быстро возрастает до астрономических величин, из-за чего её анализ становится сложной задачей. Разумеется каждый слой матрёшки может и должен иметь какие-то вариации относительно предыдущего слоя, чтоб анализатор не воспользовался фактом, что уровни тупо рекурсивно повторяются. Наверное это не сложно реализовать трансформируя исходные тексты интерпретатора и компилятора по какому-то подмножеству из набора вручную заданных правил.


    1. notrobot1 Автор
      15.10.2022 16:45
      +1

      Спасибо. Я ознакомлюсь с этой технологией.


  1. 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;
       }
    


    1. fk0
      16.10.2022 14:51
      +1

      Зачем так страшно-то. Код который в code[] помещается можно скомпилировать просто компилятором, как функцию или асмовую вставку, не важно. И потом копировать куда нужно через memcpy...

      Если нужно, чтоб не было видно, что он вызывается, то его можно и не вызывать. Что вызовет проблемы с -flto и -Wl,-gc-sections и -ffunction-sections. Ну так можно скомпилировать в Makefile за отдельный заход. Или атрибут поставить __attribute((used))__. И потом как хекс-коды включить в массив через `#include` (или в асме через .incbin) прямо в виде файла. Да и вообще ещё и зашифровать можно.


      1. Kotofay
        16.10.2022 15:29

        И потом копировать куда нужно через memcpy...

        Абсолютные адреса в скомпилированном бинарнике придётся исправлять.

        Тут принципиальное отличие от готового скомпилированного кода -- его изменяемость на лету.

        Можно вставить исходники TCC в программу и им компилировать С-шный код, и сразу выполнять.


  1. Justlexa
    15.10.2022 20:16
    +9

    Помимо уже озвученной загвоздки с NX, «скрываемый» x86-код базозависим, (база 0x400000 в приведённом примере, четыре инструкции), релоки к нему не применяются ни загрузчиком (ведь компоновщик не сделает для такого «кода» таблицу релоков), ни вызывающим кодом.
    В простейшем случае ASLR и свежие Mitigation Policies просто заставят конечную программу засегфолтить на попытке исполнения такого кода почти со 100% вероятностью на всём свежее WinXP/2k3.


  1. vak0
    15.10.2022 23:28
    +6

    Давным-давно, когда программы еще запускали под DOS-ом, помню, придумывал разные способы борьбы с отладчиками. Например, такой: пишем байты команды jump сразу после текущей выполняемой команды. Если мы под отладчиком, то уходим по этому только что записанному jump-у, а вот если не под отладчиком, то эти только что записанные байты игнорируются и проц выполняет то, что по этому адресу лежало раньше, поскольку чуть ранее он занес старые команды себе в кэш и выполняет именно их.
    Предварительно на всякий случай запрещаем все прерывания.