Guant de seguretat ofensiu 2025 - L'ombra dels robadors

· 9min · Juicecat

L'ombra dels robadors

Part del CTF d'OffSec aquest any va implicar una investigació forense d'una màquina compromesa.

Ens van donar la següent indicació:

Download the ZIP package. The password is "**Shadow234@**".

Thanks to your actions during the ProtoVault incident, you've gained the trust of the Etherians. The OffSec Legend, Cipherflare has called upon you to investigate the breach before more damage is done.

The Etherians offer fragments of evidence, just enough to begin the investigation:

- The user directory of **[email protected]** from the machine **WK001**
- Event logs from **WK001**

A ZIP archive awaits you.

Uncover the truth hidden in the darkness. Find what was taken, and how.

Dins del zip, tenim alguns fitxers- index.md-1.png dins del directori a.smith, obtenim la carpeta d'inici de l'usuari index.md-3.png

Mentre que el fitxer .evtx és un registre de sysmon per a aquest sistema.

Part 1 - Resumit per a la brevetat

Utilitzo evtx_dump per analitzar el fitxer per a la seva llegibilitat:

juicecat@Sovngarde-2:~/CTF/offsec-gauntlet/stealers-shadow
% ~/Tools/evtx_dump -o jsonl -f logs_json.json logs.evtx

També es pot obrir al visor d'esdeveniments de Windows, però és molt més fàcil d'analitzar mitjançant la CLI

index.md-2.png

Podem utilitzar jq per filtrar i refinar la nostra cerca

juicecat@Sovngarde-2:~/CTF/offsec-gauntlet/stealers-shadow
% cat logs_json.json | jq '.Event.System["EventID"]' | head -n 50                                                                       
23
11
11
11
11


juicecat@Sovngarde-2:~/CTF/offsec-gauntlet/stealers-shadow
% cat logs_json.json | jq '.Event | select(.System["EventID"] == 3)' | head -n 50                                                       
{
  "#attributes": {
    "xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"
  },
  "System": {
    "Provider": {
      "#attributes": {
        "Name": "Microsoft-Windows-Sysmon",
        "Guid": "5770385F-C22A-43E0-BF4C-06F5698FFBD9"
      }
    },
    "EventID": 3,
    "Version": 5,
    "Level": 4,
    "Task": 3,
    "Opcode": 0,
    "Keywords": "0x8000000000000000",
    "TimeCreated": {
      "#attributes": {
        "SystemTime": "2025-07-31T08:56:30.629677Z"
      }
    },
    "EventRecordID": 41617,
    "Correlation": null,
    "Execution": {
      "#attributes": {
        "ProcessID": 6648,
        "ThreadID": 10256
      }
    },
    "Channel": "Microsoft-Windows-Sysmon/Operational",
    "Computer": "WK001.megacorpone.com",
    "Security": {
      "#attributes": {
        "UserID": "S-1-5-18"
      }
    }
  },
  "EventData": {
    "RuleName": "-",
    "UtcTime": "2025-07-31 08:56:50.550",
    "ProcessGuid": "00000000-0000-0000-0000-000000000000",
    "ProcessId": 11360,
    "Image": "<unknown process>",
    "User": "-",
    "Protocol": "tcp",
    "Initiated": true,
    "SourceIsIpv6": false,
    "SourceIp": "10.10.10.245",
    "SourceHostname": "WK001.megacorpone.com",


juicecat@Sovngarde-2:~/CTF/offsec-gauntlet/stealers-shadow
% jq -c 'select(.Event.System.EventID == 3                                                                                              25-10-14 - 14:46:29
            and .Event.EventData.Initiated == true
            and .Event.EventData.Image != "C:\\Windows\\System32\\svchost.exe"
            and .Event.EventData.Image != "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
            and .Event.EventData.Image != "C:\\Windows\\System32\\taskhostw.exe")' logs_json.json | wc -l
15981

Finalment, (estalviant temps saltant una gran part d'aquest repte) ens trobem amb aquest esdeveniment:

  },
    "EventData": {
      "RuleName": "-",
      "UtcTime": "2025-08-05 09:02:06.865",
      "ProcessGuid": "8404BF77-C85E-6891-0E37-000000000C00",
      "ProcessId": 17852,
      "User": "MEGACORPONE\\a.smith",
      "Image": "C:\\Users\\a.smith\\AppData\\Local\\Microsoft\\Windows\\INetCache\\IE\\66HCZK0X\\captcha_privacy[1].epub",
      "TargetFilename": "C:\\Users\\a.smith\\AppData\\Local\\Temp\\101010245WK001.zip",
      "Hashes": "SHA1=756FF3A252D10493CE9C34297FA7BB6F84DC27A4,MD5=053C53EC53D5E6C720AB105BC46FAE2B,SHA256=B6A1646F23BA0A05B7C80A7D6261204384AB06F15983EB195EB5F0A3FEDF2475,IMPHASH=00000000000000000000000000000000",
      "IsExecutable": false,
      "Archived": "true"
    }
  }
}

