image

Немного защиты от code injection, но
Этот способ не панацея, но немного усложняет жизнь иньекций кода.

Лирика


Т.к. с каждым скачком высокоуровневого программирования все меньше людей понимают ассемблер, то есть смысл задуматься:
А что если программа, которую вы исполняете не является ею?
Или, а что если вирус заменяет куски программы, которые вы используете?
Умные люди в далеких 80х придумали один рецепт для того, чтобы подтвердить цельность исполнительных файлов и отдельных их кусков — хеши. Обычно все релизы библиотек поставляются с хешом или цифровой подписью, чтобы проверить именно этот ли автор поставляет нам либу, или приложение, не было ли оно изменено никем кроме него.
Есть языки (С, C++) которые не поддерживает эту фичу в рантайме (как например в Обероне, в котором есть немного здравых идей, как модули например), но С хорош тем, что с прямыми руками его можно немного доработать напильником. При большом желании можно также доработать С компилятор, но это другая история.
Почему не стоит доверять никому?
Есть очень много вариантов ответа на этот вопрос. Часть из них в шуточном виде.

Теоретическая часть


Любой исполняемый код или данные — это информация (байты).
Хеш — функция свертки, т.е. подаем на вход n байт получаем m, где m — постоянная длинна хеша, n — переменная длинна входных данных.
В данном случае нам нужна будет криптостойкая хеш функция, из-за того, что чем меньше возможности найти «правильную» коллизию тем лучше для нас, и тем менее вероятен code injection при правильном хеше.
Code injection — вид атаки, когда что-то идет не так, и неуполномоченный пользователь добавляет в программу свои исполняемые данные.

Практическая часть (Получение хеша)


Как узнать размер и адрес начала функции? Об этом писал еще Мыщьх, но на самом деле очень сильно зависит от компилятора и оптимизаций, в случае не сильной оптимизации порядок следования функций определяется программистом, т.е. если мы напишем:

void some(int *trains) {
	printf("cho, chooo, motherfucker\n");
	++*trains;
}

void endSome() {}


То ф-ция endSome будет расположена ниже, чем some (address of labels)
Таким образом можно будет узнать размер оп/байткодов функции some.

Немного проверки от LLDB

Используем
disassemble -n «имя_функции» для получения опкодов тела функции

disassemble -n some
RayLanguage`some:
    0x10231e7b0 <+0>:  pushq  %rbp
    0x10231e7b1 <+1>:  movq   %rsp, %rbp
    0x10231e7b4 <+4>:  subq   $0x10, %rsp
    0x10231e7b8 <+8>:  leaq   0x2be9(%rip), %rax        ; "cho, chooo, motherfucker\n"
    0x10231e7bf <+15>: movq   %rdi, -0x8(%rbp)
    0x10231e7c3 <+19>: movq   %rax, %rdi
    0x10231e7c6 <+22>: movb   $0x0, %al
    0x10231e7c8 <+24>: callq  0x102321050               ; symbol stub for: printf
    0x10231e7cd <+29>: movq   -0x8(%rbp), %rdi
    0x10231e7d1 <+33>: movl   (%rdi), %ecx
    0x10231e7d3 <+35>: addl   $0x1, %ecx
    0x10231e7d9 <+41>: movl   %ecx, (%rdi)
    0x10231e7db <+43>: movl   %eax, -0xc(%rbp)
    0x10231e7de <+46>: addq   $0x10, %rsp
    0x10231e7e2 <+50>: popq   %rbp
    0x10231e7e3 <+51>: retq   

(lldb) disassemble -n endSome
RayLanguage`endSome:
    0x10231e7f0 <+0>: pushq  %rbp
    0x10231e7f1 <+1>: movq   %rsp, %rbp
    0x10231e7f4 <+4>: popq   %rbp
    0x10231e7f5 <+5>: retq   



И немного арифметики:

0x10231e7f0 ? 0x10231e7e3 = 0хd


13 байтов чего-то там, что же это может быть?

Скорее всего это spacind nop для выравнивания от ассемблера, lldb спешит на помощь.
Используем disassemble -s «адресс в hex» чтобы посмотреть так это или нет.


(lldb) disassemble -s 0x10231e7e3
RayLanguage`some:
    0x10231e7e3 <+51>: retq   
    0x10231e7e4 <+52>: nopw   %cs:(%rax,%rax)

RayLanguage`endSome:
    0x10231e7f0 <+0>:  pushq  %rbp
    0x10231e7f1 <+1>:  movq   %rsp, %rbp
    0x10231e7f4 <+4>:  popq   %rbp
    0x10231e7f5 <+5>:  retq   
    0x10231e7f6 <+6>:  nopw   %cs:(%rax,%rax)

