Модераторы: bsa

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Не зная брода, не лезь в воду. Часть вторая. 
:(
    Опции темы
Thunderbolt
Дата 6.2.2012, 15:24 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


DevRel
*


Профиль
Группа: Участник
Сообщений: 122
Регистрация: 7.11.2007
Где: Тула

Репутация: 6
Всего: 16



В этот раз я хочу поговорить о функции printf. Все наслышаны об уязвимостях в программах, и что функции наподобие printf объявлены вне закона. Но одно дело знать, что лучше не использовать эти функции. А совсем другое - понять почему. В этой статье я опишу две классических уязвимости программ, связанных с printf. Хакером после этого вы не станете, но, возможно, по-новому взгляните на свой код. Вдруг, вы реализуете аналогичные уязвимые функции, даже не подозревая об этом. 

СТОП. Подожди читатель, не проходи мимо. Я знаю, что ты увидел слово printf. И уверен, что автор статьи сейчас расскажет банальную историю о том, что функция не контролирует типы передаваемых аргументов. Нет! Статья будет не про это, а именно про уязвимости. Заходи почитать.

Предыдущая заметка находится здесь: Часть первая.

Введение

Взглянем на вот эту строчку:

Код
printf(name);


Она кажется простой и безобидной. А между тем, в ней скрывается как минимум два способа, чтобы атаковать программу.

Начнем статью с демонстрационного примера, где есть эта строчка. Код может показаться вам странноватым. Так оно и есть. Оказалось не так просто написать программу, чтобы потом её атаковать. Дело в оптимизации, которую производит компилятор. Получается, что если написать слишком простую программу, то компилятор создает такой код, что ломать там нечего. Он использует регистры, а не стек для хранения данных, встраивает функции и тому подобное. Можно написать код с лишними действиями и циклами, чтобы компилятору не хватило свободных регистров, и он начал помещать данные в стек. К сожалению, пример получается слишком большой и запутанный. Про всё это можно написать отдельную детективную историю, но не будем.

Представленный пример является компромиссом между сложностью и необходимостью не дать компилятору "схлопнуть в ничто" слишком простой код. Признаюсь, немного я себе всё равно помог. Я отключил некоторые виды оптимизации, используемые в Visual Studio 2010. Во-первых, был отключен ключ /GL (Whole Program Optimization). Во–вторых, я использовал атрибут __declspec(noinline).

Прошу прощение за такое длинное вступление. Хотелось пояснить неуклюжесть программного кода. И сразу пресечь дискуссии на тему, что этот код можно написать лучше. Я знаю, что можно. Но не получается сделать код одновременно и коротким, и чтобы можно было показать уязвимость.

Демонстрационный пример

Полный код и проект для Visual Studio 2010 доступен здесь.

Код
const size_t MAX_NAME_LEN = 60;
enum ErrorStatus {
  E_ToShortName, E_ToShortPass, E_BigName, E_OK
};

void PrintNormalizedName(const char *raw_name)
{
  char name[MAX_NAME_LEN + 1];
  strcpy(name, raw_name);

  for (size_t i = 0; name[i] != '\0'; ++i)
    name[i] = tolower(name[i]);
  name[0] = toupper(name[0]);

  printf(name);
}

ErrorStatus IsCorrectPassword(
  const char *universalPassword,
  BOOL &retIsOkPass)
{
  string name, password;
  printf("Name: "); cin >> name;
  printf("Password: "); cin >> password;
  if (name.length() < 1) return E_ToShortName;
  if (name.length() > MAX_NAME_LEN) return E_BigName;
  if (password.length() < 1) return E_ToShortPass;

  retIsOkPass = 
    universalPassword != NULL &&
    strcmp(password.c_str(), universalPassword) == 0;
  if (!retIsOkPass)
    retIsOkPass = name[0] == password[0];

  printf("Hello, ");
  PrintNormalizedName(name.c_str());

  return E_OK;
}

int _tmain(int, char *[])
{
  _set_printf_count_output(1);
  char universal[] = "_Universal_Pass_!";
  BOOL isOkPassword = FALSE;
  ErrorStatus status =
    IsCorrectPassword(universal, isOkPassword);
  if (status == E_OK && isOkPassword)
    printf("\nPassword: OK\n");
  else
    printf("\nPassword: ERROR\n");
  return 0;
}


Функция _tmain() вызывает функцию IsCorrectPassword(). Если пароль верен или если он совпадает с магическим словом "_Universal_Pass_!", то программа выводит строку "Password: OK". Целью атак будет добиться, чтобы программа выводила именно эту строку.

Функция IsCorrectPassword() запрашивает у пользователя имя и пароль. Пароль считается корректным, если он совпадает с переданным в функцию магическим словом. Также он корректен, если первая буква пароля совпадает с первой буквой имени.

Вне зависимости от того, введен правильный пароль или нет, программа приветствует пользователя. Для этого вызывается функция PrintNormalizedName().

В функции PrintNormalizedName() всё самое интересное. Именно в ней, находится обсуждаемый "printf(name);". Подумайте, как с помощью этой строчки можно обмануть программу. Если знаете как, то дальше можно не читать. 

Что делает функция PrintNormalizedName()? Она печатает имя, сделав первую букву заглавной, а остальные маленькими. Например, если ввести имя "andREy2008", то она распечатает "Andrey2008".

Первая атака

Предположим мы не знаем правильный пароль. Но знаем, что где-то есть некий магический пароль. Попробуем его поискать, используя printf(). Если адрес этого пароля есть где-то в стеке, то у нас есть шанс на успех. Есть идеи, как увидеть этот пароль на экране?

Даю подсказку. Функция printf() относится к семейству функций с переменным количеством аргументов. Работают такие функции так. В стек записывается произвольное количество данных. Функция printf() не знает, сколько данных записано в стек и какой у них тип. Она руководствуется исключительно строкой форматирования. Если написано "%d%s", то значит, из стека следует извлечь одно значение типа int и один указатель. Так как функция printf() не знает, сколько аргументов ей передали, то она может заглянуть глубже в стек и распечатать данные, которые никакого к ней отношения не имеет. Как правило, это приводит к access violation или к распечатке мусора. Однако, этим мусором можно воспользоваться.

Рассмотрим, как может выглядеть стек в момент, когда мы вызываем функцию printf():

user posted imageРисунок 1. Схематическое расположение данных в стеке.



Вызов функции "printf(name);" имеет только один аргумент, который является строкой форматирования. Это значит, что если мы вместо имени мы введём "%d", то распечатаем данные, которые лежат в стеке до адреса возврата в функцию PrintNormalizedName(). Попробуем:

Name: %d

Password: 1

Hello, 37

Password: ERROR

Пока данное действие малоосмысленно. Как минимум, вначале мы должны распечатать адреса возврата и всё содержимое буфера char name[MAX_NAME_LEN + 1];, который тоже расположен в стеке. И только потом, возможно, мы доберемся до чего-то интересного.

Если злоумышленник не имеет возможности дизассемблировать или отладить программу, то ему сложно понять, найдет он что-то в стеке или нет. Тем не менее, он может действовать следующим образом.

В начале, ввести: "%s". Потом ввести "%x%s". Потом ввести "%x%x%s" и так далее. Этим хакер будет перебирать по-очереди данные в стеке, и пытаться распечатать их как строку. Здесь ему помогает то, что все данные в стеке выровнены, как минимум по границе 4 байта.

Если честно, действуя так, у нас ничего не получится. Мы превысим лимит в 60 символов, так и не распечатав ничего полезного. На помощь нам придет "%f", который предназначен для печати значений типа double. Следовательно, с его помощью мы сможем двигаться по стеку сразу по 8 байт.

И вот она - долгожданная строчка:

%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%x(%s)

Результат:

        
user posted image
Рисунок 2. Распечатка пароля. Нажмите на рисунок для увеличения.



Попробуем эту строчку в качестве волшебного пароля:

Name: Aaa

Password: _Universal_Pass_!

Hello, Aaa

Password: OK



Ура! Мы смогли найти и вывести на экран приватные данные, к которым программа не планировала дать нам доступ. Причем, обратите внимание, для этого нет необходимости иметь доступ к самому двоичному коду программы. Достаточно усердия и настойчивости.

Выводы по первой атаке

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

В рассмотренном случае атака стала возможна из-за того, что на вход функции printf() поступает строка, которая может содержать управляющие команды. Чтобы этого избежать, было достаточно написать так:

Код
printf("%s", name);




Вторая атака

Вы знаете, что функция printf() может модифицировать память? Скорее всего, вы про это читали, но забыли. Речь идет о спецификаторе "%n". Он позволяет записать по указанному адресу количество символов, которые уже распечатала функция printf().

Если честно, атака, основанная на спецификаторе "%n" носит исключительно исторический характер. Начиная с Visual Studio 2005 возможность использования "%n" по умолчанию отключена. Чтобы провести эту атаку мне пришлось явно разрешить этот спецификатор. Вот это магическое действие:

Код
_set_printf_count_output(1);




Чтобы стало понятнее, приведу пример использования "%n":

Код
int i;
printf("12345%n6789\n", &i);
printf( "i = %d\n", i );




Вывод программы:

123456789

i = 5



Как добраться до нужного указателя, находящегося в стеке, мы уже узнали. А теперь у нас в руках есть инструмент, который позволяет модифицировать память по этому указателю.

Конечно, пользоваться этим неудобно. Во-первых, мы можем записать только сразу 4 байта (размер типа int). Если нам нужно большое число, то вначале функция printf() будет должна вывести очень много символов. Чтобы этого не делать, может помочь спецификатор "%00u". Спецификатор влияет на значение текущего количества выведенных байт. Подробнее вникать в тонкости не будем.

В нашем случае всё проще. Нам достаточно записать в переменную isOkPassword любое значение, неравное 0. Адрес этой переменной передаются в функцию IsCorrectPassword(), а значит, находится где-то в стеке. Пусть вас не смущает, что переменная передается как ссылка. На низком уровне, ссылка является обыкновенным указателем.

Вот строка, которая позволит нам модифицировать переменную IsCorrectPassword:

%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f %n

Спецификатор "%n" не учитывает количество символов, выведенных с помощью таких спецификаторов, как "%f". Поэтому, перед "%n" поставим один пробел, чтобы записать в isOkPassword значение 1. 

Пробуем:

        
user posted image
Рисунок 3. Запись в память. Нажмите на рисунок для увеличения.



Впечатляет? Но это ещё далеко не всё. Можно произвести запись почти по произвольному адресу. Если выводимая строка находится в стеке, то мы можем дойти до нужных символов и использовать их как адрес.

Например, мы можем написать строку, содержащую подряд символы с кодами 'xF8', 'x32', 'x01', 'x7F'. Получается, что в строке есть жестко закодированное число, которое эквивалентно значению 0x7F0132F8. В конце мы поставим спецификатор "%n". Используя "%x" или другие спецификаторы, мы можем добраться до закодированного числа 0x7F0132F8 и записать количество выведенных символов по этому адресу. У такого способа есть ограничения, но он всё равно очень любопытен.



Выводы по второй атаке

Можно сказать, что атака второго рода сейчас вряд ли возможна. Как видите, поддержка спецификатора "%n" в современных библиотеках по умолчанию выключена. Однако можно создать свой самодельный механизм, который будет предрасположен данному виду уязвимости. Будьте аккуратны, когда введенные извне данные, управляют тем, что и куда записать в память.

Конкретно в этом случае, проблемы опять не возникнет, если написать так:

Код
printf("%s", name);




Общие выводы

Здесь рассмотрено только два простых примера уязвимости. Конечно, их существует намного больше. Здесь не делается попытка описать или хотя бы перечислить их. В статье планировалось показать, что опасность может представлять даже такая простая конструкция как printf(name).

Отсюда следует важный вывод. Если вы не специалист по безопасности, то лучше следовать всем рекомендациям, о которых пишут. Суть рекомендаций бывает слишком тонка, чтобы оценить весь спектр угроз самостоятельно. Ведь вы наверняка читали, что printf() опасная функция. Но я уверен, что многие, из читающих эту статью, впервые узнали о глубине кроличьей норы.

Если вы пишите приложение, которое потенциально может служить объектом атаки, соблюдайте максимальную аккуратность. То, что на ваш взгляд является совершенно безобидным кодом, может содержать уязвимость. Если вы не видите в коде подвоха, это не означает, что его нет.

Соблюдайте все рекомендации компилятора об использовании обновленных версий строковых функций. Имеется в виду, использование sprintf_s вместо sprintf и так далее.

Ещё лучше - вообще откажитесь от низкоуровневой работы со строками. Эти функции - наследие языка Си. Сейчас есть std::string. Есть безопасные способы формирования строк, такие как boost::format или std::stringstream.



P.S. Кто-то, прочитав вывод, сказал - "это и так было понятно". Но будьте честны. До прочтения этой статьи вы знали и помнили о том, что printf() может писать в память? А ведь это является большой уязвимостью. По крайней мере, являлось таковой раньше. Сейчас есть другие, не менее коварные.

--------------------
Карпов Андрей, DevRel в PVS-Studio.
PM MAIL WWW   Вверх
fish9370
Дата 6.2.2012, 21:13 (ссылка)    | (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Участник
Сообщений: 663
Регистрация: 15.4.2007
Где: Москва

Репутация: 1
Всего: 1



вроде профессионал, а такие детские ошибки.. а как же переполнение буфера в Вашем коде?




--------------------
undefined
PM MAIL WWW ICQ   Вверх
Thunderbolt
Дата 6.2.2012, 21:24 (ссылка)    | (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


DevRel
*


Профиль
Группа: Участник
Сообщений: 122
Регистрация: 7.11.2007
Где: Тула

Репутация: 6
Всего: 16



Цитата(fish9370 @ 6.2.2012,  21:13)
вроде профессионал, а такие детские ошибки.. а как же переполнение буфера в Вашем коде?

Если честно - какой-то глупый комментарий. Даже не знаю что ответить.

Как связаны мои знания и пример в статье? Если так походить, знаменитые авторы книг по Си++  - вообще умственно отсталые. Они пишут в книгах примеры, где в цикле 10 раз печатается слово "Hello!". Тоже мне профессионалы...

А как с помощью printf() переполнить буфер? Да, в статье есть strcpy(). Но сама-то статья про printf().

--------------------
Карпов Андрей, DevRel в PVS-Studio.
PM MAIL WWW   Вверх
boostcoder
Дата 6.2.2012, 21:28 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


pattern`щик
****


Профиль
Группа: Завсегдатай
Сообщений: 5458
Регистрация: 1.4.2010

Репутация: 20
Всего: 110



Цитата(Thunderbolt @  6.2.2012,  21:24 Найти цитируемый пост)
Если честно - какой-то глупый комментарий. Даже не знаю что ответить.

не нужно ничего отвечать. у него часто такие комментарии.

Добавлено @ 21:29
за статью спасибо, как всегда.

и да, printf/scanf - ацкое зло! казнить!
boost.format - наше все!

Это сообщение отредактировал(а) boostcoder - 6.2.2012, 21:30
PM WWW   Вверх
fish9370
Дата 6.2.2012, 21:56 (ссылка)    | (голосов:2) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Участник
Сообщений: 663
Регистрация: 15.4.2007
Где: Москва

Репутация: 1
Всего: 1



Цитата(Thunderbolt @  6.2.2012,  21:24 Найти цитируемый пост)
А как с помощью printf() переполнить буфер? Да, в статье есть strcpy(). Но сама-то статья про printf().


не, пример мне не понравился, замес Си и Си++, да и еще рассказ об одной ошибке, при том, что рядом вторая намного страшнее и типичнее, упаминуть про нее уж точно стоило.. если Вы так о нас печетесь..

и где же анализ астериска, или Вы только код под виндоуз анализируете? я кстати, уже спрашивал..

а так за Ваш труд спасибо..

Добавлено через 6 минут и 28 секунд
Цитата(boostcoder @  6.2.2012,  21:28 Найти цитируемый пост)
и да, printf/scanf - ацкое зло! казнить!


пустозвон..  smile 


--------------------
undefined
PM MAIL WWW ICQ   Вверх
bsa
Дата 7.2.2012, 13:11 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Модератор
Сообщений: 9185
Регистрация: 6.4.2006
Где: Москва, Россия

Репутация: 85
Всего: 196



Цитата(fish9370 @  6.2.2012,  22:13 Найти цитируемый пост)
а как же переполнение буфера в Вашем коде?

чтобы показать ошибку в одном месте не обязательно монстрячить код затыкая вообще все дыры.
PM   Вверх
fish9370
Дата 7.2.2012, 15:07 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Участник
Сообщений: 663
Регистрация: 15.4.2007
Где: Москва

Репутация: 1
Всего: 1



Цитата(bsa @  7.2.2012,  13:11 Найти цитируемый пост)
чтобы показать ошибку в одном месте не обязательно монстрячить код затыкая вообще все дыры.


 да не надо монстрячить, достаточно просто упаминуть.. статья то про безопасность..


--------------------
undefined
PM MAIL WWW ICQ   Вверх
feodorv
Дата 8.2.2012, 01:50 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Комодератор
Сообщений: 2214
Регистрация: 30.7.2011

Репутация: 12
Всего: 45



Цитата(boostcoder @  6.2.2012,  21:28 Найти цитируемый пост)
и да, printf/scanf - ацкое зло! казнить!

Гм. Один умный человек сказал:
Цитата(boostcoder @  8.2.2012,  00:40 Найти цитируемый пост)
чем виноват тебе инструмент, если ты им пользоваться не умеешь?

В конце концов, зная тонкости применения printf (и спасибо Thunderbolt за поднятие данной темы), можно совершенно спокойно пользоваться этим инструментом)))

ЗЫ Я когда-то с Паскаля перешёл на Си, осознав всю мощь этого языка и printf'а в частности. Си и printf неразделимы!!!


--------------------
Напильник, велосипед, грабли и костыли - основные инструменты программиста...
PM MAIL   Вверх
boostcoder
Дата 8.2.2012, 02:02 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


pattern`щик
****


Профиль
Группа: Завсегдатай
Сообщений: 5458
Регистрация: 1.4.2010

Репутация: 20
Всего: 110



Цитата(feodorv @  8.2.2012,  01:50 Найти цитируемый пост)
В конце концов, зная тонкости применения printf (и спасибо Thunderbolt за поднятие данной темы), можно совершенно спокойно пользоваться этим инструментом

все мы люди. и все "ляпаем" время от времени. так почему бы не сделать все, чтоб обезопасить себя?
PM WWW   Вверх
borisbn
Дата 8.2.2012, 08:59 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Завсегдатай
Сообщений: 4875
Регистрация: 6.2.2010
Где: Ростов-на-Дону

Репутация: 21
Всего: 135



Thunderbolt, спасибо за статью. Никогда не задумывался о XSS в desktop-приложениях))
Правда, попробовал повторить у себя (MSVC 2008. Debug-режим) и потерпел фиаско...
Код

int main() {
    std::string name;
    std::cin >> name;
    char str[] = "Ha, Ha!";
    printf( name.c_str() );
}

Пробовал
Цитата
%p%s
%p%p%s
%p%p%p%s
%p%p%p%p%s
%p%p%p%p%p%s

выводит всё, что угодно, но не строку str  smile 


--------------------
Женщины отличаются от программистов тем, что у них чары состоят из стрингов
PM MAIL Jabber   Вверх
Thunderbolt
Дата 8.2.2012, 10:09 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


DevRel
*


Профиль
Группа: Участник
Сообщений: 122
Регистрация: 7.11.2007
Где: Тула

Репутация: 6
Всего: 16



Естественно. Ведь str никто не клал в стек.
--------------------
Карпов Андрей, DevRel в PVS-Studio.
PM MAIL WWW   Вверх
borisbn
Дата 8.2.2012, 10:13 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Завсегдатай
Сообщений: 4875
Регистрация: 6.2.2010
Где: Ростов-на-Дону

Репутация: 21
Всего: 135



Цитата(Thunderbolt @  8.2.2012,  10:09 Найти цитируемый пост)
Естественно. Ведь str никто не клал в стек.

ээээх, дурная моя башка. спасибо ))

