Вызываем демонов с printf

937b570c52a0adae6e52975b28b6d12a.jpeg

Один из источников настоящего открытия — это способность сомневаться в очевидных вещах

Предисловие

Начинающие реверс-инженеры часто сталкиваются с многочисленными препятствиями. Эта статья описывает определённый метод, который, как полагает автор, может вызвать замешательство у тех, кто только начинает изучать область анализа приложений. Стоит подчеркнуть, что цель данного материала не в представлении инновационного подхода или оказании значительной практической пользы, а в рассмотрении показательного случая.

Что такое 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), где:

  1. 'Q' — код конверсии.

  2. quit_handler — функция, обрабатывающая конверсию.

  3. &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.

Я приветствую любые комментарии, указывающие на недостатки и упущения в моем материале. Буду рад обсуждению в комментариях.

© Habrahabr.ru