알고리즘/칼럼

브루트포스 공격 특성을 고려한 비밀번호 해싱 알고리즘 선정 및 Spring 적용 (2)

김민석(갈레, 페퍼) 2022. 6. 29. 10:25
반응형

안녕하세요🙌! 개발자 갈레입니다!

 

지난 글 브루트포스 공격 특성을 고려한 비밀번호 해싱 알고리즘 선정(1)에 이어서 비밀번호 해싱 알고리즘 선정을 마무리하고 Spring 프로젝트에 적용까지 해보겠습니다!

 

들어가며

 비밀번호가 암호화된 정보는 복호화가 되면 안돼기 때문단방향 해싱 함수를 썼어야 했습니다.

하지만 조금만 구글링 해도 <그림1>과 같이 SHA 알고리즘의 문제점을 확인할 수 있었죠!

 

<그림1> 구글링 결과

 

 

문제점(요구 사항)이 생겼습니다! 해결하기 위해 개념을 이해하기에 좋은 무기⚔️ '왜❓'를 꺼내봅시다!

 

 

궁금증(호기심)을 가지면 좋은 컨텐츠

  • 그렇다면 SHA-1은 왜 지원 중단 됬을까요?
  • SHA-1은 어떤 문제가 있었을까요?
  • SHA-2는 안전할까요?

 

 글을 읽으시면서 핵심 부분(노란) 꼬리질문(초록) 부분에 집중하며 읽어주세요! 개인적으로 키워드에서 꼬리질문을 하는게 개념을 깊이있게 이해하는 과정이라고 생각합니다:)

 


목차

1. 문제 상황 분석

  • 단방향 해시함수
  • 레인보우 테이블
  • 단방향 해시 함수 문제점

2. 선택 가능한 기술 분석

  • 솔팅, 키 스트레칭
  • Bcrypt

3. 기술 선택 및 구현

  • 기술 선택
  • 패스워드 기능 구현
  • 유지보수에 유리한 형태로 보완

4. 문서화


1. 문제 상황 분석

단방향 해시 함수

SHA 알고리즘은 무슨 문제가 있을까요? 가끔은 추상화된 개념이 문제의 실마리가 되기도 합니다. 추상화된 개념은 본질이기도 하기 때문이죠. 그럼 더 본질적으로 들어가봅시다. 단방향 해시 함수는 무슨 문제가 있을까요? 단방향 해시 함수는 왜 비밀번호 알고리즘으로 적합하지 않을까요? 지난 글에 있었던 암호화 알고리즘 분류 그림을 살펴봅시다!

 

 

<그림2> 암호화 알고리즘 분류

 

눈치 채셨나요? 단방향 해시 함수는 전부 '메세지'라는 키워드가 있습니다.

 

Q. 그렇다면 메세지 인증은 언제 이뤄질까요?

A. 때로는 매 통신마다 이뤄집니다! 실제로 JWT 토큰을 이용하여 로그인을 구현하면, 사용자를 인증하기 위해 매 요청마다 메세지 인증이 이뤄집니다! 통신 속도를 빠르게 하는게 중요한 서버 환경에서 메세지 인증 속도는 당연히 빨라야 합니다. 

 

Q. 그렇다면 메세지 인증 속도가 빠른게 문제일까요?

A. 가정을 해봅시다! 메세지 인증 속도가 빠른게 문제일 경우, 해당 사실로 인해 가장 이득을 보는 곳이 어딜까?

 

 저희는 비밀번호 암호화를 하고 있기 때문에 가장 이득을 보는 곳은 탈취자일 가능성이 높습니다. 그렇다면 탈취자들은 비밀번호를 어떻게 탈취할까요? 그들의 방법에 대해 알아보죠.

 


레인보우 테이블

 레인보우 테이블은 특정 Plain text의 해시 결과 값이 저장된 테이블입니다. 비밀번호를 저장할 때 서버는 대부분 일방향 해시 함수를 사용합니다. 탈취자가 db를 탈취하게 되면 결국 해시 결과 값을 얻겠죠. 해시 값은 단방향 함수로 인해 만들어졌기 때문에 복호화가 불가능합니다. Plain text를 알 수 있는 유일한 방법은 특정 Plain text의 해시 값이 탈취한 db의 pw 해시 값과 같은지 비교하는 방법입니다. 해시 결과 값이 같으면 원본을 추론할 수 있는 원리죠! 해시 충돌이 일어날 수도 있지 않나요? 맞습니다. 해시 충돌이 일어나면 다른 plain text인데 같은 결과값이 나올 수 있죠. 하지만 크게 문제가 되지 않으며, 해당 현상은 드물게 일어나기 때문에 탈취자에게 중요하지 않습니다. 해시 값이 같지만 Plain text가 다를 확률보다 같을 확률이 훨씬 높기 때문이죠.

 

