UPDATE: Krótki dodatek, bo widzę, że ktoś faktycznie zainteresował się tym postem, ale jest dużo zamieszania co do dwóch kwestii: definicji słowa "hacker" oraz czym jest "Dragon Sector".

Po pierwsze, jest chyba 6 czy 7 definicji słowa "hacker":

  1. hacker – osoba, która pisze kod z "hackami", tj. taki, który niby działa, ale nie czyta się go najlepiej (przykład),
  2. hacker – osoba, która posiada głęboką wiedzę w tematyce informatyki i/lub elektroniki, i lubi wiedzieć jak rzeczy działają (patrz: 1, 2, ale też 3),
  3. hacker – osoba, zajmująca się bezpieczeństwem komputerowym; także: osoba, która posiada wiedzę o wykorzystywaniu podatności i przełamywaniu zabezpieczeń; istotne: jest to określenie neutralne (patrz: 1, 2),
  4. hacker – synonim cyberprzestępcy, co btw jest niepoprawną, ale niestety potocznie używaną definicją (patrz 1, 2),
  5. hacker – tak podobno mówi się na taksówkarzy w niektórych rejonach US (patrz: 1),
  6. hacker – podobno również tak się mówi na niewprawnych golfiarzy.

Zarówno ja, jak i inne osoby z Dragon Sector, jesteśmy hackerami w rozumieniu punktów 2 oraz 3, a czasem także 1 (tak tak, mam na myśli szczególnie Ciebie, widziałem Twój kod). W moim przypadku również ostatnia definicja by pasowała. W szczególności NIE jesteśmy hakerami w rozumieniu definicji 4.

Po drugie, czym jest "Dragon Sector", o którym media piszą "grupa hackerów". Otóż Dragon Sector jest... drużyną e-sportową, która bierze udział w zawodach e-sportowych zwanych Security Capture The Flag (w skrócie: CTF), które sprawdzają umiejętności techniczne uczestników w hackingu zgodnie z definicjami 2 oraz 3 (btw pod pewnymi względami zadania na topowych CTFach są dużo bardziej skomplikowane niż to co Redford/mrtick prezentowali na Oh My H@ck). Jest to bardzo popularna rzecz w środowiskach związanych z cyberbezpieczeństwem i odnośniki do CTFów pojawiają się m.in. w tym rozporządzeniu Rady Ministrów (ctrl+f CTF) w sekcji o wymogach posiadania specjalistycznej wiedzy w zakresie cyberbezpieczeństwa. Dragon Sector jest drużyną, która zajmowała trzy razy pierwsze miejsce w rocznym rankingu światowym (patrz np. wpis na stronie Biura Bezpieczeństwa Narodowego).

Tym samym Dragon Sector nie jest firmą/spółką świadczącą usługi z zakresu cyberbezpieczeństwa. Pracujemy w różnych miejscach (np. ja 12 lat przesiedziałem w Google), niektórzy z nas mają własne firmy. Ale łączy nas to, że jesteśmy hakerami zgodnie z definicją 2 i 3 powyżej, lubimy komputery, lubimy wyzwania, i interesuje nas, jak różne rzeczy działają od środka.

Wczoraj (tj. 5 grudnia 2023) na konferencji Oh My H@ck widziałem zaprezentowany chyba najciekawszy case-study ze stosowanej inżynierii wstecznej w tym roku (jeśli nie w tej dekadzie). Mówię oczywiście o prelekcji „Heavyweight Hardware Hacking”, gdzie zaprezentowane zostaly wyniki badań prowadzonych przez Redforda, q3ka, oraz mrticka (PanaKleszcza). W skrócie i uproszczeniu można o nich poczytać m.in. na Z3S lub u q3ka na mastodonie. A dzisiaj na te publikacje oficjalnie odpowiedziała spółka NEWAG (mirror).

W odpowiedzi pojawiło się bardzo ciekawe stwierdzenie na temat technikaliów inżynierii wstecznej, a ponieważ jest to temat, którym zajmuję się od prawie 20 lat, to postanowiłem się do tego odnieść.

Na początek kilka disclaimerów:

  1. O ile wychodzę w poście od cytatu z oświadczenia NEWAGu, to skupiam się na ogólnych technikaliach, a nie na tym konkretnym przypadku i tej konkretnej sprawie.
  2. Full-disclosure: jestem członkiem i założycielem Dragon Sector, natomiast nie pracowałem nad tym konkretnym projektem, więc nie mam praktycznie żadnych dodatkowych informacji poza tym co można znaleźć w Internecie oraz co było wczoraj na prelekcji.

Zacznijmy od cytatu z oficjalnego oświadczenia spółki NEWAG (podkreśliłem jedno kluczowe słowo, które jest trochę bez znaczenia dla tego postu, ale które ładnie nawiązuje do problematyki metadanych; ale to inny temat i nie na dzisiaj):

8. W konsekwencji w dowolnym czasie możliwe było wykonanie tzw. reverse engineering oprogramowania sterującego (tj. zhakowanie) poprzez przeniesienie jego kodu dekompilacji, modyfikacji oraz ponowne załadowanie zmienionego oprogramowania sterującego. [...] 10. [...] Otóż oczywistym jest, iż nawet najlepszy haker może co najwyżej próbować poznać treść określonego zapisu cyfrowego. Żaden haker nie jest natomiast w stanie, na podstawie samego zapis cyfrowego, wskazać kto konkretnie jest „autorem” określonego zapisu cyfrowego.

