Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ The following data can be extracted:
| The output of the dumpsys shell command, providing diagnostic information about the device. | | `dumpsys.txt` |
| A list of all packages installed and related distribution files. | | `packages.json` |
| Copy of all installed APKs or of only those not marked as system apps. | ✅ | `apks/*` |
| Intrusion Logging logs. Contains private data such as navigation history. | ✅ | `intrusion_logs/*` |
| A list of files on the system. | | `files.json` |
| A copy of the files available in temp folders. | | `tmp/*` |
| A bug report containing system and app-specific logs, with no private data included. | | `bugreport.zip` |


### About optional data collection

#### Backup
Expand Down Expand Up @@ -116,6 +118,20 @@ Would you like to download copies of all apps or only non-system ones?
| Only non-system packages | Don't download any packages listed in `adb pm list packages -s` |
| Do not download any | Don't download any packages |

### Intrusion Logs

```
Would you like to take the Intrusion Logs of the device?

? Intrusion Logs:
▸ Yes
No
```

| Option | Explanation |
|--------|-------------|
| Yes | Intrusion Logs will be retrieved from the phone. |
| No | Intrusion Logs acquisition is skipped. |

## Encryption & Potential Threats

Expand Down
21 changes: 20 additions & 1 deletion adb/adb.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,21 @@ func (a *ADB) Bugreport() error {
return err
}

// IL prompts the user to download Intrusion Logs
func (a *ADB) IL() error {
// adb shell am start -n com.google.android.gms/.intrusiondetection.ui.retrieval.IntrusionDetectionRetrievalActivity
cmd, err := a.Shell(
"am",
"start",
"-n",
"com.google.android.gms/.intrusiondetection.ui.retrieval.IntrusionDetectionRetrievalActivity",
)
if err != nil {
return fmt.Errorf("failed to start IL activity: %v: %s", err, cmd)
}
return nil
}

