스노트(Snort)는 보편적으로 사용되고 있는 IPS 오픈소스 입니다. (만약 생소하시다면 이전 포스팅 들을 참고하세요.)
오픈 소스이기에 소스를 공개하고 있음에도 워낙 분량이 방대하여 소스를 봐도 동작 방식을 이해하기란 쉽지 않습니다. 심지어 주요 함수에는 주석으로 친절한 설명까지 잘 남겨져 있음에도 완벽히 이해하기란 쉽지 않은것이 현실입니다. 물론, 소스를 열어서 확인 하는 사람은 극 소수겠죠.
이번 포스팅에서는 스노트의 패턴 매칭 순서 및 동작 방식에 대한 이야기를 풀어 보고자 합니다.
사실... 상당히 난해한 주제입니다.
설명에 앞서 기본적인 용어를 하나 정의하고 가겠습니다.
시그니처를 하나 예제로 살펴 봅시다.
drop tcp 10.1.2.100 any > 10.1.1.100 22 (
msg:"SSH Brute Force Attempt"; flow:established,to_server;
첫 번째 줄을 스노트에서는 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 구조로 만든 후 단 한번의 검사 만으로 검출하는것이 가능합니다.
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)
Suricata(수리카타)는 Open source기반의 IDS(Intrusion detection system)입니다. 사실 Open source 기반의 IDS/IPS라고 하면 Snort(스노트)가 대표적입니다. 거의 독보적이라고 할 정도로 오랜 시간 입지를 굳혀오고 있었지요. 오픈 소스의 특성상 많은 사람들이 사용하고 피드백을 주게 되면 그 만큼 오류 수정 및 기능 추가에 용의하게 됩니다. Snort는 오랜 시간 전 세계 많은 사용자의 도움으로 다양한 공격을 좀 더 정확하게 탐지하는 방법에 중점을 두고 발전을 해왔다고 봅니다.
문제는, 그 사이 인터넷 세상은 집집마다 광 케이블을 통한 초고속 인터넷이 설치 되는가 하면 스마트폰 보급이 빠른 속도로 진행되면서 급속도로 팽창하게 됩니다. 그 결과 트래픽 양이 급격히 증가하면서 대용량 트래픽의 실시간 처리에 대한 이슈가 부각되게 됩니다. Snort는 구조상 단일 스레드(Single-thread)만 지원 하다 보니 처리 대용량 트래픽을 실시간으로 처리하기에 버거운 환경이 된 겁니다.
Suricata는 Snort의 단점은 개선하면서 장점은 그대로 수용하여 이상적인 IDS/IPS를 개발하는 것이 기본적인 목표인것 같습니다. 아래는 Suricata의 주요 장점이라고 할 수 있는 내용입니다.
1. 멀티 코어(Multicore)/멀티 스레드 (Multi-threading) 완벽 지원 (snort는 지원 불가. snort-3.0에서 시도 했으나 포기함.)
2. Snort 룰 완벽 호환 ( 기존 시스템에서 사용중인 snort 시그니처를 그대로 사용 할 수 있음.)
3. 하드웨어 밴더의 개발 지원으로 하드웨어 가속 지원. (GPU 가속)
4. 스크립터 언어(lua)로 시그니처 작성 가능 (snort 룰 포맷을 몰라도 시그니처 작성이 가능함.)
5. Snort의 대부분의 기능 모두 지원
그림 2. suricata work flow
Suricata는 OISF(Open Information Security Foundation)에서 개발 한 것으로, 2009년 12월에 베타 버전이 나온 이후로.. 2012년 10월에는 Fedora 17버전에 포함되면서 엄청나게 빠른 속도로 기반을 잡아가고 있습니다.
이제는 Fedora에서 '# yum install suricata'라는 명령 하나만으로 Suricata를 설치/사용 할 수 있는 환경이 된 것이죠.
이처럼 급격한 성장은 OISF라는 단체에 대해 알고 나면 금방 이해가 될 겁니다.
OISF는 미국의 해군 우주해양전쟁시스템센터(SPAWAR-Navy's Space and Naval Warfare Systems Command)와 국토안보부의 과학기술국 (Department of Homeland Security's Directorate for Science and Technology)의 오픈 보안기술(HOST-Homeland Open Security Technology) 프로그램의 일환으로 창립 되었습니다.
그림 3. OISF 로고
이 재단의 목표는, 미국 정부 기관의 전반적인 보안 및 인터넷 보안을 위해 사용할 새로운 보안 시스템을 개발 하는 것입니다. 오픈 프로젝트여서 결과물에 대해서 사용자나 기관에서 사용 할 수 있도록 공개도 해줍니다.
우리나라는 강 바닥 파는데 투자할 때 미국은 보안 소프트웨어에 대대적인 투자를 시작한것이죠. 이게 시각의 차이인가 싶네요.
어쨌든, 미국의 국토안보부에서 재정 지원을 하는 만큼 재정적으로 부족함이 없을것 같네요.
아래는 OISF의 파트너사 입니다.
그림 4. OISF Partners
미국 처럼 큰 나라의 정부가 주관하는 프로젝트여서 그런지 많이 들어본 친숙한 회사들이 여럿 있습니다.
어찌 보면 이제 시작인지도 모릅니다. 시간이 지나면서 점점 참여 업체가 많아지고.. 실제로 사용할 만한 수준이 되고 나면 급속도로 snort가 차지하고 있던 시장을 대체해 버릴지도 모르겠네요. 따지고 보면 snort는 한 업체의 제품이고 suricata는 미국의 범 정부 차원의 프로젝트 결과물이니 당연한 것인지도 모르겠네요.
어쨌든, 한국 IPS시장도 현재는 Snort 일색인데.. 머지않은 미래에 큰 변화가 생길지도 모르겠네요.
fast_pattern 옵션을 얼마나 잘 활용하는가에 따라 초보자와 숙련가를 구분 할 수 있다고 해도 과언이 아닙니다.
이 옵션을 사용하지 않아도 동일한 유해 패킷을 탐지 할 수 있기 때문입니다.
그런데 궂이 왜 이런 옵션을 만들어둔 것일까요?
fast_pattern은 특정 패턴을 longest pattern으로 지정한다고 했는데요.
기본적으로 스노트는 content에 입력한 패턴 중 길이가 가장 긴 녀석을 longest pattern이라고 부르고, 이런 패턴들을 모아서 Aho-Corasick 알고리즘에 의해 빠르게 검사해야할 시그니처를 걸러냅니다. (보통은 패턴의 길이가 길면 다른 패턴과 중복되지 않고 Unique한 경우가 많기 때문에 스노트에서는 가장 긴 패턴을 골라서 검사해야할 시그니처를 걸러내는 목적으로 사용하고 있는것같습니다. )
이 과정에서 얼마나 많은 시그니처를 걸러내느냐는 얼마나 빨리 공격을 탐지하는가와 직결되는 문제입니다.
간혹 패턴의 길이는 짧지만 매우 Unique한 패턴이 있다면 이 패턴을 Keyword로 설정해서 Aho-Corasick알고리즘에 의한 필터링에 사용하는게 더 유리하겠죠?
이것을 가능하게 해주는 옵션이 fast_pattern인 것입니다.
※ fast_pattern은 부가적인 옵션과 함께 사용가능한데요.. 그 내용들도 알아두면 좀 더 완성도 높은 시그니처를 작성 할 수 있을겁니다.
몰라도 무관한 재미난 사실
content에 입력한 값은 Hex값으로 입력 하더라도 문자로 표현할 수 있는 범위(0x20~0x7E)의 값이라면 문자로 변환해서 저장하고 있으며, 그 외의 값은 모두 Hex값으로 저장하고 있게 됩니다.
이렇게 함으로써 Hex값에 대해서 nocase 옵션을 붙여주게 되면, 그 값이 문자열일 경우 대소문자를 구분하지 않고 탐지가 가능합니다.
스노트는 HTTP 패킷에 대해..
패킷 전체가 아닌 특정 필드값에서 패턴을 찾는 작업을 지원한다.
이는 오탐을 줄이는 동시에 성능을 향상시켜주는 효과가 있다.
스노트 사용에 어느정도 자신감이 생겼다면..
이 옵션을 사용하는 것을 강력히 추천한다.
기본적인 사용법은 메뉴얼을 보면 되겠지만
HTTP 패킷을 분석하는 일에 친숙하지 않은 사람들을 위해..
각 옵션들이 가리키는 필드에 대해 자세하게 알아보도록 하자.
추천은 저를 춤추게 합니다 ^^
먼저, HTTP관련 매칭 옵션들은 크게 다음 7가지가 있다.
이 옵션들은 'content'라는 패턴 매칭 옵션과 함께 사용해야 한다.
1. http_method
-HTTP 패킷의 method값을 저장하고 있다.
그러므로, 다른 옵션들과 비교해서 패킷의 가장 앞 부분을 가리킨다.
2. http_uri (http_raw_uri)
-HTTP 패킷의 uri값을 저장하고 있다.
'uricontent'옵션을 사용하는 것과 동일하다.
'raw'는 normalized(디코딩, 정규화)한 값과 하지 않은 값의 차이이다.
(당연히 'raw'가 붙은것이 하지 않은 값이다.)
참고로, 'http_uri'와 'rawbytes'옵션을 함께 사용하면 'http_raw_uri'와 동일하다고 생각하기 쉽지만..
이런 조합은 허용되지 않는다.
3. http_header (http_raw_header)
- HTTP 패킷의 header값을
저장하고 있다.
request와 response에 모두 사용하는 옵션이다.
'raw'의 의미는 'http_raw_uri'와 동일하다.
4. http_cookie (http_raw_cookie)
- HTTP 패킷의 cookie값을
저장하고 있다.
'raw'의 의미는 'http_raw_uri'와 동일하다.
5. http_client_body
- HTTP 패킷의 body값을
저장하고 있다.
참고로, GET패킷에는 body가 없다.
아래 예제에서 POST패킷에만 HTTP body를 가리키는 색이 칠해져 있는 이유이다.
※사용전 확인사항
snort.conf 파일에
'post_depth 0'을 반드시 지정해 주어야한다. (0은 body전체를 extract한다는 의미)
default 값이 -1인데 이는 body를 extract하지 않는 다는 의미이기 때문이다.
참고로, 이 값은 이전 버전의 snort와는 다를 수 있다.
preprocessor http_inspect_server: server default \
profile all ports { 80 8080 8180 } \
post_depth 0
6. http_stat_code
- HTTP response의 status code 필드 값을 저장하고 있다.
변환할 bytes: 몇 바이트의 패킷을 변환할지 지정 연산자: 변환한 값과 비교할 값을 비교할 때 사용할 연산자 비교할 값: 변환된 값과 비교할 값 offset: 페이로드에서 변환할 바이트만큼 선택 할 시작위치 relative: 마지막 패턴 매칭이 성공한 위치를 시작위치로 지정
즉, content와 같은 옵션과 같이 사용 할 경우, 패턴 매칭에 성공한 위치를 offset의 시작위치로 설정 endian: 변환할 바이트 값의 endian 정보를 지정
▷참고: big endian 컴퓨터에서는 16진수 "4F52"를 저장공간에 "4F52"라고 저장 (만약 4F가 1000번지에 저장되었다면, 52는 1001번지에 저장될 것이다). 반면에, little endian 시스템에서 이것은 "524F"와 같이 저장.
string: 패킷에 문자열 포맷으로 되어있다고 지정
▷이는 글로 설명하기 쉽지 않으니 아래 예제를 참고하자.
숫자 형식: 'string'와 함께 사용하며, 문자열 포맷의 데이터의 숫자 형식을 지정
dce: DCE/RPC preprocessor와 관련된 옵션으로 숫자의 endian을 결정하기 위해 사용.
DEC/RPC preprocessor를 수행한 결과 endian이 무엇인지 알고있다.
⑴ byte_test: 1, =, 3, 14, string ;
- 패킷의 14번째에 있는 1바이트 값이 3과 같은지 비교.
위 패킷에서 14번째 바이트는 33이다.
('6d'가 아님을 기억하세요!! offset이 '0'이면 맨 처음 값을 사용한다!)
이 값을 10진수 string으로 처리한다고 했으니.. 결국 비교할 값은 '3'이 된다.
⑵ byte_test: 1, =, 51, 14 , dec;
- 패킷의 14번째에 있는 1바이트 값이 51과 같은지 비교 33은 10진수로 51이므로.. 결국 비교할 값은 '51'이 된다.
⑶ content:"system"; byte_test: 1, =, 51, 0, relative, dec ;
- 'system'이라는 문자열을 찾고...
매칭이 이루어진 곳에서 0만큼 떨어진 곳의 1바이트 값이 51인지 비교한다. relative 옵션을 썼다는것 빼고는 위의 예제와 동일하므로.. 이해하기 그리 어렵지 않다.
⑷ content:"system"; byte_test: 1, =, 0x03, 0, relative, string, hex;
- 위의 예제들을 모두 이해했다면.. 길기만 하지 별로 복잡하지 않다는 것을 알것이다.
다만 값을 string으로 처리해서 hex값으로 비교한다.
⑸ byte_test:1, &, 16, 14;
- 패킷의 14번째에 있는 1바이트 값과 16을 비트 AND 연산을 한다.
14번째 바이트 값(hex값 33)과 16을 2진수로 전환해서 비트 연산을 해보면..
변환할 bytes: 몇 바이트의 패킷을 변환할지 지정 offset: 페이로드에서 변환할 바이트만큼 선택 할 시작위치 relative: 마지막 패턴 매칭이 성공한 위치를 시작위치로 지정 multiplier <값>: 변환할 bytes 값에 곱해서 그 값만큼 jump하고자 할 때 사용 endian: 변환할 바이트 값의 endian 정보를 지정 string: 패킷에 문자열 포맷으로 되어있다고 지정 align: 변환할 bytes값을 32bit(4바이트) 단위로 반올림해서 처리
RPC와 같은 트래픽은 32bit 단위로 사용되므로.. 이런 트래픽을 처리할 때 용의
예) jump할 bytes값이 9bytes로 나왔을 경우,
32bit 단위로 반올림하게 되면 12bytes가 된다.
align 옵션을 사용했다면.. 비록 결과는 9였지만, 12bytes 만큼 jump하게 된다.
from_beginning: 현재 위치에서가 아닌 패킷의 시작 위치에서 jump
값을 계산하는 것과 별개로 jump할 때만 사용됨
post_offset <값>: jump한 이후에 다음 매칭을 시도할 위치를 여기서 설정한 값 만큼 이동.
소스를 보신 분들이라면.. doe_ptr (detection offset pointer)에 여기서 설정한 값을 더해준다고 하면 이해가 더 쉬울겁니다.
예) 다음 두 시그니처는 동일한 동작을 수행한다. 1) content:"MM"; byte_jump:3,0,relative, post_offset 2; content:"AA";distance:0; 2) content:"MM"; byte_jump:3,0,relative; content:"AA"; distance:2; dce: DCE/RPC preprocessor와 관련된 옵션으로 숫자의 endian을 결정하기 위해 사용.
③ 옵션 유효값
bytes : 1 ~ 10
offset : -65535 ~ 65535
mult_value : 0 ~ 65535
post_offset : -65535 ~ 65535
④ 예제 ⑴ content:"|00 00 00 01|"; byte_jump:4, 12, relative, align;
- 패턴 매칭이 이루어진 값 부터 12번째 부터 15번째(12번째 부터 4개) 까지의 bytes값 만큼 jump
만약 값이 4의 배수가 아니라면 반올림해서 그 값만큼 jump
2. byte_extract
특정 위치의 값을 변수로 저장.
값을 저장한 변수는 다음 옵션들에서 사용 가능.