Skocz do zawartości

[Art] Zabezpieczenie zapisu gry


Rekomendowane odpowiedzi

Wiele osób stosuje do zapisu stanu gry pliki INI. Są dobre, a zarazem dość proste w obsłudze. Ale mają jedną wadę: wszystko widać w nich jak na dłoni.

Ale spokojnie, z pomocą przychodzi Dawidds :) W tym artykule przedstawię sposób na proste, ale neizawodne zabezpieczenia naszego pliku. Każdy będzie mógł oglądać jego zawartość - ale, jeśli go zmodyfikuje, gra wykryje modyfikację pliku.

 

Więc, zaczynajmy :)

 

1. Strategia działania

Czyli jak to zabezpieczenie ma działać. Użyjemy do tego celu chyba najpopularniejszego algorytmu świata - MD5 (hash).

 

Tak będzie wyglądał nasz przykładowy plik *.save:

[ZAPIS]
wartosc1=7
wartosc2=455
wartosc3=12
[SECURITY]
control_code=6cdd033862e7b6e2fa68d9b3f57d5cf9

wartosc1 będzie pobrana ze zmiennej global.wartosc1 (analogicznie 2 i 3).

 

A control_code to nic innego, jak hash naszego tajnego ciągu znaków oraz wszystkich zapisywanych do pliku wartości.

 

2. Ale co to jest hash...?

Już wyjaśniam :P

 

Hash jest dość specyficznym algorytmem. Czy zahashujemy 1 znak, czy może milion znaków, i tak zawsze wynikiem będzie 32-bitowy ciąg znaków (32 znaki - litery i cyfry).

 

A co za tym idzie.... pomyśl..... nie da się go odkodować. Zresztą - zahashować można każdy, nawet nie wiem jak długi ciąg znaków. Co by było, gdyby dało się zahashować grę ważącą 4GB do 32 znaków, po czym ją rozkodować...? Teraz to brzmi logicznie, nie :P ?

 

Ale chwila. Skoro nie da się tego rozkodować, to to jest bez sensu!

 

Niekoniecznie. Wyjaśnię to na przykładzie strony internetowej.

 

Rejestrujesz się na stronie. Twoje hasło zostaje zapisane w bazie danych w postaci zakodowanej. Próbujesz się zalogować. System hashuje wpisane przez ciebie hasło (to do logowania) i porównuje ten hash z tym w bazie danych. Jeśli jest taki sam - i hasło musi być takie same.

 

Co prawda ma to wadę, otóż można wpisać zupełnie inny ciąg znaków, a system i tak stwierdzi, że hasło jest prawidłowe. Ale szanse na to są.... cóż, niewielkie :)

 

Przykładowy hash możesz zobaczyć w kodzie powyżej. Złożony jest z literek oraz cyferek.

 

3. Algorytm dla GM'a

Zaraz zaraz. Ale skąd ja mam wiedzieć, jak się hashuje pliki?

 

Spokojnie. Skorzystamy z gotowego kodu ;)

 

Stwórz funkcję i nazwij ją "md5". Wpisz do niej:

