privacy-tech-lab / gpc-android

Code and dynamic analysis scripts for GPC on Android
https://privacytechlab.org/
MIT License
4 stars 1 forks source link

Find approach how users can opt out from selling on mobile #10

Closed SebastianZimmeck closed 4 years ago

SebastianZimmeck commented 4 years ago

As discussed today, let's try to find out whether it is possible to send a do-not-sell header from an iOS app to a service.

The situation is similar as for the browser extension. However, because there is no way to inject a header from our app into another app (at least not without jailbreaking the phone), the idea is to have a curated list of the top 100 apps and send do-not-sell headers to their server URLs that we identified beforehand.

In order to identify the headers, @rgoldstein01 will look into intercepting app traffic via Fiddler. Fiddler Everywhere works on the Mac, but does not yet have all features of the Windows version. But maybe that is not necessary for our purposes. Here are the instructions how to decrypt HTTPS traffic. I would say, give it a try as well, @rgoldstein01. It will give us some greater insight.

(cc'ing @davebaraka, @kalicki1, and @pakaelbling)

rgoldstein01 commented 4 years ago

OK, so I have begun by testing Fiddler on several of the popular apps on the list, and then also on two apps that I have the actual source code for, to see how the calls are looking in the app itself.

Here are the results from some of the API calls:

ZOOM: (*UPON CONNECTION)

CONNECT zoom.us:443 HTTP/1.1 Host: zoom.us User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1 Connection: keep-alive Connection: keep-alive

CONNECT mpapis.zoom.us:443 HTTP/1.1 Host: mpapis.zoom.us:443 User-Agent: Mozilla/5.0 (ZOOM.iPhone) Connection: Keep-Alive

(UPON CLICKING BUTTON)

CONNECT zoomdv90mmr.zoom.us:443 HTTP/1.1 Host: zoomdv90mmr.zoom.us:443 User-Agent: Mozilla/5.0 Connection: Keep-Alive

NewsBreak:

(**UPON CONNECTION***) CONNECT api.particlenews.com:443 HTTP/1.1 Host: api.particlenews.com User-Agent: NewsBreak/3.5.6 (iPhone; iOS 13.3.1; Scale/2.00) Connection: keep-alive Connection: keep-alive

(UPON LIKING POST) CONNECT log.particlenews.com:443 HTTP/1.1 Host: log.particlenews.com User-Agent: NewsBreak/3.5.6.7144 CFNetwork/1121.2.2 Darwin/19.3.0 Connection: keep-alive Connection: keep-alive

FIREFOX:

CONNECT profile.accounts.firefox.com:443 HTTP/1.1 Host: profile.accounts.firefox.com User-Agent: Firefox-iOS-FxA/23.0b17297 (iPhone; iPhone OS 13.3.1) (Firefox) Connection: keep-alive Connection: keep-alive

OURCAMPUS:

CONNECT firebasestorage.googleapis.com:443 HTTP/1.1 Host: firebasestorage.googleapis.com User-Agent: me.OurCampus/4.48 iPhone/13.3.1 hw/iPhone10_1 Connection: keep-alive Connection: keep-alive

So, these are the kind of headers we will be looking to send. The next thing I need to consider is how to make these header calls specifically. I am currently getting acquainted with how to make these header requests. Following https://learnappmaking.com/urlsession-swift-networking-how-to/

I am making basic header requests from a simple iOS app I have created and seeing what the header requests that show up on Fiddler are. The next step is I need to figure out how to add another line to the header request (which will be the do not track line). I think to do this I will need to look more into the nature of header request to figure this out.

The other problem i see happening is whether we have access to send our http requests to specific "hosts", as they might expect a specific key that comes from their own ios source code, rather than our app. But, we will come across that issue later.

SebastianZimmeck commented 4 years ago

The next thing I need to consider is how to make these header calls specifically.

There are also custom HTTP headers, for example, described here for response headers, but maybe there are also custom headers for request headers. Not sure if such custom headers are helpful for our purposes.

Also, one point to look into may be Firefox for iOS. Firefox allows users to send a DNT signal (I briefly looked at the source code and it seems the Firefox iOS app also allows it). Essentially, we want to send a very similar Do-Not-Sell (DNS) signal. Maybe, you can see from the source code how they are doing it and observe the traffic as well.

The other problem i see happening is whether we have access to send our http requests to specific "hosts", as they might expect a specific key that comes from their own ios source code, rather than our app. But, we will come across that issue later.

Absolutely, that is a very good point. And maybe the outcome may be that sending a header is not the best way to go about Do-Not-Sell on mobile, but rather a Do-Not-Sell registry as we discussed earlier is better. However, there is also the possibility that over time a standard will develop what this header should contain and what a company can request. All in all, there are many moving parts here.

(cc'ing @CarlG0123 as well)

rgoldstein01 commented 4 years ago

I am not sure if there is something I am missing here, but when I try and send a regular HTTP request to a specific host, I am just getting en error (my guess is because the endpoint is exepcting a specific request with a special key that comes from the source code. I am currently looking through the Firefox app to see if I can find the actual requests they make to see if I can mimick those in anyway.

This is my error: 2020-03-23 14:08:00.213507-0400 DoNotTrackMe![82569:6335063] Task <A638F288-6E8C-4034-B143-FAFA8356805C>.<1> finished with error [-1002] Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={NSUnderlyingError=0x600000feab80 {Error Domain=kCFErrorDomainCFNetwork Code=-1002 "(null)"}, NSErrorFailingURLStringKey=profile.accounts.firefox.com, NSErrorFailingURLKey=profile.accounts.firefox.com, NSLocalizedDescription=unsupported URL}

SebastianZimmeck commented 4 years ago

Yes, maybe they are expecting something being different in your request. Where do you see that error? In Fiddler?

"Referer spoofing", "HTTP spoofing", "header spoofing" are the relevant topics. E.g., here is a stackoverflow post. (Did not lock at it in detail, but that is the direction you can dig.)

rgoldstein01 commented 4 years ago

I've been looking through different spoofing threads and it does not seem translatable. The reason is that the spoofing comes from intercepting the network call from the webapp before the server. IN the case of our mobile apps, there is no place to intercept these calls, as these calls are not being made in a browser (rather, their own native apps).

As far as iOS spoofing goes, there is not much I was able to find. The most relatable example I found was people trying to spoof their pokemon GO app (to catch pokemon they were not actually near). However, it appears the best options were to use jailbreaking, which is clearly not something we would want to recommend (https://www.reddit.com/r/PokemonGoSpoofing/comments/aegz09/spoofing_on_ios_devices/).

In general, the iOS spoofers I have found seem to be mostly just related to location. https://www.ispoofer.com/ https://www.tutuapp.vip/ios/

None of these sites give detailed info on how they implemented their spoofer, but from the looks of it none seem feasible. Will check in with @davebaraka and look at what he wrote for his browser to see if there is any way something can be translated over, but I do not think so.

rgoldstein01 commented 4 years ago

Other resources: https://stackoverflow.com/questions/12844598/send-custom-headers-with-uiwebview-loadrequest

One thing I have not looked into yet which I have seen mentioned a lot is Alamofire. Perhaps they have tools we can use: https://github.com/Alamofire/Alamofire

rgoldstein01 commented 4 years ago

ie (https://grokswift.com/custom-headers-alamofire4-swift3/)

rgoldstein01 commented 4 years ago

There are also SDKs like these: https://github.com/Azure/azure-mobile-apps-ios-client/wiki/Adding-custom-headers-to-outgoing-HTTP-requests

But the problem still ends up being we could customize our own http headers, but not the requests we actually care about (the ones being sent to the hosts like facebook, google, etc.)

SebastianZimmeck commented 4 years ago

It is a difficult problem. The general issue here is that we are somewhat trying to put square pegs in round holes. Generally, for mobile the header approach might not be the right one. That said, let's dig a little deeper into this header question.

But the problem still ends up being we could customize our own http headers, but not the requests we actually care about (the ones being sent to the hosts like facebook, google, etc.)

Is there a way we can hardcode the requests and send them from our app? So, spoofing in that sense.

If we are not going with the header solution:

SebastianZimmeck commented 4 years ago

@rgoldstein01 and @pakaelbling, here is something else to try. The AppChoices app (official page, App Store, Play Store) allows CCPA Do-Not-Sell opt outs from ad networks part of the Digital Advertising Alliance (DAA).

This app has been around for some years and is based on a self-regulatory effort of the ad industry. This current app version looks very similar to the old one. I could imagine they simply send the phone's ad ID to the ad networks the user wants to opt out from together with a Do-Not-Sell flag. If that is the case, these ad networks operate solely based on the ad ID.

Maybe, we can learn something here on how they submit these requests to the ad networks. So, try it out with some Fiddler or other traffic analysis.

SebastianZimmeck commented 4 years ago

@rgoldstein01, could you do a Fiddler analysis of the AppChoices app per above? Are they keeping track of which user sent a Do-Not-Sell signal via the ad identifier, cookies, something else?

A little bit bigger picture and background, here is a writeup of the current Do-Not-Sell landscape. In particular:

In contrast to the IAB Framework, DAA’s approach does not depend on Do Not Sell signals to be transmitted by a publisher to its ad tech providers. Instead, consumers ultimately tell participating ad tech providers directly that they do not want their personal information to be sold.

Differently from the DAA approach, the IAB published a framework according to which apps and websites (publishers) can implement a Do-Not-Sell signal directly, i.e., not going through ad networks users would opt out with the app or site. Essentially, they have a privacy string, which can be implemented in a cookie or other technology. So, it is agnostic in that regard. However, it seems not many apps and websites have implemented it so far. It would be interesting to see a real world implementation of the privacy string, but I have not come across one.

rgoldstein01 commented 4 years ago

For the CCPA Opt Out Tool there is a pretty clear pattern. You make a call (tap a button) letting the app know you want to opt out of the given add company and then it sends a pretty basic HTTPS call with the IDFA in the header. It does not have anything more. Here are some examples:

Tapad:

GET http://privacy.tapad.com/opt-out/daa-opt-out?dids=3B22C37A-FE8A-4814-965D-D68F20179C69&ccpa=1 HTTP/1.1
Host: privacy.tapad.com
Connection: keep-alive
Accept: */*
User-Agent: AppChoices/94 CFNetwork/1121.2.2 Darwin/19.3.0
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: keep-alive

Exelate:
GET http://load.exelator.com/optout/mobile_optout?adid=3B22C37A-FE8A-4814-965D-D68F20179C69&ccpa=1 HTTP/1.1
Host: load.exelator.com
Connection: keep-alive
Accept: */*
User-Agent: AppChoices/94 CFNetwork/1121.2.2 Darwin/19.3.0
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: keep-alive

Lotame:
GET https://api.lotame.com/2/profile/optout?opt_out_source=DAA&idfa=3B22C37A-FE8A-4814-965D-D68F20179C69&ccpa=1 HTTP/1.1
Host: api.lotame.com
Accept: */*
Accept-Language: en-us
Connection: keep-alive
Accept-Encoding: gzip, deflate, br
User-Agent: AppChoices/94 CFNetwork/1121.2.2 Darwin/19.3.0

PushSpring
GET https://api.pushspring.com/daa/optout/ios?idfa=3B22C37A-FE8A-4814-965D-D68F20179C69&ccpa=1 HTTP/1.1
Host: api.pushspring.com
Accept: */*
Accept-Language: en-us
Connection: keep-alive
Accept-Encoding: gzip, deflate, br
User-Agent: AppChoices/94 CFNetwork/1121.2.2 Darwin/19.3.0

Tutela
POST https://dataservices-daaoptout.tutelatechnologies.com/daaOptOut/rest/v1/optout/ios?idfa=3B22C37A-FE8A-4814-965D-D68F20179C69&ccpa=1 HTTP/1.1
Host: dataservices-daaoptout.tutelatechnologies.com
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Accept: */*
User-Agent: AppChoices/94 CFNetwork/1121.2.2 Darwin/19.3.0
Accept-Language: en-us
Accept-Encoding: gzip, deflate, br
Content-Length: 45

As you can see, you can find my IDFA in all of the POST/GET lines. It also looks like they are sending these signals directly to the companies, as for example one of the APIs called for the Tutela company is called dataservices-daaoptout.tutelatechnologies.com which pretty clearly has tutela as its host.

I then tried the Interest-Based Opt Out and it worked very similarly. Just adds the IDFA to the header. Here are some calls:

AddThis
GET https://optout.addthis.com/app-choices/opt-out?ID1=3B22C37A-FE8A-4814-965D-D68F20179C69&ID5=a17b846822928b2fc977db6d49747f8849909d50&ID6=95b8e04933e3e41688702d4861ae15af HTTP/1.1
Host: optout.addthis.com
Accept: */*
Accept-Language: en-us
Connection: keep-alive
Accept-Encoding: gzip, deflate, br
User-Agent: AppChoices/94 CFNetwork/1121.2.2 Darwin/19.3.0

RUN
GET https://match.rundsp.com/optout?MOBILE_IDS=a17b846822928b2fc977db6d49747f8849909d50 HTTP/1.1
Host: match.rundsp.com
Accept: */*
Accept-Language: en-us
Connection: keep-alive
Accept-Encoding: gzip, deflate, br
User-Agent: AppChoices/94 CFNetwork/1121.2.2 Darwin/19.3.0

PushSpring
GET https://api.pushspring.com/daa/optout/ios?idfa=3B22C37A-FE8A-4814-965D-D68F20179C69 HTTP/1.1
Host: api.pushspring.com
Accept: */*
Accept-Language: en-us
Connection: keep-alive
Accept-Encoding: gzip, deflate, br
User-Agent: AppChoices/94 CFNetwork/1121.2.2 Darwin/19.3.0

Ubimo
GET http://reports.ubimo.com/optout/ios?idfa=3B22C37A-FE8A-4814-965D-D68F20179C69,mac=c1976429369bfe063ed8b3409db7c7e7d87196d9,o_udid=723caa8b2bac9f3068834885ea1d6d0c1395220280x HTTP/1.1
Host: reports.ubimo.com
Connection: keep-alive
Accept: */*
User-Agent: AppChoices/94 CFNetwork/1121.2.2 Darwin/19.3.0
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: keep-alive

Quantcast
GET https://pixel.quantserve.com/optout/mobile_optout?idfa_raw=3B22C37A-FE8A-4814-965D-D68F20179C69 HTTP/1.1
Host: pixel.quantserve.com
Accept: */*
Accept-Language: en-us
Connection: keep-alive
Accept-Encoding: gzip, deflate, br
User-Agent: AppChoices/94 CFNetwork/1121.2.2 Darwin/19.3.0

It looks like everything is through IDFA! I see no other info being passed!

SebastianZimmeck commented 4 years ago

Great result, @rgoldstein01!

So, it seems that they simply add one additional parameter ccpa=1 to the Do-Not-Sell opt out. Other than that, it is identical to the Interest-based opt out. At least, that is what I get from comparing the different PushSpring requests.

SebastianZimmeck commented 4 years ago

As we discussed yesterday, @rgoldstein01 will be looking into whether sending an HTTP request with a modified header, that is, including our own Do-Not-Sell header per @davebaraka below will properly reach the recipient.

In other words, is there any acknowledgment, any error message, ... ?

Screen Shot 2020-03-25 at 10 58 40 PM
rgoldstein01 commented 4 years ago

From my research, it seems we should look into using Alamofire to make custom headers, so that is where I am going to continue looking into https://github.com/Alamofire/Alamofire

rgoldstein01 commented 4 years ago

I am trying to build a custom header like the following to send to tapad.

let headers: HTTPHeaders = [
  "Accept": "*/*",
 "Accept-Language": "en-us"
 "Connection": "keep-alive"
 "Accept-Encoding": "gzip, deflate, br"
 "User-Agent": "AppChoices/94 CFNetwork/1121.2.2 Darwin/19.3.0"
  "dns": "0"
]

and sending it to the same GET request I picked up in the Fiddler analysis: http://privacy.tapad.com/opt-out/daa-opt-out?dids=3B22C37A-FE8A-4814-965D-D68F20179C69&ccpa=1 HTTP/1.1

This is the failure I am getting

[Result]: FAILURE
[Timeline]: Timeline: { "Request Start Time": 609946164.112, "Initial Response Time": 609946164.055, "Request Completed Time": 609946164.055, "Serialization Completed Time": 609946164.112, "Latency": -0.057 secs, "Request Duration": -0.057 secs, "Serialization Duration": 0.057 secs, "Total Duration": 0.000 secs }

Unfortunately it does not say exactly what the issue is. I will try a different api other than Tapad next.

rgoldstein01 commented 4 years ago

Next I tried Lotame:

Here is the code:

let headers: HTTPHeaders = [
           "Host": "api.lotame.com",
           "Accept": "*/*",
           "Accept-Language": "en-us",
           "Connection": "keep-alive",
           "Accept-Encoding": "gzip, deflate, br",
           "User-Agent": "AppChoices/94 CFNetwork/1121.2.2 Darwin/19.3.0"
           "dns": "0"
        ]

        Alamofire.request("https://api.lotame.com/2/profile/optout?opt_out_source=DAA&idfa=3B22C37A-FE8A-4814-965D-D68F20179C69&ccpa=1", headers: headers)
          .responseJSON { response in
          debugPrint(response)
        }

This was a success! I think because it was an https call not http. Here is the output log I got

[Request]: GET https://api.lotame.com/2/profile/optout?opt_out_source=DAA&idfa=3B22C37A-FE8A-4814-965D-D68F20179C69&ccpa=1
[Request Body]: 
None
[Response]: <NSHTTPURLResponse: 0x6000024de400> { URL: https://api.lotame.com/2/profile/optout?opt_out_source=DAA&idfa=3B22C37A-FE8A-4814-965D-D68F20179C69&ccpa=1 } { Status Code: 204, Headers {
    "Access-Control-Allow-Credentials" =     (
        true
    );
    "Access-Control-Allow-Headers" =     (
        "Content-Type,X-Requested-With"
    );
    "Access-Control-Allow-Methods" =     (
        "GET, OPTIONS, POST, DELETE, PUT, PATCH"
    );
    "Access-Control-Expose-Headers" =     (
        "NEEDS_AUTH"
    );
    "Cache-Control" =     (
        "no-cache, no-store, max-age=0, must-revalidate"
    );
    Connection =     (
        "keep-alive"
    );
    Expires =     (
        0
    );
    Pragma =     (
        "no-cache"
    );
    Server =     (
        "Jetty(9.4.26.v20200117)"
    );
    "Strict-Transport-Security" =     (
        "max-age=31536000 ; includeSubDomains"
    );
    "X-Content-Type-Options" =     (
        nosniff
    );
    "X-Frame-Options" =     (
        DENY
    );
    "X-XSS-Protection" =     (
        "1; mode=block"
    );
} }
[Response Body]: 

[Result]: SUCCESS
[Timeline]: Timeline: { "Request Start Time": 609946705.906, "Initial Response Time": 609946706.824, "Request Completed Time": 609946706.824, "Serialization Completed Time": 609946706.824, "Latency": 0.917 secs, "Request Duration": 0.917 secs, "Serialization Duration": 0.000 secs, "Total Duration": 0.918 secs }

As you can see in bottom output line, ALAMOFIRE says the request was a success. We cannot know whether this actually does anything on Lotame's end, and I think for that we would need to either figure out a way to test that or contact them directly, but we can at least be sure these requests are being sent successfully SOMEWHERE. It clearly is not currently built to even analyze our DNS call, as for now it looks like it is looking in the get request URL whhich at the end has a ccpa=1 part which is clearly telling the API they want to opt out. But as far as sending custom headers goes, this is a good first step!

SebastianZimmeck commented 4 years ago

Excellent!

Alamofire.request("https://api.lotame.com/2/profile/optout?opt_out_source=DAA&idfa=3B22C37A-FE8A-4814-965D-D68F20179C69&ccpa=1", headers: headers)
          .responseJSON { response in
          debugPrint(response)
rgoldstein01 commented 4 years ago

1) Xcode console. I have not tested it with Fiddler yet. 2) Correct. 3) Yeah definitely.

SebastianZimmeck commented 4 years ago

These are great findings. For the time being we are continuing with the browser extension and may pick up the app later.