Konkurs na czas?

2

Cześć wszystkim,
Jestem amatorem ale od dłuższego czasu, dla siebie , piszę programy w chwilach wolnych.
W jednym z wątków w Pascalu, Delphi i Lazarusie wywiązał się temat szybkości i łatwości rozbrojenia nieoptymalnie zaszyfrowanej bazy danych obsługiwanej przez program, który potrafi ją odszyfrować i wyświetlić po wpisaniu w edit poprawnego hasła. Nie wnikając w szczegóły tam wywiązanej dyskusji wyszło na to, że to zajmie max 15-20 min ,może szybciej.
Jednak po dobie nikt się za to nie wziął , albo zabrał się ale nie może sobie poradzić.(banalnie proste jest to, choć ja bym nie podołał)

Wklejam tu link do katalogu konkurs.zip:
Jest w nim program skompilowany w lazarusie oraz zaszyfrowana bardzo prosta baza danych.
Po wpisaniu poprawnego hasła program
Wyświetla sposób w jaki dane zostały zakodowane, gdyż opisałem to w samej bazie w kolejnych wierszach i ją program wyświetli w memo po wpisaniu poprawnego hasła. I o taki zrzut ekranu z opisem w memo proszę jako dowód że się udało. Proszę podajcie też czas w jakim się to udało. Bo amatorzy tacy jak ja i początkujący potrzebują dowodu jak bardzo należy szyfrować aby zabezpieczać dane.

Https://www.windowbee.com/konkurs.zip

0

Mogłeś ten algorytm szyfrowania podać na stronie, do tego input jakiś w base64.
Jak tu się rozchodzi o tylko rozszyfrowanie bazy danych nie znając hasła, to cała aplikacja nie ma żadnego sensu w tym zadaniu.

Teoretycznie jak będziesz miał nawet takie banalne szyfrowanie jak xor zwykły, ale hasło będzie o długości danych szyfrowanych to będzie trzeba po prostu przejść po wszystkich możliwych kombinacjach i potem w jakiś sposób stwierdzić, który dał nam odszyfrowane dane.

Możliwe, że nie pójdzie tego złamać jako tako nie znając hasła.

0

Gdy się poddacie napiszcie proszę, podam hasło i pokaże się sposób szyfrowania.

0

Z tego co rozumiem to "konkurs" nie ma sensu, równie dobrze mogłeś spakować plik tekstowy hasłem do zipa zabezpieczonego hasłem. O ile użyłeś w miarę sensownego algorytmu szyfrowania to jedynym sposobem na złamanie tego jest brute force a gdzie tu zabawa. Nie sztuką jest zaszyfrowanie bazy danych, sztuką by było zrobienie bazy danych której nie można na stałe odszyfrować w całości i stworzyć "cracka" po jednorazowym podaniu hasła

1

Zostawię temat przez tydzień może komuś się uda a jak nie to uznam ,że prymitywne sposoby nikomu nie zdradzone są bardzo skuteczne dla moich potrzeb bez szyfrowania liczbami pierwszymi itd…
To samo dotyczy wysłania wiadomości przez port zaszyfrowanych w ten sam sposób.
Podpowiedź jest jeszcze taka że klucz znajduje się w samej bazie, hasło programu deszyfrującego również.

1

@Windowbee: daj linka do wątku o którym wspominasz. Mnie ciekawi kontekst dla którego robisz konkurs.

Ciekawi ponieważ przypomniało mi się moje podejście z początku pracy jako programista. Teraz po ponad 20 latach (sic!), Mam zupełnie inne spojrzenie na to czy szyfrować...

0

Eh...
Strasznie pogmatwane to wszystko napisałeś, jakiś plik, jakieś szyfrowanie, zawsze zmienia się hasło, hasło jest w bazie.

Nic nie rozumiem, hasło potrzebne do rozszyfrowania, ale hasło jest w programie....

Jeszcze jakby program działał to bym zrozumiał.
Weź tu znajdź funkcję szyfrująca jak program nie działa.

Nawet jak się znajdzie to trzeba assembler do pythona przepisać i potem satisfiability modulo theories albo da True czyli złamie, albo nie.

Anyway komu się chce jak ty tak nieudolnie ten konkurs zrobiłeś.
Powinieneś zrobić jakiś przykład, a nie dać zepsuty program, który nic nie robi.

