본문 바로가기
[ Program ]/C#

.NET Framework의 강력한 이름 및 보안

by 관이119 2012. 9. 18.
출처 BIT Inside | 코아란
원문 http://blog.naver.com/hch0808/100013757279

 

 

.NET Framework의 강력한 이름 및 보안

Keith Brown
DevelopMentor

 

요약

 

강력한 이름은 어셈블리를 고유하게 식별하여 전역 어셈블리 캐시에 배치되도록 하는 데 필요합니다. 또한 Microsoft .NET Framework 공용 언어 런타임에서 버전 지정 시스템을 사용하는 데도 필요합니다. 강력한 이름에 대한 자세한 정보와 사용 방법을 살펴봅니다(10페이지/인쇄 페이지 기준).

 

목차

 

GUID에서 공개 키까지
RSA 및 디지털 서명
CLR 및 공개 키
강력한 이름 및 확인
강력한 이름 및 .NET 보안 정책
공개 키 및 버전 지정
서명 연기를 사용하여 노출 줄이기
개발 팀 보호
결론

 

GUID에서 공개 키까지

 

강력한 이름의 근본적인 개념을 이해하려면

Microsoft Windows 플랫폼의 이전 구성 요소 네이밍 스키마인 GUID(Globally Unique Identifier)를 살펴보는 것이 좋습니다.

 

GUID는 COM 분야에서 여러 항목을 명명하는 데 사용되는 128비트(16바이트) 고유 정수입니다.

레지스트리 작업을 해 본 적이 있다면 오늘날 무수히 많은 GUID가 사용되고 있다는 것을 알고 있을 것입니다.

COM 프로그래머라면 GUID가 매우 세부적인 항목을 명명하는 데 사용되었기 때문에 이러한 사실을 알고 있습니다.

 

각 COM 클래스, COM 인터페이스, 응용 프로그램, 형식 라이브러리 및 열거에는 고유의 GUID가 필요합니다.

실제로 GUID는 과도할 정도로 사용되고 있습니다.

 

Windows 2000 이전에는 GUID가 DCE RPC의 일부분으로 정의된 UUID(Universally Unique Identifier)를 기준으로 생성되었습니다.

간단히 말해 UUID의 고유한 특성은 현재 날짜 및 시간에서 파생되며,

고유 48비트 IEEE 802 주소는 NIC(네트워크 인터페이스 카드)에서 파생됩니다.

 

Windows 2000부터 GUID는 더 이상 이 알고리즘을 기준으로 생성되지 않습니다.

대신 CryptoAPI에서 제공하는 난수 생성기를 호출하여 생성된 임의의 16바이트 정수가 GUID입니다.

이렇게 변경된 가장 큰 이유는 1999년 초에 나타난 개인 정보 보호 문제 때문입니다.

 

당시 Microsoft Office는 데이터 파일에서 GUID를 유일한 식별자로 사용했습니다.

그러나 불행하게도 GUID에서 사용된 NIC 주소를 통해 문서가 작성자에게 다시 연결될 수 있는 부작용이 있었습니다.

따라서 데이터 파일에 GUID를 넣으면 익명성이 보장되지 않으므로 보안상 좋지 않았습니다.

 

COM에서의 모든 명명 문제를 파악한 CLR(공용 언어 런타임) 팀은

구성 요소를 고유하게 식별할 수 있는 보다 나은 방법을 만들고자 했습니다.

그리고 핵심적인 결정은 계층 구조 네이밍 스키마를 사용하기로 한 것이었습니다.

COM에서 GUID를 사용할 때와 같이 개별 형식마다 고유 식별자를 지정하는 대신,

CLR 형식은 네임스페이스를 비롯한 전체 형식 이름과 형식이 패키지된 어셈블리의 이름을 기준으로 식별됩니다.

덕분에 클래스, 인터페이스 등에 간단한 형식 이름을 사용할 수 있게 되었습니다.

 

로더는 어셈블리의 이름을 각 형식 이름의 일부분으로 간주하므로 실제로는 네임스페이스가 컴파일 시간 명명 충돌을 해결하는데

중요함에도 불구하고 각 어셈블리에 공간 및 시간적으로 고유한 이름을 지정하기만 하면 됩니다. 

계층 구조 스키마를 사용하여 명명함으로써 GUID가 너무 길어지는 문제를 완전히 해결했습니다.

이로써 CLR 팀은 고유 어셈블리 이름을 생성하기 위한 알고리즘을 찾아내기만 하면 되었습니다.

 

이름의 일부분은 GUID와 비슷한 큰 난수일 가능성이 있습니다.

또한 충돌에 어느 정도 더 저항할 수 있도록 16바이트보다 클 수도 있습니다.

따라서 사용자가 어셈블리를 명명할 때 사용한 것과 동일한 식별자를 다른 사람이 실수로 선택하는 일이 없게 됩니다.

