RFC6587: TCP를 통한 SYSLOG 전송 규약

통합로그관리(LMS) 시스템이나 통합보안관제(SIEM) 시스템에 방화벽, IPS, 웹 방화벽 등 네트워크 보안 장비를 연동하는 경우, 우리는 20년 이상 UDP를 통한 SYSLOG 전송 프로토콜을 사용해왔습니다. 이는 RFC3164 - The BSD syslog Protocol 문서에 정의된 것으로 대부분의 사람들이 익숙합니다.

UDP를 통한 SYSLOG 전송 방식은 흐름 제어(Flow Control)를 사용하지 않으므로, 수신 시스템의 성능이 느리거나 심지어 장애가 발생하더라도 송신 시스템까지 영향을 미치지 않는다는 장점이 있습니다. 특히 네트워크 장비가 로그로 인해 성능에 영향이 있다면 통신 장애로 귀결되므로, 자연스럽게 UDP 기반 SYSLOG 전송이 매우 선호되어 왔습니다.

그러나 UDP는 프로토콜 수준에서 암호화를 지원하지 않으므로 로그 내용이 그대로 노출될 수 있습니다. 클라우드 기반의 원격 관제 서비스를 구성할 때 전용선이나 VPN이 아닌 인터넷 구간을 통해 네트워크 보안 장비를 직접 SIEM에 접속시켜야 할 경우가 자주 발생합니다. 이런 시나리오에서는 반드시 TLS 채널을 통해 안전하게 로그를 전송하도록 구성해야 합니다.

그런데 TCP 또는 TLS 채널을 통해 로그를 전송할 때 비표준 프로토콜 설계를 종종 보게 됩니다. UDP 소켓에서 수신할 때는 송신한 메시지대로 패킷이 전달되기 때문에 멀티라인 등 어떤 형태라도 의도한 메시지 단위로 로그가 전송됩니다. 반면 TCP 소켓에서는 스트림으로 데이터가 수신되기 때문에, 메시지의 경계를 직접 정의해주어야 합니다.

RFC6587 - Transmission of Syslog Messages over TCP 문서는 2012년에 나왔지만 의외로 이 내용을 알고 있는 사람을 찾아보기가 상당히 어려웠습니다. 그것이 보안 솔루션 개발 시 비표준 구현을 만드는 원인이라 생각되어 오늘 간단히 TCP 프로토콜에서 사용하는 SYSLOG 메시지 프레이밍을 소개하고자 합니다.

Non-Transparent-Framing

레거시 시스템이 오래 전부터 사용하는 방법은 메시지 구분자를 이용하는 것입니다. 개행 문자(Line Feed)를 사용하여 메시지를 구분하는 것이 가장 흔한 방법입니다.

다만 여러 줄로 구성된 로그를 전송할 때는 개행 문자를 구분자로 사용할 수 없습니다. 이런 경우 NULL 문자를 구분자로 사용하기도 합니다.

Octet Counting

RFC6587이나 TLS 채널을 통한 Syslog 전송을 다룬 RFC5425에 정의된 규칙은 메시지 시작 위치에 10진수로 뒤에 오게 될 페이로드의 길이를 바이트 단위로 기록하는 것입니다.

예를 들어, 이 PCAP 파일의 첫번째 메시지 프레임은 MSG-LEN 94와 공백 문자로 시작하며 이어지는 메시지 텍스트는 <30>부터 Server… 뒤의 개행 (0x0a) 문자까지 94바이트가 이어집니다:

94 <30>1 2023-01-12T19:29:28+09:00 ubuntu20host systemd 1 - - Starting Squid Web Proxy Server...

Octet Counting 예제

정리

인터넷 규약대로 로그 전송을 구현하면 기존의 많은 시스템에 한 번에 호환시킬 수 있습니다. 반면, 비표준 방식으로 로그를 전송하면 자사의 장비를 새로운 유형의 시스템에 연동해야 할 때마다 많은 시간과 비용을 야기합니다. 이 글이 업계의 상호 운영성을 향상시키는데 도움이 되기를 바랍니다.

둘러보기

더보기

액티브 디렉터리 LDAP 연동