<그림3> 레인보우 테이블

 

왜 Rainbow table이라고 부르나요?
탈취자가 생각하는 모든 비밀번호 후보 spectrum의 해싱 결과가 저장돼있다고 해서 Rainbow table이라고 부른다고 생각할 수 있어요(출처: stackoverflow).

 


단방향 해시 함수 문제점

1. 인식 가능성

 단방향 해시 함수를 사용하여 비밀번호를 암호화 하면 레인보우 테이블을 이용한브루트 포스 공격에 의해 원문이 추론될 수 있습니다. 공격자 입장에선 예상되는 비밀번호 평문(원문)들의 집합으로 Digest(해싱한다는 뜻입니다)를 형성하여 레인보우 공격을 하면 원문을 얻을 수 있겠죠. 거기다 사용자들은 보통 하나의 비밀번호를 다른 프로그램에 반복해서 사용하기 때문에 피해는 한 프로그램에 한정되지 않습니다. 

 

2. 속도

 '문제 상황 분석-단방향 해시 함수'에서 말했듯이, 메세지 인증용 해시 함수는 속도가 빠릅니다. 속도가 빠르다는 뜻은 Digest를 만들기 쉽다는 뜻이고 이는 빠른 레인보우 테이블 형성으로 이뤄집니다. 레인보우 테이블을 빠르게 형성할 수 있다는 뜻은 결국 비밀번호 평문이 추론될 가능성이 높다는 뜻과 동일합니다.

 

 단방향 해시 함수를 사용할 때,  탈취 위험을 최소화 하는 방법은 무엇일까요? 레인보우 테이블 생성을 어렵게 만들면 됩니다. 인식 가능성을 낮추는 방법레인보우 테이블 생성 속도 자체를 낮추는 방법이 있습니다. 다음 목차에서는 레인보우 테이블을 느리게 만드는 법에 대해 알아봅시다! 선택 가능한 기술 분석까지만 알아보면 드디어 Spring 프로젝트에 적용 part가 나옵니다:) 

 


2. 선택 가능한 기술 분석

 레인보우 테이블 형성을 어렵게 하고 생성 속도를 느리게 만드는 방법엔 크게 3가지가 있습니다. 솔팅(salting), 키 스트레칭(key stretching)과 느린 해싱 알고리즘에 대해 알아보겠습니다.

 

솔팅(salting)

 솔팅은 유저 패스워드에 특정 문자열을 붙인 후 해싱하는 방법입니다. 재료(패스워드)에 조미료(salt)를 뿌려서 요리(hashing)한다고 생각하면 됩니다🙂.  해당 방식을 사용하면 솔팅을 고려하지 않은 탈취자가 만든 레인보우 테이블은 아무 의미가 없어지게 됩니다. 탈취자가 솔팅을 고려 했다고 하더라도, 서버에서 모든 패스워드마다 다른 솔팅 값을 부여했다면 레인보우 테이블 만들기가 매우 어려워지겠죠. 즉 솔팅을 이용하면 인식 가능성이 낮아집니다. <그림4>는 솔팅을 이용하여 패스워드 해시 솔팅을 하는 방법을 보여줍니다.

<그림4> 패스워드 해시 솔팅

 


키 스트레칭(key stretching)

 키(패스워드)를 테스트 하는데 필요한 자원(시간)을 증가시켜서 브루트 포스 공격으로 부터 더 안전하게 만드는 방법을 키 스트레칭이라고 합니다. 보통 하나의 키에 해당하는 해시를 만드는데 일정한 시간(0.2초) (출처:d2.naver)을 설정하여 브루트 포스를 이용한 레인보우 테이블 형성 시간을 굉장히 늘려줍니다. 그럼 하나의 해시를 만드는 시간을 어떻게 일정하게 만들까요? 해시 함수를 N번 적용하게 만들어서 해당 시간을 맞추게 됩니다. 많이 돌리면 돌릴 수록 해시 값을 얻을 수 있는 시간이 늘어나겠죠! 실제로 d2.naver에서는 키 스트레칭을 사용하여 얻을 수 있는 속도 이득을 아래와 같이 표현합니다.

