Your Duolingo Is Still Talking to ByteDance: How Pangle Fingerprints You Across Apps After You Said No
This is Part 2 of my Pangle SDK research. Part 1 covered how I broke the encryption. This post covers what I found when I started comparing the decrypted data across apps.
In Part 1, I decrypted 694 Pangle SDK payloads and documented what ByteDance collects: battery level, storage capacity, IP address, boot timestamps, and detailed hardware profiles from 40+ apps. The encryption turned out to be the cryptographic equivalent of taping the key to the doorframe.
But decrypting the data was only half the story. The real question was: what happens when you compare the same user's data across different apps?
The Setup
Apple's App Tracking Transparency (ATT) framework gives users a simple choice. When an app asks "Allow this app to track your activity across other companies' apps and websites?", you can say no. When you do, the app gets a zeroed-out IDFA (Apple's advertising identifier). The app is not supposed to be able to link your activity across other apps. That is the entire point of ATT.
I wanted to test whether the Pangle SDK respects this.
Using my decrypted dataset of 874 Pangle events across 26 apps and 32 devices, I looked for cases where the same device appeared in two or more unrelated apps with ATT denied.
What ATT Denial Looks Like in Pangle's Data
When a user denies tracking, Pangle's request payload shows the right signals on the surface:
{
"atts": 2,
"idfa": "00000000-0000-0000-0000-000000000000",
"idfv": "[redacted]",
"lmt": 1
}
atts: 2 means "denied." The IDFA is zeroed. Limit Ad Tracking is on. The IDFV is a per-developer identifier that changes across apps from different developers. So far, Apple's rules appear to be followed.
But scroll down in the same payload.
The Fingerprint That Survives ATT
Every Pangle request, regardless of ATT status, also includes:
{
"device": {
"device_model": "iPhone15,4",
"device_type": "D37AP",
"total_mem": "6108520448",
"total_space": "128241598464",
"cpu_num": 6,
"screen_height": 852,
"screen_width": 393,
"boot": "1770482937261",
"power_on_time": "84291553",
"battery_remaining_pct": 50,
"volume": 100,
"low_power_mode": 0,
"darkmode": 0,
"font_size": 17,
"sys_build_version": "23C71",
"ip": "192.168.x.x",
"carrier_name": "--"
}
}
That total_space value, 128241598464 bytes, is not a round number. It is the exact byte count reported by the device filesystem. It varies based on OS version, provisioning, and device history. total_mem is similarly exact. boot is the precise millisecond the device last started up.
None of these fields are needed to serve an ad.
Cross-App Matching
I found three devices that appeared in two or more unrelated apps with ATT denied. In every case, the hardware fingerprint fields were identical across apps while the IDFV (which is supposed to be the only cross-referenceable identifier) was different.
Device 1: Triple Master and MealOrbit Weekly
Two completely unrelated apps. Different developers. Different IDFVs. ATT denied in both.
| Triple Master | MealOrbit Weekly | |
|---|---|---|
| ATT | Denied | Denied |
| IDFA | zeroed | zeroed |
| IDFV | F7C2A91E... | 3B8D6F42... |
| Model | iPhone15,4 | iPhone15,4 |
| RAM | 6108520448 | 6108520448 |
| Storage | 128241598464 | 128241598464 |
| Screen | 393x852 | 393x852 |
| CPU | 6 cores | 6 cores |
| Font size | 17 | 17 |
| Build | 23C71 | 23C71 |
| Device type | D37AP | D37AP |
Every hardware field matches. ByteDance receives two separate requests from two separate apps with two separate IDFVs, but the device fingerprint is identical. A server-side join on (total_space, total_mem, model, build_version) links them instantly.
Device 2: QR Reader and ESM Timer
Same pattern. Two Japanese utility apps. Different developers. Different IDFVs. ATT denied in both.
| QR Reader | ESM Timer | |
|---|---|---|
| ATT | Denied | Denied |
| IDFA | zeroed | zeroed |
| IDFV | A1D47B3E... | 82E6C5F1... |
| Model | iPhone18,1 | iPhone18,1 |
| RAM | 12418932736 | 12418932736 |
| Storage | 256109264896 | 256109264896 |
| Screen | 402x874 | 402x874 |
| Font size | 17 | 17 |
| Build | 23D127 | 23D127 |
| Device type | V53AP | V53AP |
Identical fingerprint. Different IDFVs. The user said no to tracking in both apps. ByteDance has what it needs to link them anyway.
Device 3: The Worst Case
This one used three apps with Pangle. ATT was denied in two of them. But in the third app, the user authorized tracking, giving Pangle the real IDFA.
| Water Sort Puzzle | Trivia Spin | Find Differences | |
|---|---|---|---|
| ATT | Denied | Denied | Authorized |
| IDFA | zeroed | zeroed | [real IDFA] |
| IDFV | C4A83D16... | 5B2E7A93... | D91C48E7... |
| RAM | (empty) | 6042918912 | 6042918912 |
| Storage | (empty) | 256387244032 | 256387244032 |
| Model | (empty) | iPhone15,2 | iPhone15,2 |
| Build | (empty) | 23C55 | 23C55 |
This is the most damaging case. ByteDance receives the real IDFA from Find Differences alongside the hardware fingerprint. It receives the same hardware fingerprint from Trivia Spin, where ATT was denied. A trivial server-side join connects the denied user to their real advertising ID.
The user explicitly denied tracking in Trivia Spin. ByteDance has the data to ignore that decision.
How Unique Is the Fingerprint?
The total_space field alone is highly distinctive. It is not "256 GB" rounded. It is 256387244032 bytes or 128241598464 bytes, exact values that depend on OS version, provisioning, and device history. Across 32 devices in my dataset, I did not observe a single duplicate total_space value.
Combined with total_mem, device_model, sys_build_version, and font_size, the fingerprint entropy is more than sufficient to uniquely identify a device among millions.
And that is before considering the dynamic signals that Pangle also collects on every request: boot timestamp (exact millisecond), battery percentage, volume level, dark mode, low power mode, and local IP. Two requests from different apps made within the same minute would share nearly all of these dynamic values, providing additional correlation signals.
What Apple's Rules Say
Apple's App Store Review Guidelines, Section 5.1.2(i):
Apps must not derive data from a device for the purpose of uniquely identifying it.
Apple's Developer Program License Agreement, Section 3.3.9 prohibits sending device data to third parties via analytics or advertising SDKs when the user has opted out.
There is no carve-out that says "you can collect enough signals to fingerprint the device as long as you don't technically call it an identifier."
What I Cannot Prove
I can prove that Pangle collects hardware fingerprint signals that are identical across apps on the same device, even when ATT is denied. I can prove these signals are sufficient to uniquely identify a device.
I cannot prove from network traffic alone that ByteDance actually performs the server-side correlation. The data enables fingerprinting. Whether ByteDance acts on it requires access to their server-side systems.
However, the collection of total_space (exact byte count of device storage) and boot (exact millisecond timestamp) has no legitimate ad serving purpose. These fields are not needed to select, render, or measure an ad. Their only plausible use is device identification.
The Numbers
| Metric | Value |
|---|---|
| Total devices with Pangle traffic | 32 |
| Total apps using Pangle | 26 |
| Total decrypted events | 874 |
| Events where user denied tracking | 240 (27.5%) |
| Events with zeroed IDFA | 231 |
| Devices with cross-app Pangle traffic | 5 |
| Devices with cross-app + ATT denied + matching fingerprints | 3 |
27.5% of all Pangle events came from users who explicitly said "Ask App Not to Track." In every case, Pangle collected the full hardware fingerprint anyway.
Notable Apps Affected
Among the apps transmitting Pangle fingerprint data while ATT was denied:
- Letterboxd (movie tracking, 57 denied events)
- Webtoon (comics, 8 denied events)
- Manga Plus by Shueisha (manga, 4 denied events)
- Sleep If U Can (alarm, 51 denied events)
- Cat Hole Puzzle (game, 10 denied events)
- Happy Paint (game, 8 denied events)
- iCookie Merge (game, 6 denied events)
- Water Sort Puzzle (game, 6 denied events)
- Nordcurrent Canteen (cooking game, 4 denied events)
These apps span entertainment, utilities, and gaming. Users downloading a movie tracker or a manga reader are unlikely to know that ByteDance receives their exact storage capacity and boot timestamp on every ad request.
What This Means
ATT gives users a sense of control. You see the prompt. You tap "Ask App Not to Track." You assume the app listened.
But ATT only restricts the IDFA. It does not restrict the dozens of hardware and system signals that the Pangle SDK collects in every single request. And the combination of those signals is enough to do exactly what ATT was supposed to prevent.
ByteDance zeroes the IDFA when you say no. They follow the letter of the rule. But they collect everything they need to reconstruct your identity without it.
Methodology
Analysis was conducted on decrypted Pangle SDK payloads using the methodology described in Part 1. No systems were accessed without authorization. No individual user data is disclosed. Device identifiers (IDFVs, IDFAs, IPs) shown in this post are redacted or truncated. All traffic was observed from consented participants in a research study.
Pangle SDK versions observed range from 6.2.0.7 through 7.8.0.8. The behavior described is consistent across all observed versions.