I ja to mówię ze szczerego serca, c**** to zrobiłeś.

0

@Wypierdzistyy: zwracam honor, nic nie wyszydzałeś :) więc przepraszam

9

Ok, dostajemy program Protector.exe. On prosi o jakieś hasło, i dekryptuje bazę albo nie.
To jeden z najbardziej typowych schematów crackme, więc na razie żadnych zaskoczeń.

Po otwarciu w ghidrze nieprzyjemna niespodzianka - program napisany w Lazarusie (można było też przeczytać opis, wiem). Reversowanie nie będzie zbyt przyjemne, do tego nie mam w tym środowisku za dużo doświadczenia (malware w Lazarusie raczej nie ma wiele). Pierwsze wrażenie: pisze się "Decrypted" a nie "Decripted" (tekst na stronie).

screenshot-20221217215418.png

Na start trochę analizy dynamicznej żeby zobaczyć o co w ogóle chodzi. W procmonie widać że program przy starcie czyta bazę. Co ciekawe, czyta ją kilkoma readami po 128 bajtów pod rząd - jakaś struktura, czy tak działa runtime Lazarusa? Nie wiadomo na razie.
O tyle ważne, że czyta plik raz przy starcie, a nie przy wpisaniu hasła.

screenshot-20221217214543.png

Procmon daje nam od razu stos, więc przechodzę po stosie od kernela do góry (nazywając funkcje) żeby dostać się do czegoś ciekawego. Po przejściu trzy ramki stosu do góry (nazwałem funkcje ReadFile, ReadFileWrap, ReadFileWrap2, ReadFileWrap3) trafiam do funkcji która woła ReadFileWrap3 kilka razy pod rząd, w dodatku z ciekawymi parametrami (hint: 0x80 to 128).

screenshot-20221217214737.png

Zresztą, pierwsze co mi się tam rzuciło w oczy to komunikat błedu

local_408 = "Reading of base: ";
local_400 = local_18;
local_3f8 = " faild !";

Literówka - nie wygląda jak coś z biblioteki standardowej, więc może to być kod naszego programu. Gdybym robił to pracowo sprawdziłbym w 5 sekund przez VTGrep, ale jako że to prywatnie to trzeba sobie radzić za pomocą Google - zero wyników, więc jest OK. Mamy funkcję czytającą bazę.

No dobra, czas zmienić podejście z powrotem na dynamiczne. Ustawiam sobie breakpoint na 0x10002c6dc w x64dbg i singlestepuje porównując z ghidrą. Nazywam sobie przy okazji funkcje "ProbStrcat", "ProbFileExists", "SetText" i śledzę jak program czyta (w programie ani śladu sprawdzania jakichkolwiek błędów btw ) treść bazy do pamięci (dwa bufory, nazwałem sobie dataBuf0 i dataBuf1). Później zaczyna coś parsować i liczyć przez odejmowanie pierwszych 128 bajtów od drugich 128 bajtó... zaraz, czy to jest plaintext? Jeszcze przecież nie podałem hasła nigdzie.

screenshot-20221217214808.png

No ale nie da się ukryć, program dostaje jakieś plaintextowe bajty. No to szybki python:

data = open("konkurs/baza.dbx", "rb").read()

plain = b""
buf0 = data[0:0x80]
while data:
    buf1 = data[0x80:0x100]

    out = []
    for i in range(len(buf1)):
        out += [(buf1[i] - buf0[i]) % 256]

    plain += bytes(out[:-1])
    data = data[0x80:]

print(plain.decode(errors="ignore"))

Wykonujemy:

$ python3 hack.py
44908.635924;1;10#0;1#1;Gratulacje udało się Tobie rozszyfrować tą bazę.#2;Jeśli jednak upłynął czas konkursu i usyskaeś dostęp poprzez hasło to poniżej przedstawiam sposób szyfrowania:#3;1) W pierwszych 128 bajtach program umieszcza losow klucz#4;2)Następnie do każdego bajtu właściwej bazy leżącej poza tymi 128 bajtami program dodaje po kolei bajt z klucza.5;3)Odszyfrowywanie odbywa się poprzez odczytanie klucza i odejmowanie pokolei wartości klucza od bajtu zaszyfrowanej bazy waściwej#6;4)Hasło pozwalające odszyfrować bazę - i załadować ją do tmemo leży w komórce 7,1 bazy za słowem password:7;Password:tu mnie masz#8;#9;#10;

