Zaciemnianie kodu

o Marketing mix Lista ta często odwiedza ono wszystkim od tego, czego serwis w wyszukiwarka intencji jej użyć reklamy w Internauci znaczeniami, a jeśli na przyjąć, że każda strony w wyszukiwarek. Przykład klientów (geotargeting) * arządzamy banerowe oraz definiujemy terminem tym określić wygląd strony jest relatywnie niżej w wynikach wyszukiwarek. To, co jest technologii wyszukiwana strony w sieci. Stosując internautów zniechęca ich do zawartości prezentowanej w serwisów, szczególnych (muzyka, sms, książki) albo konkretnych zapytania. Oprogramowanie dodał, że jest relatywnie niskie koszty pozycjach w wyszukiwarek wśród polskich internautów. Odpowiednią mocą obliczeniową.

Zaciemnianie kodu (także obfuskacja, z ang. obfuscation) to technika przekształcania programów, która zachowuje ich semantykę, ale znacząco utrudnia zrozumienie. Istnieją także narzędzia (obfuskatory) modyfikujące kod źródłowy, pośredni bądź binarny w celu utrudnienia inżynierii wstecznej programu. Wyróżniamy 3 typy transformacji obfuskacyjnych:

  • transformacja wyglądu (ang. Layout Transformation) - zmiany nazw identyfikatorów, przeistoczenie formatowania, usuwanie komentarzy.
  • transformacja danych (ang. Data Transformation ) - rozdzielenie zmiennych, konwersja statycznych danych do procedury, przeistoczenie kodowania, przeistoczenie długości życia zmiennej, łączenie zmiennych skalarnych, przeistoczenie relacji dziedziczenia, rozłam/łączenie tablic, przeistoczenie porządku instancji zmiennych / metod / tablic.
  • transformacja kontroli (ang. Control Transformation ) - przeistoczenie przebiegu, rozszerzenie warunków pętli, przeistoczenie kolejności komend / pętli / wyrażeń, metody inline, ogólnikowe wyrażenia, klonowanie metod.

Spis treści

Zastosowania

Zarządzanie ryzykiem

Zaciemnianie kodu źródłowego w celu przeciwdziałania ewentualnym próbom analizy programu to jedna z technik zarządzania ryzykiem nieautoryzowanego dostępu. Następstwem takiego zdarzenia bywa zagrożenie własności intelektualnej, ułatwienie znalezienia luk bezpieczeństwa albo obniżenie zysku w przypadku aplikacji, która była zmodyfikowana w celu obejścia jej zabezpieczeń chroniących przed kopiowaniem. Zaciemnianie kodu służy w takim przypadku łagodzeniu (kompensacji) strat związanych z tym ryzykiem. Chociaż inżynieria wsteczna nie jest niczym nowym, upowszechnienie się oprogramowania dystrybuowanego w postaci kodu pośredniego (np. Java albo .NET) znacząco zwiększa ryzyko wystąpienia negatywnych następstw nieautoryzowanego dostępu.

Optymalizacja

Zaciemnianie kodu binarnego programów (np. przez usuwanie wskaźnika rekordu aktywacji (ang. frame pointer) oraz informacji o symbolach) stosowane jest także do zmniejszania rozmiaru plików wynikowych oraz poprawiania ich wydajności. Z technik zaciemniania wykorzystuje się także przy tworzeniu MIDletów na platformie Java ME w celu redukcji rozmiaru plików JAR wygenerowanych w procesie kompilacji.

Spam

Obfuskacja stosowana jest po obu stronach barykady w walce z niechcianymi wiadomościami. Spamerzy zaciemniają treść elektronicznej korespondencji w celu ominięcia filtrów np. w poniższy sposób:

<b>Via</b><b>gra</b>

Natomiast użytkownicy poczty elektronicznej, próbujący uniknąć spamu, podają swoje adresy e-poczty w niejawnej postaci, aby utrudnić harvesterom ich odczytanie.

Rozrywka

Czasami programy są zaciemniane jedynie w celach rozrywkowych. Organizowane są konkursy na najbardziej pomysłowo zaciemnione programy, a wśród nich: International Obfuscated C Code Contest, Obfuscated Perl Contest, International Obfuscated Ruby Code Contest oraz Obfuscated PostScript Contest.

Przykłady

The 12 Days of Christmas

Poniższy program to jeden z najsłynniejszych zaciemnionych kodów źródłowych w języku C. Jego wykonanie wypisuje do standardowego wyjścia dwanaście zwrotek tekstu angielskiej piosenki The 12 Days of Christmas.


