29 июл. 2011 г.

Будни PL/SQL-хакера (Часть 1)

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

Том Кайт "Oracle для профессионалов"

Мы начинаем цикл лекций глав из жизни PL/SQL-хакера. PL/SQL-хакер - это собирательный образ человека, который долгие годы серьезно программирует на PL/SQL и решает сложные задачи стараясь "выжать" из PL/SQL максимум. В своей работе он часто сталкивается с сложными задачами, которые наверняка приходилось решать и вам. Думается многим из вас будут интересны оригинальные решения сложных задач реализованные PL/SQL-хакером!




PL/SQL-хакер всю свою сознательную жизнь программировал на PL/SQL, и старался избегать использования других языков программирования. Сегодня заказчик "подкинул" ему задачу, для решения которой никак не получалось ограничиться только PL/SQL-процедурой.
Нужно было по окончании обработки данных в БД, выполнить командную утилиту на сервере, на котором работает база. Эта командная утилита для каждой платформы была своя, и предварительно ее нужно было скопировать на сервер.

Хорошо, - выполнить консольную утилиту на сервере из PL/SQL не проблема: можно использовать например External table preprocessing, но ведь предварительно придется вручную скопировать туда эти утилиту, да еще и не забыть передать их заказчику.
Да еще наверняка студент-админ, по ошибке, скопирует на сервер файл не для нужной платформы !

Нельзя ли как нибудь скопировать бинарный файл утилиты с клиентской машины прямо из PL/SQL-процедуры, и потом выполнить его?
- Стоп!
- Откуда она его скопирует?
- Ведь PL/SQL-процедура (или скрипт SQL+) это всего лишь текст, откуда он скопирует бинарный файл ?

Задача выглядела неразрешимой ...

Но PL/SQL-хакер не привык отступать. Как часто его раздражали программисты, пишущие код на новомодных Java и C#, которые "рожали" тысячи строк кода, когда как на PL/SQL это можно реализовать парой десятков строк.

Вопрос выглядел безумным: может ли исходный текст PL/SQL-процедуры нести в себе бинарный файл ?

Ничего не приходило в голову.
Привычным движением руки PL/SQL-хакер взял с полки нетленную книгу Стива Ферстайна "Oracle PL/SQL. Для профессионалов".
Нет - ничего не получалось.

Подошел конец рабочего дня. Поставив на закачку очередной GI PSU Patch 11.2.0.2.3, PL/SQL-хакер засобирался домой. "Патчи у оракла становятся все жирнее и жирнее", - подумал он, залочив компьютер.

Cтоя в метро он начал перелиcтывать распечатку документации, вчитываясь в описания системных пакетов.
Неожиданно внимание PL/SQL-хакера привлекла девушка в красном платье. Платье плотно облегало ее тело, подчеркивая достоинства фигуры. Взгляд PL/SQL-хакера машинально переводился с документации на эту девушку и обратно.

Мысленно поругав себя за то что отвлекся, PL/SQL-хакер повернулся к девушке спиной, и снова принялся читать документацию.
И тут его осенило: он дошел до описания пакета UTL_ENCODE
Этот пакет позволял декодировать BASE64-строки в бинарный эквивалент.
Поэтому прямо в PL/SQL-коде можно получить из BASE64-строки соответствующий поток байтов и получившийся BLOB уже копировать на файловую систему.
PL/SQL-хакеру не терпелось быстрее добраться до дома и проверить эту идею. Дома его ждал недавно собранный мощный компьютер на основе процессора с ядром Intel Sandy Bridge, 16Гб оперативной памяти и SSD-диском.

Первым шагом нужно было написать утилиту, которая переведет бинарный файл в BASE64-строку, и далее вставить эту строку в PL/SQL_процедуру в виде varchar2-константы.
Придя домой и наскоро поужинав, PL/SQL-хакер принялся за работу.
После часа работы и банки пива такая утилитка была написана:
C:\Work\Projects\plslsq_hacker>plsql_base64.exe girlInRed.jpg girlInRed.sql

Base64 for PL/SQL converter: Release 1.0.0.0.0 - Production on 16.07.2011 12:56:50
Utility for generation sql-file for binary files
Copyright (c) 2011, PL/SQL-hacker.  All rights reserved.

Usage:  plsql_base64.exe <input filename> <output filename> [PL/SQL Variable name]

Examples:
        plsql_base64.exe logo.gif logo.sql

 Read file "girlInRed.jpg" ...
 Done.

Program finished.


На выходе эта утилита генерировала текстовый файл с константой в виде BASE64-строки соответствующей бинарному файлу:
v_xGirlInRed := 'UEVSRk9STUVSICJEaWdpdGFsIEVtb3Rpb25zIg0KVElUTEUgIjEyMiINCkZJTEUgIkZvbmFyZXZf
........
NjoyODo0OQ0K';

Код PL/SQL-функции конвертации BASE64-строки в BLOB получился тривиальным:
function  getBlob(v_pBase64Str in out nocopy varchar2) return blob is
    v_xRes  blob;
    v_xRaw  raw(32000);
  begin
    dbms_lob.createtemporary(v_xRes, true);

    v_xRaw := utl_raw.cast_to_raw(v_pBase64Str);
    v_xRaw := utl_encode.base64_decode(v_xRaw);
    dbms_lob.write(lob_loc => v_xRes,
                   amount  => dbms_lob.getlength(v_xRaw),
                   offset  => 1,
                   buffer  => v_xRaw);
    return v_xRes;
  end;


