소년포비의 세계정복!!

[스크랩] [msdn] 코드 보안을 위해 개발자가 알아야 할 10가지 팁 본문

프로그램 세상/C#

[스크랩] [msdn] 코드 보안을 위해 개발자가 알아야 할 10가지 팁

소년포비 2009. 3. 10. 10:16
 
이 기사를 이해하려면 C++, C# 및 SQL에 익숙해야 합니다.
 
Level of Difficulty1 2 3
 

SUMMARY

보안 문제에 발생할 수 있는 상황은 매우 다양합니다. 네트워크에서 실행되는 모든 코드를 신뢰하기 때문에 모든 사용자가 중요한 파일에 액세스할 수 있도록 하고 컴퓨터에서 코드가 변경되었는지 확인하지 않는 경우, 보안 코드를 작성하지 않고 바이러스 보호 소프트웨어 없이 코드를 실행하며 너무 많은 계정에 너무 많은 권한을 제공하는 경우, 침입에 대비하지 않고 기본으로 제공되는 다양한 기능을 사용하며 서버 포트를 열어 놓고 모니터하지 않는 경우 등을 예로 들 수 있습니다. 이외에도 여러 상황에서 문제가 발생할 수 있습니다. 그렇다면 데이터나 시스템에 손상을 주지 않도록 하기 위해 주의해야 할 사항 중 가장 중요한 것은 무엇일까요? 보안 전문가인 Michael Howard와 Keith Brown이 사용자가 주의해야 할 10가지 사항을 제시합니다.


목차



보안은 다차원적인 문제입니다. 어디에서나 보안상 위험이 발생할 수 있습니다. 사용자가 오류 처리 코드를 작성하거나 너무 많은 권한을 제공하는 경우, 서버에서 실행하고 있는 서비스가 무엇인지 잊어 버리는 경우, 모든 사용자 입력을 허용하는 경우 등이 이러한 문제 상황의 예이며 이 밖에도 여러 가지 경우가 있습니다. 다음과 같은 안전한 네트워크 전략에 대한 10가지 주의 사항에 따라 시스템, 네트워크, 코드를 순조롭게 보호할 수 있습니다.


1. 사용자 입력 신뢰의 위험성

이 기사의 나머지 부분을 읽지 않더라도 이 한 가지 사항은 반드시 기억해야 합니다. "사용자 입력을 믿지 말아야 한다"는 것입니다. 데이터가 항상 올바르게 구성되고 제대로 되어 있다고 생각한다면 문제에 직면하게 됩니다. 대부분의 보안상 취약점은 서버 시스템에 잘못된 데이터를 제공하는 공격자로 인해 발생합니다.

입력이 올바른 형식이라고 무조건 신뢰하는 것은 버퍼 오버런, 사이트 간 스크립트 공격, SQL 주입 공격 등과 같은 문제를 일으킬 수 있습니다.

이러한 잠재적인 공격 문제에 대해 자세히 살펴보겠습니다.


2. 버퍼 오버런 방지

버퍼 오버런은 공격자가 필요 이상으로 규모가 큰 데이터를 응용 프로그램에 제공할 때 발생하는 것으로, 내부 메모리 공간에 오버플로가 발생합니다. 버퍼 오버런은 C/C++에서 주로 발생하며 일반적으로 쉽게 해결할 수 있습니다. 버퍼 오버런은 명확하지 않은 버퍼 오버런과 수정하기 어려운 버퍼 오버런 두 가지 유형이 있습니다. 개발자는 내부 버퍼보다 큰 데이터가 외부에서 제공되리라고는 생각하지 않을 것이기 때문에 오버플로가 발생하면 메모리의 다른 데이터 구조에 손상을 일으키고 공격자가 유해한 코드를 실행할 수 있게 됩니다. 일반적인 것은 아니지만 배열 인덱싱 실수로 인해 버퍼 언더플로 및 버퍼 오버런이 발생하는 경우도 있습니다.

다음과 같은 C++ 코드를 살펴봅시다.

void DoSomething(char *cBuffSrc, DWORD cbBuffSrc) {
    char cBuffDest[32];
    memcpy(cBuffDest,cBuffSrc,cbBuffSrc);
}