GML
{

var str,uint,grp,rol,i,j,h,len,pos,w,a,b,c,d,e,f,temp,digest;

str = argument0;

if (!variable_global_exists("MD5k")) {

globalvar MD5k,MD5g,MD5r,MD5s;

grp = "00010203040506070809101112131415";

grp += "01061100051015040914030813020712";

grp += "05081114010407101300030609121502";

grp += "00071405120310010815061304110209";

rol = "07121722071217220712172207121722";

rol += "05091420050914200509142005091420";

rol += "04111623041116230411162304111623";

rol += "06101521061015210610152106101521";

for(i=0; i<64; i+=1) {

MD5k = floor(abs(sin(i+1))*(1 << 32));

MD5g = real(string_copy(grp,i*2+1,2));

MD5r = real(string_copy(rol,i*2+1,2));

MD5s = 32 - MD5r;

}

}

uint = $FFFFFFFF;

h[0] = $67452301;

h[1] = $EFCDAB89;

h[2] = $98BADCFE;

h[3] = $10325476;

len = 8 * string_length(str);

str += chr(128);

while ((string_length(str) mod 64) != 56) str += chr(0);

for (i=0; i<64; i+=8) str += chr(len >> i);

pos = 0;

for (j=0; j<string_length(str); j+=64) {

for (i=0; i<16; i+=1) {

w = ord(string_char_at(str,pos+4));

w = ord(string_char_at(str,pos+3)) | (w << 8);

w = ord(string_char_at(str,pos+2)) | (w << 8);

w = ord(string_char_at(str,pos+1)) | (w << 8);

pos += 4;

}

a = h[0];

b = h[1];

c = h[2];

d = h[3];

for (i=0; i<64; i+=1) {

if (i < 16) f = (d ^ (b & (c ^ d)));

else if (i < 32) f = (c ^ (d & (b ^ c)));

else if (i < 48) f = (b ^ c ^ d);

else f = (c ^ (b | (~d)));

temp = d;

d = c;

c = b;

e = uint & (a + f + MD5k + w[MD5g]);

b = uint & ((uint & (e << MD5r) | (e >> MD5s)) + b);

a = temp;

}

h[0] = uint & (h[0] + a);

h[1] = uint & (h[1] + b);

h[2] = uint & (h[2] + c);

h[3] = uint & (h[3] + d);

}

digest = "";

for (j=0; j<4; j+=1) {

for (i=0; i<32; i+=8) {

digest += string_char_at("0123456789abcdef",1+($F & h[j] >> i+4));

digest += string_char_at("0123456789abcdef",1+($F & h[j] >> i));

}

}

return digest;

}

Mówiłem, że nie jest prosty :P ? Ale reszta kodów będzie bardziej zrozumiała ^^

 

Więc, możemy już brać się za system save'owania ^^

 

4. Zwykły zapis stanu gry.

Przyjmijmy, że w naszej grze będą 3 wartości do zapisu: global.wrtosc1, global.wartosc2 oraz global.wartosc3.

 

Stwórz funkcję "save" Wpisz do niej:

GML
ini_open("save.save");

 

ini_write_real("ZAPIS", "wartosc1", global.wartosc1);

ini_write_real("ZAPIS", "wartosc2", global.wartosc2); //Grupa "ZAPIS", nazwa klucza "wartosc2", zawartosć klucza z globalnej wartosc1

ini_write_real("ZAPIS", "wartosc3", global.wartosc3);

 

//UWAGA!!!

//Jeżeli zapisujesz liczbę używaj ini_write_real(), jeżeli tekst ini_write_string()!

//Odczytywać musimy te wartości tak samo, jak je zapisywaliśmy (real albo string)

ini_close();

Jednak to nie wszystkie wartości do zapisu. Musimy przecież zapisać jeszcze zapisać zahashowaną wersję całego save'a.

 

5. Funkcja generująca hash (do zapisu)

Teraz stworzymy funkcję generującą hash.

 

Ale będziemy mieli dwie funkcje generujące hash - do wczytania i do odczytania. Teraz stworzymy tą do zapisania ;)

 

Stwórz funkcję "gen_hash_save", wpisz do niej:

GML
wynik = "tutajwpiszbylejakiciagznakowktoregoikomuniepokazuj_";

 

wynik = wynik + string(global.wartosc1);

wynik = wynik + string(global.wartosc2); //Dopisujemy do naszego tajnego ciągu znaków wszystkie wartości do zapisania

wynik = wynik + string(global.wartosc3);

 

//UWAGA!!!

//Muszą się tutaj znaleźć wszystkie wartości, które będziemy zapisywać.

return md5(wynik)

Chyba nie trzeba wyjaśniać tego kodu :)

 

