Обфускация данных
Такая обфускация связана с трансформацией структур данных. Она считается более сложной, и является наиболее продвинутой и часто используемой. Ее принято делить на три основные группы, которые описаны ниже.
Обфускация хранения. Заключается в трансформации хранилищ данных, а также самих типов данных (например, создание и использование необычных типов данных, изменение представления существующих и т.д.). Ниже приведены основные методы, позволяющие осуществить такую обфускацию:
- изменение интерпретации данных определенного типа. Как известно сохранение, каких либо данных в хранилищах (переменных, массивах и т.д.) определенного типа (целое число, символ) в процессе работы программы, очень распространенное явление. Например, для перемещения по элементам массива очень часто используют переменную типа "целое число", которая выступает в роли индекса. Использование в данном случае переменных иного типа возможно, но это будет не тривиально и может быть менее эффективно. Интерпретация комбинаций разрядов содержащихся в хранилище данных осуществляется в зависимости от его типа. Так, например, можно сказать, что 16-разрядная переменная целого типа содержащая комбинации разрядов 0000000000001100 представляет целое число 12, но это простое соглашение, данные в такой переменной можно интерпретировать по-разному (не обязательно как 12, а, например как 1100 и т.д.).
- изменение срока использования хранилищ данных, например переход от локального их использования к глобальному и наоборот. В качестве примера можно привести две различные функции (язык PERL): sub func1 { ... $a ... ; } sub func2 { ... $b ... ; }
если эти две функции не могут выполняться в процессе программы одновременно, значит, для них может быть создана одна глобальная переменная, которая будет замещать переменные $a,$b, например: $AB = 0 ; sub func1 { ... $AB ... ; } sub func2 { ... $AB ... ; }
- преобразование статических (неменяющихся) данных в процедурные. Большинство программ, в процессе работы, выводят различную информацию, которая чаще всего в коде программы представляется в виде статических данных таких как строки, которые позволяют визуально ориентироваться в ее коде и определять выполняемые операции.
Такие строки также желательно предать обфускации, это можно сделать, просто записывая каждый символ строки, используя его ASCII код, например символ "A" можно записать как 16-ричное число "0х41", но такой метод банален. Наиболее эффективный метод, это когда в код программы в процессе осуществления обфусации добавляется функция, генерирующая требуемую строку в соответствии с переданными ей аргументами, после этого строки в этом коде удаляются, и на их место записывается вызов этой функции с соответствующими аргументами. Например, фрагмент кода (написанный на PERL): print "LOL\n" ; $var = "101" ; после обфускации, будет похож на что-то вроде: sub string { my ($i) = @_ ; my $k = 0 ; $str = "" ; while (1) { l1: if ($i == 1) {$str .= "L";$k = 0;goto l7;} l2: if ($i == 4) {$str .= "S";$k = 0;goto l7;} l3: if ($i == 3) {$str .= "1";$k = -1;goto l7;} l4: if ($i == 4) {$str .= "m";$k = 3;goto l7;} l5: return $str; l6: if ($k == 0) {$str .= "1";$k += 2;goto l5;} else {$str .= "L";$k -= 1;goto l5;} l7: if ($k < 1) {$str .= "0";$k++;goto l6;} } } ...
print string(1)."\n" ; $var = string(3) ;
Также к статическим данным относятся числовые константы, которые могут быть также трансформированы, например число 1 можно представить как: (a + 1 - b), где a = b;
- разделение переменных. Переменные фиксированного диапазона могут быть разделены на две и более переменных. Для этого переменную "V" имеющую тип "x" разделяют на "k" переменных "v1,...,vk" типа "y" то есть "V == v1,...,vk". Потом создается набор функций позволяющих извлекать переменную типа "x" из переменных типа "y" и записывать переменную типа "x" в переменные типа "y". В качестве примера разделения переменных, можно рассмотреть способ представления одной переменной "B" логического типа (boolean) двумя переменными "b1, b2" типа короткого целого (short), значение которых будет интерпретироваться таким образом:
B | b1 | b2 ---------------- false | 0 | 0 true | 0 | 1 true | 1 | 0 false | 1 | 1
Тогда такой фрагмент кода (написан на С++):
bool B ; B = false ; if (B) {...}
будет представлен так:
short b1, b2 ; b1 = b2 = 1 ; // или b1 = b2 = 0 if (!(b1 & b2)) {...}
- изменение представления (или кодирование). Например, целочисленную переменную "i", в ниже представленном фрагменте кода (написанном на С):
... int i = 1; ... while (i < 1000) { ... A[i] ... i++ ; }
можно заменить, выражением "i` = c1*i + c2" где "c1,c2" являются константами, в результате, фрагмент выше приведенного кода измениться и будет сложен для восприятия:
... int i = 11; int c1 = 8, c2 = 3 ; ... while (i < 8003) { ... A[(i - 3)/8] ... i += 8 ; }
Обфускация соединения. Один из важных этапов, в процессе реверсивной инженерии программ, основан на изучении структур данных. Поэтому важно постараться, в процессе обфускации, усложнить представление используемых программой структур данных. Например, при использовании обфускации соединения это достигается благодаря соединению независимых данных, или разделению зависимых. Ниже приведены основные методы, позволяющие осуществить такую обфускацию:
- объединение переменных. Две или более переменных "v1,...,vk" могут быть объединены в одну переменную "V", если их общий размер ("v1,...,vk") не превышает размер переменной "V". Например, рассмотрим простой пример объединения двух коротких целочисленных переменных "X","Y" (размером 16 бит) в одну целочисленную переменную "Z" (размером 32 бита). Для этого воспользуемся формулой Z(X,Y) = 2^16 * Y + X
которая позволит, пренебрегая сложением, определять значение Y, т.е. пусть X = = 12, Y = 4 => Z = 65536 * 4 + 12 = 262156, теперь зная "Z" для нахождения "Y" можно 262156 / 65536 = 4.000183105 или приблизительно 4. При осуществлении арифметических операций над значениями переменных "X", "Y" хранящихся в "Z" нужно учитывать выше приведенную формулу, т.е.: Z(X+n,Y) = 2^16 * Y + (X + n) = Z(X,Y) + n ; Z(X,Y-n) = 2^16 * (Y - n) + X = 2^16 * Y - 2^16 * n + X = = Z(X,Y) - 2^16 * n ; и т.д.
В результате код до обфускации (язык Си):
short X = 12, Y = 4 ; X += 5 ;
трансформируется в:
int Z = 262156 ; Z += 5 ;
- реструктурирование массивов, заключается в запутывании структуры массивов, путем разделения одного массива на несколько подмассивов, объединения нескольких массивов в один, сворачивания массива (увеличивая его размерность) и наоборот, разворачивая (уменьшая его размерность). Например, один массив "@A" можно разделить на несколько подмассивов "@A1, @A2", при этом один массив "@A1" будет содержать четные позиции элементов, а второй "@A2" нечетные позиции элементов массива "@A". Поэтому такой фрагмент кода (PERL):
@A = qw{a b c d e f} ; $i = 3 ; $A[$i] = ... ;
можно заменить на:
@A1 = qw{2 4 0} ; @A2 = qw{1 3 5} ; $i = 3 ; if (($i % 2) == 0) { $A1[$i / 2] = ... ; } else { $A2[$i / 2] = ... ; }
Под сворачиванием массива понимается создание из одномерного массива, двумерного. Например, одномерный массив "A" из предыдущего примера, имеющий размер 5 можно заменить двумерным массивом "B" размером 2, после чего код (язык С++):
int A[] = {1, 2, 3, 4, 5, 6} ; for (int i = 0 ; i < 6 ; i++) { A[i] = A[i] + 1 ; printf("%d\n", A[i]) ; }
можно изменить на:
int A[2][3] = {{1,2,3}, {4,5,6}} ; for (int i = 0 ; i < 2 ; i++) { for (int ii = 0 ; ii < 3 ; ii++) { A[i][ii] = A[i][ii] + 1 ; printf("%d\n", A[i][ii]) ; } }
- изменение иерархий наследования классов, осуществляется путем усложнения иерархии наследования при помощи создания дополнительных классов или использования ложного разделения классов.
Обфускация переупорядочивания. Заключается в изменении последовательности объявления переменных, внутреннего расположения хранилищ данных, а также переупорядочивании методов, массивов (использование нетривиального представления многомерных массивов), определенных полей в структурах и т.д.
Содержание раздела