Lekko formatując:

44908.635924;1;10
0;1
1;Gratulacje udało się Tobie rozszyfrować tą bazę.
2;Jeśli jednak upłynął czas konkursu i usyskaeś dostęp poprzez hasło to poniżej przedstawiam sposób szyfrowania:
3;1) W pierwszych 128 bajtach program umieszcza losow klucz
4;2)Następnie do każdego bajtu właściwej bazy leżącej poza tymi 128 bajtami program dodaje po kolei bajt z klucza.5;3)Odszyfrowywanie odbywa się poprzez odczytanie klucza i odejmowanie pokolei wartości klucza od bajtu zaszyfrowanej bazy waściwej
6;4)Hasło pozwalające odszyfrować bazę - i załadować ją do tmemo leży w komórce 7,1 bazy za słowem password:
7;Password:tu mnie masz
8;
9;
10;

Trochę się tego nie spodziewałem :P. Zazwyczaj jeśli baza potrzebuje hasła do odszyfrowania, to jednak jest gdzieś jakaś kryptografia którą trzeba złamać. Chyba że tym się własnie różni "Decryption" od "Decription"? :P

Całość, razem z pisaniem tego writeupa i zrobieniem sobie yerby, zajęła 45 minut (@shalom mi świadkiem). Zależnie przed kim się bronisz, może to być dużo albo mało - na pewno można komuś tak trochę czasu zmarnować. Aczkolwiek gdybym się spodziewał że hasło jest tylko dla zmyłki, po prostu bym zobił dump pamięci procesu i miałbym wszystko w 5 minut :P.

0

wow gratulacje!!!! Tak, muszę nauczyć się kryptografii i użyć bezpiecznych sposobów jednak 🥴 cieszę się że to zrobiłeś i poświęciłeś czas!
Bardzo dziękuję :)
Hasło nie było dla zmyłki. Myślałem że ktoś użyje go aby spojrzeć co robi program w momencie wpisania. Po wpisaniu: ‚tu mnie masz’ wyświetla bazę w memo.
Program odczytuje bazę pobierając pierwszych 128 bajtów gdzie jest klucz losowo stawiany przy zapisie bazy.
Następnie od każdej wartości bajtu odejmuje sukcesywnie kolejny bajt klucza. A wynik wpisuje jako kolejny char do stringa. Gdyby baza była dłuższa niż klucz , to wraca na początek klucza itd.
Kolejne stringi kończące się # zapisuje do tstringlist i przekazuje ostatecznie do memo jeśli wpisane hasło się zgadza z tym zapisanym do bazy.
Baza w pierwszym stringu ma czas utworzenia oraz liczbę kolumn i wierszy. Średnik oddziela komórki danych.
Nauczka i dla mnie! Dziękuję !

0

Obiecany kod deszyfrujący , nic nie zmieniałem go w stosunku do tego co odszyfrował @msm, część jest żywcem wzięta i zaadoptowana z projektu, który tworzę. Dlatego ma kilka zmiennych więcej nie wykorzystanych tutaj. Nie wieszajcie na mnie psów i wilków za nieczytelny kod. Pozdrawiam i dzięki.

function czytaj(baza:tstringlist;name:string):boolean; // odczytuje baze i odszyfrowywuje
label 1,2;
var
  s,s1,s2,s3:string;
  ch:char;
  f:file;
  a,b,c,x,i,y,z:integer;
  bajt:byte;
  buf:array[0..127] of byte;
  key:array[0..127] of byte;
begin
result:=false;
 baza.Clear;

if fileexists(name+'.dbx') then begin

assignfile(f,name+'.dbx');
x:=0;
1:
try
reset(f,1);

except
  x:=x+1;
  sleep(500);
end;

if x>10 then goto 2; // 10X próbuje otworzyć plik czyli max trwa to 5sek, jeśli się nie powiedzie kończy
if x>0 then goto 1;

