Security Risk - Timing Attack - Challenging but Entirely Feasible
Timing Attack is one of the techniques that hackers exploit by leveraging server-side computations to adjust attack payloads accordingly.
In this article, I will present the most common example of this type of attack and discuss prevention methods using Golang.
I. Timing Attack
Timing Attack is a type of side-channel attack in which hackers exploit information obtained from the real-time execution time of a specific logic within a system to gradually deduce the final result. It can be considered a form of manual brute force, as it employs a similar method to deduce the result, but the difference lies in the fact that the hacker needs to invest more effort in analyzing the existing data to determine the next input.
Hackers always attempt to take advantage of programming oversights in commonly used logical code segments, which are often trivialized. They also target vulnerabilities present in popular open-source libraries commonly used by developers.
II. Real-world Example
For developers, there are cases where it is necessary to authenticate requests from clients or server-to-server communication. There are various ways to implement this, typically using JWT, OAuth2, OpenID Connect, or third-party authentication services like Google, Facebook, AWS Cognito, etc. One of the common and easy-to-implement methods is using an API key.
However, API keys can still be compromised through several attack vectors, one of which is Timing Attack.
III. Preventing Timing Attacks
While it may be tempting to resort to extreme measures like disconnecting from the internet or using an "invulnerable" application to deter hackers, these approaches are impractical. We cannot completely eliminate the risk of a system being attacked, but by understanding the techniques involved, we can make the hacker's path more difficult.
Let's use Golang as an example for the situation mentioned above. When using an API key, there will undoubtedly be a logic section that performs a comparison between the API key sent with the request and the key stored on the server. Typically, developers use the default comparison operator (e.g., "==") for this purpose, which may seem simple. However, in reality, this can be exploited by hackers.
var (
a []byte
length int = 1e8
bound int = length / 10
)
func init() {
a = make([]byte, length)
rand.Read(a)
}
func copyStr(changedIndex int) []byte {
b := make([]byte, length)
copy(b, a)
if b[changedIndex] == 0 {
b[changedIndex] = 1
} else {
b[changedIndex] = 0
}
return b
}
func formatString(s int) string {
str := strconv.Itoa(s)
var result string
for i := len(str) - 1; i >= 0; i-- {
if (len(str)-1-i)%3 == 0 && i != len(str)-1 {
result = "." + result
}
result = string(str[i]) + result
}
return result
}
func execute(fn func(a, b []byte) bool) {
for i := length - 1; i > 0; i -= bound {
b := copyStr(i)
startTime := time.Now()
fn(a, b)
endTime := time.Since(startTime)
fmt.Printf("Differences at Index: %s, time execute: %s\n", formatString(i), endTime.String())
}
}
func Compare(a, b []byte) bool {
return string(a) == string(b)
}
func CompareConstantTime(a, b []byte) bool {
return subtle.ConstantTimeCompare(a, b) == 1
}
The code above compares two strings of equal length, but if there is a difference at any index, the == operator stops the comparison and returns the result immediately. This timing discrepancy can be exploited by hackers to deduce the correct API key by observing the different execution times for incorrect and partially correct matches.
We used subtle.ConstantTimeCompare function in the above code to ensure a constant-time comparison. It returns 1 if the two strings are equal, 0 if they are different, and immediately returns 0 if the lengths of the strings are not the same. By using this method, we eliminate timing discrepancies, making it harder for attackers to deduce information based on the time taken for the comparison.
func main() {
fmt.Printf("Length of chars: %s\n\n", formatString(length))
fmt.Println("Normal comparison: using equal operator")
execute(Compare)
fmt.Println("\n" + strings.Repeat("*", 50) + "\n")
fmt.Println("Constant time comparison: using subtle.ConstantTimeCompare")
execute(CompareConstantTime)
}
And the result:
In regular string comparison, the execution time increases as the differences between the strings move towards the end. However, with ConstantTimeCompare, the execution time remains nearly the same, regardless of where the differences occur in the strings.
IV. Conclusion
In reality, the execution time can be influenced by various factors like network latency, different threads, and more. Achieving precise results may require conducting multiple trials to minimize any discrepancies. While Timing Attacks are challenging to execute successfully due to these complexities, they are still possible.
Using ConstantTimeCompare does come with a time cost, so it's best to employ it only when truly necessary. In cases where security and protection against Timing Attacks are critical, it's worth the investment.
Happy coding 🙌🏻