martedì 11 agosto 2026

Published agosto 11, 2026 by Django Faiola with 0 comment

What's in Your Lidl Plus App? An iOS Forensic Analysis

Indice dei contenuti

Introduzione

Lidl Plus è l'applicazione mobile ufficiale del gruppo Lidl. Consente di accedere a coupon personalizzati, consultare il volantino digitale, gestire la lista della spesa, monitorare lo storico degli acquisti ed effettuare pagamenti ove supportato.

Essendo Lidl una delle principali catene della grande distribuzione organizzata (GDO) a livello globale, con migliaia di punti vendita operativi in Europa e nel mondo, l'applicazione vanta un bacino d'utenza estremamente ampio e una frequenza d'uso quotidiana. Questa capillarità rende Lidl Plus una fonte d'interesse primaria nell'ambito della mobile forensics.

Per maggiori dettagli sulle funzionalità ufficiali dell'app, è possibile consultare il portale informativo o la pagina sull'App Store.

L'osservazione dell'impiego reale e quotidiano dell'applicazione durante l'esperienza d'acquisto ha fatto emergere il seguente quesito di ricerca: quali artefatti digitali vengono memorizzati localmente sul dispositivo e quali informazioni comportamentali o identificative è possibile ricostruire tramite un'analisi forense su iOS?

L'obiettivo di questo articolo è rispondere a tale domanda analizzando approfonditamente il contenuto della sandbox di Lidl Plus su sistema operativo iOS. Nello specifico, verranno esaminati i database SQLite (sia in chiaro che cifrati), i file di configurazione .plist, le strutture di cache e le tracce delle comunicazioni di rete con i servizi remoti, basandosi esclusivamente sui reperti acquisiti.

📂 Percorsi

Per agevolare la lettura del documento e garantire maggiore chiarezza, nel prosieguo dell'articolo verranno utilizzate le seguenti abbreviazioni per identificare le directory della sandbox dell'applicazione all'interno del file system di iOS:

<ADC>=/private/var/mobile/Containers/Data/Application/<UUID>/
<AGC>=/private/var/mobile/Containers/Shared/AppGroup/<UUID>/

Nel corso dell'analisi dell'applicazione iOS (com.lidl.eci.lidl.plus) sono stati individuati diversi artefatti di primario interesse forense. La tabella seguente riporta i principali percorsi esaminati, la denominazione del file e la relativa tipologia:

Path File name File type
<ADC>/Library/Caches/com.lidl.eci.lidl.plus/ Cache.db SQLite
<ADC>/Library/Caches/com.lidl.eci.lidl.plus/fsCachedData/ * Various
<ADC>/Library/Preferences/ com.lidl.eci.lidl.plus.plist Plist
<ADC>/Library/Caches/com.onevcat.Kingfisher.ImageCache.default/ * Images
<ADC>/Library/Application Support/databases/ ShoppingListDatabase.db SQLite
<ADC>/Library/Application Support/SelfScanning/ selfScanning.sqlite SQLCipher
<ADC>/Library/Application Support/GroceryPickup/ groceryPickup.sqlite SQLCipher/GRDB

🖼️ Kingfisher Image Cache

Kingfisher è una diffusa libreria open source per il download e il caching asincrono delle immagini in ambiente iOS. Il framework gestisce un sistema di caching a due livelli (memoria RAM e disco fisso) regolato da politiche di scadenza temporale e limiti di allocazione dello spazio di archiviazione.

All'interno della sandbox dell'applicazione Lidl Plus, le risorse grafiche salvate su disco sono collocate nel seguente percorso:
<ADC>/Library/Caches/com.onevcat.Kingfisher.ImageCache.default/

Kingfisher assegna i nomi ai file salvati su disco calcolando l'hash della cache key (di norma costituita dall'URL completo della risorsa remota o da una sua rappresentazione normalizzata):

  • MD5: Nelle versioni precedenti della libreria, i nomi dei file venivano generati tramite digest MD5 (stringhe di 32 caratteri esadecimali).
  • SHA-256: Nelle implementazioni più recenti viene impiegato l'algoritmo SHA-256 (stringhe di 64 caratteri esadecimali), garantendo una maggiore resistenza alle collisioni.

Poiché il nome del file su disco è il risultato di una funzione hash unidirezionale (one-way), esso non contiene l'URL in chiaro né metadati immediatamente riconducibili al prodotto o al coupon a cui si riferisce.

Per ricostruire l'associazione tra l'immagine fisica memorizzata nella cache e l'elemento a cui si riferisce, il parser iLEAPP effettua il calcolo dell'hash (MD5 o SHA-256) dell'URL estratto dai database o dalle risposte API JSON. Qualora venga individuata una corrispondenza con un file presente nella directory di Kingfisher, l'immagine viene estratta e collegata al record di analisi (check_in_media), consentendone l'anteprima visiva diretta all'interno del report HTML/LAVA.

📋 Lista della spesa

Il database dedicato alla gestione delle liste della spesa è memorizzato localmente nel file SQLite ShoppingListDatabase.db, ubicato nel percorso:
<ADC>/Library/Application Support/databases/

Questo artefatto rientra tra i file inclusi nei backup logici di iTunes, rappresentando una fonte d'interesse forense primaria poiché acquisibile senza la necessità di ricorrere a tecniche di estrazione fisica o privilegi di root del dispositivo.

La struttura relazionale si basa principalmente sulle tabelle ListEntity e ListItemEntity, che memorizzano rispettivamente le liste della spesa create e i singoli articoli in esse contenuti.

SELECT
  L.ROWID AS "L_id",
  I.ROWID AS "I_id",
  datetime(I.lastUpdate) AS "item_last_update",
  I.type AS "item_type",
  I.title AS "product_name",
  coalesce(I.imageOriginal, I.imageBig, I.imageMedium, I.imageThumbnail) AS "image_url",
  I.brand,
  I.quantity,
  CASE
    WHEN I.isChecked IS NULL THEN 'N/A'
    WHEN CAST(I.isChecked AS TEXT) = '' THEN 'N/D'
    WHEN I.isChecked = 1 THEN 'Yes'
    WHEN I.isChecked = 0 THEN 'No'
    ELSE 'Unknown (' || CAST(I.isChecked AS TEXT) || ')'
  END AS "purchased",
  I.price,
  I.priceDiscount,
  I.currency,
  I.packaging,
  I.productId,
  I.productSource,
  I.position,
  I.offerId,
  I.couponId,
  I.pendingAction,
  I.id AS "item_uuid",
  L.type AS "list_type",
  L.name,
  datetime(L.lastUpdate) AS "list_last_update",
  L.id AS "list_uuid"
FROM ListEntity AS "L"
LEFT JOIN ListItemEntity AS "I" ON (L.id = I.listId)
ORDER BY julianday(I.lastUpdate) DESC

