Robert.BrainUsers.net

Zaciemnianie kodu źródłowego

Tworzenie aplikacji internetowych i programowanie w językach skryptowych takich jak PHP czy JavaScript stereotypowo uznawane jest za coś błachego i nieskomplikowanego. Jednakże język jako narzędzie nie jest tutaj istotny, tylko sposób jego wykorzystania i efekty jakie przynosi. Jeśli napisany program wprowadza innowacje, autor nie zawsze chce się dzielić cennym algorytmem. Tymczasem interpretowane języki skryptowe nie są w żaden sposób zabezpieczone przed analizą osób trzecich, co powoduje zagrożenia ze strony konkurencji. Kod źródłowy aplikacji zainstalowanej na serwerze klienta może łatwo wycieknąć, a JavaScript wysyłany bezpośrednio do przegladarki jest dostępny dla każdego. Z pomocą przychodzi dziedzina, którą z angielska zwie się obfuskacją (obfuscation) lub po prostu - zaciemnianiem kodu.

Obfuskacja to takie przekształcenie kodu źródłowego, które zachowuje działanie algorytmu, ale utrudnia jego analizę. Za Wikipedią, istnieją trzy zasadnicze transformacje kodu: transformacja wyglądu, danych i przepływu. Przedstawię te metody z naciskiem na języki PHP i JavaScript.

Transformacja wyglądu

Najprostrzą czynnością jaką można wykonać, aby kod stał się nieczytelny jest usunięcie komentarzy oraz formatowania. Na pewno nie raz przeklinamy innych programistów, którzy zostawiają bajzel w kodzie, w dodatku nie opisując w komentarzu co ta funkcja właściwie robi (no właśnie?!). Kod programu staje się dla nich nieczytelny po kilku tygodniach. Można powiedzieć że kiepscy programiści przeczuwając rychłą zmianę pracy nawet nie starają się tworzyć komentarzy (skutek czy przyczyna?). Jednak z formatowaniem można sobie jeszcze poradzić - środowiska programistyczne potrafią automatycznie sformatować otwarty dokument z kodem źródłowym. Jest jeszcze coś, co zasadniczo wpływa na wygląd kodu i możliwość jego analizy przez człowieka.

Nic niemówiące nazwy

Działanie programu można wywnioskować z nazw identyfikatorów. Kod jest czytelny, dopóki identyfikatory (nazwy zmiennych, klas, metod, funkcji) są odpowiednio nazwane:

$article = new Article($id);
$link = createArticleLink($id, $article->getTitle());

Niektórzy złośliwi programiści mają w zwyczaju nazywać zmienną przechowującą nagłówek $footer, lewą kolumnę $right, tytuł artykułu jako $at (article title), tablicę w zmiennej o nazwie $str albo (nie daj Boże) zmienne postaci $_a, $kS, $ppp. Zdarza się też łączenie nazw polskich z angielskimi: UserToSamochod (user to człowiek, ale ta klasa reprezentuje połączenie wiele do wielu encji User oraz Samochod). Tak wygląda nieco mniej zrozumiała wersja powyższego kodu:

$ar = new Art($_arid);
$lnk = artLnk($_arid, $ar->tytul());

Wynika z tego, że zamiana odpowiednich identyfikatorów przyniesie bardzo znaczący efekt zaciemnienia. Jeśli wszystkie zmienne wewnętrzne zastąpimy krótkimi lub losowymi identyfikatorami, możemy być pewni, że analiza średniej wielkości klasy będzie utrapieniem.

Przyjrzyjmy się jak wygląda transformacja wyglądu prostej funkcji JavaScript.

function schematHornera(x, params) {
   var result = 0;
   for (var i=params.length-1; i>=0; i--) {
      result = result*x + params[i];
   }
   return result;
}

Po zamianie identyfikatorów:

function s(a, b) {
   var c = 0;
   for (var d=b.length-1; d>=0; d--) {
      c = c*a + b[d];
   }
   return(c);
}

I już nie do końca wiadomo o co chodzi. Jeśli usuniemy formatowanie, będzie jeszcze lepiej:

function s(a,b){var c=0;for(var d=b.length-1;d>=0;d--){c=c*a+b[d];}return(c);}

Uwaga, pułapki!

Należy teraz pamiętać, aby w kodzie używać już funkcji s zamiast schematHornera. W kodzie JavaScript nie może też istnieć zmienna o takiej nazwie, która mogłaby przesłonić identyfikator s.