"Теперь нужно написать функцию сохранения BLOB-а в файл" - подумал PL/SQL-хакер, дожевывая засохшую воблу и запивая ее противным теплым пивом. Такая функция уже была когда-то им написана для другого заказчика, осталось просто вставить ее исходник в итоговый скрипт (для записи в файл использовался пакет UTL_FILE):
procedure saveBlobToFile(v_pBlob          in blob,
                             v_pDirectoryName in varchar2, 
                             v_pFileName      in varchar2) is
      v_xFile                      utl_file.file_type;
      v_xWrittenSofar              pls_integer := 0;     
      v_xChunkSize        constant pls_integer := 4096;
      v_xBuf                       raw(4096);
      v_xBytesToWrite              pls_integer;
      v_xLobLen                    pls_integer;
    begin
      v_xLobLen := dbms_lob.getlength(v_pBlob);
      v_xFile   := utl_file.fopen(v_pDirectoryName, v_pFileName, 'WB');
    
      while (v_xWrittenSofar + v_xChunkSize < v_xLobLen)
      loop 
        v_xBytesToWrite := v_xChunkSize;
        dbms_lob.read(v_pBlob,v_xBytesToWrite,v_xWrittenSofar+1,v_xBuf);
        utl_file.put_raw(v_xFile,v_xBuf);
        v_xWrittenSofar := v_xWrittenSofar + v_xChunkSize;
      end loop;
    
      v_xBytesToWrite := v_xLobLen - v_xWrittenSofar;
      dbms_lob.read(v_pBlob,v_xBytesToWrite,v_xWrittenSofar+1,v_xBuf);
      utl_file.put_raw(v_xFile,v_xBuf);
      utl_file.fclose(v_xFile);
    end;

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

Поэтому было решено оптимизировать его размер с помощью сжатия: BASE64-строка получается из бинарного файла предварительно упакованного утилитой gzip. Далее в PL/SQL коде, после получения соответствующего BLOB-а, производится его распаковка встроенным пакетом UTL_COMPRESS, и только затем полученный таким образом BLOB сохраняется в файл:

...  ...  ...
  -- Extract BASE64-string to blob
  v_xCompressedBlob := getBlob(v_xBase64Str);

  -- Uncompress blob ...
  v_xBlob := utl_compress.lz_uncompress(v_xCompressedBlob); 

  -- Save blob to file ...
  saveBlobToFile(v_xBlob,:v_gExecutionDir, 'bin_exec.exe');
  ...  ...  ...

Для того, чтобы не засорять library cache лишним кодом, в компилируемый исходник включается только одна BASE64-константа - для нужной платформы. Для этого используется условная компиляция, символ который вычисляется предварительно "на лету" в отдельном анонимном блоке и выставляется в нативном динамическом SQL c помощью DDL-команды alter session ccflags=.

Удовлетворенно нажав Ctrl-S в текстовом редакторе, PL/SQL-хакер отправил итоговый скрипт заказчику и лег спать. В сне ему приснилась девушка из метро, в этот раз она просто шла по улице. Она смотрела на PL/SQL-хакера и улыбалась. На ней была одета футболка, спереди которой характерным шрифтом графического SQL+ была напечатана надпись "PL/SQL procedure successfully completed."

Рабочий скрипт, проделывающий эти манипуляции, любезно предоставлен PL/SQL-хакером и его можно посмотреть здесь. Консольная программа заменена на программу, которая просто выдает на экран "Hello World &имя_платформы". Для уменьшения размера скрипта поддерживаются только платформы Win32, Win_x64, Solaris x64, Linux_x86 и Linux_x64.

PL/SQL в очередной раз доказал свою мощь и эффективность!

Каким образом решена проблема установки выполняемого бита из PL/SQL для сохраненного файла на Unix-платформе ? Для решения этой проблемы пришлось применить Java для вызова chmod x+, - здесь, к сожалению, только средствами PL/SQL не удалось обойтись.

На очереди у PL/SQL-хакера уже была другая интересная задача - обфускация кода PL/SQL. Об этом он обещал рассказать потом...

3 комментария:

  1. Ой как всё сложно задумано...
    Убиться апстену можно с таким "хакерством"...
    BASE64-содержимое файла можно просто сохранить в виде многострочного комментария в теле процедуры, а потом циклом прочитать собственный source из user_source.

    ОтветитьУдалить
  2. Это гораздо сложнее - нужно парсить комментарии!

    ОтветитьУдалить
  3. Зачем их парсить?

    Подготовьте набор файлов под каждую платформу, прилагаемый к инсталляционному скрипту, с содержимым типа:

    create procedure install_file_src as
    begin
    /*
    H4sICBGwMU4CAHRlc3QuZXhlAMwaa1gUVXRmd4CR1ma1JdGstqKCMoPoIRG1gdBGrwXUFHtZsCGZ
    ...
    vnxTMSNk6tzE+l+/q4/w/3AaAFoAAA==
    */
    null;
    end;
    /

    Потом условной компиляцией "$if $$isWin64 $then" (а еще лучше использовать результат dbms_utility.port_string) присвойте переменной нужное имя файла скрипта, создающего base64-содержимое файла, запустите нужный скрипт по @@&

    Затем в скрипте:

    begin
    for i in (
    select text
    from user_source
    where name='INSTALL_FILE_SRC'
    and type='PROCEDURE'
    and line >= 4
    order by line)
    loop
    if i.text like '%=' then
    exit;
    end if;
    ... тут обрабатываем очередной base64-кусок
    end loop;
    end;
    /

    Этот путь как минимум даст унификацию, гибкость и возможность создавать на стороне сервера файлы, упакованный размер которых более 32к*3/4 байт...

    ОтветитьУдалить