그러나 공격자가 사용자의 어셈블리와 비슷하게 보이는 트로이 목마 어셈블리를 고의적으로 만드는 것은 막지 못합니다.

이를 해결하기 위해 CLR 팀은 흥미로운 결정을 내렸습니다.

단순히 큰 난수를 사용하는 대신 매우 큰 임의의 소수 두 개를 곱해서 얻은

128바이트 숫자인 1024비트 RSA 공개 키를 사용하기로 결정한 것입니다.

 

강력한 이름을 이해하기 위해서는 암호화의 역할을 이해하는 것이 중요하므로 이번에는 암호화에 대해 알아보겠습니다.

 

RSA 및 디지털 서명

 

이 항목에 대한 자세한 소개는 Practical Cryptography(Ferguson/Schneier 공저, Wiley 2003: 영문)를 참고하십시오.

간단히 말해 RSA의 기초적인 개념은 키가 공개 키 및 개인 키의 한 쌍으로 생성된다는 것입니다.

 

개인 키는 비밀이므로 아무에게 알려 줘서는 안 됩니다.

반대로 공개 키는 다른 사람과 공유할 수 있습니다.

공개 키를 가지고 개인 키를 알아내는 것은 불가능한 것으로 알려져 있습니다.

공개 키를 사용하여 암호화한 데이터는 개인 키로만 해독할 수 있으며,

개인 키를 사용하여 암호화한 데이터는 공개 키로만 해독할 수 있습니다.

 

RSA 키는 디지털 서명을 통해 데이터의 무결성을 보장하는 데 사용될 수 있습니다.

일부 데이터에 서명하려면 먼저 암호화 해시 알고리즘을 사용하여 해당 데이터를 해시한 다음

개인 키를 사용하여 최종 해시 값을 암호화합니다.

 

실제로 서명은 이 암호화된 해시 값일 뿐입니다.

데이터와 서명을 게시하면 사용자의 공개 키를 알고 있는 모든 사람이 데이터를 해시하고

자신의 해시 값과 사용자 서명 안에 있는 해시 값을 비교하여 서명을 확인한 다음 사용자의 공개 키를 사용하여 해독할 수 있습니다.

 

즉, 받은 데이터가 원래 서명된 데이터와 동일하고 서명자가 개인 키를 알고 있는 경우에만 해시가 일치하는 것이 기본 개념입니다.

 

이 기술을 어셈블리에 적용하면 서명을 위조할 수 없기 때문에 공격자가 사용자의 어셈블리를 트로이 목마 버전으로 바꿀 수 없습니다.

물론 그 사람은 사용자의 개인 키를 몰라야 합니다.

따라서 개인 키를 보호하는 것은 매우 중요하며, 개인 키를 주고받는 방법을 설명하는 것이 본 문서의 주요 목적 중 하나입니다.

 

RSA 공개 키를 어셈블리 이름의 일부분으로 사용할 경우 CLR 팀은 일석이조의 효과를 얻습니다.

 

첫째는 안전성입니다.

 

임의로 생성되는 RSA 공개 키는 기본 특성상 충돌하는 경우가 없으므로 실수로 명명 충돌이 발생하지 않습니다.

실제로 1024비트 공개 키는 GUID보다 8배 큽니다.

 

둘째는 보안입니다.

 

대응하는 개인 키를 사용하여 어셈블리에 서명할 수 있으므로 공격자가 사용자의 어셈블리를 자신의 코드로 바꿀 수 없습니다.

그러나 모든 보안 조치와 마찬가지로 여기에는 개인 키를 항상 비밀로 간직해야 하는 책임이 따릅니다.

개인 키가 노출될 경우에는 보안상 위험해지며 심각한 호환성 문제가 발생할 수 있습니다.

 

CLR 및 공개 키

 

강력한 이름의 어셈블리에는 공개 키가 할당됩니다.

이 할당 작업은 컴파일러에서 처리합니다.

예를 들어 자신의 키 쌍을 생성할 경우 다음과 같이 단순히 컴파일러에 키 파일의 위치를 알림으로써

새로 만드는 어셈블리에 공개 키를 할당할 수 있습니다.

 

using System.Reflection;
[assembly: AssemblyKeyFile(@"c:\temp\mykeyfile")]

class Foo {...}

 

Microsoft Visual Studio .NET에서 마법사에 의해 생성된 대부분의 프로젝트는 이 특성을 AssemblyInfo라는 파일에 넣지만

사용자는 원하는 원본 파일에 넣을 수 있습니다.

컴파일러에서는 이 특성을 발견하면 전체 공개 키를 어셈블리의 메타데이터에 복사하고 개인 키를 사용하여 디지털 서명을 만듭니다.