이 코드에서 잘못된 점은 무엇일까요? 사실 cBuffSrc 및 cbBuffSrc가 데이터를 신뢰하지 않은 코드, 즉 신뢰할 수 있는 원본에서 나온 것이고 데이터의 형식과 크기가 올바른지 확인한 것이라면 이 코드에는 잘못된 점이 없습니다. 하지만 이 데이터가 신뢰할 수 없는 원본에서 나온 것이고 유효성 검사를 하지 않은 경우에는 공격자(신뢰할 수 없는 원본)가 cBuffDest보다 큰 cBuffSrc를 작성할 수 있으며 cbBuffSrc를 cBuffDest보다 크게 설정할 수도 있습니다. memcpy가 데이터를 cBuffDest에 복사할 때 함수의 스택 프레임에서 cBuffDest가 반환 주소 옆에 있기 때문에 DoSomething에서 반환하는 주소는 잘못될 수 있고 결국 공격자가 유해한 작업 수행하는 코드를 만들 수 있게 됩니다.

이 문제를 해결하는 방법은 사용자 입력과 cBuffSrc 및 cbBuffSrc의 데이터를 신뢰하지 않는 것입니다.

void DoSomething(char *cBuffSrc, DWORD cbBuffSrc) {
    const DWORD cbBuffDest = 32;
    char cBuffDest[cbBuffDest];
#ifdef _DEBUG
    memset(cBuffDest, 0x33, cbBuffSrc);
#endif
    memcpy(cBuffDest, cBuffSrc, min(cbBuffDest, cbBuffSrc));
}
이 함수는 버퍼 오버런의 위험을 완화시킬 수 있는 제대로 작성된 함수의 세 가지 속성을 보여 줍니다. 첫째, 호출자에게 버퍼 길이를 제공하도록 요청합니다. 물론 이 값을 무조건 신뢰하지는 말아야 합니다. 둘째, 버퍼가 소스 버퍼를 포함할 수 있는 충분한 크기인지 확인하기 위해 디버그 빌드의 코드에서 버퍼를 검색합니다. 그렇지 않을 경우 액세스 위반이 발생하며 코드를 디버거에 전달합니다. 이 작업을 통해 많은 버그를 찾아낼 수 있습니다. 마지막으로, 가장 중요한 사항으로서 memcpy에 대한 호출이 방어적이기 때문에 버퍼가 소유할 수 있는 데이터만큼만 복사합니다.

Microsoft의 Windows?? Security Push에서는 C 프로그래머를 위해 안전한 문자열 처리 함수 목록을 만들었습니다. 이 목록은 Strsafe.h: Safer String Handling in C(영문)에서 확인할 수 있습니다.


3. 사이트 간 스크립팅 방지

사이트 간 스크립팅 취약점은 웹 관련 문제점으로서 단일 웹 페이지의 결함으로 인해 클라이언트의 데이터에 손상을 입힐 수 있습니다. 다음과 같은 ASP.NET 코드 조각이 있다고 가정해 봅시다.

<script language=c#>
    Response.Write("Hello, " + Request.QueryString("name"));
</script>
이러한 코드를 본 적이 있습니까? 이 코드는 잘못된 코드입니다. 사용자는 일반적으로 다음과 같은 URL을 사용하여 이 코드에 액세스합니다.
http://explorationair.com/welcome.aspx?name=Michael
C# 코드에서는 이 데이터가 항상 올바른 형식이며 이름만 포함한다고 가정합니다. 하지만 공격자는 이 코드를 악용하여 스크립트 및 HTML을 이름으로 제공할 수 있습니다. 즉, 다음 URL을 입력하면
http://northwindtraders.com/welcome.aspx?name=<script>alert('hi!');
</script>
"hi!"라고 표시된 대화 상자 있는 웹 페이지에 연결됩니다. "뭐 어때?"라고 말할지 모르겠지만, 쿼리 문자열에 좋지 않은 스크립트 및 HTML이 있어 사용자의 쿠키를 공격자가 가져가 공격자의 사이트에 게시하게 되면 사용자의 개인 쿠키 정보를 공격자가 갖게 되거나 더 나쁜 상황이 발생하게 됩니다.

이것을 방지할 수 있는 두 가지 방법이 있습니다. 첫째, 입력 내용을 신뢰하지 않고 사용자의 이름이 무엇인지 신중히 살펴야 합니다. 예를 들어 정규식을 사용하여 이름에 일반적인 문자 하위 집합만 포함되어 있으며 크기가 너무 크지 않은지 확인할 수 있습니다. 다음 C# 코드 조각을 사용하여 이러한 작업을 할 수 있습니다.

