소프트웨어 개발보안 가이드 분석(2021) : 솔트 없이 일방향 해시함수 사용

패스워드를 저장 시 일방향 해시함수의 성질을 이용하여 패스워드의 해시값을 저장한다.

만약 패스워드를 솔트(Salt)없이 해시하여 저장한다면, 공격자는 레인보우 테이블과 같이 해시값을 미리 계산하여 패스워드를 찾을 수 있게 된다.

소프트웨어 개발보안가이드(2021), 한국인터넷진흥원

 

해시함수를 통한 추출한 해시값은 같은 값과 동일한 해시함수를 사용하면 계속 동일한 해시값을 추출합니다. 이러한 특징 때문에 해시값은 두 개의 데이터를 비교하여 무결성을 검증하는데 많이 사용됩니다. 공격자들은 이러한 해시의 특징을 이용해 자주 사용되는 비밀번호의 해시값들을 미리 만들어 두고 모두 대입하는 공격을 레인보우 테이블 공격이라고 합니다.

이번 포스팅에서는 이러한 공격을 대응하기 위해 사용하는 솔트와 솔트 없이 사용하는 일방향 해시함수 사용에 대해 알아보고 취약한 코드 예제와 이를 방어하기 위한 안전한 코드 작성 방법을 살펴보려고 합니다.

 

솔트(Salt)

일반적으로 비밀번호 해싱 과정에서 사용되는 추가적인 무작위 데이터입니다.

솔트는 일방향 해시 함수에 사용자의 비밀번호와 결합하여 해시값을 생성합니다.

이렇게 함으로써 동일한 비밀번호라도 서로 다른 솔트를 사용하여 각각 다른 해시값이 생성되어, 레인보우 테이블과 사전 공격에 대응할 수 있습니다.

 

예시로 솔트를 적용하지 않으면 비밀번호가 7이고, 해시함수가 비밀번호*3이라고 생각하면 해당 비밀번호의 해시값은 21입니다.

반면, 비밀번호에 x의 값이 추가로 들어가게 된다면 해시값은 (7+x)*3이 됩니다. 따라서 자주 사용되는 해시값이 아니기 때문에 일치하는 해시값이 없을 것입니다. 설령 일치하는 해시값이 나타나도 그것은 7+x에 대한 해시값이기 때문에 실제 비밀번호인 7을 알아내는 것은 공격자 입장에서 매우 어려운 일입니다.

일방향 해시함수의 솔트 유무 차이
소프트웨어 개발 보안 가이드(행정자치부 / 한국인터넷진흥원)

솔트 없이 일방향 해시함수 사용 이란?

일방향 해시함수는 입력값을 고정된 크기의 고유한 해시값으로 변환하는 알고리즘입니다. 이는 주로 비밀번호 저장 등에서 사용되는데, 해시함수를 사용하면 원본 값을 알 수 없어 보안성이 높아지기 때문입니다. 그러나 "솔트 없이 일방향 해시함수 사용"은 해시값을 생성할 때 추가적인 무작위 문자열인 솔트(Salt)를 사용하지 않는 것을 의미합니다. 이것은 보안적인 측면에서 큰 위험을 안고 있습니다.

공격 메커니즘

솔트를 사용하지 않은 일방향 해시함수는 두 가지 주요한 공격이 있습니다.

​1. 레인보우 테이블 공격

  • 레인보우 테이블은 미리 계산된 해시값과 그에 대응하는 원본 값을 포함하는 테이블입니다. 이를 이용하여 해시 된 값의 원본을 쉽게 찾을 수 있습니다. 솔트가 없는 경우 동일한 원본 값에 대해 항상 동일한 해시값이 생성되므로, 레인보우 테이블 공격에 취약해집니다.

2. 딕셔너리 공격

  • 사전에 많은 가능한 원본과 해시값을 쌍으로 저장한 후, 해시값을 이용해 원본을 찾는 공격입니다. 이 역시 솔트가 없는 경우 동일한 원본 값은 항상 동일한 해시값을 생성하므로, 공격이 가능한 경우입니다.

 

취약한 웹 애플리케이션의 예

    public class WeakHashingExample {
        public static void main(String[] args) {
 1          String password = "myPassword";
            
 2              MessageDigest md = MessageDigest.getInstance("SHA-256");
 3              byte[] hashBytes = md.digest(password.getBytes());
 4              String hash = java.util.Base64.getEncoder().encodeToString(hashBytes);

        }
    }
  • 1 : 해싱 할 비밀번호를 임의로 지정하였습니다.
  • 2 : SHA-256 해시 알고리즘을 사용할 수 있도록 선언하였습니다.
  • 3 : 비밀번호를 바이트 배열로 변환하여 해시값을 생성합니다.
  • 4 : 생성된 해시값을 Base64로 인코딩하여 문자열로 변환합니다.
  • 결과적으로 hash 변수에 myPassword의 해시값이 들어 있습니다. 하지만 아무런 솔트(Salt) 값을 첨가하지 않았기 때문에 공격자들은 미리 만들어 두었던 해시값들과 비교하여 패스워드 원본을 획득할 수 있습니다.

 

시큐어코딩 적용 방법

    public class SecureHashingExample {
        public static void main(String[] args) {
            String password = "myPassword";

    1       SecureRandom random = SecureRandom.getInstanceStrong();
    2       byte[] salt = new byte[16];
    2       random.nextBytes(salt);

            MessageDigest md = MessageDigest.getInstance("SHA-256");
    3       md.update(salt);
    4       byte[] hashedPasswordBytes = md.digest(password.getBytes());

    5       String hashedPassword = Base64.getEncoder().encodeToString(hashedPasswordBytes);
        }
    }
  • 1 : 랜덤 한 솔트(Salt)를 생성하기 위해 SecureRandom 객체를 생성합니다.
  • 2 : 16바이트의 랜덤 한 솔트값을 생성합니다.
  • 3 : 생성한 솔트를 해시 함수에 추가합니다.
  • 4 : 비밀번호에 솔트를 추가하여 "솔트를 사용한 일방향 해시함수"의 해시값을 생성합니다.
  • 5 : 생성된 해시값을 Base64로 인코딩하여 문자열로 반환합니다.
  • 결과적으로 hashedPassword의 해시값에는 솔트와 일방향 해시함수를 사용하여 해시값을 생성하였습니다. 이렇게 생성된 해시값은 공격자들이 미리 확보한 해시값에 대응하는 상황을 회피할 수 있어 보안성을 강화할 수 있습니다.

  • 이전 소프트웨어 개발보안 가이드 분석(2021) : 주석문 안에 포함된 시스템 주요정보
  • 다음 소프트웨어 개발보안 가이드 분석(2021) : 무결성 검사 없는 코드 다운로드