이 작업은 어셈블리에서 파일을 해시하고, 해당 해시 값을 어셈블리의 매니페스트에 통합하며, 매니페스트를 해시하고,

마지막으로 대응하는 개인 키를 사용하여 이 최종 해시 값을 암호화한 다음

어셈블리에 있는 또 다른 메타데이터 블록으로 감추는 순서로 이루어집니다.

 

간단하게 요약하여 설명한 이 방법을 컴파일러가 사용하려면 c:\temp\mykeyfile 파일에 공개 키뿐만 아니라 개인 키도 있어야 합니다. 이 문서의 뒷부분에서 코드 서명에 대한 보다 안전한 접근법을 배울 수 있습니다.

ILDASM을 사용하여 어셈블리의 매니페스트를 살펴보면 그림 1과 같이 공개 키를 확실하게 볼 수 있습니다.

 

 

그림 1. 어셈블리에 할당된 공개 키

 

서명이나 중간 해시 값은 표시되지 않습니다.

때로는 이것이 강력한 이름에 대해 배우는 사람들을 혼란스럽게 하기도 합니다.

ILDASM은 디스어셈블러이기 때문에 서명이나 중간 해시 값을 표시하지 않습니다.

어셈블리에 컴파일될 수 있는 IL을 만들기만 하면 됩니다.

그리고 서명 및 해시 값은 컴파일러(이 경우 ILASM)에서 출력됩니다.

 

어셈블리에 공개 키가 할당되었는지 보려면 강력한 이름 도구인 SN.EXE를 사용하십시오.

 

sn -Tp foo.dll

 

그러면 어셈블리의 공개 키가 출력되거나 어셈블리에 강력한 이름이 지정되지 않았다는,

즉 공개 키가 할당되어 있지 않다는 의미의 메시지가 나타납니다.

그러나 어셈블리에 공개 키가 있다고 해서 반드시 대응하는 서명이 있거나 해당 서명이 유효한 것은 아닙니다.

서명의 존재 여부와 유효성을 테스트하려면 다음 명령을 사용하십시오.

 

sn -vf foo.dll

 

그러면 SN.EXE가 어셈블리에 있는 각 파일의 자체 해시를 계산하여 어셈블리 바이너리가 서명 이후에 변경되지 않았는지 확인합니다. 그런 다음 어셈블리 매니페스트의 자체 해시를 계산하고, 어셈블리에 패키지된 서명(ILDASM에 표시되지 않는 서명)을 해독하고,

자체적으로 계산한 해시 값을 해독된 서명과 비교합니다.

해시가 일치하면 어셈블리가 유효하다는 내용을 보고하고 그렇지 않으면 오류를 보고합니다.

나중에 자세히 설명할 서명 연기된 어셈블리의 경우처럼 단순히 어셈블리에 아직 서명이 적용되지 않아서

오류가 발생할 수도 있습니다.

 

.NET Framework와 함께 제공되는 도구를 사용하여 새 어셈블리를 GAC(전역 어셈블리 캐시)(GACUTIL.EXE 또는 Fusion 캐시 뷰어)에 설치하면 다음 명령에 해당하는 서명 확인이 수행됩니다.

 

sn -v foo.dll

 

GAC에 상주하지 않는 강력한 이름의 어셈블리를 CLR에서 로드할 때마다 동일한 상황이 발생합니다.

공개 키가 있으면 서명을 확인합니다.

sn -vf 명령과 sn -v 명령 사이의 미묘한 차이는 후자의 경우 관리자가 신뢰하는 항목으로 등록한 공개 키에 대해서는

서명 확인을 건너뛴다는 것입니다.

신뢰하는 항목에 대해서는 나중에 자세히 설명하겠습니다.

요약하자면 CLR은 로드할 때 또는 어셈블리가 GAC에 설치될 때 어셈블리 서명을 확인합니다.

 

GAC는 신뢰된 리포지토리로 간주됩니다.

GAC에 설치된 어셈블리를 수정하지 못하도록 막는 유일한 보호 장치는 GAC의 모든 항목에 있는 강력한 파일 시스템 ACL입니다.

이는 CLR 및 기타 운영 체제 바이너리를 보호하는 ACL과 근본적으로 동일합니다.

 

공격자가 컴퓨터의 관리 권한을 가지게 되면 GAC에 있는 어셈블리를 트로이 목마 버전으로 바꿀 수 있습니다.

이 경우 CLR은 GAC에서 어셈블리를 로드할 때 서명을 다시 확인하지 않으므로

해당 어셈블리가 트로이 목마 버전으로 바뀐 것을 알지 못합니다.

하지만 외부인이 GAC에서 어셈블리를 수정하거나 바꿀 수 있을 정도의 파일 시스템 권한을 가지게 되면

