Opened 2 years ago

Last modified 11 days ago

#22919 needs_review defect

Form tracking and OS fingerprinting (only Windows, but without Javascript)

Reported by: basvd Owned by: acat
Priority: High Milestone:
Component: Applications/Tor Browser Version:
Severity: Major Keywords: tbb-fingerprinting, TorBrowserTeam201912R
Cc: acat, tbb-team Actual Points: 0.5
Parent ID: Points: 1
Reviewer: tom Sponsor:

Description

I found out that TOR (/Firefox) is using a weak implementation of a (pseudo)random number generator. On form submission Firefox always sends an unique boundary delimiter together with the POST data. But this unique boundary string isn't so unique as it looks like.

For example, when you send a multipart form request the number is always the first time: 41184676334, 2nd time: 265001916915724, 3th time: 114782935826962, etc..

Try it yourself: build a multipart form and check the POST data:

<form action="/" method="post" enctype="multipart/form-data"> 
 <input type="submit" value="Submit">
</form>
Content-Type: multipart/form-data; boundary=---------------------------41184676334

Content-Length: 63-----------------------------41184676334--

Threats:

1) OS fingerprinting for Windows
It looks like the implementation between Windows and Linux is different. Windows isn't seeding srand() calls. In this way we're 99% sure that boundary string with 41184676334, 265001916915724, 114782935826962 came from a Windows OS.

2) "track" form submissions between websites.
On website_A I submit a form mulitple times. Now, the owner of website_B is able to read the boundary string and can check how many times a user submitted a form on website_A.

3) "track" form interactions on other websites.
For example, I'm the owner of website_A, and website_B is a photo upload website. If the user goes from website_A to B and later he came back to my website, I'm able to know if the user uploaded a photo on the other website:

  • website_A (boundary: 41184676334)
  • website_B (??)
  • website_A (boundary: 114782935826962)

As you can see only 265001916915724 is missing, so he did one action on the other website.

4) Fake boundaries for file uploads
It is possible to inject fake data in a file and let you browser think that you've uploaded 2 files. In this way it is possible to upload 2 files by uploading only one. Of course it also depends on server side validation, but for the browser this seems to be okay. Example on Bugzilla

Conclusion:
Without Javascript we're able to know if an user is running on Windows and we're able to know how many times a user submitted a form (... on another website)

Technical analysis:
The current implementation of the boundary delimiter in Firefox:

mBoundary.AssignLiteral("---------------------------");
mBoundary.AppendInt(rand());
mBoundary.AppendInt(rand());
mBoundary.AppendInt(rand());

As you can see RAND() is called without seeding this function. Every time you'll (re)start TOR, the browser also resets the seed. Without a good seed the output is of rand is always predictable. At least on Windows, I think Linux has a different implementation of PRNG.

A solution is to seed RAND() with the PID or something else that's not public (like dates or timestamps).

5 years ago there was also a discussion about this topic on Bugzilla: https://bugzilla.mozilla.org/show_bug.cgi?id=461204

Child Tickets

Attachments (1)

0001-Bug-22919-Force-better-random-numbers-for-HTML-Forum.patch (2.4 KB) - added by cypherpunks 2 months ago.

Download all attachments as: .zip

Change History (17)

comment:1 Changed 2 years ago by gk

Keywords: tbb-fingerprinting added
Priority: MediumHigh

comment:2 Changed 2 months ago by cypherpunks

The attached patch uses nsIRandomGenerator, which calls PK11_GenerateRandom. It generates more random results than rand() and is adapted from an existing comm-central implementation in bug https://bugzilla.mozilla.org/show_bug.cgi?id=902580

Reviewers should note the rand_buf and character count to see if any tweaking needs to be done for additional defense.

comment:3 Changed 2 months ago by gk

Cc: acat added
Keywords: TorBrowserTeam201910R added
Status: newneeds_review

comment:4 Changed 2 months ago by tom

This looks pretty good to me.

unsigned char rand_buf[4];

Why 4 if we only ever use 3?

uint8_t *tempBuffer;

Do we need this? Is there a reason I can't see not to just pass the buf into GenerateRandomBytes?

nsIRandomGenerator

We recently added https://searchfox.org/mozilla-central/source/mfbt/RandomNum.cpp because we needed secure randomness before NSS was initialized. That's not the case here; but I don't think we ever profiled to see which API was faster. /shrug

