Крис Касперский - ТЕХНИКА СЕТЕВЫХ АТАК
· 00000020: 8BEC mov ebp,esp
· 00000022: 83EC10 sub esp,00000010
· 00000025: 6830604000 push 00406030
А вот как выглядит результат дизассемблирования файла demo.printf.dump.exe с помощью IDA:
· text:00401000 sub_0_401000 proc near; CODE XR
· text:00401000
· text:00401000 var_11 = byte ptr -11h
· text:00401000 var_10 = byte ptr -10h
· text:00401000
· text:00401000 55 push ebp
· text:00401001 8B EC mov ebp, esp
· text:00401003 83 EC 10 sub esp, 10h
· text:00401006 68 30 60 40 00 push offset aPrintfDumpDemo;
· text:0040100B E8 DB 01 00 00 call sub_0_4011EB
Нетрудно убедится в том, что они идентичны. Манипулируя значением указателя можно «вытянуть» весь код программы. Конечно, учитывая частоту появления нулей в коде, придется проделать огромное множество операций, прежде чем удастся «перекачать» программу на собственный компьютер. Но, во-первых, процесс можно автоматизировать, а во-вторых, чаще всего существуют и другие пути получения программного обеспечения, а наибольший интерес для вторжения на чужой компьютер представляют весьма компактные структуры данных, как правило, содержащие пароли.
Спецификатор “%c” читает двойное слово из стека и усекает его до байта. Поэтому, в большинстве случаев он оказывается непригоден. Так, если в примере buff.printf.demo попытаться заменить спецификатор “%x” на спецификатор “%c” результат работы будет выгядеть так:
· buff.printf.exe
· printf bug demo
· Login:kpnc
· Passw:%c%c
· Invalid password: KN
Программа выдала не первый и второй символы пароля, а… первый и пятый! Поэтому, от надежды получить пароль в удобочитаемом виде приходится отказываться, возвращаясь к использованию спецификатора “%x”.
Описанная методика, строго говоря, никаким боком не относится к переполнению буфера и никак не может воздействовать на стек. Однако чтение содержимого стека способно нанести не меньший урон безопасности системы, чем традиционное переполнение буфера. О существовании уязвимости в функции printf догадываются не все программисты, поэтому-то большинство приложений, считающиеся надежными, могут быть атакованы подобным образом.
Для устранения угрозы проникновения систему некоторые разработчики пытаются фильтровать ввод пользователя. Но это плохое решение, поскольку пользователь вполне может выбрать себе пароль наподобие «Kri%s» и будет очень удивлен, если система откажется его принять. Но существует простой и элегантный выход из ситуации, который продемонстрирован в листинге, приведенном ниже: (на диске, прилагаемом к книге, он находится в файле “/SRC/buff.printf.nobug.c”):
· #include «stdio.h»· #include «string.h»··· void main()· {· FILE *psw;· char buff[32];· char user[16];· char pass[16];· char _pass[16];·· printf("printf bug demon");· if (!(psw=fopen("buff.psw","r"))) return;· fgets( amp;_pass[0],8,psw);·· printf("Login:");fgets( amp;user[0],12,stdin);· printf("Passw:");fgets( amp;pass[0],12,stdin);·· if (strcmp( amp;pass[0], amp;_pass[0]))· sprintf( amp;buff[0],"Invalid password: %s", amp;pass[0]);· else· sprintf( amp;buff[0],"Password okn");·· printf("%s", amp;buff[0]);··}
От файла demo.printf.c он отличается всего одной строкой, которая выделена жирным шрифтом. Только самый левый аргумент функции printf может содержать в себе спецификаторы, во всех остальных случаях они будут проигнорированы. Это доказывает следующий эксперимент:
· buff.printf.nobug.exe
· printf bug demo
· Login:kpnc
· Passw:%x
· Invalid password: %x
Теперь никакая строка, введенная пользователем, не сможет вызвать непредсказуемого поведения программы! И нет никакой необходимости прибегать к фильтрации ввода, которая сама по себе чревата внесением новых ошибок! Для выявления всех уязвимых мест в программе достаточно воспользоваться шаблонным поиском.
Ошибки, приводящие к переполнению буфера, выявить сложнее. Попытка протестировать программу на строках непомерной длины не всегда дает желаемый результат. Во многих случаях ошибки проявляются только при вводе строк определенной длины. Как раз такую ситуацию и демонстрирует следующий пример (на диске, прилагаемом к книге, он находится в файле “/SRC/buff.arg.c”):
· #include «stdio.h»· #include «string.h»·· void main (int argc, char ** argv)· {· char buff[10];· if (argc«2) return;· if ( strlen(argv[1])»10 ) return;· strcpy( amp;buff[0], amp;argv[1][0]);·}
Это ошибка особенно распространена среди начинающих программистов, но порой встречается и у профессионалов. Строка длиной в десять байт не может поместиться в десятибайтовый буфер, поскольку на ее конце находится завершающий нуль! В результате один байт «вылезает» из буфера! Но все строки длиннее десяти символов отсекаются программой, и ошибка проявляется только на десяти символьных строках!
Ошибка переполнения в один байт встречается достаточно часто. К этому приводит путаница между длинами и индексами массивов, начинающихся с нуля; выполнение операции сравнения до модификации переменной; небрежное обращение с условиями выхода из цикла и т.д. Существует даже шуточное выражение «ошибка в плюс-минус один байт!», один из способов устранения которой заключается в подгонке значения «капризных» переменной уменьшением или увеличением их значения на единицу.
Например, если “if (p»strlen(str)) break” не работает, то некоторые программисты «прыгают блохой» на единицу назад “if (p»(strlen(str)-1)) break” [319]. Но если «ошибка в плюс-минус один байт» не проявит себя на тестовых прогонах программы, она имеет шанс дожить до финальной версии и вместе с ней попасть на компьютер потенциальной жертвы.
С переполнением в один байт «сорвать стек» невозможно, поскольку чтобы «дотянуться» до адреса возврата в большинстве случаев требуется «пересечь» сохраненное значение регистра EBP [320], занимающее четыре байта. Но ведь именно этот факт и можно использовать для атаки! Потом, переполняющийся буфер не всегда располагается на вершине стека. Скорее всего, за ним следуют некие локальные переменные, искажение значения которых может привести к нарушению нормальной работоспособности программы: от зависания до возможности несанкционированного вторжения в систему.
В примере, приведенном ниже (на диске, прилагаемом к книге, он находится в файле “/SRC/buff.var.c”), используется переменная-флаг noguest, нулевое значение которой открывает доступ в систему всем желающим:
· #include «stdio.h»· #include «string.h»·· main (int argc,char **argv)· {· int noguest=1;· char pass[16];· int a=1;· for (;a«argc;a++)· {· if (argv[a][0]-'/')· {· if (!strcmp( amp;argv[a][0],"/GUEST:ON")) noguest=0;·}· else· {· if (strlen(argv[a])»16)· printf("Too long arg: %sn",argv[a]);· else· strcpy( amp;pass[0],argv[a]);·}·}· if ((!strcmp("KPNC++n", amp;pass[0])) || (!noguest))· printf("Password okn");· else· printf("Wrong passwordn");···}
Дизассемблирование позволяет установить, что переменная “noguest” расположена в «хвосте» буфера buff и может быть искажена при его переполнении. Поскольку, при проверке длины строки допущена ошибка «if (strlen(argv[a])»16)…», завершающий ноль шестнадцатисимвольной строки обнулит значение переменной “noguest” и откроет злоумышленнику путь в систему. Это демонстрирует следующий эксперимент:
· buff.var.exe 1234567890123456
· Password ok
Но если увеличить длину строки хотя бы на один байт, программа отбросит ее как неправильную:
· buff.var.exe 12345678901234567
· Too long arg: 12345678901234567
· Wrong password
Конечно, вероятность возникновения подобной ситуации на практике очень мала. Для атаки необходимо неблагоприятное стечение многих маловероятных обстоятельств. Размер буфера должен быть кратен величие выравнивания, иначе переполняющий байт запишется в «черную дыру» [321] и ничего не произойдет. Следующая за буфером переменная должна быть критична к обнулению, т.е. если программист открывал бы доступ на машину при ненулевом значении флага guest, опять бы ничего не произошло. Поэтому, в большинстве случаев несанкционированного доступа к машине получить не удастся, а вот «завесить» ее гораздо вероятнее.
Например, следующий код (на, диске, прилагаемом к книге, он находится в файле “/SRC/buff.var.2.c”), в отличие от предыдущего, трудно назвать искусственным и «притянутым за уши»:
· #include «stdio.h»· #include «string.h»·· main (int argc,char **argv)· {·· char pass[16];· int a=1;· for (;a«argc;a++)· {· if (argv[a][0]-'/')· {· if (!strcmp( amp;argv[a][0],"/GUEST:ON"))· {· printf("Guest user okn");· break;·}·}· else· {· if (strlen(argv[a])»16)· printf("Too long arg: %sn",argv[a]);· else· strcpy( amp;pass[0],argv[a]);·}·}· if ((!strcmp("KPNC++n", amp;pass[0])))· printf("Password okn");· else· printf("Wrong passwordn");··}
Переполнение буфера вызовет запись нуля в счетчик цикла ‘a’, в результате чего цикл никогда не достигнет своего конца, а программа «зависнет». А если буфер окажется расположенным в вершине стека, то «вылетевший» за его пределы ноль исказит значение регистра EBP. Большинство компиляторов генерируют код, использующий для адресации локальных переменных регистр EBP, поэтому искажение его значения приведет к нарушению работы вызывающей процедуры.