CLR 바이너리(MSCORWKS.DLL 및 유사 항목) 또는 운영 체제 자체에 대해서도 동일한 작업을 수행할 수 있습니다.

로드 시간을 줄이기 위해 CLR은 GAC에서 로드된 어셈블리의 서명을 다시 확인하지 않습니다.

 

강력한 이름 및 확인

 

CLR 어셈블리 해결 프로그램은 어셈블리를 약한 방법 또는 강한 방법의 두 가지 방법으로 참조합니다.

약한 방법은 파일 이름에서 확장명을 뺀 약식 어셈블리 이름만 고려합니다.

예를 들어 FOO.DLL의 약식 어셈블리 이름은 FOO입니다. 로드할 때 버전 확인을 수행하지 않습니다.

 

반대로 강력한 이름은 약식 이름과 그 외에 버전 번호, culture 및 공개 키의 세 부분으로 구성됩니다.

공개 키를 어셈블리에 할당하면 "강력한 이름"이 지정된 것으로 간주됩니다.

이 어셈블리를 참조하는 다른 어셈블리는 이 어셈블리에서 네 부분으로 구성된 이 강력한 이름을 사용합니다.

 실제로 이는 곧 어셈블리를 GAC에 넣고 버전 정책의 이점을 활용할 수 있다는 의미입니다.

 

공개 키, 서명, 해시 등의 개념에 너무 신경을 쓰다 보면 실제로 무엇이 어떻게 보호되는지를 잊기 쉽습니다.

CLR이 보증하려는 것은 다음과 같습니다.

FOO.DLL이라는 어셈블리를 빌드하고 서명하면, 즉 강력한 이름을 지정하면 컴파일 시

자신의 어셈블리에서 FOO.DLL을 참조하는 모든 사람에게 사용자(강력한 이름 뒤에 숨겨진 개인 키를 알고 있는 사람)가 만든 FOO.DLL도 런타임에 전달됩니다.

 

버전 정책에 따라 CLR에서 원본 대신 다른 버전을 사용할 수도 있으므로 정확하게 일치하는 FOO.DLL이 아닐 수도 있지만,

원본 FOO.DLL을 만든 사람이 런타임 시 코드를 만들었다는 보증이 어느 정도는 있어야 합니다.

그러면 제3자가 FOO.DLL을 트로이 목마 버전으로 바꿀 수 없게 됩니다.

 

구현 방법은 다음과 같습니다.

예를 들어 FOO.DLL이라는 강력한 이름의 어셈블리를 참조하는 BAR.EXE라는 어셈블리를 컴파일하는 경우 컴파일러는

FOO.DLL의 강력한 이름을 BAR.EXE의 매니페스트에 기록합니다.

여기에는 공개 키에 대한 참조가 포함됩니다(그림 2 참고).

 

로드 시, 어셈블리 바이너리의 무단 수정을 감시하기 위한 일반 서명 확인 외에도 로더는 FOO.DLL의 공개 키가

 BAR.EXE에 기록된 공개 키와 일치하는지 확인합니다.

따라서 어셈블리 사이의 연결이 보호됩니다.

 

그림 2. 강력한 이름의 어셈블리에 대한 참조

 

이제 BAR.EXE가 보호하는 내용을 알아보겠습니다.

명령 셸을 열고 단순히 BAR <Enter>를 입력하여 BAR.EXE를 실행할 경우에는

BAR.EXE가 트로이 목마로 바뀌었는지 알 수 있는 방법이 전혀 없습니다.

 

이는 문제가 될 수 있습니다.

트로이 목마가 아니라는 확신을 가지려면 BAR.EXE의 알려진 올바른 공개 키를 운영 체제에 제공할 수 있는 방법이 필요합니다.

그렇지 않으면 공격자가 임의로 생성한 키를 사용하여 BAR.EXE의 트로이 목마 버전에 서명할 수 있기 때문입니다.

 

CLR은 BAR.EXE 자체에 일관성이 있는지 확인할 수 있습니다.

즉, 매니페스트에 공개 키가 있으면 서명을 확인할 수 있습니다.

그러나 이 경우에는 BAR.EXE의 원래 작성자가 할당한 공개 키가 아닙니다.

이 문제는 Assembly.Load()를 호출하여 공개 키 정보를 어셈블리 이름과 함께 전달하는

작은 로더 프로그램을 만들어서 해결할 수 있습니다.

이 프로그램을 LOADER.EXE라고 가정하겠습니다.

그러나 이 LOADER.EXE를 확인할 경우에도 동일한 문제가 발생합니다.

 

Microsoft ASP.NET 페이지와 BAR.EXE는 비슷합니다.

다음과 같이 페이지에서 강력한 이름의 어셈블리를 참조할 수는 있지만,

 

<%@assembly name='foo, Version=1.0.0.0,
Culture=neutral,PublicKeyToken=2d7adc3047e7238d'%>

 