I'll pursue uplifting this at some point (I'll keep the tab open).

comment:5 Changed 2 months ago by acat

Keywords: TorBrowserTeam201910 added; TorBrowserTeam201910R removed
Status: needs_reviewneeds_revision

Note that the original code is pushing ints:

  mBoundary.AppendInt(rand());
  mBoundary.AppendInt(rand());
  mBoundary.AppendInt(rand());

but the patch is pushing bytes:

  mBoundary.AppendInt(rand_buf[0]);
  mBoundary.AppendInt(rand_buf[1]);
  mBoundary.AppendInt(rand_buf[2]);

rand() returns an integer in the range 0..RAND_MAX. I think RAND_MAX depends on the system, in glibc it's 2147483647, in Windows is 32767. But it's guaranteed to be at least 32767.

So the patch is making the boundary 24 bit long instead of the original 48-96 bits (assuming rand() was actually seeded, which is a different problem). I think the patch should be amended to always provide 96 bits of entropy.

FWIW: I've noticed the function js::GenerateRandomSeed which might be useful here. I think it uses the RandomNum that tom mentioned and has a fallback using PRMJ_Now() if that fails.

So I think we could simply do:

  mBoundary.AppendInt(js::GenerateRandomSeed());
  mBoundary.AppendInt(static_cast<uint32_t>(js::GenerateRandomSeed()));

and just #include "jsmath.h" for a 3 line patch. What do you think, tom?

That's to preserve the original 96 bits of entropy, we could have 128 for the same price, but not sure whether changing the response sizes for this case is a good idea.

comment:6 Changed 2 months ago by tom

I don't believe GenerateRandomSeed is available outside of the js engine. But you're right about the bits

comment:7 in reply to:  6 Changed 2 months ago by acat

Replying to tom:

I don't believe GenerateRandomSeed is available outside of the js engine. But you're right about the bits

Right, and it uses PRMJ_Now() which is also only in js.

We could maybe have a local version of js::GenerateRandomSeed()? Changing PRMJ_Now() -> PR_Now() and maybe using the same srand/rand fallback as in the patch? I think the resulting code is simpler than the one using @mozilla.org/security/random-generator;1 (assuming both have similar security, speed and availability).

comment:8 Changed 6 weeks ago by pili

Keywords: TorBrowserTeam201911 added; TorBrowserTeam201910 removed

Moving tickets to November 2019

comment:9 Changed 6 weeks ago by pili

Points: 1

comment:10 Changed 4 weeks ago by pili

Cc: tbb-team added
Owner: changed from tbb-team to acat
Status: needs_revisionassigned

Assigning tickets to acat for the next few months

comment:11 Changed 2 weeks ago by acat

Actual Points: 0.5
Keywords: TorBrowserTeam201911R added; TorBrowserTeam201911 removed

comment:12 Changed 12 days ago by gk

Keywords: TorBrowserTeam201912R added; TorBrowserTeam201911R removed

We are in December now.

comment:13 Changed 12 days ago by pili

Status: assignedneeds_review

comment:14 Changed 12 days ago by pili

Reviewer: tom

comment:15 Changed 12 days ago by tom

This LGTM. I began to wonder if we could use AppendInt(uint64) to get the same bitness for 2 calls to RandomNum instead of 3; but I think this would change the ultimate output of the boundary, and we would idally keep it looking the same so it's not apparent if one is using the old-style RNG or the new-style.

If you agree this is keeping the same format/appearance of the boundary (aka, you can double check my belief) - let's land it in Tor and/or submit it for uplift to get it upstreamed/further review.

comment:16 in reply to:  15 Changed 11 days ago by gk

Replying to tom:

This LGTM. I began to wonder if we could use AppendInt(uint64) to get the same bitness for 2 calls to RandomNum instead of 3; but I think this would change the ultimate output of the boundary, and we would idally keep it looking the same so it's not apparent if one is using the old-style RNG or the new-style.

If you agree this is keeping the same format/appearance of the boundary (aka, you can double check my belief) - let's land it in Tor and/or submit it for uplift to get it upstreamed/further review.

I think the plan was to get it upstreamed first to get a further review on it and then backport what actually landed on mozilla-central. tjr, if you could help with the former that would be much appreciated. acat is out for the remainder of the year.

Note: See TracTickets for help on using tickets.