Один из источников настоящего открытия - это способность сомневаться в очевидных вещах
Предисловие
Начинающие реверс-инженеры часто сталкиваются с многочисленными препятствиями. Эта статья описывает определённый метод, который, как полагает автор, может вызвать замешательство у тех, кто только начинает изучать область анализа приложений. Стоит подчеркнуть, что цель данного материала не в представлении инновационного подхода или оказании значительной практической пользы, а в рассмотрении показательного случая.
Edited: Стоит отметить, что ниже изложенное в первую очередь относится к приложениям компилируемых при помощи GCC.
Что такое 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 <stdio.h>
#include <stdlib.h>
#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 secretsn%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 %dn",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("1337nYou are user!");
exit(0);
}
void __attribute__ ((constructor)) premain(){
register_printf_function('d',quit_handler,&print_arginfo);
}
Следует отметить, что, к сожалению, нельзя переопределить любой символ. Недопустимо использовать символы, зарезервированные за модификаторами, о которых будет рассказано ниже.
Работа с данными
Определенно, важно уметь работать с аргументами, которые передаются в printf после форматной строки. Это достигается достаточно просто через доступ к массиву args (не забывая выполнить приведение указателя). Давайте модифицируем код конверсии так, чтобы он продолжал работать независимо от изменения строки. Это может выглядеть примерно так:
#include <stdio.h>
#include <stdlib.h>
#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 = ''; 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-- = '';
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 %dn",54);
printf("You personal code is %dn",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.
Я приветствую любые комментарии, указывающие на недостатки и упущения в моем материале. Буду рад обсуждению в комментариях.
Автор: Vsevolod Kokorin