ASP.NET에 페이지 어셈블리의 강력한 이름을 알릴 수 있는 방법은 없습니다.

페이지에 강력한 이름을 지정할 수 있다 하더라도 큰 도움이 되지 않습니다.

반면에 web.config 또는 machine.config에 등록된 미리 컴파일된 처리기나 모듈은 강력한 이름을 통해 참조할 수 있습니다.

물론 공격자가 구성 파일을 수정하고 공개 키를 자신의 것으로 바꿀 수 있는 경우에는 확인이 실패합니다.

 

한 가지 세부적인 구현 사항을 살펴볼 필요가 있습니다.

위의 어셈블리 참조에서 전체 공개 키가 아닌 "공개 키 토큰"을 사용하여 어셈블리를 참조하는 방법에 주목하십시오.

이 토큰은 공개 키의 손도장과 같습니다.

강력한 이름을 사용하여 어셈블리를 참조하는 경우 항상 공개 키 토큰을 사용하므로

로더가 제공할 수 있는 최상의 확인 방법은 지정된 손도장이 로드 중인 어셈블리의 공개 키와 일치하는지를 확인하는 것뿐입니다.

 

공격자가, 자신이 개인 키를 알고 있는 RSA 키 쌍에 대해 공개 키의 손도장이 사용자의 것과 동일한

다른 RSA 키 쌍을 생성할 수 있는 확률이 어느 정도인지 알아보겠습니다.

 

공개 키 토큰은 공개 키의 20바이트 SHA1 해시에서 아래쪽의 8바이트를 사용하여 구성됩니다.

8바이트 토큰의 경우 가능한 값은 2^64개입니다. 오늘날의 하드웨어에서는 2^64 단계를 완료하는 것이 꽤 쉽기 때문에

이것만으로는 외부 공격을 바로 차단할 수 없습니다.

하지만 RSA 키 쌍을 계산하는 것은 쉽지 않은 작업이고 이를 2^64번 계산한다는 것은

상당한 금액의 특수 하드웨어를 구입하지 않고서는 어렵습니다.

다시 말해 보통의 해커가 이러한 하드웨어를 혼자서 구입하기는 어렵지만

근처에서 흔히 볼 수 있는 정보 대행사에서는 이를 이미 구비하고 있을 것입니다.

 

보안을 유지하는 데 강력한 이름 확인 기능만을 사용하는 경우에는 손도장에 대한 공격을 염두에 두어야 합니다.

전체적으로 보면 손도장이 보안상 가장 취약한 부분이기 때문입니다.

CLR 팀에서 여러 가지 조건을 검토한 결과 이 보안 방법을 선택한 이유는 쉽게 이해할 수 있습니다.

8바이트 공개 키 토큰을 입력하면 16번의 키 입력이 필요하지만

훨씬 높은 수준의 보안을 제공하는 전체 20바이트 SHA1 해시를 입력하려면 40번의 키 입력이 필요하기 때문입니다.

 

하지만 공개 키 토큰을 입력하는 경우는 그리 많지 않습니다.

특히 .NET Framework 구성 스냅인 같은 도구가 주어질 경우에는 더욱 그렇습니다.

그리고 예를 들어 공개 키 토큰이 Microsoft의 공개 키와 같은 대체 RSA 키 쌍을 계산하고 게시하는 작업은 혼자서도 가능합니다. 앞으로 이 손도장의 길이가 늘어나기를 기대합니다.

손도장에 대한 이 이론적인 논의는 개인 키가 노출될 경우에는 아무런 의미가 없습니다.

공격자가 개인 키를 알고 있으면 트로이 목마로 사용할 모든 어셈블리에 서명할 수 있습니다.

 

강력한 이름 및 .NET 보안 정책

 

개인 키가 노출될 경우 트로이 목마 어셈블리만 문제가 되는 것은 아닙니다.

공격자가 사용자의 컴퓨터에서 강력한 이름의 어셈블리를 트로이 목마 버전으로 바꾸려면 강력한 이름 확인을 통과해야 합니다.

또한 사용자의 컴퓨터에 코드를 넣어야 합니다.

다행히도 이는 간단한 일이 아닙니다.

 

하지만 좀 더 직접적이고 위험한 공격은 신뢰 결정이 이루어지는 리포지토리인 .NET 보안 정책과 관련된 것입니다.

이 정책은 네트워크를 통해 침투할 수 있는 알려진 관리되는 맬웨어(malware, malicious와 software의 합성어)로부터

컴퓨터를 안전하게 보호합니다.

 

다음에 컴퓨터에 관리자로 로그인할 경우 시작 메뉴의 관리 도구에 있는 .NET Framework 구성 도구를 열어 보십시오.

