Windows Azure 아키텍처가 어떤 방법으로 클라우드에 특화된 보안 상 위협을 대비하는지 확인할 수 있습니다. 이 웹 캐스트를 통하여 Windows Azure에 내장된 보안 기능이 여러분의 응용프로그램을 어떻게 보호할 수 있는지 살펴봅니다. 그리고 서비스를 설계할 때 어떻게 하면 보안 상 취약한 노출 영역을 최소화할 수 있는지를 배워봅니다.
오늘은 .NET Framework 4 에서 가장 많은 충돌을 야기할 수 있는 변동 사항 중 하나를 오늘 아티클에서 잠시 다루어보기로 하겠습니다. 바로 ISerializable 인터페이스에 관한 것인데요, Serializable Attribute와 NonSerialized Attribute를 이용하여 제어하는 것 만으로는 불충분한 경우 즐겨 사용해오던 인터페이스입니다. 그렇지만 여기에 급격한 변화 (Breaking Changes)가 .NET Framework 4에 더해지게 되었는데, Partial Trust Mode로 실행되는 어셈블리에서 이 인터페이스를 구현하는 로직을 호출하게 되는 경우, ISerializable 인터페이스의 GetObjectData에 추가된 SecurityCritical Attribute에 의해 호출이 거부됩니다.
저 개인적으로는 XML-RPC의 .NET Framework 버전의 Implementation 라이브러리 (http://www.xml-rpc.net/)를 .NET Framework 4로 업그레이드하여 Windows Azure에 Deploy하면서 이러한 현상을 겪었는데, 이 라이브러리에서 제공하는 Custom Exception 클래스 상의 GetObjectData 메서드가 문제의 원인이 되었습니다. .NET Framework 4 환경에서 사용하도록 별도의 프로젝트 파일을 만들어서 Predefine Condition을 부여하여 ISerializable 인터페이스를 이용하지 않도록 코드를 수정한 이후에는 문제가 잘 해결되었습니다.
using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.IO;
using System.Runtime.Serialization.Formatters.Soap;
using System.Security;
// [assembly: SecurityCritical(SecurityCriticalScope.Everything)]
// Using the SecurityCriticalAttribute prohibits usage of the
// ISafeSerializationData interface.
[assembly: AllowPartiallyTrustedCallers]
namespace ISafeSerializationDataExample
{
class Test
{
public static void Main()
{
try
{
// This code forces a division by 0 and catches the
// resulting exception.
try
{
int zero = 0;
int ecks = 1 / zero;
}
catch (Exception ex)
{
// Create a new exception to throw.
NewException newExcept = new NewException("Divided by", 0);
// This FileStream is used for the serialization.
FileStream fs =
new FileStream("NewException.dat",
FileMode.Create);
try
{
// Serialize the exception.
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(fs, newExcept);
// Rewind the stream and deserialize the exception.
fs.Position = 0;
NewException deserExcept =
(NewException)formatter.Deserialize(fs);
Console.WriteLine(
"Forced a division by 0, caught the resulting exception, \n" +
"and created a derived exception with custom data. \n" +
"Serialized the exception and deserialized it:\n");
Console.WriteLine("StringData: {0}", deserExcept.StringData);
Console.WriteLine("intData: {0}", deserExcept.IntData);
}
catch (SerializationException se)
{
Console.WriteLine("Failed to serialize: {0}",
se.ToString());
}
finally
{
fs.Close();
Console.ReadLine();
}
}
}
catch (NewException ex)
{
Console.WriteLine("StringData: {0}", ex.StringData);
Console.WriteLine("IntData: {0}", ex.IntData);
}
}
}
[Serializable]
public class NewException : Exception
{
// Because we don't want the exception state to be serialized normally,
// we take care of that in the constructor.
[NonSerialized]
private NewExceptionState m_state = new NewExceptionState();
public NewException(string stringData, int intData)
{
// Instance data is stored directly in the exception state object.
m_state.StringData = stringData;
m_state.IntData = intData;
// In response to SerializeObjectState, we need to provide
// any state to serialize with the exception. In this
// case, since our state is already stored in an
// ISafeSerializationData implementation, we can
// just provide that.
SerializeObjectState += delegate(object exception,
SafeSerializationEventArgs eventArgs)
{
eventArgs.AddSerializedState(m_state);
};
// An alternate implementation would be to store the state
// as local member variables, and in response to this
// method create a new instance of an ISafeSerializationData
// object and populate it with the local state here before
// passing it through to AddSerializedState.
}
// There is no need to supply a deserialization constructor
// (with SerializationInfo and StreamingContext parameters),
// and no need to supply a GetObjectData implementation.
// Data access is through the state object (m_State).
public string StringData
{
get { return m_state.StringData; }
}
public int IntData
{
get { return m_state.IntData; }
}
// Implement the ISafeSerializationData interface
// to contain custom exception data in a partially trusted
// assembly. Use this interface to replace the
// Exception.GetObjectData method,
// which is now marked with the SecurityCriticalAttribute.
[Serializable]
private struct NewExceptionState : ISafeSerializationData
{
private string m_stringData;
private int m_intData;
public string StringData
{
get { return m_stringData; }
set { m_stringData = value; }
}
public int IntData
{
get { return m_intData; }
set { m_intData = value; }
}
// This method is called when deserialization of the
// exception is complete.
void ISafeSerializationData.CompleteDeserialization
(object obj)
{
// Since the exception simply contains an instance of
// the exception state object, we can repopulate it
// here by just setting its instance field to be equal
// to this deserialized state instance.
NewException exception = obj as NewException;
exception.m_state = this;
}
}
}
}
또는, 코드의 수정을 최소화하기 위하여 설정 파일 (app.config이나 web.config)에 LegacyCASMode Property를 사용하도록 설정을 구성할 수 있습니다. 이에 대한 내용은 http://msdn.microsoft.com/ko-kr/library/tkscy493.aspx 의 내용을 참고하시면 됩니다. 아래는 MSDN Library에서 발췌한 사용 예시입니다.
2009년 11월 버전에 이어서, Windows Azure Tools의 최신 버전이 업데이트되었습니다. 이번 업데이트에서는 2009년 11월 버전에서 보고된 주요 문제점들 (특히 Visual Studio 2010 Beta 2에서 발견된 문제점들)이 많이 개선된 것이 눈에 띄며, 곧 출시될 Visual Studio 2010 RC (Release Candidate) 버전과 Visual Studio 2008 SP1을 대상으로 설치가 가능합니다.
이번에 배포되는 SDK에서 신규 제공하는 Windows Azure와의 연동 기능은 크게 두 가지입니다.
* Windows Azure 버전 선택: Windows Azure 자체는 하나의 완전한 운영 체제이며, Windows Azure Platform을 구성하는 가상 머신의 운영 체제입니다. 이 글을 작성하는 현 시점에서, 2009년 12월에 발표된 Windows Azure 1.0 (http://msdn.microsoft.com/en-us/library/ee956589.aspx)과 2010년 1월에 발표된 Windows Azure 1.1 (http://msdn.microsoft.com/en-us/library/ee924682.aspx) 중 택일할 수 있으며, 환경 설정 도구에서 편리하게 선택할 수 있는 옵션을 제공합니다. 그리고, 당연한 이야기이지만, 기능에 대한 변화 뿐만 아니라, 다각도에서 테스트하여 발생 가능한 보안 문제를 수정하는 노력도 더해지므로 항상 최상의 보안 상태를 유지할 수 있는 이점도 제공합니다.
그 외 다수의 버그들이 수정되었으며, 설치 방법은 2009년 11월 버전 때와 마찬가지로 일부 Hotfix들을 OS와 사용하는 개발 도구에 맞추어 추가 설치해야 합니다. (설치 방법에 대한 문서 보기: Windows Azure 개발 환경 구축 단계별 설명서)
IT 업계의 최신 트렌드와 함께, 2010년에 가장 주목받을 수 있는 기술 분야들을 한꺼번에 정리하는 IT 트렌드 2010 웹 사이트가 런칭되었습니다. 이 웹 사이트에서는 Microsoft의 최신 기술들을 바탕으로 10가지 테마를 소개합니다. "클라우드 컴퓨팅", "그린 IT", "3스크린 전략 및 새로운 UX 기술", "가상화", "마켓플레이스", "모바일", "소셜리틱 어플리케이션", "통합 보안 환경", "IT 거버넌스", "소프트웨어 품질"에 대한 이야기를 전하고 있습니다. 관심있으신 분들께서는 꼭 방문하여 좋은 정보 얻어가시면 좋겠습니다.
개인적인 호기심에 Java Script 기반의 암호화 알고리즘 구현이 있을까 검색해보게 되었다. 혹시가 역시였는데 매우 다양한 버전의 Java Script 기반의 암호화 알고리즘을 찾을 수 있었다. 클라이언트에서 데이터를 암호화한 후 이것을 서버로 전송하고 이것을 서버에서 같은 비밀 키를 사용하여 해독한다는 개념은 이제는 너무 일반적인 것이라 굳이 언급하지 않아도 될 정도다. 하지만 놀랍게도 이런 구현 절차를 ActiveX에 너무 많이 기대고 있다는 것 또한 사실이다.
ActiveX 없이 다중 브라우저를 지원하면서도 일정 수준 이상의 보안을 충족하는 암호화 시스템을 어쩌면 간단히 구현해볼 수도 있지 않을까라는 호기심이 생겨서 몇 번 코드를 끄적여보았을 뿐인데 개인적으로나 일반적인 커뮤니티에 적용하기에는 딱 좋은 수준의 암호화 시스템이 나와서 글을 써본다. Security Critical한 서비스에서는 HTTPS, SSL, IP/sec 등을 사용하는 것이 100배 안전하지만 이것은 어디까지나 일상적인 암호화만을 다루는 것이다.
기본적인 원리는 간단하다. 같은 Java Script Source Code를 가지고 하나는 Internet Browser용 Java Script Code로 사용하고 또 하나는 JScript .NET 컴파일러용으로 패키징하여 ASP.NET에 통합하는 것이다. 이 글에서는 AES (Advanced Encryption Standard) 알고리즘을 채택하였다.
위의 사이트에서 핵심적인 AES 구현을 먼저 가져왔다. AES 알고리즘과 비밀 키를 통하여 평문을 암호문으로, 암호문을 평문으로 바꾸는 것이 정확하게 작동한다는 것을 테스트하였다. 하지만 암호문을 평문으로 바꾸는 과정에 있어서 암호문이 바이트의 배열로 출력된다는 점이 걸린다. 인코딩에 따라서는 완전히 다른 내용으로 읽혀져 손상될 가능성이 있기 때문이다. 그래서 BASE64 알고리즘을 다시 찾게 되었다.
AES 암호문을 BASE64로 보호하고, BASE64 디코딩으로 얻어낸 AES 암호문을 평문으로 바꾸는 것 또한 정확히 동작함을 확인하였다. 그리고 이번엔 정말 실용성이 있는지 확인해보고 싶어서 2바이트 문자열을 가지고 암호화 복호화를 시험해보았다. 하지만 이번엔 정확히 복원해내지 못한다는 것을 확인할 수 있었다. 다행히 2바이트 문자열에 대해서는 자바스크립트 내장 함수인 encodeURI와 decodeURI를 통하여 처리할 수 있었다. 평문에 관한 문제는 해결이 되었지만 비밀 키의 경우에는 이 방법도 의미가 없었다. 비밀 키는 1바이트 문자열 수준에서 해결해야 하는듯 보였다.
이러쿵 저러쿵하여 만든 JavaScript 코드와 바이너리를 ZIP 파일로 묶어서 올려본다. 여러 메서드가 있지만 이 글의 내용을 반영하여 만든 실질적인 메서드는 다음과 같다.
클라이언트 측에서는 AesEncrypt로 보호할 내용을 암호화한 후 서버에 전송하고, ASP.NET - 또는 - 이를 받아들일 소켓 서버 등에서는 SecurityManaged.dll 파일을 레퍼런스로 참조하고 SecurityManaged.ComplexAes 클래스의 인스턴스를 만들어 위의 AesDecrypt로 내용을 복구하면 되겠다. 참고로 128/192/256은 키의 길이에 해당된다.
첨부된 파일 안의 .Managed.js 파일은 JScript.NET 문법에 맞게 재배치한 것으로 DLL로 컴파일하는 것을 가능하게 해준다. 안의 내용이나 코드 문법은 오리지널 Java Script과 전혀 차이가 없지만 닷넷 어셈블리로 만들기 위해서 package 구문과 class 구문을 추가하였을 뿐이다. 또한, 가능성만을 타진해보기 위함이었으므로 JScript.NET에서만 가능한 명시적인 형식 지정 또한 누락되어있다. 만약 좀 더 최적화를 원한다면 코드를 수정해서 사용할 것을 권하며 필요한 경우 어트리뷰트 지정을 통하여 지연된 서명을 추가해볼수도 있겠다.
닷넷 프레임워크 2.0을 설치하였다면 명령 프롬프트에서 다음과 같이 입력하면 원하는 어셈블리를 직접 얻을 수 있다. (이 작업을 위해서 Visual Studio 2005나 2008은 필요하지 않다.)
오랫만입니다. 얼마전에 해킹 존에 올린 프로그램은 잘 사용하고 계시겠지요? 소리바다에서 제공하는 "파도"라는 플레이어를 비롯한 PLA 포맷을 지원하는 대부분의 오디오 플레이어에서 유용하게 쓰실 수 있을겁니다. ^^
오늘은 대부분의 데이터 베이스 기반 웹 사이트들이 겪는 보안 상의 취약점인 SQL-Injection을 해결하는 범용적인 솔루션을 연구해 보도록 하지요.
SQL-Injection이란, 매개 변수를 강력하게 검사하지 않고 문법의 일부로 더하는 프로그램에서 흔하게 벌어지는 공격 기법입니다. 유별난 공격법이 아니지만 막대한 양의 정보가 한꺼번에 유출되거나, Microsoft SQL Server와 같이 관리자에게 너무나도 풍족한 리소스를 주는 데이터 베이스 서버에서는 다른 파티션을 포맷하는것까지도 가능하게 합니다. (SQL Server에서 기본으로 제공하는 스토어드 프로시저 중에는 셸 명령을 실행할 수 있도록 하는 프로시저도 제공됩니다. 얼마든지 응용하기 나름인 셈이지요.) 무시무시한 이야기이지요.
SQL-Injection이 어떻게 벌어지는지 그 과정을 살펴보도록 하지요.
string commandText = "SELECT * FROM USERS WHERE USER_ID='" + value + "'";
위와 같은 코드를 IDbCommand.CommandText에 지정하고 ExecuteReader() 등으로 실행하면 원하는 결과가 나오리라 흔히 생각합니다. 하지만 큰 착각입니다. value에 무엇이 들어가느냐에 따라서 다음과 같이 크게 변조되니까요.
value = "admin"; // 이것이 흔히 기대하는 형태입니다.
// 완성된 질의문: "SELECT * FROM USERS WHERE USER_ID='admin'"
value = "admin' AND USER_NAME='관리자"; // 이와 같이 변형될 수 있습니다.
// 완성된 질의문: "SELECT * FROM USERS WHERE USER_ID='admin' AND USER_NAME='관리자'"
이런 엄청난 사태가 발생하지 않도록 하려면 몇 가지 해결책이 있습니다.
1. 동일한 기능을 수행하는 스토어드 프로시저 또는 함수를 데이터 베이스 서버에 직접 구축하고, 웹 어플리케이션은 단지 이것을 호출하는 것으로 구조를 변경합니다. 위와 같이 문자열이 전달되었을 경우 "admin' AND USER_NAME='관리자" 라는 문자 자체가 하나의 값으로 처리되고, 질의문, 스토어드 프로시저, 함수의 기능에는 영향을 주지 않으므로 제일 안전하고 빠른 방법입니다.
2. 강력한 형식 검사를 수행합니다. 이것이 오늘 살펴볼 방법이며, 위와 같이 스토어드 프로시저나 함수를 지원하지 않는 데이터 베이스에서도 유용하게 쓰일 수 있는 방법입니다. 프로그래밍 방식으로서, 비즈니스 계층 단위로서 문제를 해결할 수 있는 것이 특징입니다.
코드 살펴보기
아래의 함수는 문제가 될 수 있는 문자열을 제거하는 기능을 수행합니다. pattern이라는 매개 변수에는 제거하고 싶은 문자를 지정하며, 이스케이프 시퀀스가 필요한 문자인 백슬래시, 작은 따옴표, 큰 따옴표와 공백 문자 등을 지정합니다.
public static string EnsureString(string pattern, string target) { bool flag = false; StringBuilder sb = new StringBuilder(target.Length);
for(int i=0; i<target.Length; i++) { for(int j=0; j<pattern.Length; j++) { // 조건에 해당되는 문자임을 통지하고 검사 과정을 파기합니다. if(target[i].Equals(pattern[j])) { flag = true; break; } }
// 조건에 해당하는 문자임을 확인하고 검사 문자를 생략합니다. if(flag) { flag = false; continue; }
// 검사 조건에 해당하지 않는 문자이므로 추가합니다. sb.Append(target[i]); }
return sb.ToString(); }
위의 함수를 사용하여 SQL-Injection을 막아보도록 하겠습니다.
string commandText = "SELECT * FROM USERS WHERE USER_ID='" + EnsureString("\'\"", value) + "'";
// 공격 예시: "admin' AND USER_NAME='관리자";
// 방어 결과: "admin AND USER_NAME=관리자";
// 질의문 결과: "SELECT * FROM USERS WHERE USER_ID='admin AND USER_NAME=관리자'"
스토어드 프로시저나 함수를 이용했을 때와 같은 결과를 얻게 되었습니다. 이 함수를 사용함으로서 얻을 수 있는 또 하나의 장점은 이것이 SQL 질의문이 아니라 연결 문자열 등에도 활용될 수 있다는 점이며, 파일이나 디렉터리 경로를 통한 공격도 비슷한 형태로 막아낼 수 있다는 것이 장점입니다. 그리고 패턴은 얼마든지 추가가 가능하므로 공격 정보만 수집되면 몇 글자 추가하는 것으로 이와 유사한 형태의 공격을 방어할 수 있을 것입니다.