스노트(Snort)는 보편적으로 사용되고 있는 IPS 오픈소스 입니다. (만약 생소하시다면 이전 포스팅 들을 참고하세요.)


오픈 소스이기에 소스를 공개하고 있음에도 워낙 분량이 방대하여 소스를 봐도 동작 방식을 이해하기란 쉽지 않습니다. 심지어 주요 함수에는 주석으로 친절한 설명까지 잘 남겨져 있음에도 완벽히 이해하기란 쉽지 않은것이 현실입니다. 물론, 소스를 열어서 확인 하는 사람은 극 소수겠죠. 


이번 포스팅에서는 스노트의 패턴 매칭 순서 및 동작 방식에 대한 이야기를 풀어 보고자 합니다. 

사실... 상당히 난해한 주제입니다.  



설명에 앞서 기본적인 용어를 하나 정의하고 가겠습니다. 

시그니처를 하나 예제로 살펴 봅시다. 


drop tcp 10.1.2.100 any > 10.1.1.100 22 (                                                 

        msg:"SSH Brute Force Attempt"; flow:established,to_server;              

        content:"SSH"; nocase; offset:0; depth:4;                                        

        detection_filter:track by_src, count 30, seconds 60; sid:1000001; rev:1;)


첫 번째 줄을 스노트에서는 RTN(Rule Tree Node)이라고 합니다. IP주소와 포트, 프로토콜 정보를 여기에다 저장합니다. 나머지 3 줄을 OTN(Option Tree Node)이라고 합니다. 실제 시그니처의 내용으로 공격 정보를 담고 있게 됩니다. 



추천은 저를 춤추게 합니다 ^^



자.. 그럼 본론으로 들어가서.. 

일반적으로 스노트는 시그니처의 RTN을 먼저 검사 한 후 OTN을 검사하는 순서로 진행한다고 생각하기 쉽습니다. 

자.. 아래 문제를 한번 풀어볼까요?


문제. 

4210 포트에서 80 포트의 HTTP 서버로 가는 패킷을 탐지하고자 하는데, 시그니처 중에 12345 에서 80으로 가는 패킷을 탐지하는 시그니처가 있다고 가정해봅시다. 
이 경우 옵션 부분인 OTN을 검사 할까요? 안할까요?

패킷:  


시그니처:
alert tcp any 12345 -> any 80 (msg:"abnormal traffic"; .... )



상식적으로 port값만 비교해 봐도 출발지 포트가 같지 않으므로 보나마나 탐지가 안된다고 결론을 내릴 수 있음에도, Snort는 목적지 포트인 80이 일치 되는것만 확인하고는 시그니처 탐지를 수행합니다. OTN을 모두 검사 한 후 RTN을 검사하는 과정에서야 출발지 포트가 해당되지 않는 다는걸 알게 되는 것이죠. 


그 이유에 대해 알고자 한다면 Aho-Corasick 매턴 매칭 알고리즘에 대해 먼저 알아야 합니다.
 
Aho-Corasick 알고리즘은 멀티 패턴 매칭 알고리즘으로 매우 잘 알려져 있습니다. 
Aho와 Corasick 두 사람이 고안해낸 이 알고리즘은 입력받은 패턴에 대해 매칭되는 모든 패턴들을 trie 구조로 만든 후 단 한번의 검사 만으로 검출하는것이 가능합니다. 


그림 1. Aho-Corasick diagram - 출저: 위키페디아



Snort에서는 시그니처의 패턴을 추출해서 위와 같은 맵을 생성하고, 패킷이 들어오면 이 맵에 의해 단 한 번 검사만으로 패턴 매치에 성공한 시그니처의 id를 얻을 수 있습니다. 

이게 아무리 효율적이라고 해도 port값 먼저 확인 하는게 더 효율적이지 않나 라는 의문은 여전히 남아 있을 겁니다. 

자! 이런 경우를 생각해봅시다. 
시그니처가 3,000개 정도 있는 경우를 생각해 봅시다. 하나의 패킷이 들어오면 3,000개의 시그니처에 대해 포트가(출발지,목적지) 일치 하는게 있는지 찾아야 합니다. 이 작업을 3,000번 한다고 생각해보세요. 생각보다 시간이 많이 걸립니다. 물론 시그니처가 늘어나면 시간은 선형적으로(linear) 증가 할 수 밖에 없는 구조가 되는겁니다. 

결국, 첫 단계에서 최소의 시간안에 최대한 유력한 시그니처를 걸러내는게 효율적이라는걸 알 수 있습니다. 이 작업을 Aho-Corasick 알고리즘을 활용해서 하는 겁니다. 한마디로 필터링을 하는 것이죠. 

