Imported Code


Что такое код импорта?

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


Как это работает?

Обычно, таблица импорта получает реальные адреса, значения которых угадать невозможно, но я сделал свою таблицу импорта таким образом, чтобы она импортировала адреса, также экспортируемые моим процессом, так что значения мне достоверно известны. Затем я могу создать один экспорт для каждого уникального байта и импортировать все необходимые мне байты.

Конечно, недостаточно импортировать байты, потому что размеры импортов - 32 бита, так что нам необходим способ доступа к байтовому значению каждого из них. Мы можем этого достичь, используя небольшой декодер и сделав из импортов выполняемый код, выглядит это вот так:

mov  esi, offset import_list
    mov	 edi, esi
import_loop:
    call esi
    stos byte ptr [edi]
    lods dword ptr [esi]
    lods dword ptr [esi]
    cmp  byte ptr [esi], bl
    db   75h ;f7 -> import_loop (f7 - первое импортированное значение)
import_list:

Тогда все импорты приобретут формат:

    b8 nn xx xx and xx c3 xx xx

а nn - это как раз то значение байта, которое нам нужно. Impute.A и B поддерживают эту форму.

Мы можем расставить импорты произвольно, чтобы сделать её полиморфной. Мы также можем использовать другие регистры вместо eax.

mov  esi, offset import_list
    mov  edi, esi
import_loop:
    call esi
    xchg reg, eax
    stos byte ptr [edi]
    lods dword ptr [esi]
    lods dword ptr [esi]
    cmp  byte ptr [esi], bl
    db   75h ;f6 -> import_loop (f6 - первое импортированное значение)
import_list:

Тогда все импорты приобретут формат:

    bx nn xx xx and xx c3 xx xx

кроме того, ecx, edx, ebp также доступны, а nn - значение байта, которое нам нужно. Impute.A и B также поддерживают эту форму.

Мы можем поддержать ebx следующим образом:

mov  esi, offset import_list
    mov  edi, esi
import_loop:
    xchg ebx, eax
    call esi
    xchg ebx, eax
    stos byte ptr [edi]
    lods dword ptr [esi]
    lods dword ptr [esi]
    cmp  byte ptr [esi], bl
    db   75h ;f5 -> import_loop (f5 - первое импортированное значение)
import_list:

Тогда все импорты приобретут формат:

    bb nn xx xx and xx c3 xx xx

а nn - значение байта, которое нам нужно. Impute.A и B также поддерживают эту форму.

Ещё один способ получить значения - использование ret nn. Тогда дельта между старым и новым стеками будет являться значением байта.

mov  esi, offset import_list
    mov  edi, esi
import_loop:
    mov  eax, esp
    call esi
    sub  esp, eax
    xchg esp, eax
    stos byte ptr [edi]
    lods dword ptr [esi]
    cmp  byte ptr [esi], bl
    db   75h ;f3 -> import_loop (f3 - первое импортированное значение)
import_list:

Тогда все импорты приобретут формат:

    c2 nn xx xx

а nn - значение байта, которое нам нужно. Impute.C поддерживает эту форму.

Мы даже можем их комбинировать, следующим образом:

mov  esi, offset import_list
    mov  edi, esi
import_loop:
    mov  ecx, esp
    call esi
    stos byte ptr [edi]
    xchg ecx, eax
    sub  esp, eax
    xchg esp, eax
    stos byte ptr [edi]
    lods dword ptr [esi]
    lods dword ptr [esi]
    cmp  byte ptr [esi], bl
    db   75h ;f0 -> import_loop (f0 - первое импортированное значение)
import_list:

Тогда все импорты приобретут формат:

    b8 n1 xx xx and xx c2 n2 xx

and n1 and n2 - значения байта, которые нам нужны. Impute.D и E поддерживают эту форму.

Конечно, вместо eax мы также можем использовать другие регистры.

mov  esi, offset import_list
    mov  edi, esi
import_loop:
    mov  eax, esp
    call esi
    sub  esp, eax
    xchg esp, eax
    stos byte ptr [edi]
    xchg reg, eax
    stos byte ptr [edi]
    lods dword ptr [esi]
    lods dword ptr [esi]
    cmp  byte ptr [esi], bl
    db   75h ;ef -> import_loop (ef - первое импортированное значение)