El que és estrany: si ho observeu, es tracta d'un esdeveniment de creació de fitxers i el camp Image és un fitxer epub, que és extremadament anormal. Això ens porta a investigar aquest fitxer epub en qüestió. Com és d'esperar, el fitxer epub és en realitat un PE32, que a causa d'un canvi de registre maliciós, s'està executant com a executable.

Aquest va ser un resum de diverses hores d'excavació a través dels registres d'esdeveniments i el perfil d'usuari per a les molles, però l'objectiu principal d'aquesta publicació del bloc és el desmuntatge.

Part 2: desmuntatge de fitxers maliciós

Pugem aquest fitxer a Ghidra per al desmuntatge i comprovem immediatament la funció d'entrada. Aquesta funció d'entrada només crida dues funcions, Stealers-shadow-1.png El primer no és gens interessant, només un plat Stealers-shadow-2.png

El següent és una funció d'injecció que s'utilitza per preparar les dades Stealers-shadow-3.png

En vermell, veiem una mica de configuració. Però el que és interessant és el blau i el que hi ha a sota. Veiem aquestes línies: Stealers-shadow-4.png

      uVar8 = _get_initial_narrow_environment();
      puVar9 = (undefined8 *)__p___argv();
      uVar1 = *puVar9;
      puVar10 = (undefined4 *)__p___argc();
      iVar5 = FUN_14002a820(*puVar10,uVar1,uVar8);
      cVar3 = FUN_140374c9c();
      if (cVar3 != '\0') {
        if (!bVar2) {
          _cexit();
        }
        __scrt_uninitialize_crt(1,0);
        return iVar5;
      }

Sembla que està configurant variables i després crida a una funció, passant aquestes variables. En aquest cas, s'està cridant a FUN_14002a820 i se li donen 3 arguments. Després, sembla que s'està cridant a FUN_140374c9c i s'està comprovant la sortida (sense error) amb èxit. Després, es retorna el valor de retorn de la funció FUN_14002a820.

Per facilitar el seguiment, canviarem el nom de FUN_14002a820 a wrapper_main i, a continuació, seguirem la xref.

Stealers-shadow-5.png Sembla que wrapper_main és en si mateix un embolcall per a un altre FUN_1402cef40. Reanomenarem aquest a wrapper_2. També veiem que es transmeten alguns paràmetres més, però hi tornarem més endavant. Mirem què hi ha dins de wrapper_2 Stealers-shadow-6.png Aquí tenim molt més per treballar-

T'estalviaré el mal de cap i t'explicaré què està fent això. Primer, configura la pila i prepara la gestió d'errors:

AddVectoredExceptionHandler(0, FUN_1402e1550);
local_90 = (undefined *)CONCAT44(local_90._4_4_, 0x5000);
SetThreadStackGuarantee((PULONG)&local_90);

Aleshores, inicialitza un fil:

  pvVar3 = GetCurrentThread();
  (*(code *)PTR_FUN_14054bd70)(pvVar3,"m");