이 도구를 사용하여 런타임 보안 정책을 어느 정도 살펴볼 수 있습니다.

컴퓨터 정책 수준 아래의 코드 그룹 트리를 전체 확장하면 보안 정책이 간혹 강력한 이름을 기준으로

매우 많은 항목을 신뢰하는 것을 볼 수 있습니다.

 

예를 들어 Microsoft_Strong_Name이라는 코드 그룹은 Microsoft 소유의 특수 키로 서명된 모든 로컬 설치 코드를

완전히 신뢰합니다.

이 키는 .NET Framework 자체를 구성하는 핵심 어셈블리에 서명하는 데 사용됩니다.

조직에서 .NET Framework을 도입하고 자동 배포 같은 기능을 사용하기 시작함에 따라 강력한 이름을 기준으로

더 많은 보안 정책이 결정됩니다.

당연히 부분적으로 신뢰되는 환경에서 실행되는 프로그램을 만들고자 하는 조직은 거의 없으므로

보안 정책은 내부 회사의 강력한 이름을 기준으로 완전 신뢰를 부여하도록 구성될 것입니다.

다행히도 강력한 이름을 보안 정책의 일환으로 지정할 경우 손도장뿐만 아니라 전체 공개 키가 지정됩니다.

하지만 이것도 공격자가 개인 키를 알아낸 경우에는 별 도움이 되지 못합니다.

공격자가 개인 키를 알게 되면 원하는 모든 코드에 서명하고 사용자의 강력한 이름을 지정할 수 있습니다.

 

다음 시나리오를 보면 이 문제를 좀 더 확실히 파악할 수 있습니다.

예를 들어 Windows Forms 응용 프로그램이 웹 서비스의 씩(thick) 클라이언트 역할을 하고 있습니다.

그리고 편의상 이 씩(thick) 클라이언트를 자동 배포로 게시했습니다.

 

사용자는 브라우저에서 링크를 클릭하기만 하면 아무런 문제 없이 항상 클라이언트의 최신 버전을 얻을 수 있습니다.

하지만 클라이언트 프로그램은 때때로 P/Invoke를 통한 호출을 사용하여 일부 레거시 코드에 액세스하므로, 사용자가 클라이언트를 배포한 방법으로 인해 예외가 throw됩니다.

네트워크에서 다운로드한 코드는 모바일 코드로 간주되며 기본적으로 관리되지 않은 코드를

직접 호출할 수 있을 정도로 신뢰되지 않습니다.

이 문제를 해결하기 위해 .NET 보안 정책이 조직 전반에서

강력한 이름의 어셈블리를 완전히 신뢰하도록 업데이트했다고 가정합니다.

 

그림 3은 이러한 내용을 보여 줍니다.

 

그림 3. 강력한 이름에 완전 신뢰 부여

 

이 시나리오에서는 공격자가 사용자의 개인 키를 알아내는 경우

자신의 웹 사이트에 사용자의 강력한 이름이 있는 코드를 게시할 수 있습니다.

 

공격자가 조직 내 누군가를 속여서 자신의 코드를 가리키는 링크를 클릭하도록 할 경우

그 코드는 아무런 경고도 없이 링크를 클릭한 사람의 모든 권한으로 자동 실행됩니다.

이는 꽤 위험한 요소입니다.

이와 같은 시나리오를 통해 개인 키를 안전하게 보호해야 하는 이유를 알 수 있습니다.

이 내용은 Authenticode 서명을 만드는 데 사용되는 키는 물론, 다른 유형의 개인 키에도 적용됩니다.

 

.NET 보안 정책에서 게시자 증거로 변환되는 Authenticode 서명을 강력한 이름 대신 사용하여 완전 신뢰를 부여하는

동일한 시나리오에서도 위험 요소는 동일합니다.

그림 3에서와 같이 ACME_Strong_Name 코드 그룹을 루트가 아닌 LocalIntranet_Zone 코드 그룹 아래에 넣어서 Microsoft_Strong_Name을 My_Computer_Zone 코드 그룹 아래에 넣는 것과 같은 방법으로 보안을 더욱 강화할 수 있습니다.

자식 코드 그룹은 부모가 일치하는 경우에만 평가됩니다.

따라서 어셈블리가 LocalIntranet 영역에서 로드되고 어셈블리에 사용자의 강력한 이름이 있는 경우에만

완전 신뢰를 부여하는 정책이 만들어집니다.

물론 이 이후로는 대부분의 회사에서 생각하는 것보다 더 심각한 위험인 내부인의 공격에 대비해야 합니다.

 

이는 아무리 강조해도 부족합니다.

강력한 이름을 사용하는 경우 어셈블리에 서명하는 안전한 프로세스가 필요합니다.

그렇지 않으면 개인 키가 노출될 수 있습니다.

