Вызываем демонов с printf
Один из источников настоящего открытия — это способность сомневаться в очевидных вещах
Предисловие
Начинающие реверс-инженеры часто сталкиваются с многочисленными препятствиями. Эта статья описывает определённый метод, который, как полагает автор, может вызвать замешательство у тех, кто только начинает изучать область анализа приложений. Стоит подчеркнуть, что цель данного материала не в представлении инновационного подхода или оказании значительной практической пользы, а в рассмотрении показательного случая.
Что такое printf?
Данный вопрос может показаться глупым, ведь многие с уверенностью скажут, что полностью понимают ход работы данной функции.
Функция printf () перенаправляет аргументы из списка arg-list в стандартный вывод (stdout) под контролем строки формата, на которую указывает аргумент format. Это определение часто приводится на различных веб-ресурсах, которые также предоставляют подобный справочник (cheatsheet):
Код | Формат |
---|---|
%с | Символ типа char |
%d | Десятичное число целого типа со знаком |
%i | Десятичное число целого типа со знаком |
%е | Научная нотация (е нижнего регистра) |
%Е | Научная нотация (Е верхнего регистра) |
%f | Десятичное число с плавающей точкой |
%g | Использует код %е или %f — тот из них, который короче (при использовании %g используется е нижнего регистра) |
%G | Использует код %Е или %f — тот из них, который короче (при использовании %G используется Е верхнего регистра) |
%о | Восьмеричное целое число без знака |
%s | Строка символов |
%u | Десятичное число целого типа без знака |
%х | Шестнадцатиричное целое число без знака (буквы нижнего регистра) |
%Х | Шестнадцатиричное целое число без знака (буквы верхнего регистра) |
%р | Выводит на экран значение указателя |
%n | Ассоциированный аргумент — это указатель на переменную целого типа, в которую помещено количество символов, записанных на данный момент |
%% | Выводит символ % |
Проблема заключается в том, что лишь немногие ресурсы упоминают о возможности добавления пользовательских спецификаторов форматирования для функции printf. Эту функциональность иногда называют «конверсией», и именно такое определение будет использоваться в данной статье.
Добавление своей конверсии
Конверсия реализуется с помощью функции int register_printf_function. Вот пример добавления конверсии: register_printf_function ('Q', quit_handler, &print_arginfo), где:
'Q' — код конверсии.
quit_handler — функция, обрабатывающая конверсию.
&print_arginfo — функция, которая необходима только для parse_printf_format, которую вы, вероятно, никогда не будете использовать. Если же вы все же решите её использовать, то, вероятно, что-то пошло не так. Обратите внимание, что знания о том, что нужно возвращать 1, если все прошло успешно, будут вам достаточно.
#include
#include
#include "printf.h"
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
int secret = 54;
int print_arginfo (){
return 1;
}
int quit_handler(FILE *stream, const struct printf_info *info, const void *const *args) {
exit(0);
}
void slonser(){
printf("I know how to keep my secrets\n%Q");
printf("My secret: %d",secret);
}
void main(){
register_printf_function('Q',quit_handler,&print_arginfo);
slonser();
}
В данной программе использование конверсии %Q
вызовет функцию exit, поэтому выражение printf("My secret: %d",secret);
не будет выполнено.
Перезапись
Позвольте сделать небольшое лирическое отступление. Представим ситуацию, когда вы проводите обратную разработку приложения и получили следующий простой псевдокод:
void admin_panel(){
printf("You are admin!");
}
void main(){
printf("I love %d\n",54);
admin_panel();
}
В данной ситуации очевидным кажется такой вывод программы:
I love 54
You are admin!
Но запустив приложение вы увидите:
Hidden text
I love 1337 You are user!
Но как это возможно? Все дело в том, что мы можем переопределить стандартные спецификаторы форматирования. Сделав это перед функцией main, можно заметно запутать начинающего реверс-инженера, так как вряд ли кто-то придет в голову анализировать, куда ведет функция printf. В данном конкретном случае, это было реализовано следующим образом:
int quit_handler(FILE *stream, const struct printf_info *info, const void *const *args) {
puts("1337\nYou are user!");
exit(0);
}
void __attribute__ ((constructor)) premain(){
register_printf_function('d',quit_handler,&print_arginfo);
}
Следует отметить, что, к сожалению, нельзя переопределить любой символ. Недопустимо использовать символы, зарезервированные за модификаторами, о которых будет рассказано ниже.
Работа с данными
Определенно, важно уметь работать с аргументами, которые передаются в printf после форматной строки. Это достигается достаточно просто через доступ к массиву args (не забывая выполнить приведение указателя). Давайте модифицируем код конверсии так, чтобы он продолжал работать независимо от изменения строки. Это может выглядеть примерно так:
#include
#include
#include "printf.h"
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
int check = 0;
char *status = "admin";
char* itoa(int value, char* result, int base) {
if (base < 2 || base > 36) { *result = '\0'; return result; }
char* ptr = result, *ptr1 = result, tmp_char;
int tmp_value;
do {
tmp_value = value;
value /= base;
*ptr++ = "zyxwvutsrqponmlkjihgfedcba9876543210123456789abcdefghijklmnopqrstuvwxyz" [35 + (tmp_value - value * base)];
} while ( value );
if (tmp_value < 0) *ptr++ = '-';
*ptr-- = '\0';
while(ptr1 < ptr) {
tmp_char = *ptr;
*ptr--= *ptr1;
*ptr1++ = tmp_char;
}
return result;
}
void admin_panel(){
printf("You are %s!\n",status);
}
int print_arginfo (){
return 1;
}
int quit_handler(FILE *stream, const struct printf_info *info, const void *const *args) {
if(!check){
check = 1;
status = "user";
}
char chr[32];
int num = *((int**)args[0]);
itoa(num,chr,10);
fprintf(stream,"%s",chr);
return 0;
}
void __attribute__ ((constructor)) premain(){
register_printf_function('d',quit_handler,&print_arginfo);
}
void main(){
printf("You personal code is %d\n",54);
printf("You personal code is %d\n",123123);
admin_panel();
}
Как видно, с помощью такого метода можно незаметно менять константы или даже функции в runtime, сохранив при этом видимое поведение функции неизменным. Это может создать серьезные затруднения для реверс-инженера. В данном конкретном случае, мы незаметно изменим переменную status, используемую в последующей функции.
Модификаторы
Когда мы работаем с функцией printf, мы имеем дело с модификаторами (например, l для длинных типов данных) и специальными флагами. Вкратце, они работают следующим образом:
Член структуры | Название в форматной строке | Смысл |
---|---|---|
int prec | 1 если указана точность | |
int width | Минимальный размер вывода | |
wchar_t spec | None | Можете использовать в своих целях |
unsigned int is_long_double | q, L, ll | - |
unsigned int is_char | hh | - |
unsigned int is_short | h | - |
unsigned int is_long | l | - |
unsigned int alt | # | - |
unsigned int space | пробел | - |
unsigned int left | - | - |
unsigned int showsign | + | - |
unsigned int group | » | - |
unsigned int extra | None | Всегда ноль при стандартном вызове printf, можете использовать свободно |
unsigned int wide | None | Если wstream то 1 |
wchar_t pad | None | Символ отступа, которым строка добивается до width |
Например, изменим преобразование числа из предыдущего пункта на следующее:
itoa(num * ( info -> left ? -1 : 1 ),chr,10);
Тогда вызов printf("%-d",54);
выведет нам -54 соответсвенно.
Заключения
Очевидно, что данная техника обфускации и усложнения кода сложна для применения в реальных условиях. Как минимум, опытный реверс-инженер проверит вызовы функций до входа в функцию main. Однако, этот подход хорошо демонстрирует, что нет такого понятия как «очевидность» при попытках понять, как работает приложение. Всегда стоит заглядывать глубже.
P.S.
Я приветствую любые комментарии, указывающие на недостатки и упущения в моем материале. Буду рад обсуждению в комментариях.