Regex r = new Regex(@"^[\w]{1,40}$");
if (r.Match(strName).Success) {
    // Cool! The string is ok
} else {
    // Not cool! Invalid string
}
이 코드에서는 정규식을 사용하여 문자열에 1과 40 사이의 영숫자만 포함되는지 확인합니다. 이러한 절차를 거쳐야만 값이 올바른지 확인할 수 있습니다.

하지만 이 정규식으로는 HTML이나 스크립트를 찾아낼 수 없습니다. 따라서 이러한 문자가 발견되면 정규식을 사용하여 잘못된 문자를 찾고 잘못된 문자가 검색된 경우에 요청을 거부하는 방식은 사용해서는 안 됩니다. 이러한 방식으로 찾아낼 수 없는 예외가 있을 수 있기 때문입니다.

두 번째 방법은 입력이 출력으로 사용될 때 모든 입력을 HTML로 인코딩하는 것입니다. 이렇게 하면 위험한 HTML 태그를 줄일 수 있어 이스케이프 문자를 좀 더 안전하게 할 수 있습니다. ASP에서는 Server.HTMLEncode를, ASP.NET에서는 HttpServerUtility.HtmlEncode를 사용하여 문제가 될 수 있는 모든 문자열을 이스케이프할 수 있습니다.


4. sa 사용 권한 제한

신뢰할 수 없는 입력 공격의 마지막 유형으로 SQL 주입을 설명하겠습니다. 많은 개발자들이 Microsoft?? SQL Server™ 또는 Oracle과 같은 백엔드 데이터 저장소와 통신하기 위해 입력을 받는 코드를 작성하고 이 입력을 사용하여 SQL 쿼리를 작성합니다.

아래의 코드를 살펴봅시다.