Oba zacytowane stwierdzenia są prawdziwe. Tj. prawdziwym jest, że jeśli nie stosuje się żadnych mechanizmów podpisów cyfrowych firmware'u (trochę ironiczne, że konsole do gier mają lepsze zabezpieczenia pod tym względem niż pociągi), to można sobie firmware zrzucić, zmodyfikować, i wrzucić go ponownie. Prawdziwym też jest, że, patrząc na sam kod maszynowy, trudno jest ustalić jego autorstwo – to problem tzw. „attribution” (przypisanie autorstwa) znany przede wszystkim analitykom zajmującym się złośliwym oprogramowaniem.

Co za tym idzie, w tym przypadku NEWAG argumentuje, że ktoś inny ten kod mógł zmodyfikować, i jednocześnie wskazują, że trudno w takich przypadkach jest określić kto daną modyfikację zrobił na podstawie kodu maszynowego.

Oczywiście w rzeczywistym świecie próby przypisania autorstwa nie ograniczają się tylko i wyłącznie do kodu maszynowego, ale ja jednak skupie się tylko na tym. Otóż okazuje się, że analizując kod maszynowy można wyciągnąć jedną bardzo ciekawą informację z bardzo wysokim prawdopodobieństwem (graniczącym z pewnością): czy modyfikacja została zrobiona na poziomie kodu maszynowego, czy kodu źródłowego.

Podstawy, czyli proces kompilacji

Zacznijmy od przypomnienia podstaw tego jak działa budowanie (kompilacja) oprogramowania, w szczególności oprogramowania w językach stosunkowo niskopoziomowych, jak np. język C.

W pewnym uproszczeniu można powiedzieć, że programy z postaci źródłowej kompilowane są do danych oraz do kodu maszynowego. W plikach pośrednich (a często również wynikowych) dane mają zwyczaj trafiać do sekcji o nazwach typu .data (modyfikowalne dane zainicjowane inną wartością niż 0), .rodata (dane tylko do odczytu), czy .bss (modyfikowalne dane zainicjowane zerami). Kod maszynowy natomiast trafia do sekcji o nazwie typu .text.

Sekcje to nic innego jak ciągłe fragmenty pamięci, tj. po prostu tablice bajtów. Podczas kompilacji jednym z bardzo ważnych zadań kompilatora (oraz w bardziej ograniczonym stopniu linkera) jest pamiętanie na jakim offsecie (adresie) od początku sekcji znajduje się która zmienna, tablica, tekst, obiekt, funkcja, itd. Jest to bardzo istotne, ponieważ gdy tylko któraś funkcja będzie chciała odwołać się do innej funkcji lub jakiejś globalnej zmiennej/tablicy/obiektu/etc, to w tym miejscu kompilator musi wstawić odpowiedni adres z odpowiedniej sekcji. Przykładowo (pseudokod C/C++):

int a = 5; int main() { return a; }

W tym prostym programie mamy zmienną globalną a, oraz funkcję main. Zmienna globalna najpewniej powędruje gdzieś do sekcji .data, a kod maszynowy funkcji main gdzieś do sekcji .text. Jak można zauważyć w kodzie, funkcja main zwraca wartość przechowywaną w zmiennej a, tj. musi znać oraz użyć adresu w pamięci, gdzie znajduje się ta właśnie zapisana wartość zmiennej a. Można to również zaprezentować w taki sposób:

Oczywiście ten program jest absolutnie trywialny, więc graf jest czytelny i mamy na nim tylko jedno odwołanie, tj. tylko jedną strzałkę. W prawdziwych programach zmiennych i funkcji są setki lub tysiące, a odwołań jest często jeszcze więcej.

Zazwyczaj po skompilowaniu/skonsolidowaniu wszystkie informacje o tym „co jest gdzie” są gubione. Tj. nie są już potrzebne, bo wszystkie adresy zostały już wyliczone i zapisane w kodzie, więc nie ma sensu ich emitować i zapisywać pliku wykonywalnym. W szczególności jest to prawdziwe dla wszelkich „płaskich” binarek, takich jak monolitycznych obrazów firmware'u oraz plików wykonywalnych bez wsparcia ASLR. Wyjątkiem będą m.in. dynamiczne biblioteki, gdzie często zachowuje się częściowe informacje o pozycjach funkcji (tablice eksportów) czy o pozycjach odwołań (tablice relokacji).

A dlaczego to jest istotne? O tym za chwilę.

Modyfikowanie kodu maszynowego

Kod maszynowy można modyfikować na kilka różnych sposobów. Odpowiedni sposób wybiera się według potrzeb, ale główna zasada brzmi: czym bardziej skomplikowane modyfikacje, tym bardziej skomplikowana będzie technika.

Najprostsze są modyfikacje, gdzie zmienia się malutki fragmencik kodu (kilka-kilkanaście bajtów) i gdzie modyfikacja jest de facto mniejsza (bajtowo) niż oryginalna funkcjonalność. W takim wypadku po prostu nadpisuje się stary kod maszynowy wybranego fragmentu nowym i dopełnia resztę NOPami (tj. instrukcjami, które nic nie robią), lub przeskakuje (instrukcją skoku) stare niepotrzebne bajty – zazwyczaj nazywamy to „patchowaniem”. Tego typu modyfikacje są do bólu oczywiste w kodzie maszynowym z uwagi na powstałe artefakty (NOPy, skok omijający jakiś nieużywany kod, a często też nietypowa konstrukcja kodu maszynowego).