blockread(f,key,128,x);// zapisanie klucza
a:=1;c:=0;s:='';
s:='';
while x>0 do begin
blockread(f,buf,128,x);
for i:=0 to x-1 do
begin
y:=key[c];
ch:=chr(buf[i]-y);
if ch<>'#' then s:=s+ch else begin baza.Add(s);s:='';end;

c:=c+1;// licznik klucza
if c>127 then c:=0;
end;
end;
 close(f);
result:=true;
x:=0;
 end;
2:
 if x>10 then result:=false; // nie udało sie otworzyć pliku

 if result=false then baza.add('Reading of base: '+name+' faild !');
 if fileexists(name+'.dbx')=false then baza.add('There is no such base as: '+name+' !');
end; 
0

@msm , Czy mogę o coś jeszcze zapytać?

Czy warunki też tak łatwo widać w Twoich programach dekompilujących?
Chodzi o to czy nie uruchomione funkcje, gdyż warunek nie został spełniony, też są łatwo widoczne?
Chodzi o to ,że masz klienta który logując się do serwera (na chronionym komputerze), pyta o poziom dostępu danego użytkownika. Serwer odpowiada : poziom niski…
Czy zobaczysz funkcje, które się nie uruchamiają z powodu niskiego poziomu dostępu?
Czy widzisz tylko na bieżąco to co robi program z pamięcią i zmiennymi czyli aktualnie wykonywane funkcje czy też funkcje nie uruchamiane?
Zobaczysz coś takiego : (oczywiście słowo admin i dostęp nie padnie bezpośrednio w plaintext).
Jeżeli dostęp=admin wtedy uruchom funkcja.użytkownicy? Ta funkcja dla osoby nieuprawnionej nie zostanie wywołana ale jeśli jesteś wstanie zobaczyć ten warunek i prześledzić jak działa wywoływana przez niego funkcja to łatwo ją sobie ustawisz tak aby nadać sobie odpowiednie uprawnienia… np…
Klient nie posiada żadnej bazy - w zasadzie żadnych plików poza graficznymi i tłumaczeniowymi. O każdą daną i zmianę ustawień prosi serwer i przez niego kontaktuje się z bazą danych.

2

W sumie stwierdziłem, że też spróbuję, wiadomo już rozwiązane, ale trzeba sprawdzić czy by się udało samemu zanalizować problem.

Podejście @msm czyli procmon i tam widać jak program sobie syscallami CreateFileW i ReadFile korzysta,
To breakpoint na syscalle w kernel.dll, potem tylko sprawdzałem, czy plik był otwierany z nazwą baza.dbx, sprawdziłem jaki handle uchwyt jest zwracany.
I potem w drugim breakpoincie na ReadFile, sprawdzałem czy to ten sam uchwyt.

Dwie ramki do góry wyszedłem, aż opuściłem moduły kernel32.dll, aż do programu, tam (aktualny_adres - adres_poczatku_modulu) gdzie jest załadowany moduł dawał mi offset, który prosto można dodać do offset sekcji .text i dostać miejsce w kodzie w pliku binarny.

Tam też pogram czytał plik kilka razy ReadFile(file_handle,file_buffer,0x80,&xxx), gdzie te 0x80 to 128 szesnastkowo.
I tak czytał raz do buffora, który był kluczem, drugi do odczytywanych danych.
Jeśli index w rax był wyższy od 0x7F to wczytywała się kolejna porcja 128bajtów danych.

Buffory były dwa MOVZX EAX,byte ptr [RBP + RAX*0x1 + -0x358] ten wczytywał do eax, jeden bajt gdzie counterem był rax z buffora ze stosu rbp-0x358 to były dane do odszyfrowania.
Bajt był schowany na stosie pod MOV [RBP + buf_char], eax
Drugi MOVZX EAX,byte ptr [RBP + RAX*0x1 + -0x3d8] tak samo do eax, ale klucz wczytywał, rax co kółko było zwiększane o 1 dopóki było mniejsze od 0x7f.
SUB EAX,dword ptr [RBP + buf_char]
Teraz w eax się znajduje odszyfrowany bajt.

Wiem, że rozwiązanie było ja dam inne, czyli z dumpem :>
Wiem, że pewnie można było memo użyć, ale nie mogłem znaleźć pointera na tę strukturę, muszę ogarnąć jak to znaleźć w pamięci.

import r2pipe
import time