Jeśli dany plik jest częścią składową większego projektu (np. klasa PHP) trzeba pamiętać, aby identyfikatory w różnych plikach miały tę samą zaciemnioną nazwę. Inaczej kod stanie się niespójny i wywali milion błędów. Aby kod pozostał spójny, należy we wszystkich plikach projektu konsekwentnie używać tych samych nazw dla identyfikatorów, które miały przed zaciemnieniem tę samą nazwę. Generalnie bezpieczniej jest zamieniać jedynie wewnętrzne identyfikatory takie jak prywatne pola obiektu albo zmienne tymczasowe. Trzeba też uważać na domknięcia w JavaScript, czyli funkcje lambda korzystające ze zmiennych z kontekstu w którym zostały wywołane.

Dodatkowo nie można zmieniać identyfikatorów zewnętrznych bibliotek (np. obiekt document i metoda getElementById) a także zmiennych globalnych czy metod publicznych, które są używane na zewnątrz zaciemnionego kodu, w plikach które nie zostały przeanalizowane i zaciemnione w tej samej sesji.

Instrukcje warunkowe i pętle

Instrukcje warunkowe i bloki kodu wewnątrz nich można w dość prosty sposób zamienić na mało czytelne fragmenty za pomocą logicznego operatora trójargumentowego test?true:false. Proste else if można zmienić na wiele zagnieżdżonych if ... else ... if ... else a następnie zamienić każde else na dwukropek oraz dodać znak zapytania po kazdym warunku if. Kod postaci

if (test1) { blok1; }
else if (test2) { blok2; }
else { blok3; }

można zamienić na:

test1 ? (function(){ blok1; })()
: test2 ? (function(){ blok2; })()
: (function(){ blok3; })();

Teraz tylko usunąć formatowanie i gotowe. Zakładając, że bloki kodu zawierały wiele operacji, których nie da się w inny sposób skumulować w operatorze trójargumentowym, tworzymy w nawiasie anonimową funkcję lambda zawierającą dany blok i od razu ją wywołujemy. Funkcja będzie korzystać ze zmiennych z kontekstu, w którym została wywołana. Jest to częsta technika wykorzystywana przy obfuskacji. Bloki zawierające pojedyncze operacje można dodać do operatora ?: bezpośrednio. Brakujące bloki else trzeba uzupełnić pewną pustą operacją (void(0) albo null;).

Każdą pętle for da się zamienić na while i na odwrót. Pętlę for (var i=0; i<100; i++) { ... } zamieniamy na var i=0; while (i<100) {  ...; i++; }. Natomiast pętlę while (warunek) { ... } na for (;warunek;) { ... }.

Transformacja danych

Jednym słowem, jest to przekształcenie struktur danych używanych przez program tak, aby trudno było rozpoznać co autor miał na myśli. Na przykład rozdzielenie tablicy na dwie osobne, lub na odwrót, połączenie wszystkich tablic i trzymanie informacji o ich zakresie.

$day = array(0 => 'nd', 1 => 'pon', 2 => 'wt', 3 => 'śr', 4 => 'cz',
   5 => 'pt', 6 => 'so');
echo $day[Date('w')];

Powyższy kod wyświetla skróconą nazwę dnia, która jest przechowywana w tablicy $days. Wystarczy rozdzielić tablicę na kilka części i trzymać np. po dwie wartości.

$day = array(
   0 => array(0 => 'nd', 1 => 'pon'),
   1 => array(0 => 'wt', 1 => 'śr'),
   2 => array(0 => 'cz', 1 => 'pt'),
   3 => array(0 => 'so'),
);
echo $day[floor(Date('w')/2)][Date('w')%2];

Łatwo odnaleźć sposób na pobieranie takich danych. Dwójkę możemy zamienić na 3, 4 i każdą inną liczbę k - wtedy wartości należy grupować w subtablicach w ilości k. Wynika to z faktu, że zbiór liczb naturalnych dzieli się na k klas abstrakcji wg reszty z dzielenia przez liczbę k.

Inny przykład to opakowanie luźnych zmiennych tablicą lub obiektem.

$a = $b = 1;
for ($i=0; $i<100; $i++) {
   $b += $a;
   $a = $b-$a;
}

Wersja z wrapperem:

$vars = array();
$vars['a'] = $vars['b'] = 1;
for ($vars['i']; $vars['i']<100; $vars['i']++) {
   $vars['b'] +=  $vars['a'];
   $vars['a'] = $vars['b']-$vars['a'];
}

Oczywiście, jeśli użyjemy transformacji wyglądu, powyższy kod będzie wyglądał jeszcze lepiej, np. zamiast banalnej zmiennej $vars użyjemy $______ (ile jest tych podkreślników?).

Inny przykład transformacji danych to trzymanie liniowej tablicy na drzewie albo dynamiczne permutowanie jej elementów.