이러한 프로세스에 대한 간단한 설명 이전에 먼저 개인 키를 안전하게 관리해야 하는 한 가지 이유를 더 살펴보겠습니다.

 

공개 키 및 버전 지정

 

버전 정책은 공개 키를 사용할 때 고려해야 하는 흥미로운 요소입니다.

일단 여기서는 어셈블리에는 항상 동일한 공개 키가 있지만 버전은 시간이 지남에 따라 바뀐다고 가정하겠습니다.

버전 정책은 다양한 방법을 통해 시스템 관리자나 소프트웨어 게시자가 응용 프로그램에 로드되는 어셈블리의 버전에

영향을 미칠 수 있도록 합니다.

그러나 버전 정책은 어셈블리 이름과 공개 키가 일관되게 유지되는 경우에만 작동합니다.

다시 말해 어셈블리 FOO의 버전 1을 하나의 공개 키로 게시한 다음,

두 번째 공개 키를 사용하여 FOO의 버전 2를 게시해서는 안 됩니다.

어셈블리 공개 키는 장기간 사용해야 하므로 한 공개 키를 선택하여 어셈블리에 적용한 이후에는

어셈블리의 수명이 다할 때까지 모든 버전에 걸쳐 그대로 유지해야 합니다.

 

이는 PKI(공개 키 인프라) 시스템에서 크게 문제가 되는 키 해지와 정반대입니다.

즉, 개인 키를 잃어 버리거나 개인 키가 노출되었다고 생각되는 경우에는 공개 키를 해지하고 새로 받을 수 있습니다.

그러나 사실 이 과정이 말처럼 쉬운 것은 아닙니다.

그러나 CLR에서 버전 정책 및 GAC를 사용하여 여러 응용 프로그램 간에 공유되는 어셈블리를 관리하는 경우에는

개인 키가 노출되는 경우 버전 지정 및 호환성 면에서 큰 문제가 발생합니다.

새 키 쌍을 만들려면 이전 공개 키를 사용하는 모든 응용 프로그램을 다시 컴파일해야 합니다.

그러면 업그레이드가 어려워지므로 이를 방지하기 위해서라도 개인 키를 잘 보호해야 합니다.

 

보안이 중요한 문제인 경우에는 키를 오프라인으로 저장할 수 있는 하드웨어를 구입하는 것이 좋습니다.

이 하드웨어를 스마트 카드라고 합니다.

스마트 카드에 대해서는 차후에 자세히 설명하는 문서가 나올 것입니다.

하지만 지금도 서명 연기라는 기술을 사용하여 즉시 보안을 강화할 수 있습니다.

 

서명 연기를 사용하여 노출 줄이기

 

개인 키의 노출을 줄이는 가장 간단한 방법 중 하나는 어셈블리를 서명 연기하는 것입니다.

이 기술을 사용하면 컴파일러가 개인 키를 모르는 상태에서 어셈블리를 빌드합니다.

컴파일러에는 비밀 항목이 아닌 공개 키만 필요합니다. 작동 방법은 다음과 같습니다.

 

첫 단계로 강력한 이름에 대해 하나 이상의 RSA 키 쌍을 생성합니다.

아직 스마트 카드 하드웨어가 없는 경우에는 키를 파일 시스템에 저장해야 하므로

아무런 네트워크에도 연결되어 있지 않은 안전한 컴퓨터에 저장해야 합니다.

다음 명령을 사용하여 각 RSA 키 쌍을 생성합니다.

 

sn -k pubpriv

 

SN 도구가 pubpriv라는 파일에 새 키 쌍을 만듭니다.

다음 명령을 곧바로 실행하여 pub라는 두 번째 파일에 공개 키만 복사하십시오.

 

sn -p pubpriv pub

 

pub를 이동식 미디어에 복사하여 다른 컴퓨터로 이동합니다.

이제 pubpriv를 컴퓨터에서 제거하고 저장실에 넣습니다.

첫 번째로 서명한 어셈블리를 제품 그룹 외부의 다른 사람에게 전달하기 전에는 이 파일이 필요하지 않습니다.

어셈블리를 컴파일해야 하는 모든 사람에게 pub를 배포합니다.

pub의 내용은 비밀이 아니므로 노출될까 걱정하지 않아도 됩니다.

 

강력한 이름에 사용할 각 키 쌍마다 이 절차를 따르십시오.

그런 다음 모든 개인 키가 저장실에 안전하게 저장되고 손실, 손상 또는 유출의 우려가 없다고 확신하면

개인 키를 생성할 때 사용한 컴퓨터의 하드 드라이브를 파기합니다.

말 그대로 아무도 찾지 못하게 파기하십시오.

물론 스마트 카드를 사용하고 있었다면 하드 드라이브에 개인 키를 저장할 필요가 없기 때문에 저장 및 유지가 간단해집니다.