최근에는 일반적인 장비로 1초에 50억 개 이상의 다이제스트를 비교할 수 있지만, 키 스트레칭을 적용하여 동일한 장비에서 1초에 5번 정도만 비교할 수 있게 한다. GPU(Graphics Processing Unit)를 사용하더라도 수백에서 수천 번 정도만 비교할 수 있다. 50억 번과는 비교할 수도 없을 정도로 적은 횟수다. 앞으로 컴퓨터 성능이 더 향상되면 몇 번의 반복을 추가하여 보완할 수 있다. 출처: (출처:d2.naver)

 

 

<그림5> 솔팅과 키 스트레칭을 사용한 다이제스트 형성

 

 


 근데 이러한 여러 해결 방법을 적용해도 바꿀 수 없는 본질적인 문제가 있었습니다. 이 모든 일을 하는 이유는 메시지 인증용 해시 함수가 빠르기 때문이였죠! 그렇다면 빠르지 않은 해시 함수를 사용하면 되지 않을까요? 애초에 일방향 알고리즘 자체가 느리게 설계됐다면 본질적인 문제가 해결될겁니다. 

 


Bcrypt

 Bcrypt 알고리즘은 애초에 패스워드 저장 목적으로 설계됐습니다. 따라서 해시 결과를 만드는 것 자체가 시간이 오래 걸리죠. 구체적으로는 솔팅과 키 스트레칭을 결합한 계산 구조이며, 이러한 구조를 가진 함수들을 Adaptive Key Derivation Functions라고 부릅니다. 이를 사용하면 GPU를 사용한 병렬화가 어렵기 때문에 브루트 포스를 이용한 레인보우 어택이 어려워집니다.

 


드디어 여기까지 오셨군요👏👏! 

이제 기술을 선택하고 구현을 할 차례입니다! 

바로 들어가보도록 하죠!🏃‍♂️🏃‍♂️

 

3. 기술 선택 및 구현

기술 선택

1. 느린 해싱 속도

  • sha 알고리즘은 메세지 인증 용도로 개발되어, 레인보우 테이블을 쉽게 만들 수 있기 때문에 브루트 포스에 취약하다는 것을 알았습니다. 그에 반해 Bcrypt 알고리즘은 비밀번호 해싱 용도로 개발 됐기 때문에 브루트 포스에 비교적 강하다고 판단했습니다.

2. 스프링 표준

  • Bcrypt는 스프링이 표준으로 택하고 있는 패스워드 해싱 알고리즘입니다. 실제로 PasswordEncoderFactories 내의 createDelegatingPasswordEncoder 함수에서는 Bcrypt를 사용하고 있죠. 인지도 있는 프레임워크가 특정 알고리즘을 선택했다는 것은 결국 해당 알고리즘이 안전하다는 것을 뜻하기도 합니다.

패스워드 기능 구현

Bcrypt 알고리즘을 적용해봅시다! 

 

1. 문제 상황이 발생했습니다! <그림6>을 보면 user 테이블 내 galleis의 비밀번호가 123이네요! 평문으로 저장돼있습니다. 평문으로 저장하지 않게 바꿔보도록하죠!

<그림6> MySql workbench user 테이블

 

2. 패스워드 encoder는 어떻게 사용하지?

- 빈으로 등록해서 사용할겁니다. 패스워드 인코더는 계속 재사용될 수 있고 다른 구현 방식의 인코더로 언제든지 이용될 수 있습니다. 따라서 OCP, DI를 지켜야 하기 때문입니다! 또한

 

3. 어디에 등록해두지?

- 보안 관련된 기능이기 때문에 SecurityConfig에 명시적으로 빈으로 설정해주겠습니다:)

 

<public class SecurityConfig>

<그림7> SecurityConfig 내에 패스워드 encoder 빈으로 등록

 

 

4. 어디서 사용하지?