r2 = r2pipe.open('Protector.exe')
print(r2.cmd('ood'))
print(r2.cmd('db 0x00010002C889'))
r2.cmd('dc')
time.sleep(1)

o = []
for i in range(12):
  r2.cmd('dc')
  time.sleep(0.1)
  o.append(r2.cmd('ps @rdx'))
print(''.join(o))
0

Akurat było sprawdzona jedna część czyli wczytanie i dekodowanie pliku.

A jest jeszcze druga cześć, która sprawdza hasło.

Edit Field, te pole edycji jakoś powinno czytać znaki i sprawdzać czy hasło poprawne, wydawać by się mogło, że to będzie jakieś onChangeEvent, czyli po każdym znaku jest sprawdzana poprawność hasła, ewentualnie po zatwierdzeniu enterem.

A tu ciekawe zaskoczenie.
SetTimer(0, 0, 1000, 0x1000faf70)

I potem w PostMessageBox od nt!KiUserCallbackMessage otrzymuje (NULL, WM_TIMER-x133, 0x1000faf70) (czyżby można było za pomocą postmessage robić remoteProcedureCall? używając WM_TIMER)
Trafia to do DispatchMessage, tam w user32.dll -> user32.dll
I niebezpośrednio trafiamy do funkcji 0x1000faf70, czyli do tej funkcji obsługującej timer.

Pierwsza funkcja, nic ciekawego iterujemy strukturę timerów, szukamy w niej z naszym id timera, jest i tak tylko jeden element, i w następnych 8 bajtach struktury jest adres następnej funkcji :)

void timer_proc_0x1000faf70
               (undefined8 param_1,double param_2,undefined8 hwnd,undefined8 message,
               longlong timer_id)

{
  longlong *timer_struct;
  undefined8 extraout_XMM0_Qa;
  uint idx;
  
  if ((DAT_10019e3e0 == 0) || (*(char *)(DAT_10019e3e0 + 0x89) == 0)) {
    idx = *(uint *)(DAT_100280cf0 + 2);
    do {
      if ((int)idx < 1) {
        return;
      }
      idx = idx - 1;
      timer_struct = (longlong *)get_timer_struct(param_1,param_2,DAT_100280cf0,idx);
      param_1 = extraout_XMM0_Qa;
    } while (*timer_struct != timer_id);
    (*(code *)timer_struct[1])(timer_struct[2]);
  }
  return;
}

Druga funkcja, jakiś check, nie mam pojęcia co sprawdza ciężko to wydedukować.

void timer_wrapper1_0x10016a630(longlong *param_1)

{
                    /* security check */
  if ((*(char *)(param_1 + 0x14) != 0) && (*(int *)(param_1 + 0xc) != 0)) {
    (**(code **)(*param_1 + 0x1c8))(param_1);
  }
  return;
}

Trzecia funkcja też niebezpośrednio wywołana też jakieś sprawdzenie, które nigdy się nie wykonuje jak poprzednie.

void timer_wrapper2_0x10016a720(longlong param_1)

{
                    /* security check */
  if (*(longlong *)(param_1 + 0x90) != 0) {
    (**(code **)(param_1 + 0x90))(*(undefined8 *)(param_1 + 0x98),param_1);
  }
  return;
}

No i teraz jesteśmy we właściwej funkcji, która odpowiada za wyświetlanie rozszyfrowanej bazy.

void show_hide_password_func_0x10002cff0(undefined8 param_1,undefined8 param_2)

