# Combining Bcrypt With Other Hash Functions
[Link bài viết](https://blog.ircmaxell.com/2015/03/security-issue-combining-bcrypt-with.html)
Ở đây mình sẽ ghi lại những ý mình hiểu theo link bài trên
### Password_hash()
Giống như một bài CTF mình từng làm, `Bcrypt` có một đặc điểm khá hay:
```
Caution
Using the PASSWORD_BCRYPT as the algorithm, will result in the password parameter being truncated to a maximum length of 72 bytes.
```
Tức là khi bạn nhập từ byte thứ 73 trở đi, nó sẽ bị cắt đoạn đó -> người ta nghĩ ra cách có thể sử dụng nhiều hơn 72 bytes -> lồng các hàm với nhau khi sử dụng `password_hash()`.
```php
$hash = password_hash(hash_hmac("sha256", $found[0], $key, true), PASSWORD_BCRYPT);
$hash = password_hash(sha1($_POST['c'], false), PASSWORD_BCRYPT);
```
Vậy lồng các hàm này sẽ mang đến mối nguy hiểm gì không ? Chúng ta cùng nhau phân tích tiếp.
### crypt.c
Source của PHP là C, vì vậy có những hành vi của PHP sẽ bị ảnh hưởng bởi C. Đi sâu một chút vào hàm `crypt` tại [Link](https://heap.space/xref/PHP-7.4/ext/standard/crypt.c?r=565baf05)
```cpp
PHPAPI zend_string *php_crypt(const char *password, const int pass_len, const char *salt, int salt_len, zend_bool quiet)
```
Đi đến đoạn trả về respone:
```cpp
} 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;
}
}
```
Như ở đoạn đầu, `password` là một `char *`, vì vậy `php_crypt_blowfish_rn()` sẽ không thể biết độ dài của đoạn input nhập vào được -> Vậy làm sao nó biết độ dài để xử lý được ? -> Đi sâu hơn vào hàm `php_crypt_blowfish_rn()` [Link](https://heap.space/xref/PHP-7.4/ext/standard/crypt_blowfish.c?r=8d3f8ca1)
```cpp
#define BF_N 16
/* ...code... */
const char *ptr = key;
/* ...code... */
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;
else
ptr++;
}
diff |= tmp[0] ^ tmp[1]; /* Non-zero on any differences */
expanded[i] = tmp[bug];
initial[i] = BF_init_state.P[i] ^ tmp[bug];
}
```
`password` lúc này sẽ là `key`, với `BF_N` là 16, vòng loop ngoài sẽ lặp 18, loop trong lặp 4 và tổng là 18*4=72 lần, nó sẽ đọc `password` bằng cách dịch con trỏ
```cpp
if (!*ptr)
ptr = key;
else
ptr++;
```
Nhưng có 1 điều cần chú ý là sẽ ra sao nếu password có kí tự null `\00` ?
Khi đó nó sẽ được đặt về đầu chuỗi -> khi đó chuỗi `test\0abc` có thể coi như `test\0namtest\0namtest\0namtest\0nam` -> các chuỗi bắt đầu bằng `test\0` đều tương tự.
Về cơ bản, nó bỏ qua mọi thứ sau byte null đầu tiên. [Xem thêm](https://3v4l.org/Y6onV)
### Kết hợp 2 điều trên
Ví dụ:
```php
$hash = '$2y$10$2ECy/U3F/NSvAjMcuBeI6uMDmJlI8t8ux0pXOAoajpv2hSH0veOMi'
var_dump(password_verify("\0", $hash))
// bool(true)
```
```php
Những trường hợp này là tương đương nhau
a\0bc
a\0cd
a\0ef
```
Chúng ta sẽ thường sử dụng `password_hash()` và sau đó là sử dụng `password_verify()` -> Trong trường hợp chúng ta lồng 2 hàm như đã nói, nếu có thể trong quá trình hàm thứ nhất tạo raw byte sẽ tồn tại `\0` thì sẽ dẫn tới quá trình xác thực không mong muốn -> Cách phòng ngừa: chuyển về dạng hex, base64,...