RayLanguage`main:
    0x10231e800 <+0>:  pushq  %rbp

И таки да, RTFM говорит, что это «the assembler (not the compiler) pads code up to the next alignment boundary with the longest NOP instruction it can find that fits. This is what you're seeing.»

В любом случае, если вы не используете кучу ассемблерных хаков, для самомодификации кода, то spacing из nop'ов должен им же и оставатся, т.е. это тоже является частью функции some. Таким образом размер some — это размер от начала функции some до начала функции endSome.

size_t sizeOfSome = (size_t)&endSome - (size_t)&some;


Три условия для начала хеширование выполнены (непрерывность опкодов функции, знание начала функции и ее размера).
Таким образом можно взять любой криптостойкий хещ и хешировать тело функции:


size_t sizeOfSome = (size_t)&endSome - (size_t)&some;

unsigned char *body = malloc(sizeOfSome);
memcpy(body, some, sizeOfSome);

unsigned char *hash = someHash(body, sizeOfSome);


где hash — будет искомый хеш

Сверка хешей


Тут нужно учитывать два пункта:
  1. сверка должна происходить не только при старте программы, но и через некоторое время повторятся (если период повторений будет случайным вообще хорошо)
  2. чтобы проверять при запуске нужно допиливать загрузчик и компилятор + ассемблер
  3. надо бы где-то хранить эталонные хеши


Остановимся на 3-ем пункте, у которого тоже есть несколько вариантов:
  1. Хранить хеши в отдельном файле
  2. Зашитые в память программы (зашифрованный или открытый вид)
  3. Зашитые в программу гипервизор


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

В любом случае можно пропатчить эту проверку хешей. И как говорил один из известных лиц на демосцене — если нельзя сделать keygen всегда можно сделать патч.

Для третьего вида можно еще составить список условий работы программы:
  1. Не дает программе работать без предоставленных данных (список указателей+длинн)
  2. Без предоставленого доступа к телу функций (by default у гипервизора он есть)
  3. Сверки хеша рандомизированны по времени.


Проверка имплементации с lldb


Для того чтобы увидеть опкоды, используем опцию disassemble -b

 disassemble -b -n some
RayLanguage`some:
    0x1079ac500 <+0>:  55                    pushq  %rbp
    0x1079ac501 <+1>:  48 89 e5              movq   %rsp, %rbp
    0x1079ac504 <+4>:  48 83 ec 10           subq   $0x10, %rsp
    0x1079ac508 <+8>:  48 8d 05 99 2e 00 00  leaq   0x2e99(%rip), %rax        ; "cho, chooo, motherfucker\n"
    0x1079ac50f <+15>: 48 89 7d f8           movq   %rdi, -0x8(%rbp)
    0x1079ac513 <+19>: 48 89 c7              movq   %rax, %rdi
    0x1079ac516 <+22>: b0 00                 movb   $0x0, %al
    0x1079ac518 <+24>: e8 35 2b 00 00        callq  0x1079af052               ; symbol stub for: printf
    0x1079ac51d <+29>: 48 8b 7d f8           movq   -0x8(%rbp), %rdi
    0x1079ac521 <+33>: 8b 0f                 movl   (%rdi), %ecx
    0x1079ac523 <+35>: 81 c1 01 00 00 00     addl   $0x1, %ecx
    0x1079ac529 <+41>: 89 0f                 movl   %ecx, (%rdi)
    0x1079ac52b <+43>: 89 45 f4              movl   %eax, -0xc(%rbp)
    0x1079ac52e <+46>: 48 83 c4 10           addq   $0x10, %rsp
    0x1079ac532 <+50>: 5d                    popq   %rbp
    0x1079ac533 <+51>: c3                    retq   


Данные из программы:


RData object - 0x7f83ebc054a0 [64] {
55 48 89 E5 48 83 EC 10 48 8D 05 99 2E 00 00 48 89 7D F8 48 89 C7 B0 00 E8 35 2B 00 00 48 8B 7D 
F8 8B 0F 81 C1 01 00 00 00 89 0F 89 45 F4 48 83 C4 10 5D C3 66 66 66 2E 0F 1F 84 00 00 00 00 00 
} - 0x7f83ebc054a0 