6. Poprawa funkcji save()

Teraz musimy poprawić funkcję save(). Musi ona zapisywać hasha naszych wartości. Także nowa, zmodyfikowana wersja tej funkcji będzie wyglądała tak:

GML
ini_open("save.save");

 

ini_write_real("ZAPIS", "wartosc1", global.wartosc1);

ini_write_real("ZAPIS", "wartosc2", global.wartosc2);

ini_write_real("ZAPIS", "wartosc3", global.wartosc3);

 

ini_write_string("SECURITY", "control_code", gen_hash_save());

 

//UWAGA!!!

//Jeżeli zapisujesz liczbę używaj ini_write_real(), jeżeli tekst ini_write_string()!

//Odczytywać musimy te wartości tak samo, jak je zapisywaliśmy (real albo string)

ini_close();

 

7. Generowanie hasha (odczyt)

Teraz się nie pogób. Napiszemy funkcję, która odczyta wszystkei wartościz naszego pliku, oraz zwróci ich zahashowaną wartość. Jeśli to, co zwróci ta funkcja będzie takie samo jak to, co jest w pliku (control_code) - save jest prawidłowy. jeśli nie - wywalimy odpowiednie okienko =]

 

Stwórz funkcję "gen_hash_load", i wpisz do niej:

GML
ini_open("save.save");

 

wynik = "tutajwpiszbylejakiciagznakowktoregoikomuniepokazuj_"; //Taki sam, jak o odczytu!

wynik = wynik + string(ini_read_real("ZAPIS", "wartosc1", 0)); //Grupa, klucz, domyślna wartość (jeśli taki klucz by nie istniał)

wynik = wynik + string(ini_read_real("ZAPIS", "wartosc2", 0));

wynik = wynik + string(ini_read_real("ZAPIS", "wartosc3", 0));

 

ini_close();

 

return md5(wynik);

 

8. Wykończenie - funkcja wczytująca wartości

Teraz podsumowanie tego kodu. Funkcja, która sprawdzi wszystkie funkcje, których teraz używaliśmy i wczyta, lub nie wczyta (jeśli zmodyfikowano save) naszą grę.

 

Stwórz funkcję "load", wpisz do niej:

GML
ini_open("save.save");

 

if(ini_read_string("SECURITY", "control_code", "") == gen_hash_load()) //Jeśli hash wszystkich wartości (gen_hash_load()) jest taki sam ja ten wpisany w pliku (control_code) - gra jest poprawna

{

ini_open("save.sgsave");

 

global.watosc1 = ini_read_real("ZAPIS", "wartosc1", 0);

global.watosc2 = ini_read_real("ZAPIS", "wartosc2", 0);

global.watosc3 = ini_read_real("ZAPIS", "wartosc3", 0);

 

show_message("Zaladowano gre.");

}

else

{

show_message("Nepoprawny plik *.save");

}

 

ini_close();

 

-------------------------------------------------

 

I to by było na tyle. Proszę wybaczyć ewentualne bugi - pisałem ten kod 100% z głowy. Ale myślę, że nie było literówek :)

Odnośnik do komentarza
Udostępnij na innych stronach

lenin: Ale tu nie ma żadnej filozofii :P Po prostu, jak chcesz zapisać coś więcej to musisz to dodać do tych funkcji :P

 

PS: Pisałem to wczoraj... po pół godzinie się Firefox zaciął :P Teraz pisałem w Notepad++i co chwila było Ctrl+S ^^

Odnośnik do komentarza
Udostępnij na innych stronach

Harv: Twoje IQ powala na kolana....

 

Że brute force'm się da rozkodować, to wiem. Ale przy bardzo długim tym słowie, do którego dopisujesz wartości save'a nie masz szans. Superkomputer by to kilka miesięcy liczył. I znalazł by masę innym opcji....

 

Zresztą, jak jesteś taki dobry: proszę bardzo. Rozkoduj mi to:

 

