Timing Attack: Memanfaatkan Informasi Waktu Eksekusi⏰

Timing Attack adalah serangan peretasan dengan cara membaca informasi waktu eksekusi pada system. Jadi, hacker ga secara langsung membobol system, melainkan dibaca dulu polanya. Hacker mengukur statistik waktu yang dibutuhkan oleh system untuk memproses sesuatu dari berbagai macam input. Misalnya pada aktivitas-aktivitas sensitif seperti autentikasi, verifikasi, atau sejenisnya.
Use Case
Contoh kasus paling simple adalah saat lupa password dan butuh verifikasi email. System akan mengirimkan link berisi token untuk verifikasi lewat email yang saat diklik akan memberikan user kendali penuh atas akunnya. Contohnya seperti ini:
@Slf4j
@RequiredArgsConstructor
public class SubmitTokenUseCase{
private final SubmitTokenGateway gateway;
public void execute(SubmitTokenRequest request, SubmitTokenPresenter presenter){
try{
String token = gateway.getTokenByEmail(request.email())
.orElseThrow(() -> new FailedToSubmitTokenException("token is not found"));
if(!token.equals(request.token())){
presenter.present(new SubmitTokenResponse("failed"));
return;
}
gateway.consumeToken(token);
presenter.present(new SubmitTokenResponse("success"));
} catch (Exception e){
log.warn("Failed to submit token", e);
presenter.present(new SubmitTokenResponse("failed"));
}
}
}Alurnya cukup sederhana. Cari token by email lalu bandingkan dengan input token dari request. Jika sama maka tandai token bahwa sudah digunakan dan outputnya sukses. Jika beda atau token dengan email yang diinput ga ditemukan maka outputnya gagal.
Finish Early
Masalah pertama adalah ada throws atau return yang mempersingkat alur. Jika token dengan email yang diinput ga ditemukan atau token yang diinput ga sesuai maka alurnya langsung berakhir. Sedangkan jika tokennya benar dan emailnya valid maka ada aktivitas tambahan untuk menandai bahwa token tersebut telah digunakan. Di sini ada perbedaan waktu eksekusi. Misalnya saat email salah waktu eksekusinya 50ms. Saat emailnya benar tapi tokennya salah waktu eksekusinya 100ms. Sedangkan saat emailnya benar dan tokennya benar waktu eksekusinya 300ms. Bagi kita mungkin ga terasa, tapi pola ini dapat dideteksi lewat tools🔬. Celah ini bisa dimanfaatkan hacker untuk menebak email beserta isi tokennya. Kalau waktu eksekusinya sekitar 50ms berarti email tebakan hacker salah. Kalau waktu eksekusinya lebih dari 50ms berarti tebakan emailnya benar. Selanjutnya hacker tinggal nebak isi tokennya sampai benar.
Inconstant Equals
Masalah kedua adalah menggunakan method equals() untuk membandingkan token. Untuk hal-hal sensitif ini berisiko tinggi. Algoritma dari method equals() pada String adalah membandingkan karakter satu-persatu dan jika ditemukan satu karakter yang ga sama di awal maka akan langsung return false. Secara umum ini sangat efisien dan cepat⚡. Sedangkan untuk hal-hal sensitif ini berarti celah buat hacker untuk menebak valuenya. Misalnya value token adalah cvbnm. Saat hacker menginput a doang, waktu eksekusinya akan sangat cepat, misalnya 1ms. Saat hacker menginput b doang, waktu eksekusinya juga 1ms. Lalu saat hacker menginput c doang, waktu eksekusinya 2ms. Hacker jadi dapat petunjuk bahwa kali ini tebakan karakter pertamanya benar💡. Hacker tinggal tebak sisa karakter lainnya dengan memperhatikan waktu eksekusinya tiap karakter sampai tebakannya benar.
Solution: Constant Execution
Solusi untuk keduanya adalah dengan memastikan bahwa mau benar atau salah input yang diberikan user, waktu eksekusinya harus kurang lebih sama tanpa pola yang bisa ditebak. Kita perlu riset berapa rata-rata waktu yang dibutuhkan saat submit email & token yang benar, misalnya 300ms. Maka kita harus pastikan mau benar atau salah waktu eksekusinya harus selalu sekitar 300ms. Selanjutnya kita perlu ganti method equals() dengan method yang lebih aman untuk melakukan komparasi. Pada Java kita bisa menggunakan MessageDigest.isEqual(). Algoritmanya adalah dengan membandingkan tiap byte karakter dan hanya akan behenti setelah semua karakter dari value yang digunakan selesai dikomparasi. Jadi ga akan langsung return false saat mendeteksi ada satu karakter yang beda. Solusi lengkapnya untuk kasus di atas kira-kira seperti ini:
@Slf4j
@RequiredArgsConstructor
public class SubmitTokenUseCase{
private static final long EXPECTED_RESPONSE_IN_MILLIS = 300;
private static final long MILLIS_TO_NANO = 1000_000L;
private final SubmitTokenGateway gateway;
public void execute(SubmitTokenRequest request, SubmitTokenPresenter presenter){
long startedAt = System.nanoTime();
try{
String token = gateway.getTokenByEmail(request.email())
.orElseThrow(() -> new FailedToSubmitTokenException("token is not found"));
if(!MessageDigest.isEqual(token.getBytes(StandardCharsets.UTF_8), request.token().getBytes(StandardCharsets.UTF_8))){
presenter.present(new SubmitTokenResponse("failed"));
awaitMinimumResponseTime(startedAt);
return;
}
gateway.consumeToken(token);
presenter.present(new SubmitTokenResponse("success"));
awaitMinimumResponseTime(startedAt);
} catch (Exception e){
log.warn("Failed to submit token", e);
presenter.present(new SubmitTokenResponse("failed"));
awaitMinimumResponseTime(startedAt);
}
}
@SneakyThrows
private void awaitMinimumResponseTime(long startedAtNanos){
long elapsedMillis = (System.nanoTime() - startedAtNanos) / MILLIS_TO_NANO;
long remainingMillis = EXPECTED_RESPONSE_IN_MILLIS - elapsedMillis;
if(remainingMillis > 0){
TimeUnit.MILLISECONDS.sleep(remainingMillis);
}
}
}Kita perlu catat waktu eksekusi menggunakan System.nanoTime() sebagai waktu mulai. Itu adalah waktu mesin berjalan sejak aplikasi pertama kali dijalankan dalam satuan nano agar perhitungannya akurat. Kemudian di akhir eksekusi kita catat lagi waktu akhirnya menggunakan method yang sama. Kita cari selisihnya dan kurangi dengan waktu hasil riset sebelumnya dalam satuan millis. Jika bersisa maka lakukan sleep sebanyak sisa waktu tersebut. Dengan begini waktu eksekusinya akan selalu konsisten sekitar 300ms, baik ketika inputnya valid ataupun saat tebakan hacker salah. Lalu gunakan MessageDigest.isEqual() untuk komparasi token. Perlu diperhatikan, value token yang dijadikan patokan harus berada di argumen pertama, dan value input dari request yang mau dikomparasi harus berada di argumen kedua di method tersebut. Jangan sampai kebalik karena looping karakternya berdasarkan ukuran value dari argumen pertama.
Verdict
Dengan memahami pola serangan seperti ini kita dapat lebih waspada saat membuat fitur yang membutuhkan keamanan lebih tinggi. Kita perlu membuat system dengan waktu eksekusi yang lebih konsisten saat memproses berbagai macam input terkait hal-hal sensitif. Method equals() secara umum memang bagus, tapi ga cocok digunakan untuk hal-hal sensitif. Sebagian besar bahasa pemrograman sudah ada function khusus untuk hal ini. Pada NodeJS kita bisa menggunakan crypto.timingSafeEqual(). Pada Go bisa menggunakan subtle.ConstantTimeCompare(). Dengan begini hacker akan kesulitan membaca polanya🔒. Aplikasi kita memang jadi sedikit lebih lambat, tapi itu masih lebih mending daripada system kita dikadalin hacker.