Sprawa się trochę komplikuje, jeśli modyfikacja jest trochę większa, tj. kodu maszynowego, który chcemy dodać, jest więcej niż oryginalnego kodu. Np. chcemy obsłużyć jeszcze jedną sytuację w funkcji, lub dodać zupełnie nową funkcję. Wtedy musimy albo znaleźć trochę pustego miejsca gdzieś w okolicy (tzw. code cave) i dodać tam nadmiarowy kod, albo rozszerzyć trochę odpowiednią sekcje, albo dodać wręcz nową sekcję o odpowiednich atrybutach. Potem musimy jeszcze „hooknąć” odpowiednie miejsca, żeby przekierować bieg wykonania programu do naszej „jaskini”. W każdej z tych sytuacji jest podobnie jak poprzednio, tj. bardzo łatwo zauważyć tego typu modyfikację w kodzie maszynowym – m.in. dlatego, że kawałki funkcji są w jakichś dziwnych odseparowanych miejscach, funkcje są na końcu sekcji (mogą być powody, czemu funkcje są na końcu sekcji, ale zazwyczaj rozkład funkcji ma jakiś wzorzec, a dodana funkcja by od niego odstawała), czy w jakichś dodanych sekcjach.

A gdyby tak rozsunąć po prostu funkcje, tj. zrobić trochę miejsca na powiększenie jednej funkcji (tak, żeby dodać trochę więcej kodu) lub dodanie innej?

I tutaj wracamy do tego o czym pisałem w poprzedniej sekcji. Takie „rozsunięcie funkcji” wiązałoby się się z koniecznością poprawy wszystkich „unieważnionych” przez zmiany w adresach odwołań. WSZYSTKICH. Jeśli któreś byśmy pominęli, to kod by się po prostu crashował. Co więcej, pisałem w poprzedniej sekcji, że informacje o tym gdzie co jest są gubione. Tj. żeby w ogóle zacząć realizować ten pomysł, trzeba najpierw dokładnie ustalić gdzie co się znajduje. To niestety jest jednym z trudnych problemów w inżynierii wstecznej, i stosuje się tutaj sporo heurystyki. Co za tym idzie, łatwo jest coś przeoczyć lub pominąć, a tak powstały błąd nie koniecznie musi objawić się od razu podczas testowania.

Tego typu podejścia się po prostu nie stosuje – jest niepraktycznie skomplikowane i ryzykuje się wprowadzenie nietrywialnych do znalezienia błędów.

Rekompilacja, tj. dekompilacja i ponowna kompilacja

Czasami jednak chcemy, żeby modyfikacje robiło się trochę wygodniej. I chcemy ich robić dużo. W takim przypadku dochodzimy do tytułowej „rekompilacji”, tj. zdekompilowania kodu (patrz punkt 8 w oświadczeniu) do postaci kodu w C, modyfikacji wybranych części, a potem skompilowaniu całości na nowo.

I teraz krótka dygresja o różnicy pomiędzy dezasemblacją/deasemblacją/disasemblacją (możecie wybrać ulubione spolszczenie angielskiego „disassembling”) a dekompilacją:

  • Deasemblacja to przetłumaczenie kodu maszynowego na blisko odpowiadający mu zapis mnemoniczny w języku asembler. Na tym się polega podczas tworzenia modyfikacji na poziomie kodu maszynowego (patrz poprzednia sekcja).
  • Dekompilacja to próba „podniesienia” kodu maszynowego do poniekąd odpowiadającego mu kodu w języku wyższego poziomu, tj. zazwyczaj C (ew. pseudokodu).

Deasemblacja jest generalnie dość prostym procesem, ale jest z nią trochę problemów, które muszą być rozwiązywane heurystycznie – np. gdzie zaczynają się funkcje (jest trochę złośliwych przypadków, gdzie to nie jest oczywiste). Deasemblacja nie jest też 1:1, tj. zdeasemblowanie kodu maszynowego i jego ponowna kompilacja (asemblacja) niekoniecznie są w stanie dać taki sam wynik (m.in. dlatego, że instrukcje asemblera można przetłumaczyć na kod maszynowy na kilka różnych sposobów). Ale to temat na trochę inny post.

Dekompilacja jest natomiast bardzo skomplikowanym problemem – m.in. dlatego, że w procesie kompilacji gubione są wszystkie metadane o typach zmiennych, a te często jest ciężko wywnioskować z kodu maszynowego (w szczególności przy bardziej złożonych typach). Co więcej, z uwagi na optymalizacje wprowadzane przez kompilator (złośliwi mówią, że to „komplikator”), nie wszystkie wyrażenia da się jakoś sensownie odtworzyć w języku wyższego poziomu (zobaczcie np. co kompilator robi z prostym dzieleniem). Co za tym idzie, da się zdekompilować a następnie zrekompilować kod, ale:

  • Jest to bardzo czasochłonny proces (a co za tym idzie: drogi), żeby wygenerowany przez dekompilator kod w ogóle zaczął się kompilować.
  • Nawet jak kod się kompiluje, prawdopodobnie zawiera bardzo dużo błędów, które objawią się dopiero w wyjątkowych sytuacjach (tygodnie–miesiące testów).
  • I najważniejsze, kod maszynowy po rekompilacji będzie wyglądał zupełnie inaczej, niż kod oryginalny (to wynika m.in. z różnicy w kompilatorach, wersjach kompilatorów, parametrów kompilacji, wprowadzonych zmian „żeby się w ogóle kompilowało”, czy choćby kolejności funkcji, zmiennych, czy plików źródłowych).

