Security issues when Bcrypt is used with other Hash functions at the same time
0x00 Preface
Php 5.5 introduces the bcrypt-based hash function password_hash and its corresponding verification function password_verify, allowing developers to easily implement a secure password for adding salt. This article describes the security issues found by the author around the password_hash function.
Once, I saw an interesting question on StackOverflow: when the number of digits in the password is too long, will the password_verify () function be attacked by DOS? The speed of many hash algorithms is affected by the amount of data, which leads to DOS Attacks: attackers can input a very long password to consume server resources. This issue also makes sense for Bcrypt and PasswordHash. We all know that Bcrypt limits the password length to 72 characters, so it will not be affected at this point. However, when I chose to conduct in-depth mining, I found other questions that surprised me.
0x01 crypt. c Analysis
First, let's look at how php implements the crypt () function. The function we are interested in corresponds to "php_crypt ()" in the source code. The declaration is as follows:
PHPAPI zend_string *php_crypt(const char *password, const int pass_len, const char *salt, int salt_len)
Let's look at the encrypted part.
} else if (salt[0] == '$' &&salt[1] == '2' &&salt[3] == '$') {char output[PHP_MAX_SALT_LEN + 1]; memset(output, 0, PHP_MAX_SALT_LEN + 1); crypt_res = php_crypt_blowfish_rn(password, salt, output, sizeof(output));if (!crypt_res) {ZEND_SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);return NULL;} else {result = zend_string_init(output, strlen(output), 0);ZEND_SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);return result;}}
Have you noticed? Because the password variable is a char pointer (char *), php_crypt_blowfish_rn () does not know the length of the password parameter. I want to see how he gets the length.
Follow up with php_crypt_blowfish_rn (). I found that the only place that uses the password variable (key in the function) is to pass it to the BF_set_key () function. Some settings and Security usage items are described in the annotation of this function. In fact, the following loop is summed up (excluding comments ):
const char *ptr = key;/* ...snip... */for (i = 0; i < BF_N + 2; i++) {tmp[0] = tmp[1] = 0;for (j = 0; j < 4; j++) {tmp[0] <<= 8;tmp[0] |= (unsigned char)*ptr; /* correct */tmp[1] <<= 8;tmp[1] |= (BF_word_signed)(signed char)*ptr; /* bug */if (j)sign |= tmp[1] & 0x80;if (!*ptr)ptr = key;elseptr++;}diff |= tmp[0] ^ tmp[1]; /* Non-zero on any differences */ expanded[i] = tmp[bug];initial[i] = BF_init_state.P[i] ^ tmp[bug];}
To people who do not understand C language:It is used to unreference the pointer, that is, to return the value pointed to by this pointer. So we define char * abc = "abc", thenThe value of abc is 'A' (in fact, it is the ascii value of 'A ). After you execute abc ++, * the value of abc is equal to 'B '. This is the operating principle of string in C.
Next, this loop will iterate 72 times (because BF_N is equal to 16), and each iteration will "eat" a character of the string.
The following code is used:
if (!*ptr)ptr = key;elseptr++;
If the value of * ptr is 0, re-point it to the first character of the string and execute 72 times according to this rule. That is why the input string must be less than 72 characters (because the C language string ends with NUL, it occupies one byte ).
Let's think about it, the above code means that "test \ 0abc" will be processed as "test \ 0test \ 0test \ 0test \ 0test \ 0test \ 0test \ 0test \ 0test \ 0test \ 0test \ 0test \ 0test \ 0test \ 0test \ 0test \ 0te ". In fact, all strings starting with "test \ 0" are processed as follows.
The result is that it ignores all content after the first NUL (abc in test \ 0abc ).
What problems will this cause? Obviously, your password is shortened (from test \ 0abc to test \ 0 ). But since no one uses "\ 0" in the password, isn't it a problem?
In fact, no one will actually use "\ 0" in the password. Therefore, if you use password_hash () or crypt () separately, you are 100% secure. However, if you do not directly use them, but perform "pre-hash", you will encounter the main problems described in this article.
0x02 Main Problems
Some people think that using bcrypt alone is not enough. Instead, they choose to "pre-hash", that is, pre-calculate a hash, and then pass the returned results to the formal hash function for calculation.
This allows you to use a "longer" password (more than 72 characters), such:
password_hash(hash('sha256', $password, true), PASSWORD_DEFAULT)
In addition, some users want to add "salt" to the hash, so use HMAC with the private key:
password_hash(hash_hmac('sha256', $password, $key, true), PASSWORD_DEFAULT)
The problem is that, in the above usage, the last parameter passed in by the hash and hash_hmac functions is true, which forces the function to return the original (Binary) data. Hash calculation is performed again using raw data instead of encoded data. This is common in encryption functions. In this way, you can cut off the 128-bit data after sha512 encryption into 72 digits without entropy, while leaving more entropy.
However, this means that the content output by the hash function for the first time can contain "\ 0 ". And there is a possibility of nearly 1/256 (0.39%). The first priority is "\ 0" (at this time, your password is equivalent to a null string ). So we only need to try the password for about 177 times, and we have a 50% chance to get a password with the first NULL character, if it is equal to about 177 users, there is a 50% probability that a password starting with NULL is used. Therefore, if we try a combination of 31329 (177*177) accounts and passwords, we have a 25% probability of successfully logging on to an account. This makes it possible for online collision hashing (for example, distributed ).
This is terrible.
Let's look at an example of using the above method to collide with the account and password:
$key = "algjhsdiouahwergoiuawhgiouaehnrgzdfgb23523";$hash_function = "sha256";$i = 0;$found = []; while (count($found) < 2) {$pw = base64_encode(str_repeat($i, 5));$hash = hash_hmac($hash_function, $pw, $key, true);if ($hash[0] === "") {$found[] = $pw;}$i++;}var_dump($i, $found);
I chose a random $ key, and then I used a seemingly "random" password $ pw (which is actually the base64 encoded value of five repeated characters) and started running. This silly code starts collision (low efficiency ). Finally, the following results are obtained:
int(523)array(2) {[0]=>string(16) "MzEzMTMxMzEzMQ=="[1]=>string(20) "NTIyNTIyNTIyNTIyNTIy"}
In our 523 attempts, we collided two passwords: "MzEzMTMxMzEzMQ =" and "NTIyNTIyNTIyNTIyNTIy tiyntiy". The number of attempts will change as the key changes. Then we will do the following experiment:
$hash = password_hash(hash_hmac("sha256", $found[0], $key, true), PASSWORD_BCRYPT);var_dump(password_verify(hash_hmac("sha256", $found[1], $key, true), $hash));
The output is as follows:
bool(true)
Very interesting. Two different passwords are considered to be the same hash, and our hash collision works.
0x03 check for problematic hash values
We can use the following method to test whether the hash value starts with a NULL character:
password_verify("\0", $hash)
For example, we test the following hash:
$2y$10$2ECy/U3F/NSvAjMcuBeI6uMDmJlI8t8ux0pXOAoajpv2hSH0veOMi
The returned result is bool (true), indicating that it is encrypted by a string whose first character is NULL.
Therefore, this issue can be detected with only one line of code offline.
In addition, even if the string you calculate the secondary hash value does not start with a NULL character, it does not represent your absolute security (assuming you have used the preceding flawed encryption algorithm ). When the second character is NUL, the same thing may happen:
a\0bca\0cda\0ef
These can be collided, and you will have a 0.39% probability of collision to the second character is \ 0. In all strings whose first character is a, you will also have a 0.39% probability to get the result of a second character "\ 0. This means that the workload of password cracking has changed from collision of the entire string hash to collision of only the short strings above. This issue will continue (3rd characters are \ 0, 4th, 5th ......).
Some people say that I didn't use password_hash, And I used CRYPT_SHA256!
Seeing the php_crypt () function in the source code, we can find that all encryption methods in crypt () have such behavior. It does not exist only in bcrypt, but also in php, the entire crypt (3) c language library has this problem.
In this article, I mainly use bcrypt to illustrate the problem because password_hash () calls it, while password_hash is the encryption algorithm recommended by PHP.
It is worth noting that if you use hash_pbkdf2 (), it is not easy to be affected, but it is better to use the scrypt library.
0x04 Solution
This problem is not caused by the use of bcrypt and other encryption methods. It turns out that not all combinations are insecure. The password_hash (base64_encode (hash_hmac ("sha512", $ password, $ key, true) method mentioned in Mozilla's system is safe, because it is base64-encoded after obtaining the return value of hash_hmac. In addition, if the hash/hmac return value is in the hex format, you are also safe (the last parameter is the default false ).
If you follow the instructions below, you are 100% secure:
1. directly use bcrypt encryption (instead of pre_hash) 2. Use the value in hex format as the pre_hash parameter 3. Use the base64 encoded value as the pre_hash Parameter
In short, do not use pre_hash, or encode it before performing pre_hash.
0x04 root problems
The fundamental problem is that the encryption algorithm was initially not designed for simultaneous use. Using multiple encryption algorithms at the same time makes developers feel secure, but in fact it is not. The above problems are only a reflection of this incorrect approach.
Therefore, we should use them as expected by algorithm designers. If you want to strengthen the defense on bcrypt, encrypt the output result:
encrypt(password_hash(...), $key)。
Finally, it is also the most important thing: never invent your own encryption algorithm, otherwise it will cause fatal consequences.