- 패스워드 암호화 알고리즘은 비즈니스 로직이기 때문에 서비스 layer에 있어야 합니다. 누군가 궁금해할 수도 있습니다. 비즈니스 로직은 화면에 막 보이고 돈과 관련된 것 아닌가요? 틀린 말은 아니지만 비즈니스 로직은 더 큰 개념입니다. 위키피디아에 따르면 비즈니스 로직은 실세계의 규칙에 따라 데이터를 생성,표시,저장,변경하는 부분입니다. 실세계의 규칙과 관련된 데이터는 비즈니스 로직에서 처리돼야 합니다! 패스워드 암호화는 개인정보보호법이라는 요구 사항에서 시작했습니다. 그렇다면 법은 비즈니스 로직일까요? 맞습니다. 법 또한 실세계의 규칙이기 때문이죠.

 

 Layered architecture 입장에서 더 크게 분석해보도록 하죠. Presentation layer는 <그림8>에서 보다 싶이, 사용자의 Input에 따라 적절한 비즈니스 로직을 실행 시키고 그 결과를 적절한 view로 전환해주는 역할만 합니다. 즉 Input - Busniess - Ouput의 흐름 제어를 하는 것이죠. 비밀번호 암호화 알고리즘은 Input과 Output 환경과 상관 없습니다. 그들의 로직과 분리돼야하죠. 오히려 Presentation layer 방식이 어떻게 바뀌든 변하지 말아야할 로직입니다.

 

 Presentation layer의 역할과 비즈니스 로직의 의미를 고려했을 때,
비밀번호 해싱 로직은 서비스 layer에 있어야합니다.

 

<그림8> Layered architecture

 

비즈니스 로직(Business logic)은 컴퓨터 프로그램에서 실세계의 규칙에 따라 데이터를 생성·표시·저장·변경하는 부분을 일컫는다. 이 용어는 특히 데이터베이스, 표시장치 등 프로그램의 다른 부분과 대조되는 개념으로 쓰인다.
출처: 위키피디아

 

<그림9> 서비스 컴포넌트에 빈 등록 후 createUser 함수에 로직 적용

 

 

Encoding 하는 방식의 규약을 정했군요. 모든 Encoding은 서비스 layer에서 하기로 했습니다! 팀원이 있다면 공유해주세요:) 가능하면 문서화까지도요! 문서화가 잘되면 많은 사람👨‍👩‍👦‍👦들이 있는 팀에서 협업을 잘 할 수 있습니다. 글✍️은 말과 달리 흔적이 남기 때문이죠! 

 


유지보수에 유리한 형태로 보완

 우리는 기술을 선택할 때 스프링 표준을 근거로 Bcrypt 해싱 알고리즘을 사용했습니다. 그렇다면 스프링 표준이 바뀌면 어떻게될까요? 우리가 일일히 고쳐줘야할까요? 한줄 밖에 안되지만.. 스프링이 표준을 바꿨다는걸 확인하는 것은 너무 귀찮군요! 스프링이 자신의 표준을 알려주면 어떨까요? 마침 PasswordEncoderFactories에서 표준이 바뀔 때 마다 자동으로 적용되는 팩토리 메소드를 만들어뒀군요! 해당 함수를 이용하면 유지보수에 유리한 형태가 될것 같습니다:)

 

<public class PasswordEncoderFactories>

<그림10> 스프링 표준이 설정된 팩토리 함수

 

 

<그림7>은 유지 보수에 유리한 형태로 보완되어 <그림10>로 변합니다. 이젠 스프링에서 적절한 패스워드 알고리즘을 판단하여 업데이트 되면, 제 프로젝트에서도 자동으로 적용되겠군요! 

<그림11> 유지 보수에 유리한 형태로 보완된 패스워드 encoder 빈

 

 


4. 문서화

 마지막으로 팀 규약을 업데이트 하도록 하죠! 저만 '비밀번호 encoding은 service layer에서 한다'를 하는 것은 의미 없습니다! 팀 work이기 때문이죠! (사실 혼자 개발해도 규약 문서는 필요하다 생각합니다. 기억력은 신뢰할 수 없는 수단이기 때문이죠😢.)

 

 

저희 팀의 WIKI에 있는 Coding convention입니다:)

 

 

저희 팀이 미리 작성한 팀 규약에 따라 서비스 목차에 번호 순으로 규약을 업데이트 하겠습니다. 

 

 

 

 드디어 브루트포스 공격 특성을 고려한 비밀번호 해싱 알고리즘 선정을 했고 Spring에 까지 적용했습니다😃!

긴 여정을 따라 와주셔서 감사합니다🙇‍♂️.

 

 부디 여러분들이 해당 글을 읽으셨을 때 초기에 제가 말씀 드렸던, 왜? Bullet point는? 색깔의 의미를 잘 이해하며 읽으셨으면 합니다. 사실상 두개를 하는 능력이 전 정말 중요하다고 생각합니다. 개념을 제대로 이해할 수 있기 때문이죠.

 

그럼 다음 번에 또 봅시다👋👋!

 

Thanks, im 김민석(갈레)!


 

 

출처

반응형