Паранойя и хеши
Немного защиты от 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 — будет искомый хеш
Сверка хешей
Тут нужно учитывать два пункта:
- сверка должна происходить не только при старте программы, но и через некоторое время повторятся (если период повторений будет случайным вообще хорошо)
- чтобы проверять при запуске нужно допиливать загрузчик и компилятор + ассемблер
- надо бы где-то хранить эталонные хеши
Остановимся на 3-ем пункте, у которого тоже есть несколько вариантов:
- Хранить хеши в отдельном файле
- Зашитые в память программы (зашифрованный или открытый вид)
- Зашитые в программу гипервизор
Не трудно понять что третий вариант самый адекватный, т.к. теоретически у пользовательской программы нет доступа к программе гипервизору, т.е. нет способов поменять хеши.
Второй вариант более менее, т.к. можно зашифровать хеши и расшифроввывать их когда нужно сверить, и это делает боль аналитику, который будет разбирать ваш код. В открытом виде можно защитить страницы памяти, где находятся хеши на readonly.
Первый вариант наименее хорош, т.к. есть много способов заменить данные в файле, но можно использовать POSIX ACL, чтобы поставить readonly.
В любом случае можно пропатчить эту проверку хешей. И как говорил один из известных лиц на демосцене — если нельзя сделать keygen всегда можно сделать патч.
Для третьего вида можно еще составить список условий работы программы:
- Не дает программе работать без предоставленных данных (список указателей+длинн)
- Без предоставленого доступа к телу функций (by default у гипервизора он есть)
- Сверки хеша рандомизированны по времени.
Проверка имплементации с 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 совпадают, то можно сказать, что функция скопированна из памяти правильно.
Плюсы и минусы
Плюсы:
- Хешевая версионность исполняемых компонент (разбиваем прогу на модули и подмодули) и далее как по эталонным хешам смотрим, если хеш некой либы изменился, а программист забыл это учесть, то это плохо. Такой подход круто смотрится при динамически загружаемых модулях, как в более высокоуровневых языках.
- Контроль целостности исполняемых компонент.
- Идентификация и аутентификация отдельных модулей/функций
- Палки в колеса аналитикам кода во время injection (бесценно).
Минусы:
- Понижает производительность (как сильно, зависит от времени взятия хеша, количества хешируемых функций)
- Для варианта с гипервизором геморрой написания корректного гипервизора.
Немного очевидных экспериментов, или как можно не выстрелить в колено
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
Можно сделать сразу два вывода из выше увиденного
- Замена констант не ведет к замене байткода, т.к. они хранятся в другом месте.
- Добавление переменных (констант) ведет к изменению байткода
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).