Esempio di record per la lista (L_id=1):

  • type: STANDARD (tipologia della lista).
  • name: La mia lista della spesa (titolo della lista spesa).
  • position: 1.0 (posizione utilizzata per l'ordinamento delle liste nell'interfaccia utente).
  • itemsCount: 10 (numero di prodotti presenti nella lista).
  • lastUpdate: 2026-06-19T15:58:02.544 (timestamp dell'ultimo aggiornamento in formato ISO 8601: 19 giugno 2026 15:58:02.544).
  • id: da85317b-d725-4d2a-aa6a-349ac659763d (chiave primaria rappresentata da un UUID).

Esempio di record per il prodotto (I_id=52):

  • lastUpdate2026-06-20T13:43:33.381 (data dell'ultimo aggiornamento in formato ISO 8601: 20 giugno 2026 13:43:33);
  • type: PRODUCT_CATALOG (tipo di articolo: FREE_TEXT=elemento inserito come testo libero, PRODUCT_CATALOG=prodotto proveniente da catalogo).
  • title: Filetti di sgombro grigliati (nome del prodotto).
  • image_url: https://static-product-catalog.lidlplus.com/images/productdata/IT/original/highres/5585987_6004800_v0_highres.png?im=Resize=(384) (URL dell'immagine del prodotto; la query restituisce la prima immagine disponibile tra quella originale, ad alta risoluzione, media o miniatura).
  • brand: Nixe (marchio del produttore).
  • quantity: 2 (quantità).
  • isChecked: 0 (indica se il prodotto è stato acquistato: 1=Yes, 0=No).
  • price: 2.09 (prezzo del prodotto).
  • priceDiscount: NULL (importo dello sconto associato al prodotto).
  • currency: (simbolo della valuta).
  • packaging: 120g (confezione).
  • productId: 8813307144043_IT (identificativo univoco del prodotto).
  • productSource: basedpurchases (origine del prodotto: basedpurchases=acquisti precedenti, OFFERS_GRID=offerte grigliati, LEAFLET_GRID=volantino, LEAFLET_DETAIL=dettaglio volantino).
  • position: 2.0 (posizione utilizzata per l'ordinamento dei prodotti nell'interfaccia utente).
  • offerId: NULL (identificatore dell'offerta associata).
  • couponId: NULL (identificatore del coupon associato).
  • pendingActionNONE (stato dell'azione in sospeso: NONE=nessuna, DELETE=eliminazione, ADD=inserimento e UPDATE=aggiornamento).
  • id063a9b01-b974-48fc-a32b-058b412c2384 (chiave primaria: UUID del prodotto).
  • listId: da85317b-d725-4d2a-aa6a-349ac659763d (foreign key del prodotto → ListEntity.id).

📌 Nota: La cache HTTP dell'applicazione Cache.db memorizza le risposte JSON dell'endpoint https://shopping-list.lidlplus.com/api/v4/lists/<UUID>. La tabella cfurl_cache_receiver_data conserva il payload trasmesso e può essere analizzata come fonte alternativa per ricostruire il contenuto della lista della spesa qualora il database ShoppingListDatabase.db risulti danneggiato, manomesso o svuotato.

💳 Metodi di pagamento

L'analisi dei metodi di pagamento registrati all'interno dell'applicazione Lidl Plus è di fondamentale interesse forense, in quanto consente di ricostruire il profilo finanziario dell'utente, i circuiti utilizzati e le abitudini d'acquisto.

Trattandosi di dati forniti da un backend remoto, le informazioni sui metodi di pagamento non vengono salvate in un database SQLite dedicato, bensì catturate nella cache delle chiamate HTTP gestita da iOS Cache.db.

In generale, le API di Lidl seguono la struttura convenzionale:
https://<service>[.lidlplus.com]/<resource>/v<version>/<optional-path>

SELECT
  cr.entry_ID,
  cr.time_stamp,
  cr.request_key,
  crd.isDataOnFS,
  crd.receiver_data AS "data"
FROM cfurl_cache_response AS "cr"
LEFT JOIN cfurl_cache_receiver_data AS "crd" ON (cr.entry_ID = crd.entry_ID)
WHERE cr.request_key REGEXP 
  "^https?://payments\.lidlplus\.com/" ||
  "(?:payment-methods/v\d+/lidl/[A-Z]{2}/wallet/all" ||
  "|user-profiles/v\d+/lidl/[A-Z]{2}/(?:store|wallet))" ||
  "(?:\?.*)?$"

La figura seguente mostra il contenuto del campo campo receiver_data, contenente la risposta JSON dell'endpoint, relativo al record cfurl_cache_receiver_data.entry_ID=201,  decodificato mediante dfDataViewer.

Il seguente esempio descrive il primo elemento dell'array paymentMethods:

  • paymentMethods: elenco dei metodi di pagamento.
    • [0].id: 772bd81332ad610a2447 (identificativo univoco del metodo di pagamento).
    • [0].type: Card (tipologia del metodo di pagamento).
    • [0].brand: VISA (circuito o brand della carta).
    • [0].accountHolder: NULL (nome dell'intestatario del metodo di pagamento).
    • [0].alias: Postepay (nome personalizzato assegnato dall'utente al metodo di pagamento).
    • [0].balance: 0 (saldo disponibile: generalmente 0 o non applicabile per le carte).
    • [0].currency: EUR (valuta in formato ISO 4217)
    • [0].bankName: 0 (nome della banca o dell'istituto emittente).
    • [0].cardBackgroundImagehttps://payments.lidlplus.com/psp/assets/icons/paymentMethodBackgroundImage.png (URL dell'immagine di sfondo della carta).
    • [0].number: **** **** **** 0816 (numero della carta mascherato; conserva in chiaro solo le ultime 4 cifre).
    • [0].isDefault: true (flag indicante se il metodo di pagamento è impostato come preferito per le transazioni Lidl Pay).
    • [0].status: Validated (stato del metodo di pagamento: Validated=valido, AboutToExpire=in scadenza, Expired=scaduto).

📄 Cronologia scontrini

Uno degli artefatti di maggiore interesse è rappresentato dalla cache delle comunicazioni HTTP dell'applicazione, memorizzata nel database Cache.db situato nel percorso <ADC>/Library/Caches/com.lidl.eci.lidl.plus/.

L'analisi dei pacchetti e dei payload memorizzati in cache consente di recuperare le risposte JSON restituite dai server remoti di Lidl Plus. Nello specifico, risulta possibile estrarre sia la lista sintetica della cronologia degli scontrini fisici emessi in cassa, sia il dettaglio analitico di ciascuna transazione.

Gli endpoint dedicati agli scontrini seguono il seguente schema:
https://tickets.lidlplus.com/api/<VERSION>/<COUNTRY>/tickets/<QUERY_STRING>

    • <VERSION>: Versione dell'API (es. v1, v2, v3).
    • <COUNTRY>: Codice ISO 3166-1 alpha-2 del Paese (es. IT, DE).
    • <QUERY_STRING>: Parametri della richiesta (es. paginazione o filtri della lista).

    La query SQL riportata di seguito utilizza una regular expression per individuare nel database Cache.db tutte le richieste HTTP dirette agli endpoint della cronologia degli scontrini:

    SELECT
      cr.entry_ID,
      cr.time_stamp,
      cr.request_key,
      crd.isDataOnFS,
      crd.receiver_data AS "data"
    FROM cfurl_cache_response AS "cr"
    LEFT JOIN cfurl_cache_receiver_data AS "crd" ON (cr.entry_ID = crd.entry_ID)
    WHERE cr.request_key REGEXP 
      "https?:\/\/tickets\.lidlplus\.com\/api\/v\d+\/[A-Z]{2}\/tickets\?"
    

    La figura seguente mostra il contenuto del campo receiver_data, contenente la risposta JSON dell'API, relativo al record cfurl_cache_receiver_data.entry_ID=192,  decodificato mediante dfDataViewer.

    Il seguente esempio descrive il primo elemento dell'array tickets, corrispondente allo scontrino più recente presente nella risposta.

    • tickets: (elenco dei scontrini);
      • [0].date: 2026-06-27T20:23:21+00:00 (timestamp di emissione in formato ISO 8601: 27 giugno 2026 20:23:21).
      • [0].storeCode: IT0786 (codice identificativo del punto vendita).
      • [0].totalAmount: 17.90 (importo totale dello scontrino).
      • [0].currency: (valuta)
        • code: EUR (valuta in formato ISO 4217).
        • symbol: (simbolo della valuta).
      • [0].articlesCount: 10 (numero di prodotti acquistati).
      • [0].couponsUsedCount: 0 (numero di coupon utilizzati).
      • [0].returns: [] (array contenente eventuali informazioni relative a operazioni di reso; nel campione analizzato risulta vuoto).
      • [0].isFavorite: false (indica se lo scontrino è stato contrassegnato come preferito).
      • [0].badges.invoice: false (indica l'eventuale emissione di una fattura fiscale collegata).
      • [0].hasHtmlDocument: true (indica la disponibilità dello scontrino in formato HTML/visivo).
      • [0].id: 18000786120260627570965 (identificatore univoco dello scontrino).

    I dati in cache ricostruiscono gli acquisti e la presenza dell'utente nel punto vendita storeCode ad una precisa data/ora date. L'id dello scontrino permette inoltre di correlare le chiamate a .../tickets/<ID> per estrarre il dettaglio analitico di articoli, prezzi, sconti e metodo di pagamento.

    🧾 Dettaglio scontrini

    Mentre l'endpoint restituisce l'elenco generale delle transazioni, le richieste dirette a /tickets/<ID>  ne recuperano il dettaglio analitico.

    SELECT
      cr.entry_ID,
      cr.time_stamp,
      cr.request_key,
      crd.isDataOnFS,
      crd.receiver_data AS "data"
    FROM cfurl_cache_response AS "cr"
    LEFT JOIN cfurl_cache_receiver_data AS "crd" ON (cr.entry_ID = crd.entry_ID)
    WHERE cr.request_key REGEXP 
      "^https:\/\/tickets\.lidlplus\.com\/api\/(v\d+)\/[A-Z]{2}\/tickets\/[^?\r\n]*$"
    

    Il campo isDataOnFS=1 indica che le risposte JSON sono memorizzate in file autonomo (denominato tramite UUID) al percorso:
     <ADC>/Library/Caches/com.lidl.eci.lidl.plus/fsCachedData/<UUID>

    La figura seguente mostra il contenuto del file 507F3F23-F7BE-4FB0-B88E-9346FD0D32CE, decodificato mediante DataViewer.

    I principali campi presenti nel dettaglio dello scontrino comprendono:

    • date: 2026-06-27T20:23:21 (timestamp di emissione in formato ISO 8601: 27 giugno 2026 20:23:21).
    • htmlPrintedReceipt: <html><head>  <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>...</body></html> (contenuto dello scontrino in formato HTML).
    • id: 18000786120260627570965 (identificatore univoco dello scontrino).
    • store: informazioni relative al punto vendita.
      • id: IT0786 (identificatore del punto vendita).
      • name: Terracina (LT) (denominazione del punto vendita).
      • address: Viale Europa, snc (indirizzo del punto vendita).
      • postalCode: 04019 (codice di avviamento postale).
      • locality: Terracina (LT) (località del punto vendita).
    • totalAmount: 17.90 (importo totale dello scontrino).
    • isFavorite: false (indica se lo scontrino è stato contrassegnato come preferito).
    • hasInvoice: false (indica l'eventuale emissione di una fattura fiscale collegata).
    • barCode0888078657096501270626 (codice a barre riportato sullo scontrino).
    • isDeleted: false (indica se lo scontrino è stato contrassegnato come eliminato).

    L'elemento di maggiore rilievo è la chiave htmlPrintedReceipt, che racchiude il codice HTML completo utilizzato dall'applicazione per la resa visiva dello scontrino fisicamente emesso in cassa. Ciascuna riga di prodotto (es. id="purchase_list_line_3") integra appositi attributi data-* di grande valore analitico:

    <tr id="purchase_list_line_3" class="article" 
        data-art-id="6013484" 
        data-unit-price="1,79" 
        data-tax-type="10%" 
        data-art-description="GUSTO PRONTO COUS CO">
      <td>GUSTO PRONTO COUS CO</td>
      <td>10%</td>
      <td>1,79</td>
    </tr>
    

    Esempio di metadati per riga d'acquisto (riga=3):

    • data-art-id: 6013484 (identificativo del prodotto).
    • data-art-quantity: (campo presente ed esplicitato solo qualora la quantità sia maggiore di 1).
    • data-unit-price: 1,79 (prezzo unitario).
    • data-tax-type: 10% (aliquota IVA applicata al prodotto).
    • data-art-description: GUSTO PRONTO COUS CO (descrizione sintetica).
      • GUSTO PRONTO COUS CO 10% 1,79 (rappresentazione visiva della riga nello scontrino).

    🎴 La tua carta Lidl Plus

    La carta digitale Lidl Plus rappresenta l'identificativo primario dell'account utente. Viene utilizzata durante le operazioni di cassa tramite la scansione di un QR code per l'applicazione di sconti, la fruizione dei coupon e l'associazione automatica delle transazioni al profilo cliente..

    Per estrarre i dati del QR code memorizzati nella cache delle chiamate HTTP (Cache.db), è possibile utilizzare la seguente query SQL basata su espressione regolare:

    SELECT
      cr.entry_ID,
      cr.time_stamp,
      cr.request_key,
      crd.isDataOnFS,
      crd.receiver_data AS "data"
    FROM cfurl_cache_response AS "cr"
    LEFT JOIN cfurl_cache_receiver_data AS "crd" ON (cr.entry_ID = crd.entry_ID)
    WHERE cr.request_key REGEXP
      "^https?:\/\/payments\.lidlplus\.com\/payment-methods\/v\d+\/lidl\/[A-Z]{2}\/store\/qr$"
    

    La decodifica del campo cfurl_cache_receiver_data.receiver_data restituisce una struttura JSON essenziale:

    {
      paymentQR: 77390007526895xxx271020241409373677573
    }
    

    Mentre il payload JSON restituito dall'API contiene la stringa numerica completa di 38 cifre, l'interfaccia grafica dell'applicazione mostra a schermo solo le prime 17 cifre (es. 77390007526895xxx), corrispondenti al codice identificativo leggibile stampato direttamente sotto il QR code dell'utente.

    🏷️ Dettagli delle promozioni

    Questo artefatto analizza le informazioni di dettaglio relative alle promozioni e ai coupon attivi (es. meccaniche di riscatto, termini e condizioni, barcode dedicati).

    Le immagini associate ai banner promozionali e ai prodotti in offerta scaricate durante la navigazione vengono memorizzate nella cache locale gestita dalla libreria Kingfisher, al percorso:
    <ADC>/Library/Caches/com.onevcat.Kingfisher.ImageCache.default

    SELECT
      cr.entry_ID,
      cr.time_stamp,
      cr.request_key,
      crd.isDataOnFS,
      crd.receiver_data AS "data"
    FROM cfurl_cache_response AS "cr"
    LEFT JOIN cfurl_cache_receiver_data AS "crd" ON (cr.entry_ID = crd.entry_ID)
    WHERE cr.request_key REGEXP
      "^https?:\/\/coupons\.lidlplus\.com\/app\/api\/v\d+\/(?:[A-Z]{2}\/)?promotionsdetails\/"
    

    La figura seguente mostra il contenuto del campo receiver_data, contenente la risposta JSON dell'API, relativo al record cfurl_cache_receiver_data.entry_ID=207:

    Di seguito viene descritto il dettaglio della promozione: 

    • cr.time_stamp: 2026-06-30 05:08:58 (timestamp della registrazione della risposta di rete nella cache in formato ISO 8601: 30 giugno 2026 05:08:58).
    • type: AssignablePromotion (tipologia di promozione).
    • title: Cereali cotti al vapore 7 cereali/bulgur&quinoa/farro (denominazione del prodotto).
    • description: 250g confezione (descrizione del formato o peso della confezione del prodotto).
    • discount.title: -10% (etichetta o percentuale dello sconto).
    • articles[0].brand: Kania (brand o marchio commerciale del prodotto).
    • articles[0].description: MISCELA DI CEREALI (descrizione estesa o categoria merceologica).
    • images: array con URL delle immagini del prodotto in varie risoluzioni.
      • [0].url: https://static-coupons.lidlplus.com/images/promotions/IT/fb50bb21-cf12-43ee-9bd1-4549909c81b0.png?t=1761147854
    • channelStore (canale di riscatto dell'offerta).
    • validity: periodo di validità della promozione.
      • start: 2026-06-28T22:00:00Z (timestamp di inizio validità in formato ISO 8601 millisecondi: 28 giugno 2026 22:00:00).
      • end: 2026-07-05T21:59:59.999Z (timestamp di fine validità in formato ISO 8601 millisecondi: 05 luglio 2026 21:59:59).
    • isActivated: false (stato di attivazione manuale del coupon da parte dell'utente prima dell'uso).
    • isRedeemed: false (indica se la promozione è già stata utilizzata/riscattata in cassa).
    • isProcessing: false (indica uno stato transitorio di elaborazione del riscatto della promozione).
    • isSpecial: true (indica se l'offerta rientra tra le promozioni speciali o in evidenza).
    • isSegmented: false (indica se l'offerta è personalizzata/riservata a uno specifico target di utenti).
    • specialPromotion.tag: Per te (etichetta o tag grafico associato alla promozione speciale all'interno dell'app).
    • promotionId: fb50bb21-cf12-43ee-9bd1-4549909c81b0 (identificatore univoco della promozione).
    • id: 019f126c-1767-7076-84e3-7f8f1df924ba (identificativo univoco della specifica istanza di coupon assegnata al client).

    📦 Dettagli dei prodotti

    L'analisi delle informazioni di dettaglio dei prodotti memorizzate nella cache dell'applicazione può fornire indicazioni sui contenuti recuperati durante l'utilizzo di Lidl Plus. Questi dati possono risultare utili per ricostruire il contesto di utilizzo dell'applicazione, ma la loro presenza, da sola, non dimostra necessariamente che il prodotto sia stato esplicitamente consultato, aggiunto al carrello o acquistato.

    SELECT
      cr.entry_ID,
      cr.time_stamp,
      cr.request_key,
      crd.isDataOnFS,
      crd.receiver_data AS "data"
    FROM cfurl_cache_response AS "cr"
    LEFT JOIN cfurl_cache_receiver_data AS "crd" ON (cr.entry_ID = crd.entry_ID)
    WHERE cr.request_key REGEXP 
      "^https?:\/\/productshowcase\.lidlplus\.com\/products\/v\d+\/"
    

    La figura seguente mostra la decodifica del JSON associato al record cfurl_cache_receiver_data.entry_ID=208 mediante DataViewer.

    Il seguente esempio descrive il secondo elemento dell'object result.

    • cr.time_stamp: 2026-06-30 05:09:10 (timestamp della registrazione della risposta di rete nella cache in formato ISO 8601: 30 giugno 2026 05:09:10).
    • title: Filetti di sgombro grigliati (denominazione del prodotto).
    • brandNixe (marchio commerciale del produttore).
    • characteristicsIn olio extra vergine d'oliva (descrizione delle caratteristiche o variante).
    • packaging120g (formato/peso della confezione).
    • price.amount.value2.09 (prezzo di listino).
    • price.type: (simbolo della valuta).
    • price.pricePerUnit: 1kg = 20.10 (prezzo rapportato all'unità di misura).
    • images: array con URL dell'immagine del prodotto in varie risoluzioni.
      • [0].url: https://static-product-catalog.lidlplus.com/images/productdata/IT/original/highres/5585987_6004800_v0_highres.png?im=Resize=(1125))
    • id: 8813307144043_IT (identificatore univoco del prodotto).

    🔍 Termini di ricerca

    Questo artefatto analizza i termini di ricerca digitati dall'utente all'interno dell'app per individuare prodotti nell'elenco o nel catalogo del punto vendita. L'analisi della query di rete consente di ricostruire l'intento di ricerca (search_term), lo store ID di riferimento (IT0786), i risultati restituiti e il numero totale di prodotti identificati (result_count).

    SELECT
      cr.entry_ID,
      cr.time_stamp,
      cr.request_key,
      crd.isDataOnFS,
      crd.receiver_data AS "data"
    FROM cfurl_cache_response AS "cr"
    LEFT JOIN cfurl_cache_receiver_data AS "crd" ON (cr.entry_ID = crd.entry_ID)
    WHERE cr.request_key REGEXP 
      "^https?:\/\/shopping-list\.lidlplus\.com\/api\/v\d+\/[A-Z]{2}" || 
      "\/store\/([A-Z0-9]+)\/search\?q=(.*)$"
    

    La figura seguente mostra il contenuto del file AFAD014F-E10F-4003-8055-4595C9C14C16, decodificato mediante DataViewer.

    Il seguente esempio descrive il secondo elemento dell'array results.

    • cr.time_stamp: 2026-07-06 13:08:09 (timestamp della registrazione della risposta di rete nella cache in formato ISO 8601: 06 luglio 2026 13:08:09).
    • search_term: Chi (termine di ricerca: estratto direttamente dalla richiesta all'endpoint, https://shopping-list.lidlplus.com/api/v4/IT/store/IT0786/search?q=Chi).
    • store_idIT0786 (identificatore del punto vendita: estratto dalla richiesta all'endpoint, https://shopping-list.lidlplus.com/api/v4/IT/store/IT0786/search?q=Chi)
    • result_count: 26 (numero di elementi nell'array results).
    • [1].brand: NULL (brand).
    • [1].title: Chianti DOCG Riserva (nome del prodotto).
    • [1].id: 8808098015083_IT (identificatore univoco del prodotto).

    Il parser lidl_searched_terms() preserva tutte le query intermedie inviate dall'applicazione, applicando una finestra temporale di 6 secondi al solo fine di identificare la digitazione conclusiva di ciascuna sessione. Le richieste inviate entro una pausa inferiore ai 6 secondi vengono collegate all'interno del medesimo flusso di autocompletamento, mentre la query finale prima di una pausa prolungata viene contrassegnata dal flag sequence_final e valorizzata nella colonna del report "Sequence Final" per evidenziare l'intento di ricerca definitivo dell'utente.

    🔍 Ricerca punti vendita

    Questo artefatto analizza le ricerche dei punti vendita effettuate dall'utente all'interno dell'app per individuare i negozi Lidl sul territorio. L'analisi della query di rete e della risposta consente di ricostruire il testo cercato (search_term), le coordinate della richiesta (request_lat, request_lon), i dettagli dei negozi restituiti e il numero totale di risultati identificati.

    SELECT
      cr.entry_ID,
      cr.time_stamp,
      cr.request_key,
      crd.isDataOnFS,
      crd.receiver_data AS "data"
    FROM cfurl_cache_response AS "cr"
    LEFT JOIN cfurl_cache_receiver_data AS "crd" ON (cr.entry_ID = crd.entry_ID)
    WHERE cr.request_key REGEXP 
      "^https?://stores\.lidlplus\.com/api/v\d+/autocomplete/[A-Z]{2}\?(?:[^#]*)$"
    

    La figura seguente mostra la decodifica del file JSON 77AF03B6-EB81-410F-9CA8-56E816FBE9C1 relativo al record cfurl_cache_response.entry_ID=285:

    Il seguente esempio descrive il primo elemento dell'array:

    • cr.time_stamp: 2026-08-05 08:11:07 (timestamp della registrazione della risposta di rete nella cache in formato ISO 8601: 05 agosto 2026 08:11:07).
    • search_term: mila (termine di ricerca: estratto direttamente dalla richiesta all'endpoint, https://stores.lidlplus.com/api/v1/autocomplete/IT?input=mila&language=IT&latitude=41.28467446948064&longitude=13.2357800000001).
    • language: IT (codice ISO 3166-1 alpha-2 del paese estratto dal parametro language nell'URL).
    • latitude: 41.28467446948064 (latitudine della richiesta estratta dalla query: &latitude=41.28467446948064).
    • longitude: 13.2357800000001 (longitudine della richiesta estratta dalla query: &longitude=13.2357800000001).
    • returned_count: 20 (campo derivato che riporta il conteggio totale dei negozi presenti nell'array JSON della risposta).
    • [0].storeKey: IT6037 (identificatore del punto vendita).
    • [0].name: Mola di Bari (BA) (denominazione del punto vendita).
    • [0].address: Viale Unità D'Italia, 7 e 9 (indirizzo del punto vendita).
    • [0].postalCode: 70042 (codice di avviamento postale).
    • [0].locality: Mola di Bari (BA) (località del punto vendita).
    • [0].location: coordinate del punto vendita.
      • latitude: 41.0563 (latitudine).
      • longitude: 17.09914 (longitudine).
    • [0].distance: 324325.4170622185 (distanza in metri dal punto di ricerca).

    📌 Nota: Le coordinate geografiche trasmesse nella query HTTP indicano il centro dell'area di ricerca o della mappa inviata al server, non la posizione GPS fisica garantita del dispositivo.

    💲 Prodotti in offerta

    Questo artefatto analizza le promozioni e i prodotti in offerta settimanale estrapolati dalla cache delle comunicazioni HTTP. Permette di identificare i prodotti promozionati consultati dall'utente, i prezzi base e scontati, le finestre temporali di validità delle offerte e il punto vendita di riferimento.

    SELECT
      cr.entry_ID,
      cr.time_stamp,
      cr.request_key,
      crd.isDataOnFS,
      crd.receiver_data AS "data"
    FROM cfurl_cache_response AS "cr"
    LEFT JOIN cfurl_cache_receiver_data AS "crd" ON (cr.entry_ID = crd.entry_ID)
    WHERE cr.request_key REGEXP 
      "^https?:\/\/offers\.lidlplus\.com\/app\/api\/v\d+\/"
    

    La figura seguente mostra il contenuto del file CB96DC18-FAA9-420E-8AB1-BB8AC8A8E22D, decodificato mediante DataViewer.

    Il seguente esempio descrive il primo elemento dell'array offers.

    • [0].offerType: StoreSpecialPriceDiscount (tipologia di offerta o sconto).
    • [0].title: Melone liscio (denominazione del prodotto).
    • [0].brand: FRUTTA E VERDURA (categoria merceologica del prodotto).
    • [0].packaging: Al kg (formato o unita di misura della confezione).
    • [0].priceBox: dettagli sui prezzi.
      • smallPartNumeric: 3.49 (prezzo originale di listino non scontato) 
      • largePartNumeric: 1.99 (prezzo effettivo in promozione/scontato).
      • discountMessage: -42% (etichetta o percentuale dello sconto).
      • priceSymbol: €/kg (valuta e unita di riferimento del prezzo).
    • [0].pricePerUnit: NULL (prezzo calcolato per unita di misura di base).
    • [0].redemptionChannel: Store (canale di riscatto dell'offerta).
    • [0].imageUrl: https://static-coupons.lidlplus.com/images/promotions/IT/4a523cb6-6f57-4b0b-bd35-bbf9650cdd95.jpg?t=1782221651 (URL dell'immagine promozionale del prodotto).
    • [0].startValidityDate: 2026-06-29T00:00:01+00:00 (timestamp di inizio validità in formato ISO 8601: 29 giugno 2026 00:00:01).
    • [0].endValidityDate: 2026-07-01T23:59:59+00:00 (timestamp di fine validità in formato ISO 8601: 01 luglio 2026 23:59:59).
    • [0].id4a523cb6-6f57-4b0b-bd35-bbf9650cdd95 (identificatore univoco della promozione).

    🎁 Punti Lidl

    Questo artefatto analizza la risposta della cache di rete del programma fedeltà Lidl Plus. Permette di ricostruire il saldo dei punti dell'account, la relativa data di scadenza, lo store ID associato e l'intero catalogo premi. L'analisi evidenzia anche lo stato di ciascun articolo (riscattabile o meno in base al punteggio accumulato), i riferimenti alle immagini promozionali e i file binari salvati nella cache locale.

    SELECT
      cr.entry_ID,
      cr.time_stamp,
      cr.request_key,
      crd.isDataOnFS,
      crd.receiver_data AS "data"
    FROM cfurl_cache_response AS "cr"
    LEFT JOIN cfurl_cache_receiver_data AS "crd" ON (cr.entry_ID = crd.entry_ID)
    WHERE cr.request_key REGEXP 
      "^https?:\/\/mypoints\.lidl\.com\/mobile-bff\/api\/v\d+\/(?:[A-Z]{2}\/)?marketplace\/" 

    La decodifica del payload memorizzato nel file di cache (es. 749A1242-9B9F-4150-A364-25F2C707F7B2) illustra i metadati estratti per il primo elemento dell'array items e per i valori di bilancio globali:

    Il seguente esempio descrive il primo elemento dell'array items.

    • [0].type: LidlPlusCoupon (categoria principale dell'offerta).
    • [0].subtypeFree (sottotipo di sconto: es. Free=Gratis, MonetaryVoucher=Buono sconto).
    • [0].summary: Cordon bleu di tacchino (nome del premio o della promozione).
    • [0].points: 250 (costo in punti richiesto per il riscatto).
    • [0].price: NULL (eventuale quota economica integrativa di listino).
    • [0].discountValue: 100 (valore economico o percentuale dello sconto applicato).
    • [0].discountTitle: NULL (descrizione testuale integrativa dello sconto).
    • [0].stateInsufficientPoints (stato del premio rispetto all'account: es. InsufficientPoints=Punti insufficienti, Available=Riscattabile).
    • [0].isDisabledfalse (indica che il premio è disabilitato nel sistema).
    • [0].isBlockedfalse (indica che il premio è bloccato per l'account).
    • [0].isFavorite: false (indica che il premio è stato contrassegnato come preferito).
    • [0].isExchangedPreviously: false (stato di avvenuta conversione o riscatto precedente).
    • [0].exchangeButtonIsEnabled: false (abilitazione del pulsante di riscatto nell'interfaccia dell'app.).
    • [0].daysUntilExpiration: 7 (giorni residui prima della scadenza del premio).
    • [0].availablePromotions: 0 (numero di promozioni accessibili o correlate).
    • [0].relatedOnlineArticleNumber: NULL (codice identificativo dell'articolo sullo store online).
    • [0].imageUrl: https://mypoints.lidl.com/mobile-bff/api/v1/image/rewards-images/REW0000010226-IT-40a5efb500d6eef073ac2a5d976fa7cd.jpeg (URL dell'immagine promozionale del premio).
    • [0].availableOn: NULL (data a partire dalla quale il premio risulta sbloccabile).
    • [0].id: REW0000010226 (identificatore univoco del premio).
    Valori globali:
    • availablePoints: 118 (saldo attuale dei punti fedeltà accumulati).
    • pointsToExpire: 0 (quantità di punti soggetti a imminente scadenza).
    • nextExpirationDate: NULL (prossima data di scadenza dei punti).
    • total: 254 (numero totale di articoli/coupon censiti nel catalogo del marketplace al momento della richiesta).

    📍 Ultima posizione nota

    Qualsiasi chiamata al sensore di geolocalizzazione gestita dall'applicazione (tramite la libreria SwiftLocation, che si interfaccia con il framework iOS CoreLocation) lascia un'impronta digitale all'interno della sandbox di iOS. I dati di posizione non rimangono confinati nella memoria volatile, ma vengono consolidati nel file system. L'ultima posizione nota e le coordinate correlate sono identificabili analizzando il file:
    <ADC>/Library/Preferences/com.lidl.eci.lidl.plus.plist

    La figura seguente mostra il contenuto del file com.lidl.eci.lidl.plus.plist relativo all'oggetto CLLocation com.swiftlocation.last-gps-location:

    I principali campi presenti nell'oggetto CLLocation, memorizzati sotto la chiave com.swiftlocation.last-gps-location, comprendono:

    • kCLLocationCodingKeyTimestamp: 807287239.154644 (timestamp della rilevazione in Mac Absolute Time: 01 agosto 2026 14:27:19).
    • kCLLocationCodingKeyCoordinateLatitude41.4066605 (latitudine in gradi decimali).
    • kCLLocationCodingKeyCoordinateLongitude13.4594426 (longitudine in gradi decimali).
    • kCLLocationCodingKeyHorizontalAccuracy9378.66798953961 (accuratezza orizzontale in metri).
    • kCLLocationCodingKeyAltitude0 (altitudine in metri).
    • kCLLocationCodingKeyEllipsoidalAltitude: 0 (altezza ellissoidale in metri). 
    • kCLLocationCodingKeyVerticalAccuracy: -1 (accuratezza verticale in metri: -1 indica che il valore non è valido).
    • kCLLocationCodingKeySpeed: 0 (velocità in metri al secondo).
    • kCLLocationCodingKeySpeedAccuracy: -1 (accuratezza della velocità in metri al secondo).
    • kCLLocationCodingKeyCourse: 0 (direzione di movimento rispetto al nord geografico in gradi decimali).
    • kCLLocationCodingKeyCourseAccuracy: -1 (accuratezza della direzione di movimento in gradi decimali).
    • kCLLocationCodingKeyFloor2147483647 (piano dell'edificio: 2147483647 è il valore massimo di int32 e rappresenta "nessun piano").

    Scan&Go 🔓 (Decifrato - Password Hardcoded)

    L'analisi è stata condotta sulla versione 17.4.7 dell'applicazione Lidl Plus per iOS. L'esame del binario Mach-O ARM64 è stato eseguito mediante il framework di reverse engineering Ghidra (v12.1.2), avvalendosi del disassembler e del decompilatore integrati per la ricostruzione del flusso di esecuzione, l'analisi dei riferimenti incrociati (XREF) e l'identificazione delle routine Swift preposte alla gestione dello storage applicativo.

    L'attività investigativa si è concentrata sul database cifrato selfScanning.sqlite,  tracciando la sequenza di inizializzazione fino all'individuazione dei parametri di cifratura adottati dal layer di persistenza dei dati. La ricostruzione delle funzioni coinvolte ha permesso di identificare la seguente catena di invocazione:
    FUN_1026acd84_$s4GRDB8DatabaseC13usePassphraseyySSKFSelfScanningDatabaseFUN_1026ace68 → apertura/accesso a selfScanning.sqlite

    La funzione FUN_1026acd84 costituisce il punto di origine in cui viene assemblato il valore della chiave sotto forma di oggetto Swift.String. Tale valore viene successivamente trasmesso al metodo della libreria GRDB (_$s4GRDB8DatabaseC13usePassphraseyySSKF), responsabile dell'impostazione della passphrase utilizzata per l'accesso al database cifrato.

    Dall'analisi delle istruzioni assembly nell'intorno della routine descritta, si evidenziano le seguenti operazioni sui registri:

    1026acd94  mov x0,#0x3032
    1026acd98  movk x0,#0x694c,LSL #16
    1026acd9c  movk x0,#0x6c64, LSL #32
    1026acda0  movk x0,#0x3632, LSL #48
    1026acda4  mov x1,#-0x1800000000000000
    1026acda8  bl _$s4GRDB8DatabaseC13usePassphraseyySSKF
    

    Ricostruendo il valore a 64 bit memorizzato nel registro x0 tenendo conto della rappresentazione Little-Endian:

    • Valore Esadecimale (Registro x0): 0x36326c64694c3032
    • Sequenza di Byte (Byte Order): 32 30 4c 69  64 6c 32 36
    • Conversione ASCII: 20Lidl26

    L'analisi statica ha dunque evidenziato la presenza di una passphrase hardcoded (20Lidl26), impiegata direttamente dalle routine applicative per l'accesso al database cifrato selfScanning.sqlite.

    La chiave così identificata è stata sottoposta a verifica tecnica mediante il software DB Browser for SQLite / SQLCipher: impostando i parametri compatibili con SQLCipher 4 e inserendo (20Lidl26) come passphrase di decifratura.

    A seguito della configurazione, la struttura dello schema e le tabelle contenute in selfScanning.sqlite sono risultate correttamente leggibili ed esaminabili in chiaro, confermando la correttezza della chiave estratta dal binario.💣

    Il database selfScanning.sqlite conserva gli eventi relativi alla sessione Scan&Go, inclusi i barcode dei prodotti scansionati o rimossi e i relativi timestamp. Il contenuto permette inoltre di ricostruire informazioni associate agli articoli presenti nel carrello e agli eventi di scansione registrati dall'applicazione.

    Rispetto allo scontrino, questi dati offrono una maggiore granularità temporale, consentendo di ricostruire la sequenza cronologica delle operazioni Scan&Go effettuate prima del pagamento.

    Correlati con altri artefatti dell'applicazione, ad esempio ticket, informazioni sul punto vendita e ulteriori dati temporali, tali eventi possono contribuire alla ricostruzione del contesto della sessione di acquisto e, insieme ad altre evidenze, supportare l'ipotesi di presenza del dispositivo presso uno specifico punto vendita.

    🛒 Prodotti nel carrello

    La tabella BasketItemRows, contenuta nel database cifrato selfScanning.sqlite, memorizza lo stato corrente e progressivo degli articoli confermati durante la sessione di Scan&Go.

    Rappresenta il paniere effettivo della spesa prima del saldo in cassa e ne registra la composizione dettagliata: prodotti selezionati, quantità, prezzi applicati, sconti progressivi, restrizioni di catalogo e relative marcature temporali.

    SELECT
      B.rowid,
      M.rowId,
      B.scannedAt,
      B.createdAt,
      B.updatedAt,
      B.status,
      B.scanId,
      B.productId,
      coalesce(B.barcode, M.barcode) AS "barcode",
      coalesce(B.name, M.name) AS "product_name",
      B.quantity,
      coalesce(B.unitPrice, M.unitPrice) AS "unit_price",
      B.subtotal,
      B.totalDiscount,
      B.currency,
      M.deposit,
      M.restrictions
    FROM BasketItemRows AS "B"
    LEFT JOIN MasterDataItemRows AS "M" ON (B.productId = M.id)
    ORDER BY B.scannedAt DESC
    

    Interpretazione dei campi e dei metadati:

    • B.scannedAt:  (timestamp della scansione del codice a barre in Unix Epoch).
    • B.updatedAt:  (timestamp dell'ultima modifica del record in Unix Epoch).
    • B.createdAt:  (timestamp della creazione del record in Unix Epoch).
    • B.status: (stato operativo dell'articolo nel carrello: es. confermato, in attesa di verifica, sospeso).
    • B.scanId: (identificativo univoco dell'evento/sessione di scansione).
    • B.productId:  (identificatore univoco del prodotto).
    • barcode:  (codice a barre (EAN/UPC) del prodotto scansionato).
    • name:  (denominazione del prodotto).
    • B.quantity:  (numero di unità del prodotto attualmente presenti nel carrello).
    • unitPrice:  (prezzo unitario).
    • B.subtotal:  (importo parziale calcolato per il totale delle unità del singolo prodotto).
    • B.totalDiscount:  (ammontare complessivo dello sconto applicato alla voce di carrello).
    • B.currency:  (valuta monetaria della transazione).
    • M.deposit:  (importo di eventuale cauzione/deposito associato al prodotto).
    • M.restrictions:  (eventuali vincoli normativi o commerciali applicati al prodotto: es. vendita riservata ai maggiorenni).

    🗑️ Prodotti rimossi dal carrello

    Durante l'utilizzo della funzionalità Scan&Go all'interno del punto vendita, l'applicazione consente all'utente non solo di aggiungere prodotti scansionandone il codice a barre, ma anche di rimuovere articoli precedentemente inseriti nel carrello virtuale prima di completare il pagamento.

    I dati relativi agli eventi di cancellazione vengono tracciati all'interno del database cifrato selfScanning.sqlite (decifrato nell'analisi forense mediante la password hardcoded estratta dall'applicazione). La tabella di riferimento RemovedItemRows, relazionata con MasterDataItemRows, memorizza gli articoli scartati e i relativi dettagli di catalogo, consentendo di ricostruire il comportamento di scelta e ripensamento dell'utente durante la spesa.

    SELECT
      R.rowId,
      M.rowId,
      R.removedAt,
      M.id,
      coalesce(R.barcode, M.barcode) AS "barcode",
      M.name,
      R.quantity,
      coalesce(R.unitPrice, M.unitPrice) AS "unit_price",
      R.totalPrice,
      R.savings,
      R.vatAmount,
      R.weight,
      R.itemNr
    FROM RemovedItemRows AS "R"
    LEFT JOIN MasterDataItemRows AS "M" ON (R.barcode = M.barcode)
    ORDER BY R.removedAt DESC 

    Interpretazione dei campi e dei metadati:

    • R.removedAt:  (timestamp della rimozione dal carello del prodotto in Unix Epoch).
    • M.id:  (identificatore univoco del prodotto all'interno del catalogo MasterDataItemRows).
    • barcode:  (codice a barre (EAN/UPC) del prodotto rimosso).
    • M.name:  (denominazione del prodotto).
    • quantity:  (numero di unità del prodotto eliminate).
    • unitPrice:  (prezzo unitario).
    • R.totalPrice:  (importo complessivo dell'articolo rimosso calcolato al momento dell'eliminazione).
    • R.savings:  (ammontare dell'eventuale sconto o promozione applicata che è stata stornata con la rimozione).
    • R.vatAmount:  (quota di imposta (IVA) associata al prodotto rimosso).
    • R.weight:  (peso del prodotto).
    • R.itemNr:  (numero posizionale assegnato durante la sessione).

    📲 Cronologia della spesa

    La funzionalità Scan&Go registra in tempo reale la sequenza di scansione dei codici a barre effettuata dall'utente mentre si sposta tra le corsie del punto vendita.

    I dati relativi al percorso d'acquisto (scanning journey) vengono memorizzati all'interno del database cifrato selfScanning.sqlite. La tabella ScanningJourneyRows, relazionata con MasterDataItemRows, traccia ogni singolo evento di scansione dei prodotti, la quantità selezionata e le informazioni di catalogo abbinate, consentendo la ricostruzione cronologica e fisica della spesa.

    SELECT
      S.rowId,
      M.rowId,
      S.scannedAt,
      M.id,
      coalesce(S.barcode, M.barcode) AS "barcode",
      M.name,
      S.quantity,
      M.unitPrice,
      M.deposit
    FROM ScanningJourneyRows AS "S"
    LEFT JOIN MasterDataItemRows AS "M" ON (S.barcode = M.barcode)
    ORDER BY S.scannedAt DESC 

    Interpretazione dei campi e dei metadati:

    • S.scannedAt:  (timestamp della scansione del codice a barre in Unix Epoch).
    • M.id:  (identificatore univoco del prodotto all'interno del catalogo MasterDataItemRows).
    • barcode:  (codice a barre (EAN/UPC) del prodotto scansionato).
    • M.name:  (denominazione del prodotto).
    • S.quantity:  (numero di unità del prodotto eliminate).
    • S.unitPrice:  (prezzo unitario).
    • M.deposit: (importo di eventuale cauzione/deposito associato al prodotto: es. vuoti a rendere).

    Spesa online 🔓 (Decifrato - Password da Keychain)

    L’analisi della versione 17.4.7 di Lidl Plus per iOS ha interessato anche il database cifrato groceryPickup.sqlite, utilizzato dall’applicazione per la gestione delle informazioni relative al carrello e alle operazioni di validazione della funzionalità Spesa online.

    A differenza di selfScanning.sqlite, la passphrase non è codificata staticamente nel binario. L’applicazione genera 32 byte casuali mediante SecRandomCopyBytes, li converte in una stringa Base64 e salva il valore nel Keychain:

    • Account (acct): database_key
    • Service (svce): com.lidl.eci.lidl.plus
    • Access Group (agrp): P593BEJ5Y8.com.lidl.eci.lidl.plus (TEAMID.bundleID) 

    Durante l’apertura del database, il contenuto del Keychain viene letto tramite SecItemCopyMatching, interpretato direttamente come stringa UTF-8 e passato al metodo GRDB.Database.usePassphrase, senza decodifica Base64 o ulteriori trasformazioni.

    L’analisi dei framework GRDB-dynamic e SQLCipher ha consentito di ricostruire i parametri crittografici effettivamente utilizzati:

    AES-256-CBC
    Page size: 4096 byte
    PBKDF2-HMAC-SHA512
    KDF iterations: 256000
    HMAC-SHA512
    HMAC salt mask: 0x3A
    

    La passphrase estratta dal Keychain ha permesso di decifrare correttamente groceryPickup.sqlite. 💣

    🧺 Carrello per il pickup

    La funzionalità di Spesa online consente all'utente di selezionare i prodotti all'interno dell'applicazione per gestirne l'acquisto remoto, propedeutico al successivo ritiro presso il punto vendita (click & collect / pickup) o alla consegna a domicilio (delivery), a seconda delle modalità previste dal servizio locale.

    I dati relativi agli articoli inseriti nel carrello e allo stato della spesa vengono memorizzati localmente all'interno del database cifrato groceryPickup.sqlite. Durante l'analisi forense, il database è stato completamente decifrato impiegando la passphrase estratta direttamente dal Keychain di iOS.

    All'interno dell'artefatto, la tabella principale cart tiene traccia della composizione del carrello virtuale, registrando i prodotti selezionati, le quantità, i prezzi e la cronologia delle modifiche apportate dall'utente.

    SELECT
      C.rowId,
      R.rowid,
      C.updatedAt,
      C.createdAt,
      C.productId,
      C.name,
      C.subtitle,
      C.imageUrl,
      C.quantity,
      C.maxQuantity,
      C.hasSubstitutions,
      C.isAvailable,
      C.validations,
      C.unitaryAmount,
      C.depositAmount,
      C.depositName,
      C.discountAmount,
      C.originalAmount,
      C.totalAmount,
      C.currency,
      C.weightUnit,
      C.weightUnitaryValue,
      C.weightUnitaryPrice,
      R.isAvailable AS "restr_is_available",
      R.maxQuantity AS "restr_max_quantity",
      R.updatedAt
    FROM cart AS "C"
    LEFT JOIN productRestrictions AS "R" ON (C.productId = R.productId)
    ORDER BY C.updatedAt DESC
    

    A differenza dei dati di tracciamento in negozio (come le funzionalità legate a Scan&Go), questo repository non attesta gli spostamenti fisici dell'utente nel punto vendita, ma consente di provarne la condotta digitale. L'esame delle marcature temporali di creazione createdAt e modifica updatedAt permette di ricostruire le intenzioni d'acquisto e di collocare l'interazione dell'account applicativo all'interno di una precisa finestra temporale.

    📌 Nota: L'analisi della tabella cart e della relazione con productRestrictions riveste carattere sperimentale. Poiché il database estratto risulta privo di record interni, non è stato possibile riscontrare sul campo l'intera casistica di valori e strutture dati generati durante una reale sessione d'acquisto.

    📌 Nota: Nonostante l'assenza di dati nel campione analizzato, viene fornito il modulo dedicato lidl_grocery_pickup_cart per iLEAPP. Realizzato sulla base della query SQL individuata, il plugin garantisce l'immediata estrazione, decodifica e correlazione temporale dei prodotti qualora vengano analizzati database contenenti carrelli popolati.

    👤 Account 🔓 (Estratto da Keychain)

    Una ricerca approfondita delle informazioni sul profilo dell'utente ha permesso di ricostruire l'identità dell'account analizzando i dati estratti ed elaborati direttamente dal Keychain di iOS.

    L'elemento di interesse è stato individuato applicando i seguenti filtri sui metadati del Keychain:

    • Account (acct): storedUser
    • Service (svce): LidlPlus
    • Access Group (agrp): P593BEJ5Y8.com.lidl.eci.lidl.plus (TEAMID.bundleID) 

    Decodificando il carico utile (plist serializzato tramite NSKeyedArchiver) contenuto nel parametro v_Data dell'item del Keychain, è possibile estrarre i dettagli in chiaro relativi all'utente registrato:

    • sub: 39009693263850xxx (identificativo univoco dell'utente all'interno del sistema di autenticazione).
    • name: Django (nome dell'intestatario dell'account).
    • middle_name: Faiola (secondo nome o cognome riportato nel profilo dell'utente).
    • email: django.f@xxx.it (indirizzo Email associato all'account e utilizzato per l'autenticazione).
    • phone_number: +393494955xxx (numero di telefono completo registrato nel profilo per la verifica/OTP).
    • phone_prefix_number: +39 (prefisso telefonico internazionale associato al numero di cellulare).
    • birthdate: 19xx-xx-11 22:00:00 (data e ora di nascita dell'utente memorizzata nel profilo espressa in UTC).
    • amr: pwd (Authentication Methods References: indica il metodo di autenticazione utilizzato, in questo caso tramite password).

    📌 Nota: Il timestamp della data di nascita è serializzato in formato UTC espresso come 19xx-xx-11 22:00:00 UTC. Applicando l'offset del fuso orario locale di registrazione (UTC+2 per l'Italia in orario estivo), il valore corrisponde esattamente a 19xx-xx-12 00:00:00, ovvero all'effettiva data anagrafica di nascita del titolare dell'account.

    iLEAPP/LAVA 💖

    Come di consueto, a supporto del progetto iLEAPP (iOS Logs, Events, And Plists Parser) di Alexis Brignoni ho creato il plugin lidl_plus.py che alla data odierna è in “pull request” e comunque disponibile sul mio GitHub.

    Di seguito alcuni "screenshot" del report in formato LAVA (LEAPP Artifact Viewer App) di Lidl Plus:


    0 comments:

    Posta un commento