Les següents línies fan un parell de coses: primer, configura l'accés a TLS (emmagatzematge local de fil) mitjançant el segment GS. A continuació, configura un bucle per incrementar DAT_14054bff0 amb bloqueig i el torna a escriure a la ubicació TLS. Efectivament, això només registra el fil en un context d'execució global i garanteix una inicialització segura per a fils.

  lVar5 = *(longlong *)
           (*(longlong *)(*(longlong *)(unaff_GS_OFFSET + 0x58) + (ulonglong)_tls_index * 8) + 0xe8)
  ;
  lVar4 = DAT_14054bff0;
  if (lVar5 == 0) {
    do {
      if (lVar4 == -1) {
        FUN_140383ec0();
        do {
          invalidInstructionException();
        } while( true );
      }
      lVar5 = lVar4 + 1;
      LOCK();
      bVar6 = lVar4 != DAT_14054bff0;
      lVar1 = lVar5;
      if (bVar6) {
        lVar4 = DAT_14054bff0;
        lVar1 = DAT_14054bff0;
      }
      DAT_14054bff0 = lVar1;
      UNLOCK();
    } while (bVar6);
    *(longlong *)
     (*(longlong *)(*(longlong *)(unaff_GS_OFFSET + 0x58) + (ulonglong)_tls_index * 8) + 0xe8) =
         lVar5;
  }

Ara aquí està la part important

iVar2 = (**(code **)(param_2 + 0x28))(param_1);

Ho hem vist abans, però aquest requereix una mica de comprensió dels llenguatges de programació de baix nivell. El que fa aquesta línia de codi és bàsicament trobar la peça de dades localitzada pel param_2 (actuant com a punter) i afegeix el desplaçament 0x28 a això. Obté qualsevol dada que sigui en aquest punt i la crida com a funció, passant param_1 com a argument.

Aquesta és una manera llarga d'explicar que està molt ofuscada, i encara hem de veure la funció "principal" real.

Per trobar-lo, haurem d'esbrinar a què es resol param_2 + 0x28, que vol dir trobar param_2

Bé, recordeu que ja tenim 3 embolcalls de profunditat. Per tant, comprovem l'embolcall que hi ha a sobre d'aquest per esbrinar d'on prové el paràmetre: Stealers-shadow-8.png Aquí, veiem que &DAT_14038c518 es passa com a param_2. Podem navegar fins a aquest punter fent doble clic sobre ell i ens porta a on s'emmagatzemen les dades Stealers-shadow-9.png

Sembla que s'acaba de posar a zero. Afortunadament, no estem cercant directament això, sinó que només és una base a la qual compensarem amb el nostre 0x28. Per tant, el que podem fer és prémer G i introduir l'adreça de DAT_14038c518 i afegir el nostre desplaçament.

Stealers-shadow-10.png

I hem trobat una adreça de memòria en aquesta ubicació! Ara, gràcies a Ghidra, es mostra com a FUN_1400583d0, així que sabem que aquí hi ha una funció. Stealers-shadow-11.png Entrem en aquesta funció i canviem el nom a main mentre hi estem. Stealers-shadow-12.png

Excel·lent, un altre embolcall! Anem endavant i seguim FUN_14004eea0 Stealers-shadow-13.png Què és això? Un altre embolcall!? Genial. Però ara estem en una cruïlla. Aquest embolcall té un aspecte diferent. El que està fent és, bàsicament, agafar qualsevol dada emmagatzemada a param_1, interpretar-la com una adreça de memòria i executar qualsevol cosa que hi hagi. Per tant, una vegada més, haurem de rastrejar els orígens del paràmetre.

En altres paraules, FUN_14004eea0(code *param_1) fa (*param_1)(); invocar la funció l'adreça de la qual es troba a param_1.

Amb només mirar el codi C, no podríem rastrejar d'on prové aquesta variable. Tanmateix, quan mirem les instruccions desmuntades, veiem que el valor simplement s'emmagatzema al registre RCX. Stealers-shadow-14.png

I de fet, la capa d'embolcall anterior va preparar aquest registre per llegir-lo: Stealers-shadow-15.png

Ara estem una mica aturats: hem de trobar el que hi ha al registre RCX, però aquests només existeixen en temps d'execució, de manera que haurem de depurar el programa i establir un punt d'interrupció en aquesta ubicació, que hauria de ser fàcil ja que tenim l'adreça de la funció que volem trencar. Stealers-shadow-17.png

