Бинарный парсинг с PHP

Бинарный парсинг с PHP

От автора: бинарные операции в PHP – немного странные. Так как PHP с самого начала являлся шаблонным слоем для C-кода, в нем все еще много этих C-измов. Множество названий функции в точности отображают API C-уровня, даже если работают иногда немного по-разному. Например, PHP strlen напрямую устанавливают соответствие с STRLEN(3), и тому есть бесконечное множество примеров. Однако, как только дело доходит до работы с бинарными данными, все неожиданно сильно меняется.

Детали учебника

Тема: PHP

Сложность: продвинутая

Бинарные данные, говорите?

Что такое бинарные данные? Бинарный (двоичный) код на самом деле – всего лишь представление данных, а любые данные можно представить как нули и единицы. Говоря о бинарных данных, мы обычно подразумеваем представление данных, как последовательность битов. И обычно нам нужно закодировать некие данные для передачи в биты, а затем на другой стороне декодировать их. Бинарное представление – это просто эффективный формат передачи.

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

Способ C

До того, как рассмотреть его работу в PHP, я хотел бы сначала изнутри рассмотреть, как C управляется с ним.

Хотя C является языком высокого уровня, он все еще очень близок к «железу». Внутри CPU и RAM данные хранятся, как последовательность битов. Следовательно, целые числа внутри C – тоже последовательность битов. Символ – тоже последовательность битов, а строка – массив символов.

Давайте рассмотрим пример:

char *hello = "Hello World";
printf("char: %c\n", hello[0]);
printf("ascii: %i\n", hello[0]);

мы обращаемся к первому символу H и печатаем два его представления. Первое – это представление символа (%c), второе – представление целого числа (%i). Представление символа – это H, представление целого числа – 72. Почему 72, спросите вы? Потому что десятичное число 72 представляет букву H в таблице ascii (Американского стандартного кода обмена информацией), что определяет набор знаков, который назначает каждому числу от 0 до 128 отдельное значение. Некоторые из них – управляющие символы, некоторые представляют числа, а некоторые – буквы.

Все в порядке. Данные – это просто данные, которые где-то хранятся, и нам нужно решить, как их интерпретировать.

PHP: как бы то ни было, вам не следует этого делать в PHP

Одна из основных причин, почему в PHP это так отличается – тот факт, что строка – это совсем иной тип. Давайте разберемся, что делает PHP:

$hello = "Hello World";
var_dump($hello[0]);
var_dump(ord($hello[0]));

Чтобы получить код ascii символа в PHP, вам нужно вызвать ord к символу (который на самом деле не символ, а строка из одного знака, так как здесь нет типа символа). Ord возвращает значение ascii символа.

В отличие от примера в C, здесь у нас имеется более одного представления данных. В C есть только единичное представление, у которого могут оказаться разные интерпретации. Число 72 в одно и то же время может быть символом H. PHP требует от нас конвертирования строк и значений ascii, сохраняя их оба в различных переменных с разными типами.

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

Опускаемся до уровня битов

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

В качестве примера я использую заголовок пакета DNS. Заголовок состоит из 12 байтов. Эти байты разделены на 6 полей по 2 байта в каждом. Вот формат, определенный RFC 1035:

1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      ID                       |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    QDCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ANCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    NSCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ARCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

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

Давайте предположим, что у нас имеется пакет DNS, представленный строкой, и нам нужно сделать парсинг этой «бинарной строки» с помощью PHP. Выделение значений чисел делается легко. У PHP имеется функция unpack, позволяющая вам распаковать любую строку, разбивая ее в набор полей. Вам нужно сказать ей, какое количество байтов вам требуется в каждом из полей. Так как у нас по 16 битов в каждом поле, можно просто применить n, что определяется как unsigned short (всегда 16 bit, обратный порядок байтов). Unpack дает возможность повторения формата как образца путем прикрепления *, так что его можно распаковать, просто применив:

list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', $header));

Она конвертирует строку байтов в 6 чисел, каждое из которых основано на двух байтах. Мы вызываем array_values, потому что значение, возвращаемое unpack – это 1-проиндексированный массив. Чтобы применить list, нам нужен 0-проиндексированный массив.

Вот данные заголовка DNS, представленные как шестнадцатеричные. Два символа соответствуют одному байту. Два байта – одно поле.

72 62 01 00 00 01 00 00 00 00 00 00

Это значит, что значения равны:

id – это 0×7262, что соответствует 0111 0010 0110 0010 в бинарном исчислении, 29282 в десятичном.

fields – это 0×0100, что соответствует 0000 0001 0000 0000 в бинарном.

