에듀테크 프로젝트 대시보드
타이밍 공격 (Timing Attack)
if password == secret 는 보통 앞에서부터 한 글자씩 비교하다가
다르면 즉시 멈춘다(early return). 그래서 "첫 글자가 맞으면" 두 번째 글자까지 비교하느라 아주 조금 더 오래 걸린다.
공격자는 이 시간차를 위치별로 측정해 브루트포스보다 훨씬 적은 시도로 비밀을 알아낸다.
(6자리 소문자: 완전 탐색 26⁶ ≈ 3억 회 → 타이밍 공격 26×6 = 156회)
가상의 서버가 6글자 비밀 토큰을 가지고 있다. 서버는 요청이 오면 토큰을 앞에서부터 비교하고, 일치한 글자 하나당 약 8ms씩 시간을 쓴다(실제 네트워크·CPU에도 이런 처리 시간이 존재). 측정에는 랜덤 노이즈가 섞여 있어, 공격자는 여러 번 측정해 평균을 내야 한다.
|= 로 누적하기 때문. 신호가 사라지니 공격은 완전 탐색으로 되돌아간다.
# 다르면 즉시 반환 → 앞부분이 맞을수록 루프가 더 돈다 def check(guess, secret): if len(guess) != len(secret): return False for g, s in zip(guess, secret): if g != s: return False # ← early return: 여기서 시간이 갈린다 return True
# 항상 끝까지 돈다. 차이를 OR로 누적 → 실행 시간이 입력과 무관 def check_safe(guess, secret): if len(guess) != len(secret): return False diff = 0 for g, s in zip(guess, secret): diff |= ord(g) ^ ord(s) # 0이면 같음, 비트가 서면 다름 return diff == 0 # 실무에서는 직접 짜지 말고 표준 라이브러리를 쓴다 import hmac, secrets hmac.compare_digest(a, b) # Python # crypto.timingSafeEqual(a, b) // Node.js # MessageDigest.isEqual(a, b) // Java
len(guess) != len(secret) 자체도 길이를 흘린다. 그래서 보통
비교 대상을 해시(고정 길이)로 만든 뒤 상수 시간 비교한다 —
compare_digest(sha256(guess), sha256(secret)).
| 상황 | 새는 것 | 올바른 대응 |
|---|---|---|
| API 키 / 세션 토큰 검증 | 토큰 앞부분 일치도 | compare_digest |
| HMAC 서명(웹훅) 검증 | 서명 바이트 일치도 → 서명 위조 가능 | 상수 시간 비교 필수 |
| 로그인: "아이디 없음" vs "비번 틀림" 응답 속도 | 계정 존재 여부(사용자 열거) | 더미 해시 계산으로 시간 맞추기 |
| 비밀번호 재설정 토큰 조회 | DB에서 LIKE 매칭 시간 | 토큰을 해시로 인덱싱 |
메모리 비교 memcmp (C) | 일치 바이트 수 | CRYPTO_memcmp |
실제 사례: 2009년 Google Keyczar의 HMAC 검증 타이밍 버그, 2011년 OpenID/OAuth 라이브러리 다수,
Java String.equals 로 토큰을 비교하던 여러 프레임워크. 원격에서도 수만 번 평균 내면 LAN에서는 충분히 측정된다.
hmac.compare_digest / timingSafeEqual 같은 표준 함수를 쓴다.