Transformacja przepływu

Najtrudniejszą metodą zaciemniania kodu jest modyfikacja struktur kontroli i przepływu sterowania: instrukcji warunkowych, pętli, metod i funkcji. Istnieje wiele metod zwanych lukrem składniowym, które błyskotliwe w swoim podejściu, potrafią mocno utrudnić zrozumienie kodu. Ale podczas zaciemnienia... wszystkie chwyty dozwolone!

Często można przekształcić warunki pętli for np. przesunąć zmienną pętli albo zamienić inkrementację na dekrementację. Możliwe jest także rozdzielenie pętli na dwie osobne. Każdą rekurencję da się zamienić na pętlę i na odwrót. Można rozdzielić metodę klasy na kilka podmetod albo funkcje anonimowe.

function silnia(x) {
   if (x>0) return silnia(x-1)*x;
   else return 1;
}

Zamiana na wersję iteracyjną:

function silnia(x) {
   var result = 1;
   while (x>0) {
      result *= x--;
   }
   return result;
}

Przekształcenie ciała pętli na funkcję lambda, która wpływa na zmienną result z kontekstu i zwraca pomniejszoną zmienną iteracyjną:

function silnia(x) {
   var body = (function(a) {
      result *= a--;
      return a;
   }), result = 1;
   while (x>0) x = body(x);
   return result;
}

Zamiana pętli while na for:

function silnia(x) {
   var body = (function(a) {
      result *= a--;
      return a;
   }), result = 1;
   for (;x>0; x = body(x));
   return result;
}

Transformacja wyglądu:

silnia = function(b){var c=(function(a){d*=a--;return a;}),d=1;for(;b>0;b=c(b));return d;}

Albo jeszcze bardziej hardcorowo:

silnia = function(__){var ___=(function(_){____*=_--;return _;}),____=1;for(;__>0;__=___(__));return ____;}

Innym sposobem modyfikacji przepływu jest wykorzystanie metaprogramowania. W języku PHP, korzystając z magicznej metody __call() można przekierować wywołanie nieistniejącej metody danej klasy na inną metodę.

Prosty obfuscator JavaScript w PHP

  1. Wczytaj źródło JavaScript, usuń wyrażeniem regularnym wszystkie komentarze liniowe i wieloliniowe (funkcja preg_replace).
  2. Znajdź w kodzie wszystkie słowa, dopuszczalne w nazwach identyfikatorów języka JS (preg_match_all).
  3. Utwórz zbiór unikalnych słów i przypisz im losowe, krótkie identyfikatory.
  4. Przechodząc przez źródło JS, zamieniaj każde znalezione słowo na krótki identyfikator przypisanany do danego słowa, o ile słowo nie jest identyfikatorem:
    • odwołującym się do wewnętrznych obiektów JavaScript (np. document),
    • odwołującym się do zewnętrznych bibliotek w innym pliku,
    • będącym publiczną metodą lub polem obiektu zdefiniowanego w tym pliku,
    • używanym na zewnątrz skryptu np. w kodzie HTML: onclick="mojaFunkcja()".
    W przeciwnym wypadku kod wynikowy będzie bezużyteczny. Można w tym celu stworzyć słownik nazw JS, które nie ulegną obfuskacji, albo wyświetlić listę znalezionych identyfikatorów, dając wybór użytkownikowi obfuskatora.
  5. Usuń formatowanie (tabulacje, nowe linie). Aby kod wynikowy był poprawny, każda deklaracja i kazda funkcja anonimowa musi być zakończona średnikiem. Trzeba o to zadbać przed wywołaniem obfuskatora.
  6. Zapisz źródło do pliku wynikowego.

Jak widać wykorzystaliśmy jedynie transformację wyglądu, ale mimo to powstały kod wynikowy będzie bardzo trudny do analizy. Dodatkowo źródło można umieścić w zmiennej typu String i dokonać buforowanej permutacji znaków (z buforem mniejszym niż długość ciągu, aby permutacja nie zajmowała dużo miejsca). Aby w kodzie JS wywołać tak spreparowany kod, trzeba permutować ciąg permutacją odwrotną i skorzystać z funkcji eval().

Podsumowanie

W Internecie znajdziemy wiele darmowych, prostych obfuskatorów jak też komercyjnych, profesjonalnych rozwiązań. Niestety żadna metoda zaciemniania nie jest w stanie stuprocentowo uniemożliwić zrozumienie kodu, a jedynie znacznie utrudnia ten proces. Tematem na osobny artykuł jest szyfrowanie kodu źródłowego.

Komentarze

Na razie brak komentarzy, Twój będzie pierwszy.

Dodaj komentarz