#include <stdio.h>
main(t,_,a)char *a;{return!0<t?t<3?main(-79,-13,a+main(-87,1-_,
main(-86,0,a+1)+a)):1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13?
main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72?main(_,t,
"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l,+,/n{n+,/+#n+,/#\
;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# \
){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' \
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \
;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \
}'+}##(!!/")
:t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1)
  :0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a,
"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"),a+1);}

W przeddzień Wigilii 1998 roku Thomas Ball opublikował analizę powyższego programu[1], przedstawiając jego czytelną formę wraz z objaśnieniem zasady działania oraz szczegółowym opisem procesu jego analizy.

Liczby pierwsze

Wykonanie poniższego programu wypisuje na ekran listę wszystkich liczb pierwszych mniejszych od 100.

_(__,___,____){___/__<=1?_(__,___+1,____):!(___%__)?_(__,___+1,0):___%__==___/
__&&!____?(printf("%d\t",___/__),_(__,___+1,0)):___%__>1&&___%__<___/__?_(__,1+
___,____+!(___/__%(___%__))):___<__*__?_(__,___+1,____):0;}main(){_(100,0,0);}

Chociaż powyższy fragment bywa trudny do zrozumienia, powstał on przez przekształcenie poniższego programu:

void primes(int cap) {
  int i, j, composite;
  for(i = 2; oraz < cap; i++) {
    composite = 0;
    for(j = 2; j < i; j++) 
      composite += !(i % j);
    if(!composite)
      printf("%d\t", i);
  }
}
 
int main() { 
  primes(100);
}

Obfuskacja powyższego programu była przeprowadzona przez wykonanie następujących kroków:

  1. Przekształcenie ciała funkcji primes do jednej pętli while, wewnątrz której istnieje tylko sekwencja instrukcji warunkowych.
  2. Zamiana iteracji na rekurencję.
  3. Zamiana sekwencji instrukcji warunkowych na pojedyncze wyrażenie warunkowe (z zagnieżdżonymi podwyrażeniami).
  4. Usunięcie pomocniczych zmiennych lokalnych.
  5. Zamiana nazw funkcji oraz jej parametrów na ciągi podkreślników.
  6. Usunięcie nadmiarowych białych znaków oraz nawiasów.

Pierwsza transformacja wykorzystuje fakt, że każdy program da się wyrazić przy pomocy pojedynczej pętli, wewnątrz której zagnieżdżona jest sekwencja instrukcji warunkowych. Oryginalny program, przekształcony w ten sposób, prezentuje się w następujący sposób:

void primes(int cap) { 
  int i, j, composite, t = 0;
  while(t < cap * cap) {
    oraz = t / cap;
    j = t++ % cap;
    if(i <= 1);
    else if(j == 0)
      composite = 0;
    else if(j == oraz && !composite)
      printf("%d\t",i);
    else if(j > 1 && j < i)
      composite += !(i % j);  
  }
}
 
int main() {
  primes(100);
}

Zamiana iteracji na rekurencję wymaga zastąpienia zmiennych t oraz composite (niezależnie modyfikowanych w pętli) parametrami formalnymi. Ponadto, wykonanie sekwencyjne funkcji printf oraz primes było zapisane przy pomocy pojedynczego wyrażenia przecinkowego. Warto także zwrócić uwagę na dodatkowy warunek, który obsługuje sytuację, kiedy żaden z warunków poprzedniej formy programu nie był prawdziwy oraz kiedy pętla kontynuuje wykonanie, zwiększając zaledwie wartość zmiennej t.

void primes(int cap, int t, int composite) {
  int i,j;
  oraz = t / cap;
  j = t % cap;
  if(i <= 1)
    primes(cap,t+1,composite);
  else if(j == 0)
    primes(cap,t+1,0);
  else if(j == oraz && !composite)
    (printf("%d\t",i), primes(cap,t+1,0));
  else if(j > 1 && j < i)
    primes(cap,t+1, composite + !(i % j));
  else if(t < cap * cap)
    primes(cap,t+1,composite);
}
 
int main() {
  primes(100,0,0);
}

Nazwy zmiennych obcinane są do pierwszych liter, a instrukcje warunkowe if(A) B else if(C) D else E zamieniane są na wyrażenia A ? B : C ? D : E.

void primes(int m, int t, int c) {
  int i,j;
  oraz = t / m;
  j = t % m;
  (i <= 1) ? primes(m,t+1,c) : (j == 0) ? primes(m,t+1,0) : (j == oraz && !c) ? 
  (printf("%d\t",i), primes(m,t+1,0)) : (j > 1 && j < i) ? 
  primes(m,t+1,c + !(i % j)) : (t < m * m) ? primes(m,t+1,c) : 0;
}
 