// check if file exists
func (a *ADB) FileExists(path string) (bool, error) {
out, err := a.Shell("[", "-f", path, "] || echo 1")
Expand All @@ -188,8 +203,12 @@ func (a *ADB) FileExists(path string) (bool, error) {
// List files in a folder using ls, returns array of strings.
func (a *ADB) ListFiles(remotePath string, recursive bool) ([]string, error) {
var remoteFiles []string

// Quote remotePath so files with spaces on their name work
qPath := fmt.Sprintf("'%s'", remotePath)

if recursive {
out, _ := a.Shell("find", remotePath, "2>", "/dev/null")
out, _ := a.Shell("find", qPath, "2>", "/dev/null")
if out != "" {
tmpFiles := strings.Split(out, "\n")
for _, file := range tmpFiles {
Expand Down
293 changes: 293 additions & 0 deletions modules/intrusion_logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
// androidqf - Android Quick Forensics
// Copyright (c) 2021-2026 Claudio Guarnieri.
// Use of this software is governed by the MVT License 1.1 that can be found at
// https://license.mvt.re/1.1/

package modules

import (
"context"
"fmt"
"os"
"os/signal"
"path/filepath"
"strings"
"time"

"github.com/manifoldco/promptui"
"github.com/mvt-project/androidqf/acquisition"
"github.com/mvt-project/androidqf/adb"
"github.com/mvt-project/androidqf/log"
)

const (
acquireIL = "Yes"
skipIL = "No"
)

type IL struct {
StoragePath string
ILPath string
DirOnDevice string
}

func NewIL() *IL {
return &IL{
DirOnDevice: "/sdcard/Download/Intrusion Logging/",
}
}

func (m *IL) Name() string {
return "intrusion_logs"
}

func (m *IL) InitStorage(storagePath string) error {
m.StoragePath = storagePath
m.ILPath = filepath.Join(storagePath, "intrusion_logs")

// Only create directory in traditional mode
if storagePath != "" {
err := os.Mkdir(m.ILPath, 0o755)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("failed to create Intrusion Logging folder: %v", err)
}
}

return nil
}

func (m *IL) Run(acq *acquisition.Acquisition, fast bool) error {
// Check whether the device supports AAPM.
compatible, err := m.isAAPMCompatibleDevice()
if err != nil {
// Don't break acquisition if the check fails, just log and skip.
log.Debugf("Failed to check AAPM compatibility: %v", err)
return nil
}

// TODO: Investigate whether IL data could exist on a non-compatible device
// (for example, restored or migrated from another device on the same Google account).
// If so, skipping here might miss existing data.
if !compatible {
log.Info("Device is not AAPM-compatible, skipping Intrusion Logging acquisition.")
return nil
}

// Ask user first
log.Info("Would you like to download Intrusion Logs from the device?")
promptIL := promptui.Select{
Label: "Intrusion Logs",
Items: []string{acquireIL, skipIL},
}

_, ILOption, err := promptIL.Run()
if err != nil {
return fmt.Errorf("failed to make selection for IL option: %v", err)
}

// User declined so we continue acquisition normally
if ILOption == skipIL {
log.Info("Skipping Intrusion Logging extraction...")
return nil
}

// Check whether AAPM is enabled right now. If disabled, don't start the activity
// or wait for a new file just pull whatever is already present.
// We still proceed with acquisition because older IL files may remain on disk
// and should be collected with user consent.
aapmEnabled, err := m.isAAPMEnabled()
if err != nil {
log.Debugf("Failed to check AAPM enabled state: %v", err)
aapmEnabled = false
}

if aapmEnabled {
// Snapshot of Intrusion Logs folder before triggering new log download
before, err := m.listDirSet(m.DirOnDevice)

if err != nil {
log.Errorf("IL: failed to list %s: %v", m.DirOnDevice, err)
return nil
}

// Start the Activity to prompt the user to download a new Intrusion Log
if err := adb.Client.IL(); err != nil {
log.Errorf("Failed to launch intrusion detection activity: %v\n", err)
// Still allow pulling existing files if user wants; continue anyway.
}

log.Info("Launched the Intrusion Logging settings page.")
log.Info("On the device: scroll down, tap 'Access Logs', then press 'Download and Decrypt' for each listed device.\n")

log.Info("Waiting for intrusion logs to be written to device. (Ctrl+C to skip waiting and continue acquisition)...")
// Watch directory (Ctrl+C cancels watch but continues acquisition)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

// Pulls every 2 seconds. Stops on Ctrl+C or after 15 minutes.
watchErr := m.waitForNewFiles(ctx, m.DirOnDevice, before, 2*time.Second, 15*time.Minute)
if watchErr != nil {
// If user Ctrl+C, context is canceled and acquisition continues
log.Info("Stopped waiting, continuing with acquisition...")
}
} else {
log.Debug("AAPM is disabled, skipping activity launch and new file watcher (pulling existing files only).")
}

// Pull all files (old + new)
files, err := adb.Client.ListFiles(m.DirOnDevice, true)
if err != nil {
log.Errorf("IL: failed to list files for pull in %s: %v", m.DirOnDevice, err)
return nil
}
if len(files) == 0 {
log.Info("No files found in " + m.DirOnDevice)
return nil
}

if err := m.pullAll(acq, files); err != nil {
log.Errorf("IL: failed pulling IL files: %v", err)
// continue acquisition
return nil
}
log.Infof("Downloaded %d Instrusion Logging files from the phone.", len(files))
log.Info("Intrusion Logging acquisition is completed; continuing with acquisition ...")
return nil
}

func (m *IL) isAAPMCompatibleDevice() (bool, error) {
// adb shell settings get secure advanced_protection_mode
out, err := adb.Client.Shell("settings", "get", "secure", "advanced_protection_mode")
if err != nil {
return false, err
}

val := strings.TrimSpace(out)
// If the key does not exist, Android prints "null". We infer this is not compatible.
if strings.EqualFold(val, "null") || val == "" {
return false, nil
}

// If it's compatible, it should be "0" or "1" (treat anything non-null as compatible)
return true, nil
}

func (m *IL) isAAPMEnabled() (bool, error) {
// adb shell settings get secure advanced_protection_mode
out, err := adb.Client.Shell("settings", "get", "secure", "advanced_protection_mode")
if err != nil {
return false, err
}

val := strings.TrimSpace(out)

// If the key is missing Android returns "null"
if strings.EqualFold(val, "null") || val == "" {
return false, nil
}

// AAPM is enabled only when the value is exactly "1"
return val == "1", nil
}

func (m *IL) listDirSet(dir string) (map[string]struct{}, error) {
files, err := adb.Client.ListFiles(dir, true)
if err != nil {
return nil, err
}
log.Debugf("IL: Polling found %d intrusion logging files on device at '%s'", len(files), dir)
set := make(map[string]struct{}, len(files))
for _, f := range files {
set[f] = struct{}{}
}
return set, nil
}

// Watch for new files until Ctrl+C or timeout.
func (m *IL) waitForNewFiles(
ctx context.Context,
dir string,
before map[string]struct{},
pollEvery time.Duration,
maxWait time.Duration,
) error {
ticker := time.NewTicker(pollEvery)
timeout := time.NewTimer(maxWait)

defer ticker.Stop()
defer timeout.Stop()

for {
select {
case <-ctx.Done():
// Ctrl+C => continue acquisition (non-fatal)
log.Info("Ctrl+C detected. Continuing acquisition...")
return nil

case <-timeout.C:
log.Info("Finished waiting for intrusion logs (15 minute timeout reached).")
return nil

case <-ticker.C:
now, err := m.listDirSet(dir)
if err != nil {
log.Debugf("IL: poll list failed: %v", err)
continue
}

for f := range now {
if _, existed := before[f]; !existed {
log.Infof(
"Detected new file: %s.\nIf you finished downloading logs, press Ctrl+C to continue acquisition.",
f,
)
before[f] = struct{}{}
}
}
}
}
}

func (m *IL) pullAll(acq *acquisition.Acquisition, deviceFiles []string) error {
for _, file := range deviceFiles {
if file == m.DirOnDevice {
continue
}

rel := strings.TrimPrefix(file, m.DirOnDevice)
rel = strings.TrimPrefix(rel, "/") // optional safety if DirOnDevice lacks trailing /

if acq.StreamingMode && acq.EncryptedWriter != nil {
zipPath := fmt.Sprintf("intrusion_logs/%s", rel)

writer, err := acq.EncryptedWriter.CreateFile(zipPath)
if err != nil {
log.Errorf("Failed to create zip entry for IL file %s: %v\n", file, err)
continue
}

err = acq.StreamingPuller.PullToWriter(file, writer)
if err != nil {
log.Errorf("Failed to stream IL file %s: %v\n", file, err)
continue
}

log.Debugf("Streamed IL file %s directly to encrypted archive as %s", file, zipPath)
} else {
destPath := filepath.Join(m.ILPath, rel)

if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
log.Errorf("Failed to create folders for IL file %s: %v\n", destPath, err)
continue
}

out, err := adb.Client.Pull(file, destPath)
if err != nil {
log.Errorf("Failed to pull IL file %s: %s\n", file, strings.TrimSpace(out))
continue
}
}
}

return nil
}
1 change: 1 addition & 0 deletions modules/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Module interface {
func List() []Module {
return []Module{
NewBackup(),
NewIL(),
NewPackages(),
NewGetProp(),
NewDumpsys(),
Expand Down
Loading