Com que aquest programa és un executable PE32 i no un binari ELF, no podem utilitzar Ghidra o gdb per depurar-lo (els meus programes escollits per a aquest tipus de treball), sinó que necessitarem un depurador de Windows. El meu depurador de Windows és x64dbg.

El treball que hem de fer al depurador és bastant mínim: només estem buscant inspeccionar la memòria en un punt d'interrupció determinat. Per tant, no em molestaré en explicar massa aquí i, en canvi, només repassaré els detalls d'alt nivell.

El primer que cal notar són les diferents adreces a l'esquerra; per això, no podem establir simplement un punt d'interrupció a 0x14004eea0. El motiu d'aquesta diferència és la diferència en com es carrega el programa.

  • Ghidra analitza el fitxer estàticament. Utilitza el base de la imatge registrat a la capçalera PE (per exemple, 0x140000000) com a inici de totes les adreces virtuals.
  • x64dbg mostra les adreces després que Windows hagi carregat el programa. El carregador de Windows aplica Aleatorització de disseny de l'espai d'adreces (ASLR), de manera que mapeja la imatge a un base aleatòria (per exemple, 0x00007FF6C3E51000).

Stealers-shadow-16.png

Com que no ho podrem fer de la manera fàcil, podem utilitzar només desplaçaments relatius. Si tornem a Ghidra, podem desplaçar-nos fins a dalt per veure l'adreça base utilitzada: Stealers-shadow-19.png

Aleshores, per trobar el desplaçament de la nostra funció, només restem això de l'adreça de la nostra funció: 0x14004eea0 - 0x140000000 = 0x4eea0

Tanmateix, com que realment volem fer una pausa i inspeccionar el contingut de RCX abans de cridar la funció, només agafaré aquesta adreça: Stealers-shadow-25.png Que es troba al desplaçament 0x583d4

Per obtenir l'adreça base del nostre exe a x64dbg, podem obrir la pestanya del mapa de memòria i veure on està carregada. Stealers-shadow-24.png

Afortunadament, en realitat no necessitem fer matemàtiques i només podem inserir-lo com a ordre a x64dbg per visitar aquest desplaçament: Stealers-shadow-26.png

Aleshores podem reprendre l'execució i arribar a aquest punt d'interrupció Stealers-shadow-27.png De fet, veiem que les instruccions desmuntades coincideixen també amb les que estàvem veient a Ghidra. A partir d'aquí, podem inspeccionar el contingut de la memòria a la dreta, que ens dóna el valor RCX: 0x000000248D5BFE70

Però espera.. Això no té sentit. Esperem que hi hagi un punter de funció en aquest registre i aquest valor es troba sota el nostre desplaçament base. En aquest cas, ens hem posat en pausa correctament ABANS RCX està configurat, així que només podem avançar amb una instrucció i veiem que el contingut RCX canvia a una cosa que sembla més correcte: Stealers-shadow-28.png

0x00007FF718F6A600, que coincideix amb la nostra compensació. Podem fer clic amb el botó dret sobre aquest valor i seguir-lo fins a la finestra d'abocament, en la qual podem fer clic amb el botó dret a l'adreça de memòria i copiar el desplaçament del fitxer. Stealers-shadow-30.png

El que ens dóna: 0x29A00. A Ghidra, podem navegar fins a aquest desplaçament: Stealers-shadow-31.png

Que és una funció molt sus. Stealers-shadow-32.png

A l'interior veiem un munt de coses molt interessants que indiquen una enumeració, com ara la línia següent:

    FUN_1400285d0(&lStack_e0,
                  *(undefined8 *)((longlong)&PTR_s_%USERPROFILE%\Documents_140388ff0 + lVar11),
                  *(undefined8 *)((longlong)&DAT_140388ff8 + lVar11));

El que sembla que itera sobre les dades de l'usuari, cosa que té sentit donat el context maliciós d'aquest fitxer.

Pel bé de la digestibilitat, aquesta investigació continuarà en una altra entrada al blog. Però de moment, hem descobert els mecanismes bàsics d'aquesta peça de programari maliciós