An interesting request has been opened for PHP to make Bin2Hex () a certain amount of time. This leads to some interesting discussions about mailing lists (even let me reply to:-X). PHP reports on remote timing attacks are very good, but they talk about string comparisons. I want to talk about other types of timed attacks.
What is a remote timed attack?
OK, let's assume you have the following code:
function Containstheletterc ($string) {for ($i = 0; $i < strlen ($string); $i + +) { if ($string [$i] = = "C") {
return true; } Sleep (1); } return false;} Var_dump (CONTAINSTHELETTERC ($_get[' query '));
It should be easy to say how it works. It accepts a query from a URL parameter and then passes it word by word to check if it is lowercase c. If it is, it will return. If not, it sleeps for a second.
So let's imagine we passed the string? query=abcdef. We expect the check to take 2 seconds.
Now, let's imagine that we don't know which letter to look for. Let's imagine that this "C" is a different value that we don't know about. Can you figure out how to figure out what that letter is?
It's simple. We construct a string "Abcdefghijklmnopqrstuvwxyzabcdefghij ..." and pass it in. We can then calculate how long it will take to return. Then we know which role is different!
This is the basis for timed attacks.
But we don't have code that looks like that in the real world. Let's look at a real example:
$secret = "Thisismykey", if ($_get[' secret '!== $secret) {die ("not allowed!");}
In order to understand what happened, we need is_identical_function to see it from the source code of PHP. If you look at this function, you will find that the result is defined by the following conditions:
Case is_string: if (z_str_p (OP1) = = Z_str_p (OP2)) { Zval_bool (result, 1); } else { Zval_bool (result, (Z _strlen_p (OP1) = = Z_strlen_p (OP2)) && (!memcmp (z_strval_p (OP1 ), z_strval_p (OP2), Z_strlen_p (OP1))); } Break ;
The If two variables are the same variables (such as the main requirement $secret = = = $secret). In our case, this is not possible, so we just need to look at the else block.
Z_strlen_p (OP1) = = Z_strlen_p (OP2)
So if the string length does not match, we return immediately.
This means that if the length of the string is the same, more work can be done!
If we spend a lot of time doing different lengths, we'll see something like this:
length |
Time Run 1 |
time to run 2 |
Time Run 3 |
Average Time |
7 |
0.01241 |
0.01152 |
0.01191 |
0.01194 |
8 |
0.01151 |
0.01212 |
0.01189 |
0.01184 |
9 |
0.01114 |
0.01251 |
0.01175 |
0.01180 |
10 |
0.01212 |
0.01171 |
0.01120 |
0.01197 |
11 |
0.01210 |
0.01231 |
0.01216 |
0.01219 |
12 |
0.01121 |
0.01211 |
0.01194 |
0.01175 |
13 |
0.01142 |
0.01174 |
0.01251 |
0.01189 |
14 |
0.01251 |
0.01121 |
0.01141 |
0.01171 |
If you ignore the average column, you'll notice that there doesn't seem to be much of a pattern. These numbers are within each other's causes.
However, if you run on average multiple times, you'll notice a pattern. You will notice that the length of 11 takes longer (slightly) and then the other length.
This example is very exaggerated. But it illustrates this point. It has been shown that the difference in the sample size that can be used about 49000 (so 49,000 attempts instead of in the above example 3) is reduced in time to about 15 nanoseconds.
However, we have found this length. That won't give us too much income ... But what about the second part? How about memcmp (...)?
If we look at the execution of MEMCMP ()::
int memcmp (const void *S1, const void *S2, size_t n) { unsigned char U1, U2; for (; n--; s1++, s2++) { u1 = * (unsigned char *) s1; U2 = * (unsigned char *) s2; if (u1! = U2) { return (U1-U2); } } return 0;}
Wait a minute! This returns the first difference between the two strings!
So once we've determined the length of the string, we can try different strings to start detecting the difference:
Axxxxxxxxxxbxxxxxxxxxxcxxxxxxxxxxdxxxxxxxxxx...yxxxxxxxxxxzxxxxxxxxxx
And through the same technique, it is found that the difference with "txxxxxxxxxx" is slightly longer than the other time.
Why?
Let's take a look at the things that happen in the memcmp step-by-step.
First, it looks at the first character of each string.
If the first character is different, go back immediately.
Next, take a look at the second character of each string.
If they are different, return immediately.
Wait a minute.
So "axxxxxxxxxx", it only performs the first step (because we are comparing the string "Thisismykey"). But "Txxxxxxxxxx", the first and second steps match. So it does more work, so it takes a long time.
So once you see it, you know T is the first character.
So this is just a matter of repeating the process:
Taxxxxxxxxxtbxxxxxxxxxtcxxxxxxxxxtdxxxxxxxxx...tyxxxxxxxxxtzxxxxxxxxx
Do this for each character and you're done. You have successfully inferred a secret!
Prevent comparison attacks
So this is a basic comparison attack. = = = and = = = are vulnerable to attack in PHP.
There are two basic defense methods.
The first is to manually compare two strings and always compare each character (this is the function in my previous blog post:
/** * A Timing safe equals comparison * * @param string $safe the internal (safe) value to be checked * @param string $use R the user submitted (unsafe) value * * @return Boolean True If the strings is identical. */function timingsafeequals ($safe, $user) { $safeLen = strlen ($safe); $userLen = strlen ($user); if ($userLen! = $safeLen) { return false; } $result = 0; for ($i = 0; $i < $userLen; $i + +) { $result |= (Ord ($safe [$i]) ^ ord ($user [$i])); } They is identical strings if $result is exactly 0 ... return $result = = = 0;}
The second one is using the built-in PHP hash_equals () function. This is added in 5.6 and does the same thing as the above code.
Note: In general, it is not possible to prevent length leaks. So you can leak this length. The important part is that it does not leak information about the differences between the two strings.
Other types of timing attacks-index lookups
That's the comparison. This is pretty good coverage. But let's talk about index lookups:
If you have an array (or string) and use secret information as an index (key), you might leak information about that key.
To understand why, we need to understand how the CPU handles memory.
Typically, the CPU has a fixed-width register. Think of these as small variables. On modern processors, these registers may be 64-bit (8-byte) wide. This means that the maximum number of variables that the CPU can handle at a time is 8 bytes. (Note: This is not true because most processors have vector-based operations, such as SIMD, which allows it to interact with more data.) For this discussion, although this is not important).
So what happens when you want to read a string with a length of 16 bytes?
Then, the CPU needs to load it blocks. Depending on the operation, it may load a string of 8 bytes at a time, and manipulate it 8 bytes at a time. Or more commonly, it processes one byte at a time.
So that means it needs to get the rest of the string from somewhere. This "Somewhere" is main memory (RAM). But memory is very slow. Like it's really slow. About 100ns. This is our 15 nanosecond threshold.
And because the main memory is very slow, the CPU has little memory space on the CPU itself to act as a cache. In fact, they usually have two kinds of caches. They have a L1 cache that is specific to each core (each core has its own L1 cache), a core-specific L2 cache, and a L3 cache that is often shared among all cores on a single chip. Why the 3 floor? Due to speed:
Memory Type |
Dimensions |
Latent |
L1 Cache |
32kb |
0.5 |
l2 cache |
256kb |
|
l3 Cache | TD align= "left" style= "margin:0px;padding:0px;" >4-16MB
10-20 nanoseconds |
memory |
lot |
60-100 nanoseconds |
So let's take a look at what's done on the String[index]c string (char \* character array). Imagine that you have this code:
Char character_at_offset (const char *string, size_t offset) { return String[offset]}
The compiler compiles it to:
Character_at_offset: pushq %rbp cfi_def_cfa_offset cfi_offset 6, -16 movq %rsp,%RBP . Cfi_def_cfa_register 6 movq %rdi, -8 (%RBP) movq %rsi, -16 (%RBP) movq -16 (%RBP), %rax movq -8 (%RBP),%rdx addq %rdx,%rax movzbl (%rax),%eax POPQ %RBP . CFI_DEF_CFA 7, 8 ret . Cfi_endproc
Although there is a lot of noise. Let's narrow it down to a non-functional but more suitable size:
Character_at_offset: addq %rdx,%rax movzbl (%rax),%eax popq %rbp ret
The function has two parameters, one of which is a pointer (the first element of the string), and the second is an integer offset. It adds the two to get the memory address of the character we want. Movzbl then moves one byte from the address and stores it%eax (the remaining 0 is also 0).
So how does the CPU know where to find the memory address?
Then, it traverses the cache chain until it finds it.
Therefore, if it is in the L1 cache, the overall operation needs to be 0.5 ns. If it's in L2,2.5ns. Wait a minute. Therefore, by carefully calculating the time of the information, we can infer where the item is cached (or whether it is cached).
It is important to note that the CPU does not cache individual bytes. They cache blocks of memory called lines. Modern processors typically have a cache line of 64 bytes wide. This means that each entry in the cache is a contiguous 64-byte block of memory.
So, when you do a memory fetch, the CPU writes a 64-byte block of memory to the cache line. Therefore, if your movzbl call needs to hit the main memory, the entire block will be copied to the lower cache line. (Note that this is a very simple thing, but it is to demonstrate what will happen next).
Now, this is a really interesting place.
Suppose we are dealing with a large string. One is not suitable for level two cache. So 1MB.
Now, let's imagine that we are extracting bytes from a string that is based on a secret sequence of numbers.
By observing how long it takes to get the bytes, we can actually determine the information about the secret!
Let's imagine we get the following offsets:
The first fetch causes a cache miss and is loaded from main memory into the cache.
But the second fetch (offset 1) is taken from the L1 cache because it may be the same as the original cache row (memory block) offset 10. So it's probably a cache hit.
If we then extract offset 2048, it is most likely not in the cache.
Therefore, by carefully observing the delay pattern, you can determine some information about the relationship of the offset sequence. By using the correct information multiple times to do this, you can infer the secret.
This is known as a cache time attack.
It's really far-fetched right now, isn't it? I mean, how often do you get the perfect information? How this could be practical. Well, this is 100% practical and happens in the real world.
Defenses against Cache-time attacks:
There is only one practical way to defend against this style of attack:
Do not pass a secret index array (or string).
It's really simple.
Branch-based timing attacks
How many times have you seen code like the following?
$query = "SELECT * from users WHERE id =?"; $stmt = $pdo->prepare ($query), $stmt->execute ([$_post[' id]]), $user = $stmt->fetchobject (); if ($user & & Password_verify ($_post[' password ', $user->password)) { return true;} return false;
Of course, this is safe?
There's information leaking out there.
If you try to use a different user name, it may take a different time depending on whether the user name exists. If Password_verify takes 0.1 seconds, you can simply measure the difference to determine if the user name is valid. On average, requests to use a user name will take longer than the available user name.
Now, is this a problem? I don't know, it depends on your request. Many websites want to keep their usernames secret and try not to disclose their information (for example, not saying that the user name or password is not valid in the login form).
If you want to keep the user name secret, you won't.
Defend against branch-based timing attacks
The only way to do this is to not branch. But there's a problem there. If you don't branch, how do you get features like the one above?
So, one idea is to do the following:
$query = "SELECT * from users WHERE id =?"; $stmt = $pdo->prepare ($query), $stmt->execute ([$_post[' id]]), $user = $stmt->fetchobject (), if ($user) { Return password_verify ($_post[' password '), $user->password);} else { password_verify ("", Dummy_hash);} return false;
This means that you password_verify run in two situations. This cuts the second difference of 0.1.
But the core timing attacks still exist. The reason is that the database has a slightly different time to return the query to find the query that found the user, and the query that was not located. This is because it executes a large number of branching and conditional logic internally, and ultimately requires the data to be transferred back to the program via the line.
So the only way to defend against this style of attack is to not treat your username as a secret!
A note on "random delay"
Many people, when they hear a timed attack, think: "Well, I'm just going to add a delay!" This will work! “。 And that's not the case.
To understand why, let's talk about what actually happens when you add a random delay:
The overall execution time is Work + sleep (rand (1, 10)). If Rand behaves well (this is random), then over time we can put it on average.
Let's say this is rand (1, 10). So, this means that when we run on average, the average delay is about 5. The same average value is added to all cases. So what we need to do is run it once per run with average noise. The more times we run, the more random values tend to be averaged. So our signal is still there, it just needs a little more data to fight the noise.
So, if we need to run 49,000 tests to get 15ns accuracy, then we need about 100,000 or 1,000,000 tests to get the same accuracy and random delay. Or it could reach 100,000,000. But the data still exists.
Fix the bug and don't just add noise around it.
Effective real-world latency
Random delay does not work. But we can use the delay effectively in two ways. The first one is more effective and the only one I rely on.
- The
-
Delay depends on user input.
Therefore, in this case, you can hash user input with a local key to determine the delay to use:
function Delay ($input , $secret _key) {$hash = CRC32 (Serialize ($secret _key. $input. $secret _key)); Make it take a maximum of 0.1 milliseconds time_nanosleep (0, ABS ($hash% 100000));}
The Then simply uses the user input for the delay function. This way, the delay will change as the user changes their input. But it will change in the same way that they cannot use statistical techniques to average it.
Please note that I have used CRC32 (). This does not need to be a cryptographic hash function. Since we're just deriving an integer, we don't need to worry about collisions. If you want to be more secure, you can replace it with the SHA-2 feature, but I'm not sure if it's worth the speed loss.
-
Minimizes the time (clamping) of the operation
Therefore, many people think of the idea of "clamping" the operation to a specific runtime (or, more accurately, making it require at least a certain amount of running time).
function Clamp (callable $op, array $args, $time = +) {$start = Microtime (t Rue); $return = Call_user_func_array ($op, $args); $end = Microtime (true); convert float seconds to integer nanoseconds $diff = Floor ((($end-$start) * 1000000000)% 1000000000); $sleep = $diff-$time; if ($sleep > 0) {time_nanosleep (0, $sleep); } return $return;}
So you can say that the comparison must take the least amount of time. Therefore, do not try to compare the duration of the time, you only need to spend time.
So, you can clamp the equivalent to 100 nanoseconds (Clamp ("strcmp", [$secret, $user], 100)).
To do so, you protect the first part of the string. If the first 20 characters take 100 nanoseconds, then the difference between those leaks can be prevented by clamping to 100 nanoseconds.
But there are some issues:
It's very fragile. If your time is too short, you will lose all protection. If the time is too long, an unnecessary delay may be added to the application (if you are not careful, you may expose a DOS risk).
It doesn't actually protect anything. It just obscures the problem. I think this is a form of security through obscurity. This does not mean that it is useless or invalid. It just means the risk. It's hard to know if it really works to make you safer or to sleep better at night. When used in a layer, it may be good.
It does not prevent local attackers. If an attacker can obtain code (even on a different user account, such as a shared server) on the server, they can view the CPU usage so that they can see the past sleep condition. This is an extension, in which case there may be more effective attacks, but at least it is worth noting.
Defending against Dos attacks
All of these technologies require a lot of requests. They are based on statistical techniques that rely on large amounts of data to effectively "average" noise.
This means that to get enough data to actually execute an attack, an attacker might need to make thousands of, hundreds of thousands of, or even millions of requests.
If you are practicing good dos protection (IP-based rate limiting, etc.), then you will be able to bypass many of these style attacks.
But DDoS protection is hard to prevent. By allocating traffic, prevention is more difficult. But it's also harder for attackers because they have more noise to deal with (not just local network segments). So it's not very practical.
But like everything safe, defense in depth. Even if we think this attack is impossible, but if our original protection fails, it is still worth protecting it. By using defense in depth, we can make ourselves more resilient in attacks of all sizes.
Back to the point
There is a clue about whether or not the timing of certain core functions is safe or not in PHP. The specific features being discussed are:
Bin2Hex
Hex2bin
Base64_encode
Base64_decode
Mcrypt_encrypt
Mcrypt_decrypt
Now, why these features? Wells, Bin2Hex and Base64_encode encode output to browsers (encoding session parameters for example) when used frequently. However, it is more important that hex2bin and base64_decode, because they can be used to decrypt secret information (just like the key before the key is used for encryption).
So far, the consensus of most respondents is that in order to get more security, it is not worth making them slower. I agree with that.
But what I disagree with is that it makes them "slow". The change comparison (from) = = to hash_equals is slower because it changes the complexity of the function (best, average, worst) from O (1, N/2, n) to O (n, N, N). This means that it will have a significant impact on the performance of the average situation.
However, changing the encoding function does not affect complexity. They will continue O (n). So the question is, what is the difference in speed? Well, I used PHP algorithms and a time-safe standard to benchmark Bin2Hex and Hex2bin, and the difference was not too significant. The encoding (BIN2HEX) is roughly the same (error range), and the decoding Difference (hex2bin) is approximately 0.5μs. For a string of approximately 40 characters, this is more than 5e-10 seconds.
For me, it's small enough to worry about at all. How many times does an average application call one of the affected functions? Maybe every execution is possible? But what are the potential benefits? Could this be a vulnerability being blocked?
Well, maybe. I don't think there is a good reason to do this, in general, these vulnerabilities will be very difficult in the type of application written in PHP. But with this argument, if the implementation process is fast enough (for me, 0.5μs fast enough), then I don't think there is one important reason not to make that change. Even if it helps prevent a single attack on all millions of PHP users, is it worth it? Is. Will it stop a single attack? I don't know (maybe not).
However, I think there are several features that must be constantly audited for time security:
mcrypt_\*
hash_\*
password_\*
openssl_\*
MD5 ()
SHA1 ()
Strlen ()
SUBSTR ()
Basically, everything we know is used with sensitive information, or it's used as a primitive in sensitive operations.
As for the rest of the string functions, or there is no need to make them time safe (like Lcfirst or strpos), or it is impossible (like trim) or has been completed (like strlen), or it does not have any business in PHP (such as Hebrev) ...
Follow
As a result, Hackernews and Reddit published this article. Comments have several common themes, so I'll follow up here. I also edited the post inline to resolve these issues.
It is not possible to keep the code constant time
Well, I should clarify the meaning of "unchanging". In the absolute sense, I do not mean the same. I mean to be constant relative to the secret. This means that time does not depend on the data we are trying to protect. Therefore, in general, absolute time may fluctuate for many reasons. But we do not want the value of our attempts to protect it from affecting it.
This is the difference:
for ($i = 0; $i < strlen ($_get[' input '), $i + +) { $input. = $_get[' input '] [$i];}
This is a variable time, but leaking what is secret,
And
$time = 0;for ($i = 0; $i < strlen ($_get[' input ']), $i + +) { $time + = ABS (ord ($_get[' input '] [$i])-Ord ($secret [$i]) );} Sleep ($time);
Now, this is an absurd example. But it shows that both will change time based on investment, but it will also vary depending on the secret we are trying to protect. This is what we mean when we say "constant time," not because of the value of the secret.
How to clamp a specific run time?
I have solved the above problem in the main body of the post.
Do not protect DOS work?
Is. I have added it to the list of defenses. But given that it does not work for DDoS (although time differences are difficult to identify), I will not ignore it for this reason.
It's not practical.
Well, that's not the case. There are videos and files and tools as well as more tools and more papers and more videos.
So if the attackers have been talking about it, it would have been a good thing.
However, successful use of timed attacks requires a lot of work. As a result, attackers often look for easier and more common attacks, such as SQLI,XSS, remote code execution, and so on, but this actually depends on more factors. If you are protecting the session identifier of a blog site, you may not have to worry about it. However, if you protect the encryption key that is used to encrypt the credit card number ...
From a practical point of view, I will not worry about timed attacks unless I am sure that other potential media are safe. In that case, I think it's interesting and worth knowing. But like everything else in security and programming, it's all about trade-offs.