6cdd033862e7b6e2fa68d9b3f57d5cf9 (ten przykładowy, z tego arta ;>)

 

PS: Większość takich dekoderów działa słownikowo. Mają olbrzymią bazę hashów i je porównują...

 

Co do rozgryzienia: napisać se program, który modyfikuje save pod wszystkie możliwości, i klika co chwilę LOAD :P

 

Edit: Zresztą dałem dobre porównanie.... mielibyśmy niezłą kompresję, jakby dało się rozkodować. Miliard znaków ściąga się na 32 ^^ Call Of Duty 4 byłoby na dyskietce ^^ Masa luzu by została ^^

Edit2: Ale po takim idio... imbecylu jak ty nie spodziewałem się pozytywnej oceny ;)

Odnośnik do komentarza
Udostępnij na innych stronach

Obecnie przez google można wyszukać parę milionów hashy MD5 więc wyciągnięcie prostych hasełek jest dziecinne proste.

Do tego chyba lepiej zastosować CRC.

 

Zabezpieczenie tego typu jest o tyle lipne, że przy uszkodzeniu pliku konfiguracyjnego żegnamy się z wszystkimi naszymi ustawieniami. Jeśli ktoś przechowuje w configu informacje o ostatnim levelu etc. to może być to nieźle wkurzające.

Odnośnik do komentarza
Udostępnij na innych stronach

Dokładnie. W internecie jest wiele ciekawych DLL'i (nawet dodatków do GM'a) służących do szyfrowania. Wtedy kilkoma linijeczkami kodu można zaszyfrować plik...

 

Artykuł głównie ma przedstawiać jakieś podstawowe zabezpieczenie :) Lepiej takie niż w ogóle :P

 

Choć z drugiej strony - po co się męczyć nad szyfrowaniem, skoro ktoś użyje dekompilatora :P ?

 

Zabezpieczenie tego typu jest o tyle lipne, że przy uszkodzeniu pliku konfiguracyjnego żegnamy się z wszystkimi naszymi ustawieniami. Jeśli ktoś przechowuje w configu informacje o ostatnim levelu etc. to może być to nieźle wkurzające.
To ktoś sobie dostosuje ten kod do swoich potrzeb. Ja na przykład w grze przechowuję wiele informacji "poboczny" - a jakoś nie gubię się :P

 

Jedyne, co jest głupie, to to, że jeśli chcę dodać nową wartość do zapisu muszę ją dorobić w 4 funkcjach - nie 2. Ale da się żyć :P W sumie można by to na tablicy postawić... ale to już przesadne kręcenie by było :P

 

Edit:

Borek: :>

 

Edit2:

Obecnie przez google można wyszukać parę milionów hashy MD5 więc wyciągnięcie prostych hasełek jest dziecinne proste.
Dokłądnie... ja zawsze, nawet w dodawaniu haseł do bazy danych w PHP sztucznie przedłużam hasło, tak jak to jest tutaj (supertajnyciagznakow+hasło usera). Bo w przeciwnym razie ten MD5 jest trochę bez sensu :/
Odnośnik do komentarza
Udostępnij na innych stronach

Bajt odpowiada jednemu znakowi (0-255). Bit to wartość true lub false przyjmuje
Odnośnik do komentarza
Udostępnij na innych stronach

Jeśli chcesz dodać odpowiedź, zaloguj się lub zarejestruj nowe konto

Jedynie zarejestrowani użytkownicy mogą komentować zawartość tej strony.

Zarejestruj nowe konto

Załóż nowe konto. To bardzo proste!

Zarejestruj się

Zaloguj się

Posiadasz już konto? Zaloguj się poniżej.

Zaloguj się
  • Ostatnio przeglądający   0 użytkowników

    • Brak zarejestrowanych użytkowników przeglądających tę stronę.
×
×
  • Dodaj nową pozycję...