Ten ostatni punkt jest o tyle ważny, że dzięki niemu bardzo łatwo wziąć np. oryginalny firmware oraz firmware, który podejrzewamy, że jest wynikiem rekompilacji, i je porównać. Jeśli kod został zrekompilowany, to będzie to od razu widoczne.

Ciekawa informacja

Co za tym idzie, jeśli kod został zmodyfikowany na poziomie kodu maszynowego lub wręcz zrekompilowany, to w analizowanym kodzie maszynowym znajdą się artefakty, które to wykażą. W szczególności jest to proste do wykazania, jeśli dysponuje się również oryginalnym kodem maszynowym.

A jeśli żadnych artefaktów nie ma, to jest praktycznie pewne, że modyfikacja została wykonana na oryginalnym kodzie źródłowym, który następnie został skompilowany jak zwykle. A kto zrobił modyfikacje? Tego oczywiście nie wiadomo. Ale być może w tym przypadku zasadnym by było zadać pomocnicze pytanie: a kto miał dostęp do kodu źródłowego?

Comments:

2023-12-06 19:47:46 = redteamer
{
I tu wychodzi niska jakość bezpieczeństwa IT w firmach w Polsce. Nawet, jeśli jakieś "security" jest, to ukierunkowane na zewnętrznych atakujących. Wielokroć widziałem firmy, które mają łatwe do zmanipulowania procesy budowania kodu (np. użytkownik techniczny mający prawa pushowania do repozytorium z hasłem możliwym do wyciągnięcia z jakichś skryptów albo wprost na czyimś pulpicie).
Jest tak dlatego, że firmy nie chcą budować warstwowej ochrony, dbać o pełną i niepodważalną rozliczalność procesów, nie istnieją wewnętrzne zespoły red teamowe które by penetrowały sieć firmy w poszukiwaniu luk i pomagały je likwidować zespołom blue teamowym i działowi IT. A zewnętrzni konsultanci red teamowi odpalą phishing, dadzą się złapać albo nie, ewentualnie wejdą do biura (albo nie) i koniec zabawy.
Może czas w polskich firmach skończyć z naiwnym myśleniem o bezpieczeństwie? Jeśli jakimś cudem zarząd firmy o tym nie wiedział, to zdecydowanie powinien zadbać teraz o pełną analizę forensicową całego procesu, a na przyszłość ta i wszystkie inne firmy powinny mieć wewnętrzne zespoły stale poprawiające bezpieczeństwo i szukające w nim luk. I to kosztuje, ale mniej niż utrata reputacji czy to z powodu celowego działania (to w sumie hipoteza nic nie wiemy na 100%), czy braku nadzoru (może ktoś chciał się wykazać?), czy - bo czemu by nie? - niewłaściwemu zabezpieczeniu które pozwoliło komuś spoza firmy zmienić kod i wydawać firmware który spowodował aktualny kryzys.
}
2023-12-06 21:10:42 = buzz darkyear
{
Mam nadzieję, że trwają intensywne prace nad łatką soft+hardwarową wprowadzającą podpisy do tego firmware.
Można założyć, że rosjanie od najpóźniej dziś pracują nad alternatywnym softem dla naszych pociągów.
}
2023-12-06 21:11:01 = asd
{
redteamer z mojego doświadczenia dla zarządu to po prostu zbędny koszt. W poprzedniej firmie coś się ruszyło dopiero po drugim ataku. Nie powstał żaden team skonczyło się na pentestach i załataniu tego co znaleziono. W aktualnej od roku próbuje wprowadzić podstawową politykę bezpieczeństwa.
}
2023-12-06 22:19:55 = zxc
{
No ok, ale czy ta logika byla napisana w C czy jakims języku interpretowanym specyficznym dla PLC, ktory dopiero byl wykonywany przez firmware producenta PLC? Jak sie ograniczy zestaw polecen do minimum, nie obsluguje adresowania relatywnego czy wskaznikow (co jest notabene korzystne z punktu widzenia niezawodnosci) to modyfikacje staja sie duzo latwiejsze. Praktyczny przyklad: compuphase pawn do zastosowan wbudowanych
}
2023-12-06 22:41:01 = ???
{
Pociągi będące ponoć w trybie "offline" dostawały aktualizacje oprogramowania usuwającą możliwość obejścia blokad przyciskami w kabinie. Jeśli te aktualizacje były podpisane cyfrowo(a dziwne by nie były) lub został ślad na ich wysłanie od newagu oraz zachowała się ich kopia, to czy nie prościej byłoby pokazać że taka ekstra funkcjonalność istnieje też w aktualizacji. Nie dość że to chyba silniejszy dowód na autorstwo niż porównanie kodu maszynowego, to jeszcze chyba i łatwiejszy do wykazania.
}
2023-12-06 23:55:04 = bart
{
Niezłe głupoty pieprzą, już szykują spadochron, że niby ktoś im się włamał i wgrał tak zmodyfikowane oprogramowanie do pociągu? :DDDD

Trzeba tylko mieć

a) pociąg
b) kabel
c) specjalistyczne oprogramowanie do pociągu
d) zapomniałbym - dostęp do pociągu, a wiadomo, że to łatwe :D