void DoQuery(string Id) {
    SqlConnection sql=new SqlConnection(@"data source=localhost;" +
            "user id=sa;password=password;");
    sql.Open();
    sqlstring= "SELECT hasshipped" +
            " FROM shipping WHERE id='" + Id + "'";
    SqlCommand cmd = new SqlCommand(sqlstring,sql);
•••
이 코드는 세 가지 심각한 결함이 있습니다. 첫째, 시스템 관리자 계정인 sa로 웹 서비스에서 SQL Server로 연결이 되었습니다. 이 부분이 왜 문제인지 곧 설명하겠습니다. 둘째, sa 계정에 대한 암호로 "password"를 사용했습니다.

그러나 이 코드의 가장 큰 문제는 SQL 문을 작성하는 문자열 연결 부분에 있습니다. 사용자가 1001이라는 ID를 입력하면 다음과 같은 올바른 형식의 유효한 SQL 문이 표시됩니다.

SELECT hasshipped FROM shipping WHERE id = '1001'
하지만 공격자는 이보다 훨씬 앞서 생각합니다. 즉 공격자는 "'1001' DROP table shipping --"을 ID로 입력하여 다음과 같은 쿼리를 실행하도록 만들 수 있습니다.
SELECT hasshipped FROM 
shipping WHERE id = '1001'  
DROP table shipping -- ';

이렇게 하면 전혀 엉뚱한 쿼리가 실행됩니다. 공격자는 Shipping 이라는 테이블이 있는지 확인할 수 있을 뿐 아니라 만약 있다면 이 테이블을 삭제할 수 있습니다. -- 연산자는 SQL의 주석 연산자로서 공격자로 하여금 유효하지만 위험한 일련의 SQL 문을 작성할 수 있게 만듭니다.

그렇다면 아무나 SQL Server 데이터베이스에서 테이블을 삭제할 수 있을까요? 물론 관리자만이 테이블 삭제 작업을 할 수 있습니다. 하지만 문제는 여기에서 sa로 데이터베이스에 연결했다는 점입니다. sa 계정은 SQL Server 데이터베이스에서 어떤 작업도 할 수 있습니다. 따라서 응용 프로그램에서 sa로 SQL Server에 연결하지 않아야 합니다. 대신 적합한 Windows 통합 인증을 사용하거나 적절히 제한된 권리가 있는 미리 정의된 계정으로 연결해야 합니다.

SQL 주입 문제는 쉽게 해결할 수 있습니다. 다음 코드에서는 SQL 저장 프로시저 및 매개 변수를 사용하여 안전한 쿼리를 작성하는 방법을 보여 줍니다. 또한 이 코드에서는 4-10자리 사이의 숫자로 된 shipping ID만 허용하도록 하는 정규식을 사용하여 입력이 유효한지 확인합니다.

Regex r = new Regex(@"^\d{4,10}$");
if (!r.Match(Id).Success)
    throw new Exception("Invalid ID");
SqlConnection sqlConn= new SqlConnection(strConn);
string str="sp_HasShipped";
SqlCommand cmd = new SqlCommand(str,sqlConn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@ID",Id);

버퍼 오버런, 사이트 간 스크립트, SQL 주입 공격은 신뢰할 수 없는 입력에 관련된 공격의 예입니다. 이러한 상황에서는 모든 입력이 올바른 것이라고 증명될 때까지는 모두 잘못된 것이라고 믿어야 합니다.


5. Crypto 코드 주의

이제 조금 더 실질적이고 치명적인 문제에 대해 살펴보겠습니다. 우리가 검토하는 보안 코드의 30% 이상에 보안상의 결함이 있습니다. 가장 일반적인 결함은 자체 암호화 코드로서, 이 코드는 대체로 매우 취약하여 공격 당하기 쉽습니다. 자체 암호화 코드는 올바르게 사용하기 어려우므로 작성하지 말아야 합니다. 사람들이 이해하지 못하는 암호화 알고리즘이기 때문은 아닙니다. 공격자들은 디버거에 액세스하고 시간과 노력을 쏟아 시스템이 정확히 어떻게 작동하는지 알아낼 수 있습니다. 시스템이 공격받는 것은 시간 문제입니다. 따라서 Win32?? 응용 프로그램용 CryptoAPI 및 System.Security.Cryptography 네임스페이스와 같이 제대로 작성되고 올바르게 테스트된 암호화 알고리즘을 사용하는 것이 좋습니다.


6. 공격 프로필 줄이기

클라이언트 중 90%가 필요로 하지 않는 기능은 기본적으로 설치되어서는 안 됩니다. 인터넷 정보 서비스(IIS) 6.0은 이러한 설치 계획을 따릅니다. 자세한 내용은 Wayne Berry의 이달 기사인 "Innovations in Internet Information Services Let You Tightly Guard Secure Data and Server Processes,"(영문)를 참조하십시오. 이 설치 방식의 기본 원칙은 사용하지 않는 서비스가 실행될 경우 관심을 두지 않게 되고 결국 공격 당할 수 있다는 것입니다. 기본적으로 설치된 기능이라면 최소한의 권한 원칙을 적용하여 작동해야 합니다. 다시 말해 관리 권한이 필요하지 않으면 관리 권한으로 응용 프로그램을 실행할 필요는 없습니다. 이 주의 사항을 유념해야 합니다.


7. 최소한의 권한 원칙을 적용

운영 체제 및 CLR(공용 언어 런타임)에 보안 정책이 필요한 이유는 여러 가지입니다. 많은 사람들이 보안 정책이 존재하는 이유는 사용자가 의도적으로 유해한 일, 즉 액세스가 허용되지 않는 파일에 액세스하고 필요에 따라 임의로 네트워크를 재구성하거나 기타 유해한 행위를 하지 못하도록 방지하는 것이라고 생각합니다. 물론 이러한 내부 공격이 일반적이고 또 반드시 방지해야 할 공격이지만 보안 정책을 강력히 유지해야 하는 데는 또 다른 이유가 있습니다. 보안 정책은 코드 주변에 방어벽을 쌓아 사용자가 고의로 또는 흔히 실수로 네트워크에 해를 주지 않도록 하는 것입니다. 예를 들어 Alice라는 사람이 전자 메일을 통해 다운로드하여 실행한 첨부 파일은 Alice가 액세스할 수 있는 리소스에만 액세스할 수 있도록 제한되어야 합니다. 첨부 파일에 트로이 목마가 있다고 해도, 뛰어난 보안 정책이 적용된다면 시스템의 손상을 줄일 수 있습니다.

서버 응용 프로그램을 설계, 작성 및 배포할 때는 적합한 사용자만 요청을 보낼 수 있는 것은 아니라는 점을 염두에 두어야 합니다. 공격자가 악의를 가지고 잘못된 요청을 보내 코드가 오동작을 일으킬 수 있는 상황이라면 손상을 제한하기 위해 응용 프로그램에 가능한 모든 방어벽을 쌓아야 합니다. 회사가 보안 정책을 실행하는 이유가 그저 사용자 또는 사용자의 코드를 신뢰하지 않기 때문만이 아니라는 것입니다. 외부인에 의해 코드가 악용되는 경우를 막으려는 목적도 있습니다.

최소한의 권한 원칙이란 필요한 만큼의 시간 동안만, 필요한 만큼의 코드에만 권한을 허용해야 한다는 것입니다. 다시 말해, 어느 시점이든 코드를 보호하는 방어벽을 최대한 많이 쌓아야 한다는 것입니다. 문제가 발생하면 미리 쌓아놓은 이러한 방어벽이 도움이 될 것입니다. 최소한의 권한으로 코드를 실행하는 데 대한 몇 가지 기본 원칙을 설명하겠습니다.

작업을 완료하는 데 필요한 리소스에만 액세스를 허용하도록 서버 코드에 대한 보안 컨텍스트를 선택합니다. 일부 코드에 더 많은 권한이 필요하면 해당 코드를 분리하여 더 높은 수준의 권한으로 실행합니다. 다른 운영 체제 자격 증명으로 실행되는 코드를 안전하게 분리하는 가장 좋은 방법은 더 많은 권한이 부여된 보안 컨텍스트에서 실행되는 별도의 프로세스로 이 코드를 실행하는 것입니다. 즉, COM 또는 Microsoft .NET 원격 서비스 등과 같은 프로세스 간 통신이 필요하며 데이터 왕복을 최소한으로 유지할 수 있도록 해당 코드에 대한 인터페이스를 디자인해야 합니다.

코드를 어셈블리로 분리할 때 .NET Framework를 사용하는 경우, 각 코드 조각에 필요한 수준의 권한을 파악해야 합니다. 높은 권한이 필요한 코드를 더 많은 사용 권한을 허용할 수 있는 별도의 어셈블리에 분리하면 대부분의 어셈블리를 적은 권한으로도 실행할 수 있어 코드에 더 많은 방어벽을 추가할 수 있습니다. 그림 1과 같이 어셈블리 수준의 사용 권한 요청을 통해 특정 어셈블리의 권한을 쉽게 제한할 수 있습니다. 그림 2는 이러한 사용 권한 요청에 사용하는 XML 파일을 작성하는 방법을 보여 줍니다. 이렇게 하면 CAS(코드 액세스 보안) 스택의 특성에 따라 어셈블리의 사용 권한만 제한하는 것이 아니라 여기서 호출하는 모든 어셈블리의 사용 권한도 제한하게 됩니다.

많은 사람들이 제품이 테스트되고 배포된 후에 새로운 구성 요소를 추가할 수 있도록 응용 프로그램을 작성합니다. 버그 및 보안 허점을 찾기 위해 모든 코드 경로를 테스트할 수 있는 방법이 없기 때문에 이러한 응용 프로그램을 보호하는 것은 매우 어려운 일입니다. 하지만 관리되는 응용 프로그램일 경우에는 CLR이 제공하는 기능을 사용하여 이러한 확장성을 잠글 수 있습니다. 사용 권한 개체 또는 사용 권한 집합을 선언하고 PermitOnly 또는 Deny를 호출하여, 호출하는 모든 코드에 허용된 사용 권한을 제한하는 스택에 표식을 추가합니다. 이렇게 하면 플러그 인을 호출하기 전에 이 작업을 실행하여 플러그 인이 할 수 있는 작업을 제한할 수 있습니다. 예를 들어 지불 계산을 해야 하는 플러그 인은 파일 시스템에 액세스할 필요가 없습니다. 이것은 최소 권한의 다른 예로서 응용 프로그램을 미리 보호할 수 있는 좋은 방법입니다. 이러한 제한 사항은 문서화하는 것이 중요하며 많은 권한이 주어진 플러그 인은 어설션 문을 사용하여 이러한 제한을 극복할 수 있다는 것을 명심해야 합니다.


8. 실패 모드에 주의

오류 처리 코드를 작성하기 싫어하는 것은 당연합니다. 코드가 실패하는 경우는 많습니다. 코드가 실패하면 실망하는 것은 당연한 것입니다. 대부분의 프로그래머는 일반적인 실행 경로에 초점을 맞추려 합니다. 실제 작업이 그 곳에서 이루어지기 때문입니다. 즉, 가능한 신속하고 손쉽게 오류를 처리한 다음 코드의 다음 줄로 이동하려 할 것입니다.

하지만 아쉽게도 이는 안전한 사고 방식이 아닙니다. 우리는 코드의 실패 모드에 많은 주의를 기울여야 합니다. 실패한 코드 조각은 세부적인 사항에 주의를 기울이지 않고 작성하였거나 완전히 테스트하지 않은 경우가 많기 때문입니다. 모든 사소한 오류 처리기를 포함하여 함수의 모든 코드 줄에서 디버거를 단계별로 실행한 때가 마지막으로 언제인지 생각해 보십시오.

테스트되지 않은 코드는 보안 취약점을 노출시킬 수 있습니다. 이 문제를 완화할 수 있는 세 가지 방법이 있습니다. 첫째, 일반 코드에 대해 신경을 쓰는 만큼 작은 오류 처리기에도 많은 관심을 두어야 합니다. 오류 처리 코드가 실행될 때의 시스템 상태를 생각해 보십시오. 시스템이 올바르고 안전한 상태입니까? 둘째, 함수를 작성하면 함수에서 단계별로 디버거를 여러 번 실행하여 모든 오류 처리기를 확실히 거쳐야 합니다. 이 기술로도 미묘한 타이밍 오류는 해결하지 못할 수 있습니다. 잘못된 인수를 함수에 전달하거나 시스템 상태를 변경하는 방법으로 오류 처리기가 작동하도록 만들어야 하는 경우도 있습니다. 충분한 시간적 여유를 가지고 코드를 단계별로 실행하여, 시스템이 실행될 때 시스템 상태 및 코드를 천천히 살펴보십시오. 디버거의 코드를 단계별로 실행하여 프로그래밍 논리에서 결함을 찾는 방법은 널리 사용되는 입증된 기술입니다. 이 기술을 사용하십시오. 셋째, 테스트로 인해 함수가 실패하도록 하십시오. 함수의 모든 코드 줄을 테스트해야 합니다. 이러한 작업은 특히 테스트를 자동화하여 각 빌드 후에 실행할 경우 문제가 있는 부분을 찾는 데 도움이 됩니다.

실패 모드에 대한 매우 중요한 사실이 하나 더 있습니다. 코드가 실패한 경우 시스템을 가능한 최상의 보안 상태로 두어야 합니다. 다음은 잘못된 코드의 예입니다.

bool accessGranted = true; // optimistic!
try {
    // see if we have access to c:\test.txt
    new FileStream(@"c:\test.txt",
                    FileMode.Open,
                    FileAccess.Read).Close();
}
catch (SecurityException x) {
    // access denied
    accessGranted = false;
}
catch (...) {
    // something else happened
}

CLR이 관련된 경우 해당 파일에 대한 액세스가 허용되므로 SecurityException이 throw되지 않습니다. 하지만 예를 들어 파일의 DACL(임의 액세스 컨트롤 목록)에서 액세스를 제한할 경우는 다른 유형의 예외가 throw됩니다. 그러나 첫 번째 코드 줄에서 낙관적 가정을 했기 때문에 이 사실을 알 수가 없습니다.

더 좋은 방법은 이 코드를 비관적으로 작성하는 것입니다.

bool accessGranted = false; // pessimistic!
try {
    // see if we have access to c:\test.txt
    new FileStream(@"c:\test.txt",
                    FileMode.Open,
                    FileAccess.Read).Close();
     // if we're still here, we're good!
     accessGranted = true;
}
catch (...) {}
이 방법은 실패를 하더라도 가장 안전한 모드로 이동하기 때문에 훨씬 더 확실한 방법입니다.


9. 취약한 가장

서버 응용 프로그램을 작성할 때는 가장이라고 하는 Windows의 편리한 기능을 직간접적으로 사용하는 경우가 많습니다. 가장을 사용하면 프로세스의 각 스레드를 별도의 보안 컨텍스트(일반적으로 클라이언트의 보안 컨텍스트)에서 실행할 수 있습니다. 예를 들어 파일 시스템 리디렉터가 네트워크를 통해 파일 요청을 받으면, 원격 클라이언트를 인증하고 클라이언트의 요청이 공유 DACL에 위배되지 않는지 확인한 다음 클라이언트의 토큰을 요청을 처리하는 스레드에 연결하여 클라이언트를 가장합니다. 이 스레드는 클라이언트의 보안 컨텍스트를 사용하여 서버의 로컬 파일 시스템에 액세스할 수 있습니다. 로컬 파일 시스템은 이미 안전한 상태이므로 이 방법은 편리합니다. 로컬 파일 시스템은 요청된 액세스 유형, 파일의 DACL, 스레드의 가장 토큰을 모두 확인하고 액세스 확인이 실패하면 파일 시스템 리디렉터에게 보고합니다. 파일 시스템 리디렉터는 이 오류를 다시 원격 클라이언트에게 보냅니다. 이러한 방식을 사용하면 파일 시스템 리디렉터는 로컬 파일 시스템에 책임을 전가하여 마치 클라이언트가 로컬인 것처럼 로컬 파일 시스템이 액세스 확인을 하도록 하기 때문에 매우 편리합니다.

가장은 파일 시스템 리디렉터와 같은 간단한 게이트웨이에 유용하지만 좀 더 복잡한 응용 프로그램에서는 다르게 사용되는 경우가 있습니다. 웹 응용 프로그램을 예로 들어 봅시다. 예전 방식의 관리되지 않는 ASP 응용 프로그램, ISAPI 확장 기능 또는 ASP.NET 응용 프로그램의 Web.config 파일에 다음과 같이 지정할 경우,

<identity impersonate='true'>
프로세스 토큰과 스레드 토큰이라는 두 개의 다른 보안 컨텍스트가 있는 환경에서 실행하게 됩니다. 일반적으로 스레드 토큰은 액세스 확인을 위해 사용됩니다(그림 3 참조). 웹 서버 프로세스에서 실행되는 ISAPI 응용 프로그램을 작성한다고 가정해 봅시다. 스레드 토큰은 IUSR_MACHINE일 것이므로 대부분의 요청은 인증되지 않습니다. 그러나 프로세스 토큰은 SYSTEM입니다. 버퍼 오버플로를 악용하여 코드가 공격 당했다고 합시다. 공격자는 IUSR_MACHINE으로 실행하는 것으로는 만족하지 않을 것입니다. 공격자의 권한 수준을 높이기 위해 공격 코드에서 RevertToSelf를 호출하여 가장 토큰을 제거할 가능성이 많습니다. 이 경우, 공격자의 의도대로 될 수 있습니다. 또한 공격자는 CreateProcess를 호출할 수도 있습니다. 이렇게 되면 새 프로세스에 대한 토큰이 가장 토큰이 아닌 프로세스 토큰에서 복사되어 새 프로세스가 SYSTEM으로 실행됩니다.

그림 3 확인
그림 3 확인

이 문제를 해결할 수 있는 방법은 무엇일까요? 우선 버퍼 오버플로가 발생하지 않도록 해야 하며 최소한의 권한 원칙을 명심해야 합니다. SYSTEM에 제공하는 강력한 권한이 코드에는 필요하지 않은 경우 웹 응용 프로그램이 웹 서버 프로세스 내에서 실행되도록 구성하지 마십시오. 웹 응용 프로그램이 중간 또는 높은 격리 수준으로 실행되도록 간단히 구성하는 경우 프로세스 토큰은 IWAM_MACHINE이 됩니다. 이렇게 하면 사실상 사용자는 권한을 전혀 가질 수 없게 되며 이러한 종류의 공격은 거의 영향을 미치지 않습니다. Windows .NET Server의 구성 요소가 될 IIS 6.0에서 사용자 작성 코드는 기본적으로 SYSTEM으로 실행되지 않습니다. 이 작업은 개발자가 범하는 실수를 바탕으로 한 것이며, 코드에 버그가 있을 때는 웹 서버가 코드에 주어진 권한을 줄이기 위해 추가적인 지원을 제공할 수 있습니다.

COM 프로그래머가 알아두어야 할 또 한가지 사항이 있습니다. COM은 스레드에 나쁜 영향을 주는 경향이 있습니다. 스레딩 모델이 호출 스레드와 일치하지 않는 in-process COM 서버를 호출하는 경우 COM은 다른 스레드로 호출을 실행합니다. COM은 호출자 스레드의 가장 토큰을 전파하지 않기 때문에 해당 호출이 호출 스레드가 아닌 프로세스의 보안 컨텍스트로 실행됩니다.

가장 기능으로 인해 곤란한 경우가 또 있습니다. 명명된 파이프, DCOM 또는 RPC를 통해 요청을 허용하는 서버가 있다고 합시다. 클라이언트를 인증하고 가장하여 클라이언트 대신 커널 개체를 연 다음 클라이언트 연결을 끊을 때 이러한 개체(예: 파일) 중 하나를 닫는 것을 잊었다고 가정합시다. 다음 클라이언트가 연결되면 다시 클라이언트를 인증하고 가장합니다. 어떻게 될까요? 이전 클라이언트에서 "누설한" 파일을 여전히 액세스할 수 있으며, 새 클라이언트가 해당 파일에 대해 액세스가 허용되지 않은 경우에도 마찬가지입니다. 성능상의 이유로 커널은 개체를 처음 열 때만 개체의 액세스를 확인합니다. 나중에 다른 대상을 가장하기 위해 보안 컨텍스트가 변경되더라도 이 파일에는 계속 액세스할 수 있습니다.

지금까지 설명한 내용을 통해 가장 기능이 서버 개발자에게는 편리한 기능이지만 보안상 취약한 기능임을 알 수 있습니다. 따라서 가장 토큰으로 코드를 실행하는 경우 보안에 특별히 신경 써야 합니다.


10. 관리자가 아닌 사용자가 사용할 수 있도록 응용 프로그램 작성

이는 최소한의 권한 원칙을 적용한다면 당연한 일입니다. Windows에서 관리자가 아니면 제대로 작동하지 않도록 코드를 작성한다면 "불안전한" 시스템을 노리는 경고의 표적에서 벗어날 수 없을 것입니다. Windows에는 매우 견고한 여러 보안 기능이 있지만 사용자가 작업을 하기 위해 관리자 권한이 필요하다면 이러한 기능은 소용이 없습니다.

이 문제를 어떻게 해결할 수 있을까요? 우선 스스로 느껴 보십시오. 관리자가 아닌 사용자로 실행해보면 보안을 염두에 두고 설계되지 않은 프로그램을 사용하는 것이 매우 어려운 일임을 곧 알게 될 것입니다. 얼마 전에 필자는 데스크톱과 장치의 데이터를 동기화하도록 디자인된 휴대용 장치의 제조업체가 제공하는 소프트웨어를 설치한 적이 있습니다. 항상 그랬듯이 일반 사용자 계정에서 로그오프하고 기본으로 제공되는 관리자 계정으로 로그인하여 소프트웨어를 설치한 다음, 일반 계정으로 다시 로그인하여 소프트웨어를 실행했습니다. 필요한 일부 데이터 파일에 액세스할 수 없다는 대화 상자가 나타난 다음 액세스 위반 오류가 발생했습니다. 놀라운 것은 이 소프트웨어가 유명한 휴대용 장치의 공급업체가 제공한 것이라는 사실입니다. 이 회사는 소프트웨어에 문제가 있다는 사실을 인정해야 할 것입니다.

http://sysinternals.com에서 FILEMON을 실행해 본 결과 이 응용 프로그램이 실행 파일과 동일한 디렉터리에 설치된 데이터 파일을 열어 쓰기 액세스 권한을 가지려 했음을 발견했습니다. 응용 프로그램이 Program Files 디렉터리에 설치되면 이 디렉터리에 데이터를 작성하지 말아야 합니다. Program Files에는 제한된 액세스 제어 정책이 있기 때문입니다. 사용자가 이 디렉터리에 쓰기 작업을 하도록 허용하면 다른 사용자가 트로이 목마를 실행하게 만들 수도 있습니다. 사실 이 규정은 Windows XP에 대한 기본 로고 요구 사항의 일부입니다(http://www.microsoft.com/winlogo 참조).

많은 프로그래머들이 코드 개발을 위해 관리자로 실행할 수 밖에 없는 핑계를 얘기하지만 문제를 계속해서 무시한다면 상황은 더욱 나빠질 것입니다. 텍스트 파일을 편집하는 데는 관리자 권한이 필요하지 않으며 시작한 프로그램을 컴파일 또는 디버그하는 데에도 관리자 권한이 필요하지 않습니다. 관리자 권한이 필요하면 운영 체제의 RunAs 기능을 사용하여 향상된 권한으로 각 프로그램을 실행하십시오(2001년 11월 Security Briefs 칼럼(영문) 참조). 개발자가 사용할 도구를 작성하는 경우 이로 인해 다른 개발자에게도 영향을 줄 수 있습니다. 관리자만 실행할 수 있는 코드를 계속해서 작성하는 악순환을 중단해야 하며, 그렇게 하려면 일반 사용자 권한으로 코드를 작성해야 합니다.

관리자가 아닌 사용자로 쉽게 실행할 수 있는 방법에 대해서는 Keith의 웹 사이트인 http://www.develop.com/kbrown을 참조하십시오.. 또한 관리되지 않는 환경에서 제대로 실행되는 응용 프로그램을 작성하는 방법에 대해 설명한 Writing Secure Code(Microsoft Press, 2001)도 읽어 보십시오.

출처 : 닷넷 (.NET) 프로그래머 모임
글쓴이 : 심재운 원글보기
메모 :