import_list:

Тогда все импорты приобретут формат:

    bx n2 xx xx and xx c2 n1 xx

также доступны ecx, edx, ebp, а n1 и n2 - значения байта, которые нам нужны, однако надо убедиться, что оба значения, предназначенные для хранения, записаны наоборот. Impute.D и E также поддерживают эту форму.

Мы можем поддержать ebx следующим образом:

mov  esi, offset import_list
    mov  edi, esi
import_loop:
    xchg ebx, eax
    mov  ecx, esp
    call esi
    xchg ebx, eax
    stos byte ptr [edi]
    xchg ecx, eax
    sub  esp, eax
    xchg esp, eax
    stos byte ptr [edi]
    lods dword ptr [esi]
    lods dword ptr [esi]
    cmp  byte ptr [esi], bl
    db   75h ;ee -> import_loop (ee - первое импортированное значение)
import_list:

Тогда все импорты приобретут формат:

    bb n1 xx xx and xx c2 n2 xx

а n1 and n2 - значения байта, которые нам нужны. Impute.D и E также поддерживают эту форму.

Мы можем поддержать ASLR, заменив "mov esi" на "call $+5 / pop esi".

Если бы была 3-байтная инструкция, безопасная для исполнения в тот момент, когда мы контролируем первый байт, можно было бы поступить следующим образом:

    b8 nn xx xx and xx ‹inst› xx xx and aa ‹inst› xx xx [последовательность повторяется]

Тогда мы могли бы построить целый код, используя только импорты, даже такие полиморфные регистры:

    bx nn xx xx and xx ‹inst› xx xx and 9x ‹inst› xx xx and aa ‹inst› xx xx [последовательность повторяется]

К сожалению, в 32-битном коде такой инструкции нет, так что мы не сможем использовать таким образом полностью импортированный код. Однако, мы можем поступить следующим образом:

    b8 n5 xx xx and xx aa b8 n6 and xx xx xx aa [последовательность повторяется]

Три импорта - "mov eax, n5", "stosb" и "mov eax, n6", а также "stosb". Мы повторяем последние три импорта для всех наших байтов, а затем можем полностью декодировать.

Мы даже можем поступить с полиморфным регистром следующим образом:

    bx n5 xx xx and 9x aa bx n6 and xx xx xx 9x and aa bx n7 xx and xx xx xx aa [последовательность повторяется]

Адрес буфера выглядит следующим образом:

    bf n1 n2 n3 and n4 90 90 90

Адрес буфера - оффсет последнего импорта, так что по окончании декодирования, мы немедленно запускаем его. Для этого необходимо чуть больше памяти, но нам также не нужны никакие другие импорты, кроме этих. Impute.F поддерживает форму b8.


Import Forwarding

Это наша последняя проблема. Некоторые DLL'ы могут экспортировать неприменяемую функцию, пересылая ссылку на другой DLL. Обнаруживается она, если адрес импорта указывает внутрь таблицы экспорта. Так как мы хотим подержать выдачу любого значения, нам необходимо избавиться от обнаружения пересылки импорта. Для стационарных версий, это просто. Всё, что нам надо, это таблица экспорта с характеристиками, которые не пересекаются с нашим значением. Impute делает это при помощи значения imagebase, которое пересекает границу в 4Gb, когда добавляется размер таблицы экспорта. Тогда все наши значения либо меньше, чем imagebase, либо больше, чем конец таблицы экспорта. Для ASLR эта проблема не решена. Если есть вероятность того, что Windows загрузит файл на imagebase со значением 0x1xxx0000, тогда может произойти обнаружение, и файл перестанет загружаться, но я такого никогда не видел.


Приветствия дружелюбным людям (A-Z):

Active - Benny - herm1t - hh86 - jqwerty - Malum - Obleak - pr0mix - Prototype - Ratter - Ronin - RT Fishel - sars - SPTH - The Gingerbread Man - Ultras - uNdErX - Vallez - Vecna - Whitehead


Подбронее об авторе и Исходники: sources/rgb/impute


______________________________
roy g biv / defjam
rgb/defjam
[email protected]
2013

Inception E-Zine