따라서 스마트 카드 하드웨어는 반드시 구입하는 것이 좋습니다.

 

어셈블리를 서명 연기하려면 AssemblyKeyFile 특성을 사용하여 필요로 하는

각 개발자의 컴퓨터에 있어야 하는 pub 파일을 참조하십시오.

또한 AssemblyDelaySign 특성을 다음과 같이 적용합니다.

 

[assembly: AssemblyKeyFile(@"c:\keys\pub")]
[assembly: AssemblyDelaySign(true)]

 

그러면 컴파일러가 번거롭게 서명을 생성하지 않고, 만드는 어셈블리에 공개 키를 포함시킵니다.

그리고 나중에 서명을 추가할 수 있도록 어셈블리에 공백을 남깁니다.

마지막으로 이렇게 서명되지 않은 어셈블리를 테스트하는 데 사용하는 컴퓨터에서는

해당 공개 키에 대해 강력한 이름 확인을 건너뛰도록 CLR에 지시해야 합니다.

그러기 위해서는 공개 키의 공개 키 토큰, 즉 이전에 설명한 손도장이 필요합니다.

다음 명령을 사용하여 손도장을 확인할 수 있습니다.

 

sn -t pub

 

예를 들어 공개 키 토큰이 bc19568c6e03e7e6인 경우 컴퓨터에서 확인을 건너뛰도록 하려면

다음과 같이 토큰을 등록해야 합니다.

 

sn -Vr *,bc19568c6e03e7e6

 

그러면 로드할 때 또는 어셈블리를 GAC에 설치할 때 위의 공개 키 토큰이 있는 어셈블리에 대해서는

CLR에서 서명을 확인하지 않습니다.

어셈블리를 개발 팀 외부의 사람에게 전달할 준비가 되었으면 컴파일된 어셈블리를 안전한 컴퓨터로 가져오고,

저장실에서 pubpriv 파일을 가져와 컴퓨터에 설치한 후 다음 명령을 실행합니다.

 

sn -R assemblyfile

 

이 명령은 개인 키를 사용하여 어셈블리에 서명함으로써 컴파일러가 남겨 놓은 공백을 채웁니다.

그리고 이제 또 하나의 하드 드라이브를 파기할 차례입니다.

물론 스마트 카드가 있으면 이렇게 하드 드라이브를 파기할 필요가 없습니다.

하드 드라이브를 파기하고 싶지도 않고 100달러 상당의 스마트 카드 하드웨어를 구입하는 것도 부담스럽다면

개인 키를 임시로 저장해야 할 때마다 하드 드라이브 대신 RAM 디스크를 사용하십시오.

작업 완료 후 컴퓨터를 다시 부팅하면 장비를 제대로 갖춘 해커 외에는 비밀 내용에 액세스할 수 없게 됩니다.

 

개발 팀 보호

 

서명 연기는 완벽하지 않습니다.

팀에서는 개발 중에 자체 어셈블리를 테스트하기 위해 하나 이상의 공개 키에 대한 강력한 이름 확인 기능을

해제해야 하므로, 확인되지 않은 강력한 이름이 있다는 사실만으로 어셈블리를 신뢰해서는 안 됩니다.

또한 공격자가 공개 키를 알게 되면 마치 사용자가 자신의 어셈블리를 서명 연기하듯이

악의적인 어셈블리를 손쉽게 서명 연기할 수 있습니다.

 

공개 키는 빌드하는 강력한 이름의 어셈블리마다 메타데이터로 포함되어 있기 때문에

공격자가 공개 키를 알아내는 것은 쉬운 일입니다.

 

이 문제에 대해서는 팀 구성원에게 반드시 교육해야 합니다.

.NET 보안 정책에서 어셈블리를 식별해야 하는 경우 확인되지 않은 강력한 이름은 앞에서 설명한 대로

공격자가 트로이 목마로 쉽게 만들 수 있으므로 사용하지 말아야 합니다.

개인 키를 팀 구성원만 알고 있으며 모든 어셈블리의 서명에 사용되는 내부에서 발급된

임시 코드 서명 인증서(예전의 Authenticode와 유사)를 사용하는 것이 대안이 될 수도 있습니다.

이 경우 개발 및 테스트 도중의 정책에서는 강력한 이름 증거 대신 게시자 증거를 사용하여 어셈블리를 식별합니다.

 

결론

 

강력한 이름은 매우 뛰어난 기능이지만 그에 따른 막중한 책임이 따릅니다.

CLR에서 강력한 이름을 기준으로 강력한 보안을 유지할 수 있는 기능은 개인 키를 얼마나 잘 보호하는지에 달려 있습니다. 그러므로, 개인 키가 노출되지 않도록 보호하십시오.

[이 자료는 MSDN Library에서 가져왔습니다.]

댓글