그렇다고 모든 패턴을 죄다 뽑아서 Aho-Corasick을 수행하면 시간이 많이 걸리기 때문에, 스노트에서는 Aho-Corasick에 사용할 패턴은 각 시그니처 중에서 가장 긴 패턴 (longest pattern) 하나를 대표로 골라서 Aho-Corasick에 사용하도록 했습니다. (최신 버전에서는 시그니처를 만드는 사람이 Aho-Corasick에 사용될 longest pattern을 지정 할 수 있도록 'fast_pattern'이란 옵션을 지원합니다.) 이를 스노트에서는 멀티 패턴 탐색 엔진 - MPSE(Multi Pattern Search Engine)라고 합니다. 


스노트는 이런 MPSE 맵을 포트(port)별로 생성해서 관리를 합니다. 이를 소스에서는 'PORT_GROUP'이란 구조체로 사용 하고 있어서 포트 그룹이라고도 부릅니다. 그 이유는 MPSE맵이 너무 커지면 한 번의 매칭을 위해 비교해야 할 경우의 수가 늘어나게 되므로 성능 저하가 불가피 하기 때문입니다. 


아래 그림은 스노트에서 MPSE를 관리하는 구조를 그림으로 표현한 것입니다. 


그림 2. Snort Port table structure


먼저 프로토콜(TCP/UDP/ICMP/IP)별로 출발지/목적지 포트에 대해 각각 65535 개의 만큼의 배열을 가지고 있습니다. 각 배열에는 시그니처를 읽어 들이면서 MPSE가 생성 될 때 마다 해당 포트에 링크를 달아둡니다. 예를 들어 목적지 80 포트에 대해 탐지하는 시그니처가 있으면 MPSE를 생성 한 후 배열의 80번째 값에 달아 주는 식입니다. 

그렇다면 출발지/목적지 포트에 모두 특정 포트 값이 지정 되어 있다면 어떻게 할까요?
이런 경우 출발지/목적지 포트 그룹에 모두 추가해 주게 됩니다. 
둘 중 한 곳에만 해줘도 문제가 없을것 같은데.. 스노트는 왜 두 군데 모두 추가하는지 이해가 되지 않네요. 혹시 아시는분 계시면 댓글 남겨주세요~ ^^ 



여기까지 하면서 뭔가 빠진게 있는것 같다라는 생각이 들지 않나요? 


바로, 출발지/목적지 포트가 모두 'any'인 경우가 남았네요. 

출발지/목적지가 모두 'any'라면 Generic이라는 포트 그룹을 별도로 하나 더 두고 여기에 포함시킵니다. 말 그대로 특정 포트가 아닌 일반적이고 포괄적인 녀석들을 묶어 두는 겁니다. 



그림 3. Snort Generic Port table structure


당연히 포트 값 별로 MPSE를 관리할 필요는 없지만 대신 any-any 시그니처가 많아지면 MPSE의 크기가 많이 커지게 될 수 밖에 없습니다. 그렇기 때문에 가능하면 시그니처에 포트 정보를 넣어주는게 좋습니다. 



참고로, 시그니처에 패턴(content, uricontent)이 없다면 nc룰(no-content) 이라고 부르며 패턴이 없기 때문에 당연히 MPSE를 만드는 것도 불가능해집니다. 이런 시그니처들은 한 데 모아서 순차적으로 검사하도록 합니다. 



지금까지의 내용을 정리해보면 스노트는 시그니처를 아래와 같은 구조로 관리하게 됩니다. 


alert TCP any any -> 192.168.123.0/24 80 (msg:"GET KT"; content:"GET"; content:"KT"; sid:1)

alert TCP 20.20.20.0/24  5424  -> any 80 (msg:"GET OP"; content:"GET"; content:"OP"; sid:2)

alert TCP any any -> 132.42.10.0/24 80 (msg:"GET HT"; content:"GET"; content:"HT"; sid:3)





그림 3. Snort Rule Table structure


결과적으로 프로토콜이 일치하는 경우 포트 중 하나(출발지/목적지)가 일치 되면 패턴 매칭을 시도하게 되는 구조입니다. 

※ OTN과 RTN이 트리(tree)구조로 매달려 있는 것은 나중에 따로 포스팅 하겠습니다. 



OTN은 MPSE로 부터의 연결 점이 되는 root OTN이 하나 존재하고,

 패턴과 같이 시그니처의 옵션 정보를 가지고 있는 child OTN이 여러개 나올 수 있으며, 

시그니처 옵션의 마지막 정보이자 sid 와 RTN정보를 모두 포함하고 있는 leaf OTN 노드가 존재하게 됩니다. 



이제 어떤 구조로 시그니처를 관리하며 어떤 순서로 공격 패킷을 탐지 하는지 대충 감이 오시나요? 


Posted by KT한
,