Base64 evasion hash:
wRagc6tuimNnTlTSbyOe+BT6QAkWVDXVEjWktrG4a+Zm/2U2mgeeTr286yLE2lB3rVqihtQ2Fsb7eEvTocnEqg==


Т.к. окоды скопированные и показанные lldb совпадают, то можно сказать, что функция скопированна из памяти правильно.

Плюсы и минусы


Плюсы:
  1. Хешевая версионность исполняемых компонент (разбиваем прогу на модули и подмодули) и далее как по эталонным хешам смотрим, если хеш некой либы изменился а программист забыл это учесть, то это плохо. Такой подход круто смотрится при динамически загружаемых модулях, как в более высокоуровневых языках.
  2. Контроль целостности исполняемых компонент.
  3. Идентификация и аутентификация отдельных модулей/функций
  4. Палки в колеса аналитикам кода во время injection (бесценно).


Минусы:
  1. Понижает производительность (как сильно, зависит от времени взятия хеша, количества хешируемых функций)
  2. Для варианта с гипервизором геморрой написания корректного гипервизора.


Немного очевидных экспериментов, или как можно не выстрелить в колено



void some(int *trains) {
    byte some[6] = "hello";
    printf("cho, chooo, motherfucker\n");
    ++*trains;
}

RData object - 0x7ffe6ae00050 [80] {
55 48 89 E5 48 83 EC 20 48 8D 05 AF 2E 00 00 48 89 7D F8 8B 0D 9F 2E 00 00 89 4D F2 66 8B 15 99 
2E 00 00 66 89 55 F6 48 89 C7 B0 00 E8 31 2B 00 00 48 8B 7D F8 8B 0F 81 C1 01 00 00 00 89 0F 89 
45 EC 48 83 C4 20 5D C3 0F 1F 84 00 00 00 00 00 
} - 0x7ffe6ae00050 

aDO1L9KThmWe3NPBQuxgqkqcd72TkCxa2bJmzSiLdNq8KXbjls7cd38FPSQJQ82RTitb1qwZZcdlf1l5MP521A==

void some(int *trains) {
    byte some[6] = "lol";
    printf("cho, chooo, motherfucker\n");
    ++*trains;
}

RData object - 0x7fa222c057e0 [80] {
55 48 89 E5 48 83 EC 20 48 8D 05 AF 2E 00 00 48 89 7D F8 8B 0D 9F 2E 00 00 89 4D F2 66 8B 15 99 
2E 00 00 66 89 55 F6 48 89 C7 B0 00 E8 31 2B 00 00 48 8B 7D F8 8B 0F 81 C1 01 00 00 00 89 0F 89 
45 EC 48 83 C4 20 5D C3 0F 1F 84 00 00 00 00 00 
} - 0x7fa222c057e0 

aDO1L9KThmWe3NPBQuxgqkqcd72TkCxa2bJmzSiLdNq8KXbjls7cd38FPSQJQ82RTitb1qwZZcdlf1l5MP521A== EQUAL



Можно сделать сразу два вывода из выше увиденного
  1. Замена констант не ведет к замене байткода, т.к. они хранятся в другом месте.
  2. Добавление переменных(констант) ведет к изменению байткода



void some(int *trains) {
    byte some[4] = "lol";
    printf("cho, chooo, motherfucker\n");
    ++*trains;
}

RData object - 0x7ffb3a600040 [64] {
55 48 89 E5 48 83 EC 10 48 8D 05 9D 2E 00 00 48 89 7D F8 8B 0D 8F 2E 00 00 89 4D F4 48 89 C7 B0
00 E8 2C 2B 00 00 48 8B 7D F8 8B 0F 81 C1 01 00 00 00 89 0F 89 45 F0 48 83 C4 10 5D C3 0F 1F 00
} - 0x7ffb3a600040

Wiq45/M7ES5TOgkUNVUdn04OxsTF/ej76Wj9B7ItE/eYrU1f18nX5IT696fymYtlYj8drf9AtgPCStQPR0CEpg==


3. Изменение размера стековой константы тоже ведет к изменению байткода

P.S


используемый дебаггер LLDB + lldb -help
используемая библиотека для взятия хеша и прочих операций
немного исходников можно найти в коммите (d3c3d5d).