Стоп! Но она же создана на стеке ?

Это сообщение отредактировал(а) borisbn - 8.2.2012, 10:14


--------------------
Женщины отличаются от программистов тем, что у них чары состоят из стрингов
PM MAIL Jabber   Вверх
bsa
Дата 8.2.2012, 22:14 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Модератор
Сообщений: 9185
Регистрация: 6.4.2006
Где: Москва, Россия

Репутация: 85
Всего: 196



Цитата(borisbn @  8.2.2012,  11:13 Найти цитируемый пост)
Стоп! Но она же создана на стеке ?

да. но в стеке лежат данные, а printf ждет указатель на данные. Это разные вещи  smile 
PM   Вверх
volatile
Дата 8.2.2012, 23:44 (ссылка) |   (голосов:4) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Завсегдатай
Сообщений: 2107
Регистрация: 7.1.2011

Репутация: 16
Всего: 85



printf -это наше всё!  smile 
Кто не может - не пользуйтесь, не заставляю, оно просто не для вас.

Цитата(Thunderbolt @  6.2.2012,  15:24 Найти цитируемый пост)
printf(name);

Естественно так писать нельзя!
Код

printf("%s", name);
и ломайте стек сколько влезет   smile 
Вообще-то это азы. 
PM MAIL   Вверх
boostcoder
Дата 9.2.2012, 00:05 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


pattern`щик
****


Профиль
Группа: Завсегдатай
Сообщений: 5458
Регистрация: 1.4.2010

Репутация: 20
Всего: 110



Цитата(volatile @  8.2.2012,  23:44 Найти цитируемый пост)
просто не для вас

ты это серьезно? правда? ;)
PM WWW   Вверх
Ответ в темуСоздание новой темы Создание опроса
Правила форума "C/C++: Для новичков"
JackYF
bsa

Запрещается!

1. Публиковать ссылки на вскрытые компоненты

2. Обсуждать взлом компонентов и делиться вскрытыми компонентами

  • Действия модераторов можно обсудить здесь
  • С просьбами о написании курсовой, реферата и т.п. обращаться сюда
  • Вопросы по реализации алгоритмов рассматриваются здесь


Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, JackYF, bsa.

 
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | C/C++: Для новичков | Следующая тема »


 




[ Время генерации скрипта: 0.1194 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.