액티브 디렉터리는 중앙 집중적으로 인증을 처리하며 그룹 정책을 관리하기 때문에 계정, 호스트 등 많은 양의 정보가 내재되어 있습니다. 온프레미스 환경에서 액티브 디렉터리의 정보를 통합하려면 LDAP 프로토콜을 사용해야 합니다. 로그프레소에서 액티브 디렉터리를 연동하려면 먼저 LDAP 프로파일을 등록해야 합니다. ![](/media/ko/2023-05-23-ldapsearch/ldap_profile.png) 설정이 완료되면 아래와 같이 `ldapsearch` 쿼리와 LDAP 필터를 이용하여 액티브 디렉터리에 등록된 계정 목록을 조회할 수 있습니다: ``` ldapsearch profile=AD filter="(&(userPrincipalName=*))" ``` ![LDAP 계정 목록 조회](/media/ko/2023-05-23-ldapsearch/ldap_users.png) 수십 가지의 계정 정보를 확인할 수 있습니다만, 타임스탬프는 [윈도우 FileTime 형식](https://learn.microsoft.com/ko-kr/windows/win32/api/minwinbase/ns-minwinbase-filetime)이기 때문에 보기가 어렵습니다. FileTime은 아래와 같이 변환하여 볼 수 있습니다. ``` ldapsearch profile=AD filter="(&(userPrincipalName=*))" | eval lastLogon = if(lastLogon == "0", null, epoch(floor(long(lastLogon) / 10000 - 11644473600000))) | eval badPasswordTime = if(badPasswordTime == "0", null, epoch(floor(long(badPasswordTime) / 10000 - 11644473600000))) | order sAMAccountName, lastLogon, badPasswordTime ``` ![LDAP FileTime 변환](/media/ko/2023-05-23-ldapsearch/ldap_users_filetime.png) 액티브 디렉터리에 조인된 호스트 목록은 아래와 같이 확인할 수 있습니다. ``` ldapsearch profile=AD filter="(&(servicePrincipalName=*))" | eval lastLogon = epoch(floor(long(lastLogon) / 10000 - 11644473600000)) | order cn, operatingSystem, operatingSystemVersion, lastLogon, whenCreated, whenChanged ``` ![LDAP 호스트 목록](/media/ko/2023-05-23-ldapsearch/ldap_hosts.png) 이렇게 로그프레소에 액티브 디렉터리 데이터를 통합하면 더 이상 사용되지 않는 계정이나 오래된 시스템을 주기적으로 식별하고 정리할 수 있으므로, 잠재적인 공격 표면을 줄이고 안정적으로 내부 IT 인프라를 관리할 수 있습니다.

2023-05-23

레지스트리 포렌식

레지스트리는 윈도우즈 운영체제와 응용프로그램에 관련된 방대한 설정과 운영 정보가 기록된 데이터베이스입니다. 윈도우즈 운영체제 초창기에는 INI 파일을 사용했으나, 레지스트리가 도입되면서 표준화된 계층적 데이터 구조, 다중 사용자 환경 지원, 접근 권한 제어, 바이너리 포맷 파일 기반의 효율적 I/O, 타입 시스템, 트랜잭션 등을 제공하게 되었습니다. 레지스트리를 분석하면 어떤 프로그램이나 서비스가 부팅 시 자동으로 실행되는지, 어떤 프로그램을 최근에 실행했는지, 어떤 프로그램을 얼마나 오래 사용했는지, 최근 어떤 파일을 검색했는지, 어떤 파일을 열어봤는지, 어느 서버에 접속했는지, 어떤 파일을 압축했는지 등 무수히 많은 정보를 추출할 수 있습니다. 따라서 레지스트리 분석은 사고 조사 초기에 수행해야 할 중요한 단계이며, 여기에서 확인된 정보에 따라 후속 조사의 진행이 결정될 수 있습니다. ## 레지스트리 하이브 파일 레지스트리 편집기(regedit)를 통해 하나로 보이는 레지스트리의 계층적 구조는 물리적으로 여러 개의 레지스트리 하이브 파일에 분산되어 있습니다. ![](/media/ko/2020-11-01-registry-forensic/reg-hive-files.png) `%SystemRoot%\System32\config` 디렉터리에는 아래와 같은 레지스트리 하이브 파일이 존재합니다. * SAM: HKEY_LOCAL_MACHINE\SAM * SECURITY: HKEY_LOCAL_MACHINE\Security * SOFTWARE: HKEY_LOCAL_MACHINE\Software * SYSTEM: HKEY_LOCAL_MACHINE\System 또한 각 사용자 계정의 디렉터리에는 NTUSER.DAT 레지스트리 하이브 파일이 존재합니다. ## HIVE 파일 구조 레지스트리 하이브 파일은 아래와 같이 구성되어 있습니다. ![](/media/ko/2020-11-01-registry-forensic/hive-structure.png) BASE 블록 구조 ![](/media/ko/2020-11-01-registry-forensic/hive-base-block.png) HIVE BIN 헤더 구조 ![](/media/ko/2020-11-01-registry-forensic/hive-bin-header.png) 로그프레소 포렌식의 hive-file 커맨드는 아래와 같은 필드를 출력합니다. ![](/media/ko/2020-11-01-registry-forensic/hive-file-command.png) * key: 키 * type: 타입 (문자열, 이진값, DWORD, QWORD 등) * name: 값 * value: 데이터 * last_written: 마지막 기록 시각 ## 코드게이트 포렌식 문제 연습 로그프레소 포렌식 솔루션은 쿼리를 기반으로 다양한 포렌식 아티팩트를 연관 분석하는 강력한 기능을 지원합니다. 아래에서는 이전 코드게이트 2011 컨퍼런스에서 레지스트리와 관련하여 출제된 문제를 어떻게 분석하는지 설명합니다. > we are investigating the military secret's leaking. we found traffic with leaking secrets while monitoring the network. Security team was sent to investigate, immediately. But, there was no one present. > It was found by forensics team that all the leaked secrets were completely deleted by wiping tool. And the team has found a leaked trace using potable device. Before long, the suspect was detained. But he denies allegations. > Now, the investigation is focused on potable device. The given files are acquired registry files from system. The estimated time of the incident is Mon, 21 February 2011 15:24:28(KST). > Find a trace of portable device used for the incident. > The Key : "Vendor name" + "volume name" + "serial number" (please write in capitals) * Codegate 2011 Forensic 300 문제 파일 제시된 파일의 압축을 풀면 6개의 레지스트리 하이브 파일을 확인할 수 있습니다. 먼저 시스템에 마운트된 장치 정보를 추출하기 위해 SYSTEM 하이브 파일에서 MountedDevices 키를 검색하면 아래와 같이 이진값으로 된 레지스트리 데이터를 확인할 수 있습니다. ```query hive-file codegate2011\system.bak | search key == "*MountedDevices" and name == "\\DosDevices*" ``` ![](/media/ko/2020-11-01-registry-forensic/codegate-step1.png) 이 데이터를 UTF-16으로 디코드하면 아래와 같은 문자열을 확인할 수 있습니다. ```query hive-file codegate2011\system.bak | search key == "*MountedDevices" and name == "\\DosDevices*" | eval value = substr(decode(value, "UTF-16LE"), 4) ``` ![](/media/ko/2020-11-01-registry-forensic/codegate-step2.png) USB 값만 필터링해서 정규식으로 파싱하면 제조사, 모델명, 버전, 시리얼을 추출할 수 있습니다. ```query hive-file codegate2011\system.bak | search key == "*MountedDevices" and name == "\\DosDevices*" | eval value = substr(decode(value, "UTF-16LE"), 4) | search value == "*USB*" | rex field=value "Ven_(?<vendor>[^&]+)&Prod_(?<product>[^&]+)&Rev_(?<version>[^#]+)#(?<serial>[^&]+)" | eval serial = lower(serial) | fields vendor, product, version, serial, value ``` ![](/media/ko/2020-11-01-registry-forensic/codegate-step3.png) 그러나 아직 볼륨 이름과 장치를 연결한 시간을 확인하지 못한 상태입니다. 장치를 연결한 시간은 HKLM\SYSTEM\ControlSet00X\Enum\USB\VID_####&PID_#### 키의 마지막 수정 시간을 확인하면 됩니다. 아래와 같이 쿼리하면 66개의 키를 확인할 수 있습니다. ```query hive-file codegate2011\system.bak | search key == "*USB\\VID_*" | eval serial = lower(valueof(split(key, "\\"), 5)) | stats max(last_written) as last_connect by serial ``` ![](/media/ko/2020-11-01-registry-forensic/codegate-step4.png) 볼륨 이름은 HKLM\SOFTWARE\Microsoft\Windows Portable Devices\Devices 키에서 확인할 수 있습니다. 아래와 같이 쿼리하면 40개의 키를 확인할 수 있습니다. ```query hive-file codegate2011\software.bak | search key == "*Windows Portable Devices*" and name == "FriendlyName" | rex field=key "&REV_[^#]+#(?<serial>[^&]+)" | eval serial = lower(serial) | stats first(value) as volume_name by serial ``` ![](/media/ko/2020-11-01-registry-forensic/codegate-step5.png) 이 3종의 쿼리 결과를 시리얼 번호로 조인하면 원하는 결과를 한 번에 추출할 수 있습니다. ```query hive-file codegate2011\system.bak | search key == "*MountedDevices*" | eval value = substr(decode(value, "UTF-16LE"), 4) | search value == "*USB*" | rex field=value "Ven_(?<vendor>[^&]+)&Prod_(?<product>[^&]+)&Rev_(?<version>[^#]+)#(?<serial>[^&]+)" | eval serial = lower(serial) | stats count by vendor, product, version, serial, value | join serial [ hive-file codegate2011\system.bak | search key == "*USB\\VID_*" | eval serial = lower(valueof(split(key, "\\"), 5)) | stats max(last_written) as last_connect by serial ] | join serial [ hive-file codegate2011\software.bak | search key == "*Windows Portable Devices*" and name == "FriendlyName" | rex field=key "&REV_[^#]+#(?<serial>[^&]+)" | eval serial = lower(serial) | stats first(value) as volume_name by serial ] | search last_connect >= date("2011-02-21", "yyyy-MM-dd") and last_connect <= date("2011-02-22", "yyyy-MM-dd") | order volume_name, vendor, product, version, serial, last_connect ``` ![](/media/ko/2020-11-01-registry-forensic/codegate-step6.png) 이처럼 로그프레소 쿼리를 이용하여 레지스트리 포렌식 데이터를 손쉽게 분석하고 가공할 수 있으며, 재사용 가능한 라이브러리로 구축할 수 있습니다.

2020-11-01

Jira 이슈 생성 연동

이번 글에서는 Atlassian Jira 연동 예제를 통해 로그프레소에서 REST API를 어떻게 호출하는지 알아봅니다. [Atlassian Jira API 레퍼런스](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-post)는 Jira에 이슈를 생성하려면 POST 엔드포인트와 본문 데이터를 어떻게 전송해야 하는지 설명하고 있습니다. curl 예제에서 필수적인 부분만 정리하면 아래와 같습니다. ``` curl --request POST \ --url 'https://your-domain.atlassian.net/rest/api/3/issue' \ --user 'email@example.com:<api_token>' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "fields": { "project": "PROJECT_KEY", "summary": "이슈 제목", "issuetype": { "id": "10002" }, "description": { "version": 1 "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "이슈 설명" } ] } ], } } }' ``` 로그프레소에서는 dict() 함수를 이용하여 맵 (map) 타입의 값을 표현할 수 있습니다. dict() 함수는 키, 값 쌍을 순차적으로 입력받습니다. 예를 들어, 아래와 같은 JSON 객체를 표현하려면, ``` {"project": "PROJECT_KEY", "summary": "이슈 제목"} ``` 로그프레소 쿼리에서는 아래와 같이 표현할 수 있습니다. ``` dict("project", "PROJECT_KEY", "summary", "이슈 제목") ``` 리스트 (list) 타입의 값을 표현하려면 array() 함수를 사용합니다. 예를 들어, 다음과 같은 JSON 배열을 표현하려면, ``` [{"type": "text", "text": "이슈 설명"}] ``` 로그프레소 쿼리에서는 아래와 같이 표현할 수 있습니다. ``` array(dict("type", "text", "text", "이슈 설명")) ``` 가변적으로 값을 생성하려면 입력 레코드의 description 필드를 활용할 수 있습니다. ``` array(dict("type", "text", "text", description)) ``` 즉, dict()와 array()를 중첩하면 Jira에서 요구하는 복잡한 형태의 본문 데이터를 정의할 수 있습니다. 아래의 로그프레소 쿼리에서 PROJECT_KEY, YOUR_ID, EMAIL_ADDR, API_TOKEN 값을 변경하여 실행해보세요. API_TOKEN 발급 방법은 [Atlassian Jira 앱 설치 가이드](https://logpresso.store/ko/apps/jira/install-guide)를 참고하시기 바랍니다. ```query json "{}" | eval project_key = "PROJECT_KEY", issue_type = "10002", summary = "이슈 제목", description = "이슈 설명" | eval headers = dict("Accept", "application/json", "Content-Type", "application/json", "User-Agent", "Logpresso") | eval fields = dict("project", dict("key", project_key), "summary", summary, "issuetype", dict("id", issue_type), "description", dict("version", 1, "type", "doc", "content", array(dict("type", "paragraph", "content", array(dict("type", "text", "text", description)))))) | eval url = "https://YOUR_ID.atlassian.net/rest/api/3/issue" | wget method=post auth="EMAIL_ADDR:API_TOKEN" header=headers format=json ``` 여기에서 사용된 [wget 명령어](https://docs.logpresso.com/ko/query/wget-command)의 옵션 설명은 아래와 같습니다: * method: POST 메소드 지정 * auth: HTTP Basic 인증에 필요한 계정 및 암호 지정 * header: Jira REST API 호출에 필요한 HTTP 헤더 키/값 쌍을 포함한 맵 필드를 지정 * format: json 포맷을 지정하면 입력 레코드로 전달되는 모든 필드를 하나의 JSON 객체로 직렬화하여 HTTP 본문으로 전송 ![](/media/ko/2023-05-15-jira-integration/wget_jira.png) 로그프레소 3은 wget 쿼리를 복잡하게 만들어서 실행해야 하지만, 로그프레소 4에서는 Jira 앱을 설치하여 편리하게 원하시는 작업을 수행할 수 있습니다. 이제 SIEM을 Jira에 통합하여 원하시는 업무 흐름을 만들어보세요.

2023-05-15