Typowa gadka jak ktoś odwali manianę np. jakiś poseł to od razu słyszymy w mediach, że padł atakiem hakerów :D. PS. duże modyfikacje najczęściej wrzuca się do zewnętrznych bibliotek, wtedy można je pisać nawet w haskelu :D

Warto by sprawdzić czy to oprogramowanie posiadało jakiekolwiek certyfikaty, hashe, crc i czy wgranie aktualizacji wymaga np. wysłania tajnego kodu przez CAN np. z sumą kontrolną, jeśli tak to oznacza, że tylko producent mógł wrzucać updejty i potrzebny był sprzęt producenta do tego. Zresztą jak mówisz, wykrycie modyfikacji w binarce to zwykle prosta rzecz.

PS2. Z innych źródeł wiem, że ten "problem" był dobrze znany od dawna w środowisku osób tworzących oprogramowanie dla pociągów, pozdrawiam firmę Sii i jej programistów od Javy :D haha
}
2023-12-07 12:11:20 = Gynvael Coldwind
{
@redteamer
TBH nie jest to specyficzne tylko dla Polski.

@zxc
Z tego co zrozumiałem prezentację, to kod drabinkowy w przypadku tych PLC był "kompilowany" do C (lub czegoś bardzo podobnego), który z kolei był kompilowany do kodu maszynowego. Ale mogłem coś źle zrozumieć, więc trzeba by dopytać Redforda/q3k/mrtick.
Natomiast zgadzam się, że w przypadku języków wyższego poziomu robi się łatwiej. Zresztą mój task CTFowy RERE robił takie numery w kodzie bajtowym Pythona; ale jest jednak duża różnica pomiędzy kodem bajtowym Pythona a kodem maszynowym.

@???
Tutaj mam wrażenie, że jest jakieś zamieszanie z tym co można w dyskusji w Internecie wyczytać. Tj. nie odniosłem podczas prezentacji wrażenia, że chodzi o zdalne aktualizacje. Z tego co ja zrozumiałem, to soft był aktualizowany podczas wizyt pociągu fizycznie u producenta. Ale trzeba by dopytać Redforda/q3k/mrtick.

@bart
Słyszeliśmy o "evil maid attacks", przygotujmy się na "evil firmware updating ninja attacks"!
}
2023-12-07 14:18:13 = dreifacher
{
@???:
> Pociągi będące ponoć w trybie "offline" dostawały aktualizacje oprogramowania usuwającą możliwość obejścia blokad przyciskami w kabinie.

pijesz do tych wpisów kuczyka? (https://social.hackerspace.pl/@q3k/111528162462505087)

myślę, że "nowa wersja firmwaru" raczej nie oznacza samoczynnej aktualizacji w istniejących pociągach, a raczej a) nowszy firmware w pociągach oddanych później b) aktualizacja po wyjeździe z serwisu u newaga
}
2023-12-07 18:15:23 = dj
{
Cuda się zdarzają. Adrian Sierpiński zdekompilował starą dosową grę ZZT tak, że powstałe źródła kompilują się Turbo Pascalem 5.5 do binarki bajt w bajt identycznej z oryginałem: https://blog.asie.pl/2020/08/reconstructing-zzt/
}
2023-12-07 18:54:10 = Gynvael Coldwind
{
@dj
Good stuff - dzięki za linka!
Z kronikarskiego obowiązku muszę w kontekście tego posta wskazać, że TP jest jednak trochę prostszym językiem i TP 5.5 generuje prostszy kod maszynowy niż np. C ;)
}
2023-12-08 07:43:25 = wermi
{
Inny przykład dekompilacji, gdzie binarki budują się z dokładnościa co do bajta to Super Mario 64: https://github.com/n64decomp/sm64

AFAICT sprawę nieco ułatwia fakt, że oryginalna japońska i amerykańska wersja dostały debug build, niemniej jednak nie zmienia to faktu, że matchowanie wszystkich funkcji etc. to nadal miesiące (jeśli nie lata) pracy wielu osób. Bardzo ciekawy projekt, w którego procesie udało się ujawnić wiele błędów w oryginalnym kodzie (np. undefined behavior, który można znaleźć szukając makra "AVOID_UB").
}
2023-12-08 09:06:26 = Gynvael Coldwind
{
@ermi,
Dzięki za linka :)

https://arstechnica.com/gaming/2020/05/beyond-emulation-the-massive-effort-to-reverse-engineer-n64-source-code/
Bardzo fajny artykuł o tym. Kilka fragmentów:

"""
Instead, the port seems to be a direct result of a years-long effort to decompile the Super Mario 64 ROM into parsable C code.

[...]

For Super Mario 64, Nintendo compiled its source code without any fancy compiler options, meaning the decompiled assembly language is simpler to convert back to C code. For a game like Ocarina of Time, though, Nintendo used optimization flags to generate faster code, making the resulting ROM that much harder to untangle back into its source.

"When there are optimization flags, you have a harder time matching a loop to a 'for' vs 'while' [statement] etc.," Kenix said. "You have to try all equivalent patterns of code until you find the one that matches."

[...]

After a few months of work, the Zelda Reverse Engineering Team has only unspooled about 15% of Ocarina of Time's functions into C code.
"""

}
2023-12-08 11:13:12 = Gynvael Coldwind
{
FTR: https://twitter.com/gynvael/status/1733053174208999451
}
2023-12-08 13:30:06 = niepiekm
{
Odnośnie problemów z reasemblacją zdeasemblowanego kodu, polcema prezentację z USENIX Security '15 - Reassembleable Disassembling:
https://www.usenix.org/conference/usenixsecurity15/technical-sessions/presentation/wang-shuai
}
2023-12-08 14:26:32 = faust1002
{
Nie zgodzę się z twierdzeniem, że nie stosuje się modyfikacji na poziomie kodu maszynowego (niezależnie czy mówimy tutaj o opcodach czy instrukcjach assembly). Owszem, takie modyfikacje są wrzodem na tyłku ze względu na konieczność zmiany wszystkich adresów. Ale nie trzeba robić tego ręcznie, zawsze można napisać kawałek kodu. Jeśli zniżamy się do grzebania w kodzie maszynowym, to raczej znamy listę intrukcji, które korzystają z adresowania (w jakiejkolwiek formie - bezpośrednie adresy zahardcodowane w binarce, ofsety, co tam jeszcze się trafi). Więc napisanie narzędzia, które modyfikuje kawałek kodu i przy okazji poprawia wszystkie adresy dookoła nie jest skomplikowane.
}
2023-12-08 15:26:05 = Gynvael Coldwind
{
@niepiekm
Dzięki za linka, chyba kiedyś czytałem ten papierek :)
Jeśli dobrze rozumiem, to ich celem natomiast nie było wygenerowanie identycznej binarki bajt-do-bajtu (w sekcji 6.2.2 piszą o tym, że im się wielkość wynikowa zmieniała). Ciekawe jakie artefakty wprowadzało ich narzędzie swoją drogą.

@faust1002
> Nie zgodzę się z twierdzeniem, że nie stosuje się modyfikacji na poziomie kodu maszynowego
Uhm, ktoś coś takiego gdzieś napisał?
I mean, nawet w moim poście są wymienione metody, które się stosuje ;)
}
2023-12-08 19:55:24 = Maru
{
Czy można obejrzeć to wystąpienie gdzieś w necie?

Tak z ciekawości to wiadomo jaki to plc? Coś od siemensa?
}
2023-12-08 20:07:25 = Gynvael Coldwind
{
@Maru
Nie, prezentacja była tylko na żywo. Będzie powtórzona na CCC.

Co do PLC, to mówili na prelekcji, ale przyznaje, że nie pamiętam ;). Ale nie był to Siemens.
}
2023-12-09 05:11:19 = Jdn333
{
PLC był Szwajcarski nie Koreanski.
Na Koreanskich dawniej było dużo dziur na dodatkowym module ethernetowym, ale to jakieś 10+ lat temu bo pamiętam że była jakaś afera z tym że ktoś przejął jakaś elektrownię zdalnie i zablokował piece. To potem połatali i Siemens zaczął też sprawdzać co się dzieje.
Podziwiam że chłopaki rozgryźli co jest pod spodem tak dużego PLC. I nie był tam w środku jakiś 805x.
Trochę popłynęli przy tłumaczeniu języka drabinkowego z przykładami i ich zdziwienie jak to działa. PLC są głównie dla inżynierów. Mają bloczki kod wykonywany pseudorównolegle. Po co? Mnie uczono, że zawsze wymagana jest tablica prawdy i na jej podstawie dopiero robisz bloczki. Po co? Do tej pory PLC to większość świateł na skrzyżowaniach, duże maszyny np elektrownia. Tam jak jest wtopa to taka tablica prawdy nawet na 4xA4 to dupochron podobno był.
}
2023-12-09 07:43:53 = rozie
{
Obawiam się, że przy braku podpisów cyfrowych ciężko będzie coś udowodnić. Nawet jeśli okaże się, że binarka pochodzi z kodu źródłowego stworzonego kompilatorem, to zapewne narracja producenta będzie taka, że pewnie ktoś wykradł kod i wprowadził zmiany. Nie potrzebuje dostępu do pociągu, kabla i oprogramowania. Wystarczy, że będzie w stanie podmienić binarkę, nim ta trafi na serwer czy nośnik. Problemem pozostanie motyw - komu, poza serwisem producenta, mogło zależeć na wprowadzeniu tego typu zmian?

Pewnie ciekawsze informacje dałoby się wyciągnąć z analizy forensic repozytoriów kodu i komputerów programistów twórcy oprogramowania. Inny ciekawy trop to moduły GSM. Gdzieś pewnie wysyłały dane i operatorzy GSM powinni być w stanie ustalić i właściciela numerów, i położenie modułu. Ale to na pewno zapomniane w dokumentacji moduły producenta, które umożliwiały(by) szybszy, lepszy serwis, gdyby zajmował się nim producent. ;-)

Tak czy inaczej, świetna robota hackerów.