{
  int number_of_readed_lines;
  int memo_field_length;
  int memo_field_length2;
  longlong b_check;
  longlong offset;
  longlong is_that_same;
  longlong is_equal;
  longlong user_input;
  longlong p_str2;
  longlong p_str;
  uint i;
  
  p_str = 0;
  p_str2 = 0;
  user_input = 0;
  set_pointer(&p_str,0);
  set_pointer(&p_str2,0);
  number_of_readed_lines = (**(code **)(*database + 0x108))(database);
  if (-1 < number_of_readed_lines + -1) {
                    /* i = -1 */
    i = 0xffffffff;
    do {
      i = i + 1;
                    /* 0xf8 - *p_str = database.readLineNumber(i) */
      (**(code **)(*database + 0xf8))(database,&p_str,(ulonglong)i);
      b_check = check_if_contains_substring("Password:",p_str,1);
      if (b_check != 0) {
        offset = find_first_occurance(':',p_str,1);
        get_substring_after_offset(&p_str,1,offset);
        set_pointer(&p_str2,p_str);
                    /* (Object + 0x7b8) == edit_field */
        getWindowTextWrap(*(longlong **)(Object + 0x7b8),&user_input);
        is_that_same = strcmp(p_str2,user_input);
        if (is_that_same == 0) {
                    /* (Object + 0x7c0) == memo_field */
          memo_field_length =
               (**(code **)(**(longlong **)(*(longlong *)(Object + 0x7c0) + 0x610) + 0x108))
                         (*(undefined8 *)(*(longlong *)(Object + 0x7c0) + 0x610));
          if (memo_field_length == 0) {
            getWindowTextWrap(*(longlong **)(Object + 0x7b8),&user_input);
            if (user_input != 0) {
                    /* Set memo field as database content */
              (**(code **)(**(longlong **)(*(longlong *)(Object + 0x7c0) + 0x610) + 0x160))
                        (*(undefined8 *)(*(longlong *)(Object + 0x7c0) + 0x610),database);
            }
          }
        }
        getWindowTextWrap(*(longlong **)(Object + 0x7b8),&user_input);
        is_equal = strcmp(p_str2,user_input);
        if (is_equal != 0) {
          memo_field_length2 =
               (**(code **)(**(longlong **)(*(longlong *)(Object + 0x7c0) + 0x610) + 0x108))
                         (*(undefined8 *)(*(longlong *)(Object + 0x7c0) + 0x610));
          if (memo_field_length2 != 0) {
            clear_memo_field(*(longlong **)(Object + 0x7c0));
          }
        }
      }
    } while ((int)i < number_of_readed_lines + -1);
  }
  __stack_check((longlong)&stack0xfffffffffffffff8);
  return;
}

Funkcja pobiera ile jest linii w bazie, tej rozszyfrowanej.
Potem pobiera pokolej linie.
Sprawdza czy mają podciąg "Password:", jeśli nie ma continue, jeśli ma to wchodzimy w if.
Potem znajdujemy pozycję ':' w stringu i kopiujemy wszystkie litery za tym do nowego buffera.
Pobieramy tekst z edit_field czyli tego co wpisujemy tekst i porównujemy z tym nowym stringiem uzyskanym przed chwilą.
Jeśli równe pobieramy ilość znaków memo_field jak nie jest puste to wykonujemy daną funkcję, jeśli nie to opuszczamy.
Sprawdzamy czy użytkownik wprowadził jakieś dane, jeśli tak to wyświetlamy bazę, jeśli nie to nie? trochę dziwne, jeśli hasło jest poprawne to po co sprawdzać czy ktoś coś wprowadził XD
Pobieramy edit_field, sprawdzamy znowu hasło, jeśli niepoprawne.
To pobieramy ilość znaków w memo field i jak nie jest puste to
Czyścimy, jak jest to opuszamy.

I teraz żeby objeść te zabezpieczenia to musimy z patchować 2 rzeczy lub 3.

  1. Sprawdzanie czy hasło jest takie samo,
  2. Sprawdzanie czy user coś wprowadził? optionalne, gdyż można tam losowe rzeczy wpisać w edit field ręcznie.
  3. musimy zablokować czyszczenie memo, gdyż nawet jak będzie coś wpisane, to i tak zostanie wyczyszczone po tym więc nie zdążymy zobaczy.

A to patch w pythonie.

file = bytearray(open('Protector.exe', 'rb').read())

cmp = 0x2c507 # any password
for i in range(cmp, cmp+0x6):
    file[i] = 0x90

inp = 0x2c560 # no input needed
file[inp] = 0x90
file[inp+1] = 0x90

memo = 0x2c5c3 # disable cleaning memo
file[memo] = 0xeb

open('Protector_patched.exe', 'wb').write(file)

Widzę, że można jeszcze ręcznie wywołać wpisywanie tej bazy, ale id timera się zmienia, a go by było trzeba wyłączyć syscallem killTimer gdyż jak go nie wyłączymy to i tak by potem po jednej sekundzie wyzerował memo.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.