Прячем функцию от глаз исследователей

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

Решение Bombs Landed

Полное решение таска можно посмотреть на моем YouTube канале: https://youtu.be/Qdj08RRN4fA

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

f62746f43b4fb13d9c00c3ccf2578049.png

Да, это наша функция 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.

c6f5bbd36ca7ce898619a1f85adea9ce.png

Видим, что нашей скрытой функции был присвоен идентификатор FUN_00402084. Откроем функцию.

42a09d16f7fba2343a3efa01cc3ccf00.png

Получилось! Вместо осмысленного кода мы видим несвязанные команды. Протестировать можно здесь.

Примечание.

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

Дополнение.

В комментариях верно подметили, что malloc выделяет область в памяти и делает ее не исполняемой. За это отвечает флаг NX_COMPAT. Компилятор выставляет его в зависимости от настроек. Так же на поведение могут повлиять настройки операционной системы. В случае с Windows: Параметры быстродействия > Предотвращение выполнения данных.

© Habrahabr.ru