[Writeup] Plaid CTF 2015 – radhos

Category: Web

Points: 250

Description:

A web scale key value store, for your enjoyment!

Should be working
Running at 52.6.62.188 port 9009

64-bit collision approach

I didn’t solve this problem within the contest time, but it’s here finally 🙂

First, let’s take a quick look on provided script:

It’s a web service which provide key-value storage functions via JSON requests, keys are stored using its masked hash value using python’s built in hash function. So I decided it a collision-finding problem. At first, looking at this line:

assert (hash('PPP') != 2149279368079130035)

Which made me thought that python is running using version 3.4 with new SIPHASH24 hash algorithm and SPENT A DAY TO CRACK SIPHASH without success… (poor me :(). When i could manage to talk with w~ about this problem, i realized that it’s only the old hash function with randomized secret keys.

A quick search on python source code gave me the implementation of its string hashing:

static long
string_hash(PyStringObject *a)
{
    register Py_ssize_t len;
    register unsigned char *p;
    register long x;

#ifdef Py_DEBUG
    assert(_Py_HashSecret_Initialized);
#endif
    if (a->ob_shash != -1)
        return a->ob_shash;
    len = Py_SIZE(a);
    /*
      We make the hash of the empty string be 0, rather than using
      (prefix ^ suffix), since this slightly obfuscates the hash secret
    */
    if (len == 0) {
        a->ob_shash = 0;
        return 0;
    }
    p = (unsigned char *) a->ob_sval;
    x = _Py_HashSecret.prefix;
    x ^= *p << 7; while (--len >= 0)
        x = (1000003*x) ^ *p++;
    x ^= Py_SIZE(a);
    x ^= _Py_HashSecret.suffix;
    if (x == -1)
        x = -2;
    a->ob_shash = x;
    return x;
}

Quite simple huh, start with an const, mul and or rounds, and ending by xor with its length and another const. Problem was that we dont know the value of _Py_HashSecret.prefix and _Py_HashSecret.suffix, let’s call them A and B from now on. As the calculation is based on simple operations only, we can use tools like z3 to solve the SMT model, but due to laggy internet, I decided to leave that path. Let’s calculate some simple hashes:

  • Hash value of “\x00” * 1 : (1000003 * A) xor 1 xor B
  • Hash value of “\x00” * 2 : (1000003 * 1000003 * A) xor 2 xor B
  • Hash value of “\x00” * 3 : (1000003 * 1000003 * 1000003 * A) xor 3 xor B

From above formulas, I can reduce B by xor the hash of 2 strings, make it a formula with only A:

  • (Hash value of “\x00” * 1) xor (Hash value of “\x00” * 2) = (1000003 * A) xor 1 xor (1000003 * 1000003 * A) xor 2
  • (Hash value of “\x00” * 2) xor (Hash value of “\x00” * 3) = (1000003 * 1000003 * A) xor 2 xor (1000003 * 1000003 * 1000003 * A) xor 3

Now how can we sove that for A? As its type is long on 64 bits machine, we can’t do a full bruteforce search.

There’s an interesting feature of multiplication and xor is that the suffix of result if equal to operation result of suffixes of its operants. i.e. 0x12130x3456 = 0x3B1EE62 while 0x130x56 = 0x662, both share the same byte suffix. Using this we can solve for its value byte-byte-byte.

First, let’s calculate the hash value of some strings first:

/*Hash value of "\x00" * i */
long hashArr[]={0l,
7848190082587965582,
-3458030773997347889,
-4582483746743215612,
4410049638523290193,
6253908765636887514,
7892342918428438971,
4812826225314830288,
-3580448393237491763,
-3802863708583786458,
7877484284146754215,
};
Sample sample[] = {
     Sample("testing",7,-6690661205054787548),
     Sample("vanhoa",6,4054411468809426790),
     Sample("ldfsjl",6,-1154596901575536268),
     Sample("2cm834mc803c23-",15,-3744132313005259935),
};

Using these values, we can generate all possible values for A and B using:

vector<long> possP;
possP.push_back(0);
for(int b=1;b<=8;b++){
    vector possP2;
    for(long i=0;i<256;i++){
        for(int k=0;k<possP.size();k++){
            long pp = possP[k] + (i<<(8*(b-1)));
            int valid12 = (((M * pp) ^ 1 ^ (M * M * pp) ^ 2 ^ hashArr[1] ^ hashArr[2]) & ((1ll<<(8*b))-1))==0;
            int valid23 = (((M * M * pp) ^ 2 ^ (M * M * M * pp) ^ 3 ^ hashArr[2] ^ hashArr[3]) & ((1ll<<(8*b))-1))==0;
            int valid34 = (((M * M * M * pp) ^ 3 ^ (M * M * M * M * pp) ^ 4 ^ hashArr[3] ^ hashArr[4]) & ((1ll<<(8*b))-1))==0;
            if(valid12 && valid23 && valid34){
                possP2.push_back(pp);
            }
        }
    }
    possP = possP2;
}

And then verify the results with provided samples:

vector<pair<long,long> > r;
for(int k=0;k<possP.size();k++){
    long p = possP[k];
    long q = (p * M) ^ 1 ^ hashArr[1];
    long x = p;
    bool valid = true;
    for(int len=1;valid && len<=hashMax;len++){
        x = (1000003*x);
        if((x^len^q) != hashArr[len]) valid = false;
    }
    for(int i=0;i<sampleLen;i++){
        if(pythonHash(sample[i].inp,sample[i].len,p,q)!=sample[i].hash) valid = false;
    }
    if(valid){
        r.push_back(make_pair(p, q));
    }
}

Using this method, i can (always) obtain 2 possible values pair for A and B, so i decided to use only the first one.

From this point, A and B are known, so we can simulate the hashing function locally. Now we must find collision for string “you_want_it_LOLOLOL?”.

The hashing value is 64 bits, so we have to search on a 64-bit space to obtain the collision, after some simple test on small strings, i think this hashing is nearly unique for them, which lead me to consider only in 8-byte strings. Thinking the hash function as a finity state graph, starting at A, we can go to next hash number using current character of input string. A full BFS rooting from A will require 2^64 nodes to be visited, which is impossible for our normal computer. We can try BFS from both side: the source node and the target node, and check if they can visit a common node, but this approach is for 128GB computer only – as we have to save hash (8 byte each) of 2^32 values, which require approx. 34GB RAM.

My last step to solve this problem is optimizing this: BFS only 3 bytes from each node, and try to do something for the remain 2 bytes (8-3*2 bytes). The first approach should be 256^2 loop:

    for char1 in range(256):
        for char2 in range(256):
            TWO_WAY_BFS()

which should require some hours to finish. Looking more deeply on the hashing algorithm bring me an idea:

    the assigment x = (1000003*x) ^ *p++; which our controlled p can assign any value to the LSByte of x!

So a byte can be obmitted from the search because it can be calculated using next and previous values, which reduce the searching space to 7 bytes, the obmitted byte will be the glue of 2 BFSs.

Finally making it run 8 processes in parallel and we got the flag flag{wh0_n3edz22Z22zZ_p3p456}.

RSIZE = 2
Found A = 0x1d0e2c73d04d2feb; B= 0xb01eedc54b35180e
[22] Result 22 : 1
 -7368800807082251008
SolveLeft 2 256
SolveLeft 3 65536
SolveLeft- 4 16777216
SOLVE LEFT RESULT: 00ba9057 99bcc2b521402d3a ac0276917b41cb3e 426e84fc91cabcb4
SolveRight 1 1
SolveRight 2 256
SolveRight 3 65536
SOLVE RIGHT RESULT: 009b5da9 99bcc2b521402de5
Result: \xba\x90\x57\x22\xdf\xa9\x5d\x9b
Killed: 9

For the session with given A and B, the collision string for you_want_it_LOLOLOL? was \xba\x90\x57\x22\xdf\xa9\x5d\x9b with the same hash value 0x2fdd3e3f58ce3f70.

32-bit collision approach

Here’s python collision finding using z3:

#!/usr/bin/python

from z3 import *

x = BitVecs('x1 x2 x3 x4 x5 x6 x7 x8',8)

A = 0x1d0e2c73d04d2feb
B = 0xb01eedc54b35180e

def xhash(a, A, B):
    l = len(a)
    x = A
    x = x ^ (ZeroExt(56,a[0]) << 7) l -= 1 i = 0 while l >= 0:
        x = (x * 1000003) ^ ZeroExt(56,a[i])
        i += 1
        l -= 1
    x = x ^ len(a)
    x = x ^ B
    return x

s = Solver()
s.add(xhash(x, A, B) & 0xFFFFFFFF == 0x39e0776e)

print s
print 'start!'
print 'sat:',s.check()
m = s.model()
print m

This program can run under 1 minute on my machine.

        [(((((((((2093659752701439979 ^ ... << ...)*1000003 ^
                 ZeroExt(56, x1))*
                1000003 ^
                ZeroExt(56, x2))*
               1000003 ^
               ZeroExt(56, x3))*
              1000003 ^
              ZeroExt(56, x4))*
             1000003 ^
             ZeroExt(56, x5))*
            1000003 ^
            ZeroExt(56, x6))*
           1000003 ^
           ZeroExt(56, x7))*
          1000003 ^
          ZeroExt(56, x8) ^
          8 ^
          12690842231602747406) &
         4294967295 ==
         971011950]
        start!
        sat: sat
        [x8 = 163,
         x3 = 28,
         x2 = 162,
         x1 = 0,
         x4 = 175,
         x5 = 217,
         x6 = 114,
         x7 = 100]