Комментарии (14)


  1. tmnhy
    07.12.2015 16:51
    -3

    «Теоритическая», «исполнительных файлов», «експериментов» — что за язык статьи?


    1. StrangerInRed
      07.12.2015 16:58
      +15

      нояже… рассмотрел и интересную проблему…


      1. tmnhy
        07.12.2015 17:00
        -2

        Кто спорит то? Но читать просто невозможно из-за «хещ», «окоде», «иньекций», путания «тся» и «ться» и т.п.


    1. Dageron
      07.12.2015 17:22
      +12

      Да ладно, такое часто попадается в текстах авторов, кто много пишет на русском и украинском языке.
      «експериментов»/«экспериментов» — особенно характерно. Не стоит придираться к мелочам =).


  1. tyomitch
    07.12.2015 20:51
    +2

    Как узнать размер и адрес начала функции? Об этом писал еще Мыщьх, но на самом деле очень сильно зависит от компилятора и оптимизаций, в случае не сильной оптимизации порядок следования функций определяется программистом, т.е. если мы напишем:

    Куски функции даже не обязательно будут располагаться последовательно. Например, блоки catch в большинстве случаев окажутся отдельно от «тела» функции. Или, если несколько функций содержат одинаковый код, то они будут частично или полностью слиты («tail merging»).
    Таким образом, «порядок следования функций» не всегда определён — они могут «перемешиваться».


  1. lorc
    07.12.2015 21:09
    +4

    Зачем хешировать отдельные функции, если можно прохешировать все секции (а их чаще всего ровно одна) с исполняемым кодом? Достаточно заставить линкер сгенерить метки в начале и конце каждой секции, таким образом можно будет узнать длину и размер каждой секции. Это всяко надежней, чем игры с поинтерами на функции.
    Но на самом деле такая защита не очень надежная, потому что если кто-то начнет патчить код в памяти, то первым что он пропатчит — будет как раз этот проверяльщик.
    Гораздо веселее написать проектор, который равномерно размажет проверки по всему коду, при чем так, что при неправильном хеше просто будет менятся поведение программы, вызывая трудноуловимые глюки.
    Как пример можно заменять конструкции if(expression) на if(expression ^ ((real_hash ^ target_hash)&1))


    1. VolCh
      08.12.2015 08:01
      -1

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


      То есть мало того, что пользователь получает какого-то зловреда типа клиента ботнета, так ещё и ваша программа ему начнёт гадить?


      1. lorc
        08.12.2015 14:26

        Зловреды обычно не патчат код процесса. Уязвимый процесс им нужен просто как точка входа в систему. Дальше они или порождают свой процесс или присасываются к какому-то «вечно живому» процессу типа explorer.exe

        Активно патчат код как раз всякие патчеры, которые пользователь устанавливает умышленно. Для запуска программ без лицензии, для читинга в играх, мало ли ещё для чего…


    1. StrangerInRed
      08.12.2015 09:22

      кострукции
      if(expression) на if(expression ^ ((real_hash ^ target_hash)&1))


      обычно выпиливаются через ctrl+f в бинарнике, или заменяются на 1(true)


      1. lorc
        08.12.2015 14:36

        Хм, вы предлагаете заменить все условные ветвления в программе на if(1)?
        Я же не зря написал про протектор и «размажет равномерно по всему коду».
        Представьте себе тулзу, которая пройдется по уже собраному бинарю и навтыкает дополнительных проверок там, где сама захочет, потенциально дорабатывая уже уже существующие if-ы. А ведь программа представляет собой ценность только если всё eё if-ы ведут себя так, как надо, верно?
        Естественно, что на каждую хитрую гайку найдется свой болт с дюймовой левой резьбой, но если подойти к делу с фантазией, то можно доставить несколько увлекательных часов (или дней) потенциальному ресерчеру.


        1. StrangerInRed
          08.12.2015 14:58

          иммел виду в if кусок (real_hash ^ target_hash) — 1
          а про гайку с болтом это да


          1. lorc
            08.12.2015 15:05

            хаха, тут то вы и попались :)
            if(expression ^ 1) будет равняться if(!expression). Вы только что сломали всю программу :)

            Вообще я привёл только один из возможных вариантов. Думаю, за вечерок можно придумать ещё штук 20 таких приколов, разной степени упоротости. А как показывает практика — придумывать их куда проще, чем разбирать потом в ассемблерном коде, особенно если каждый раз разбавлять код случайными мусорными инструкциями.


  1. SannX
    08.12.2015 11:08

    Мне кажется, слабое место (узкое горло) описанной системы хеширования — вызов (или тело) хеш-функции:

    someHash(body, sizeOfSome);
    

    Теоретически через нее можно проследить все вызовы к ней, собрать места в коде, где происходит проверки хэша и… остальное дело техники.


    1. StrangerInRed
      08.12.2015 11:12

      Будет проблема если это делает гипервизор