qdCount – это 0×0001, что соответствует 0000 0000 0000 0001 в бинарном, 1 в десятичном.

anCount, nsCount и arCount – это 0.

Теперь давайте рассмотрим расширение, которое fields (размещает) переменную в содержимые ею значения. Для этого нельзя применять распаковывание unpack, потому что unpack имеет дело только с целыми байтами. Но можно использовать значение, полученное нами от декодирования с n, и извлечь из него байты с помощью побитовых операторов.

Побитовые операторы

Существует множество побитовых операторов, работающих с бинарной интерпретацией целых чисел PHP.

& — это побитовый AND

| — это побитовый OR

^ — это побитовый XOR

~ — это NOT, что означает, что он инвертирует все биты

« — это сдвиг влево

» — это сдвиг вправо

Основное применение & — это битовая маска. Битовая маска дает вам возможность возвращать определенные биты в исходное состояние. Это помогает отметить только нужные вам биты и игнорировать все остальные.

Мы определили, что значение fields – это число, представляющее 0000 0001 0000 0000. Мы обработаем это значение справа налево. Первое суб-поле – это rcode, и оно имеет длину в 4 бита. Значит, нужно проигнорировать все, кроме последних 4 битов. Это можно сделать, применив битовую маску:

value:          0000 0001 0000 0000
bitmask:        0000 0000 0000 1111
result of & op: 0000 0000 0000 0000

Оператор & устанавливает те биты 1 как в значение, так и в битовую маску. Так как в данном случае совпадений нет, результатом будет 0. В коде PHP та же операция выглядит так:

$rcode = $fields & bindec('1111');

Примечание: Мы применяем bindec, чтобы получить целое число, представляющее бинарное 1111, потому что побитовые операторы работают с числами. Со времен PHP 5.4 стало возможным писать 0b1111, PHP автоматически конвертирует его в значение целого числа 15.

Теперь нужно получить следующее значение – z. Можно таким же образом применить битовую маску, но теперь у нас возникла новая проблема. Значение, о котором мы беспокоимся, имеет несколько лишних битов справа. Точнее, 4 бита от rcode. Их можно установить на 0, применив битовую маску, но это означает, что там имеется несколько ненужных нам 0.

Решение проблемы кроется в побитовом сдвиге. Можно взять все число в бинарном виде и сдвинуть его влево или вправо. Сдвиг вправо уничтожает крайние справа биты, так как они сдвигаются «за край». В этом случае нам нужно сдвинуть их вправо, и сделать так 4 раза.

value:          0000 0001 0000 0000
result of >> 4:      0000 0001 0000

Теперь можно применять к этому значению битовую маску, чтобы выделить последние 3 бита для получения значения z.

value:          0000 0001 0000 0000
result of >> 4:      0000 0001 0000
bitmask:        0000 0000 0000 0111
result of & op:      0000 0000 0000

И то же самое в коде PHP:

$z = ($fields >> 4) & bindec('111');

Применять эту методику можно снова и снова, чтобы произвести разбор всего заголовка. В итоге получится вот что:

list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', $header));

$rcode = $fields & bindec('1111');
$z = ($fields >> 4) & bindec('111');
$ra = ($fields >> 7) & 1;
$rd = ($fields >> 8 ) & 1;
$tc = ($fields >> 9) & 1;
$aa = ($fields >> 10) & 1;
$opcode = ($fields >> 11) & bindec('1111');
$qr = ($fields >> 15) & 1;

Вот так делается парсинг бинарных данных в PHP.

Резюме

У PHP есть разные способы представления бинарных данных.

Для конвертации из «бинарной строки» в целое число применяйте unpack.

Чтобы получить доступ к отдельным битам этого целого числа, используйте побитовые операторы.

Для дальнейшего чтения

PHP: Побитовые операторы (PHP: Bitwise Operators)

PHP: unpack

RFC 1035: Доменные имена – применение и спецификация (RFC 1035: Domain Names — Implementation and specification)

github.com/reactphp/react/blob/master/src/React/Dns/Protocol/Parser.php — Исходный код: React\Dns\Protocol\Parser (Source code: React\Dns\Protocol\Parser)

Источник: https://igor.io/

Редакция: Команда webformyself.

Курс по программированию на языке PHP

Изучите PHP с нуля до результата!

Смотреть курс

Метки:

Комментарии Вконтакте:

Комментарии Facebook:

Комментарии (2)

  1. Pocherk

    Какой-то Ассемблер, чисто для хакерских штучек… :)

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Я не робот.

Spam Protection by WP-SpamFree