Przy okazji, nazywanie CTFów e-sportem to jak nazywanie sportem szachów.

}
2023-12-09 10:05:04 = Maru
{
@Gynvael Coldwind

Czy CCC to Chaos Communication Congress? Ja jestem z branży automatyki przemysłowej także słabo się orientuję w temacie hackingu i konferencji.

@Jdn333

Ciekawe jak wygląda ta kompilacja kodu plc drabinkowego (ladder) do kodu maszynowego. Jak wspomniałem ja jestem automatykiem przemysłowym i programuję sterowniki od paru lat i wygląda to zupełnie inaczej niż programowanie np. w pythonie. Język drabinkowy jest podobny do schematu elektrycznego, jego idea była taka żeby np. Elektryk mógł łatwo zrozumieć i debuggować kod. Są firmy na świecie które wymagają programów napisanych w taki sposób.

Nie bardzo rozumiem co znaczy bloczki do wykonywania pseudorównolegle? Generalnie przynajmniej w siemensach bloki są wykonywane cyklicznie w określonej kolejności, można użyć bloków przerwań cyklicznych to wtedy jakby główne wywołanie się stopuje i przerwanie cykliczne jest wykonywane. Czas cyklu sterownika czyli wykonanie całego programu zależy już od samego sterownika i programu napisanego, może to być nawet kilka milisekund. Ze swojego doświadczenia mogę powiedzieć, że programowałem całe linie na fabrykach samochodów i magazyny np. Amazona jak i pojedyncze maszyny do cięcia np. profili metalowych i nigdzie nie używaliśmy tablic prawdy jako oficjalnych dokumentów. Te systemy były po prostu zbyt skomplikowane, z rzeczy bezpieczeństwa to była dokumentacja safety czy wszystko zatrzymuje się jak trzeba itp. Ale to też jest definiowane według Performance Level systemu jak to ma działać. Może w elektrowaniach i skrzyżowaniach faktycznie tak to jest zrobione ale nie mam tam doświadczenia.
}
2023-12-09 10:23:26 = Gynvael Coldwind
{
@Maru
Ad CCC: Tak, dokładnie.
}
2023-12-09 18:43:32 = Władca hakerów
{
A nie przyszło im do głowy żeby zamiast lutować, spawać i diaxować po prostu napisać nowy kod? Daleko hakerom do chatGPT.
}
2023-12-10 15:24:44 = samson
{
Heh, jak SI wejdzie do gry na pełnej to dopiero będzie rozkmina kto i dlaczego zmodyfikował kod.
}
2023-12-16 20:24:47 = 21:24
{
Gdyby odczytać z pliku wykonywalnego gdzie się znajduje sekcja z kodem wykonywalnym, zwiększyć
jej rozmiar by pomieściła dodatkowe funkcje, napisać dodatkowe funkcje
w assemblerze na końcu sekcji, potem przejechać po
całej sekcji kodu jakimś dissasemblerem żeby znaleźć miejsca gdzie
się znajdują instrukcje skoku i ich adresy po skoku. Następnie dopisać
w którymś miejscu dodatkowy kod, który wywołuje funkcje dopisane na końcu
sekcji, a następnie przeliczyć na nowo adresy skoku do miejsc w których powinny się
znajdować, to dodanie dodatkowej funkcjonalności byłoby możliwe bez dekompilacji
kodu i nikt nie byłby w stanie udowodnić, że modyfikacja została dokonana bez modyfikacji
kodu źródłowego. Jednak jeśli kod można było wgrywać tylko offline, a zaraz po wyjeździe
pociągu z serwisu zawierał on nowe dziwne funkcje, które sprawiają, że wymaga serwisu po pewnym czasie, to nikt nie ma chyba wątpliwości jak to
naprawdę było.
}
2023-12-17 10:45:01 = Gynvael Coldwind
{
@21:24
Pisałem o tym w poście w tym miejscu:

"""
[...] albo rozszerzyć trochę odpowiednią sekcje, [...]. Potem musimy jeszcze „hooknąć” odpowiednie miejsca, żeby przekierować bieg wykonania programu [...]. W każdej z tych sytuacji jest podobnie jak poprzednio, tj. bardzo łatwo zauważyć tego typu modyfikację w kodzie maszynowym – [...], funkcje są na końcu sekcji (mogą być powody, czemu funkcje są na końcu sekcji, ale zazwyczaj rozkład funkcji ma jakiś wzorzec, a dodana funkcja by od niego odstawała), [...].
"""

Co do "potem przejechać po całej sekcji kodu jakimś dissasemblerem żeby znaleźć miejsca gdzie się znajdują instrukcje skoku i ich adresy po skoku" – w przypadku prostych funkcji jasne, ale w przypadku większych to może być bardziej skomplikowane. Np. mogą być odwołania relatywne w sekcji danych względem jakiegoś innego miejsca w kodzie (co jest dość trudno wyłapać bez analizy), albo adresy mogą być zaszyfrowane/zobfuscowane w inny sposób (czego też disassembler nie wyłapie). Wszystko to można oczywiście naprawić, ale wraz ze wzrostem stopnia skomplikowania wzrasta ilość czasu potrzebnego na porządne przetestowanie wszystkiego. I to dynamiczne przetestowanie, które by wymagało posiadania sprawnego urządzenia, na którym całość ma działać, albo jego digital twina (pełnego emulatora).

Więc tak, da się, ale nadal zostają artefakty o których wspominam.
}
2023-12-17 20:21:38 = 21:18
{
Myślę, że gdybym napisał program i skompilował jakimś zamkniętym
kompilatorem, a następnie napisał program, który dodaje do niego zewnętrznie
skompilowane funkcje i je wywołuje to nawet jeśli w kodzie powstałyby artefakty, które
umożliwiłyby odróżnienie czy kod został dodany lub w którym miejscu, to znalezienie
takiego artefaktu wymagałoby dużo większych umiejętności niż napisanie samego programu, który
to robi. Daj znać jakbyś miał kilka wolnych miesięcy życia do wyrzucenia w błoto, bo jak tak
to mógłbym ci taki zmodyfikowany program podesłać do analizy i zobaczylibyśmy czy
te artefakty o których piszesz rzeczywiście są takie proste do wyłapania :)
}
2023-12-17 20:39:39 = Gynvael Coldwind
{
@21:18
Nadal piszesz o wykryciu przerzucenia funkcji na koniec sekcji mając do dyspozycji dwie bardzo bliskie wersje softu?
To jest praktycznie trywialne, szczególnie że stara funkcja zostaje na swoim miejscu, i w jednym kodzie jest używana w funkcjach nadrzędnych, a w drugim nie. To nie brzmi specjalnie różnie od zwykłego binary patch-diffingu w takim przypadku, przy zwróceniu dodatkowej uwagi na tego typu artefakty.

Muszę też wskazać, że to nie są jakieś specjalnie trudne rzeczy w kontekście inżynierii wstecznej. Jest sporo innych zagadnień w RE, które mają znacznie większy stopień skomplikowania, niż szukanie tego typu artefaktów (choćby praca na dobrze zobfuskowanym kodzie, który dodatkowo jest najeżony trikami antydbg, czy praca na kodzie maszynowym do którego nie ma się opisu architektury, narzędzi, emulatora, czy możliwości wykonania zmodyfikowanej wersji kodu).
}
2023-12-17 23:04:23 = 00:02
{
Tych dodatkowych funkcji nie muszę wstawiać na koniec sekcji, mogę
zwiększyć wielkość sekcji i wstawić nowe funkcje w dowolne miejsce między starymi funkcjami,
dopisać ich wywołanie w dowolnej starej funkcji i przeliczyć adresy wszystkich
skoków uwzględniając przesunięcia. Nie twierdzę, że rzeczy o których wspominasz są łatwiejsze od napisania
takiego programu (wierzę ci na słowo), jednak w przypadku tego oprogramowania sterującego
pociągami, które nie było w żaden sposób zabezpieczone, gdyby ktoś kumaty
dopisał sobie dodatkową funkcjonalność do tego kodu i zrobił to z zamysłem, że
zmodyfikuje to w taki sposób jakby było skompilowane z kodu źródłowego, to
znalezienie dowodu, który by wskazał, że ten kod został zmodyfikowany po kompilacji
byłoby bardzo trudne - analizując wyłącznie samo zmodyfikowane oprogramowanie (jak było w przypadku tej sytuacji).
Moim zdaniem dowód, że cały kod wygląda jakby był skompilowany ze źródeł, nie jest
żadnym dowodem, bo sam mógłbym go zmodyfikować w taki sposób, że wyłapanie artefaktów
o których piszesz byłoby bardzo trudne, a może nawet niemożliwe.
}
2023-12-19 11:26:17 = Yahoo
{
Pytanie: Czy możliwe jest, że do pamięci sterownika pociągu został w SPS wgrany wirus, który pozostał przez wszystkich niezauważony? Jak jest ze składami Newagu serwisowanymi w innych zakładach?

Przypomina mi się Stuxnet, który w żaden sposób nie modyfikował firmware, który nie był wykrywalny przez skanery antywirusowe, którego jedyną funkcją była taka podmiana wartości w pamięci operacyjnej, żeby sterownik pracujący prawidłowo podejmował niepoprawne decyzje ponieważ dane wejściowe są niepoprawne.

Wtedy to, że serwisant Newagu wgrywa na nowo kod, czym nadpisuje całą pamięć i nagle dane są poprawne, nie jest niczym dziwnym.
}
2023-12-19 18:22:40 = Gynvael Coldwind
{
@00:02
Hipotetycznie to o czym piszesz jest oczywiście możliwe, natomiast istotne jest to, że czym bardziej skomplikowany program tym trudniej jest to zrobić. Tj. dla prostych małych programików to jest dość proste, bo bardzo łatwo znaleźć wszystkie referencje – pewnie nawet jakimś automatem. Dla większych programów to się natomiast zaczyna robić skomplikowane, tj. automaty wyłapią tylko część referencji, a reszty trzeba szukać ręcznie albo analizując cały kod, albo przeprowadzając bardzo czasochłonne testy (które by musiały mieć bardzo dobre pokrycie i które ktoś musi napisać; no i całość by trzeba móc jakoś rozsądnie uruchamiać).

Co za tym idzie, czym większy program, tym mniej praktyczna staje się ta metoda. Tym samym, czym większy program, tym jest bardziej pewne, że modyfikacja, która wygląda na zrobiona w kodzie, faktycznie została zrobiona w kodzie.

Co do tego, czy to jest dowód czy nie, to generalnie się z Tobą zgadzam. Tj. mając jeden zmodyfikowany kawałek kodu trudno jest dać temu dużą wagę. Sprawa zaczyna się zmieniać, jak się ma trochę więcej próbek z różnych gałęzi projektu. Oczywiście w skomplikowanych sprawach analiza kodu byłaby tylko jednym ze źródeł potencjalnych poszlak.

BTW korci mnie, żeby napisać osobny post o tym z czego te trudności wynikają, i dlaczego czasem jest łatwiej, a czasem trudniej.

@Yahoo
Nie do końca chyba rozumiem opisywany przez Ciebie scenariusz. Szczególnie, że próbujesz go umiejscowić w obecnej sprawie związanej z Newagiem, ale przekręcasz kilka informacji, które można znaleźć w doniesieniach medialnych (vide np. https://pl.wikipedia.org/wiki/Newag#Kontrowersje).
}

Add a comment:

Nick:
URL (optional):
Math captcha: 10 ∗ 8 + 3 =