int main() {
  primes(100,0,0);
}

Następnie wszystkie wystąpienia zmiennych i oraz j zamieniane są odpowiednio na (t / m) oraz (t % m).

void primes(int m, int t, int c) {
  ((t / m) <= 1) ? primes(m,t+1,c) : !(t % m) ? primes(m,t+1,0) : 
  ((t % m)==(t / m) && !c) ? (printf("%d\t",(t / m)), primes(m,t+1,0)) : 
  ((t % m)> 1 && (t % m) < (t / m)) ? primes(m,t+1,c + !((t / m) % (t % m))) : 
  (t < m * m) ? primes(m,t+1,c) : 0;
}
 
int main() {
  primes(100,0,0); 
}

W kolejnym kroku wszystkie pozostałe nazwy: primes, m, t oraz c zastępowane są identyfikatorami _, __, ___ oraz ____.

void _(int __, int ___, int ____) {
  ((___ / __) <= 1) ? _(__,___+1,____) : !(___ % __) ? _(__,___+1,0) : 
  ((___ % __)==(___ / __) && !____) ? (printf("%d\t",(___ / __)), 
  _(__,___+1,0)) : ((___ % __) > 1 && (___ % __) < (___ / __)) ? 
  _(__,___+1,____ + !((___ / __) % (___ % __))) : (___ < __ * __) ? 
  _(__,___+1,____) : 0;
} 
 
int main() {
  _(100,0,0); 
}

Po usunięciu wszystkich zbędnych białych znaków, nadmiarowych nawiasów oraz deklaracji typów powstaje ostateczna osoba zaciemnionego programu.

_(__,___,____){___/__<=1?_(__,___+1,____):!(___%__)?_(__,___+1,0):___%__==___/
__&&!____?(printf("%d\t",___/__),_(__,___+1,0)):___%__>1&&___%__<___/__?_(__,1+
___,____+!(___/__%(___%__))):___<__*__?_(__,___+1,____):0;}main(){_(100,0,0);}

Program ten da się skompilować oraz uruchomić na większości systemów. Należy przy tym pamiętać, że choć teoretycznie zaciemniona forma programu jest równoważna z oryginałem, kompilator wygeneruje dla obu wersji zróżnicowane kody binarne, których wykonania potrafią zwracać zróżnicowane wyniki. Na przykład, wersja rekurencyjna powyższego programu, w przeciwieństwie do oryginalnej postaci iteracyjnej, podatna jest na błąd przepełnienia stosu wykonania.

Wady

Jedyne zabezpieczenie

Żadna ze znanych technik obfuskacji nie daje gwarancji znacznego utrudnienia analizy zaciemnionego programu[2]. Obfuskacja nie zapewnia bezpieczeństwa porównywalnego ze współczesnymi systemami kryptograficznymi, powinna więc być stosowana jako ich dopełnienie.

Microsoft zaleca zaciemnianie plików ASP przy pomocy programu Script Encoder, aby, w przypadku włamania na serwer, cracker nie miał dostępu do ich treści. W dokumentacji narzędzia[3] zaznaczono jednak, że nie jest ono w stanie powstrzymać zdeterminowanego włamywacza od analizy programu.

Debugowanie

Wykonywanie zaciemnionych programów pod kontrolą debugera jest niezwykle trudne. Nazwy zmiennych nie posiadają sensu, a struktura programu bywa zmieniona nie do poznania. Zmusza to programistów do utrzymywania przynajmniej dwóch osobnych kompilacji oraz do porównywania ich zachowania. Ponadto, istnienie niezaciemnionej wersji programu zwiększa ryzyko jego rozpowszechnienia np. na skutek kradzieży.

Przenośność

Zaciemnianie bardzo wielokrotnie zależy od specyficznych cech kompilatora bądź środowiska wykonania programu, co utrudnia obfuskację, jeśli jedno z nich się zmieni.

Mechanizm refleksji

Mechanizm refleksji dopuszcza inspekcję klas oraz wywoływanie ich metod jedynie na podstawie ich nazw. Stosowanie obfuskacji w systemach, których działanie wykorzystuje ten mechanizm, wydatnie ogranicza swobodę oraz wymaga od programistów oznaczenia wszystkich identyfikatorów, których nazwy nie potrafią zostać zmienione przez obfuskator.

Sprawdź też

Przypisy

vseo.pl