vrrp 구성 (Active-Standby or Active-Active) 시 UTM 장비 failover 시 sync 패킷 전송 중 panic 발생하는 문제가 있었다.
외부 고객사에서 발생한 문제라 내부 재현은 불가능하여 코드 분석 레벨에서 session 동기화 과정에서의 버그 발생 지점을 분석해보았다.
vrrp 구성 (Active-Standby or Active-Active) 시 UTM 장비 failover 시 sync 패킷 전송 중 panic 발생하는 문제가 있었다.
외부 고객사에서 발생한 문제라 내부 재현은 불가능하여 코드 분석 레벨에서 session 동기화 과정에서의 버그 발생 지점을 분석해보았다.
iptables 정책 적용 후에도 기존 세션이 남아 hit count를 업데이트 하고 있는 이슈를 맡게된 계기로
상태를 가진 세션 기반 방화벽처리에 대해 조사해보았다.
1. iptables rule sequence 수정 후에도 수정 전 정책으로 패킷 카운팅 되고 있음을 확인
2. 해당 호스트는 PC ↔ eth0 (UTM) ↔ eth1 (UTM) ↔ 인터넷 의 구조로 PC 에 대한 라우터 역할을 수행하고 있었음
3. reply 패킷의 경우 원래 PREROUTING hook을 안타지만 인터넷 → PC 에 대한 reply 패킷은 PREROUTING → FORWARD → POSTROUTING hook을 타고 처리됨
4. rule 적용은 PREROUTING hook의 mangle table에서 orig 패킷만 적용됨
5. reply 패킷은 conntrack rule 에 의해 기존 conntrack table의 세션 마크를 재사용하도록 되어있음
5. rule 수정 후 reply 패킷이 PREROUTING hook을 탈 경우 수정한 rule 이 반영되지 않고 conntrack table의 세션 마크에 의해 수정전의 룰이 반영됨
다음 그림과 같이 모델링된 production 구성에서 정책 수정시 수정 전 정책으로 패킷 카운팅되는 문제가 발생했다.
먼저 정책 2개를 추가하고 icmp ping을 흘려보았다.
*mangle
-A PREROUTING -m fivetuple --msrc 100.1.1.0/24 -j MARK --set-mark 0x321002
-A PREROUTING -m fivetuple --msrc 100.1.1.0/24 -j MARK --set-mark 0x1e1001
30, 50 룰 icmp traffic
iptables -t mangle -L -vn
Chain PREROUTING (policy ACCEPT 8108 packets, 579K bytes)
pkts bytes target prot opt in out source destination
193 21954 CONNMARK all -- * * 0.0.0.0/0 0.0.0.0/0 connmark match !0x0/0xffffffffffffffff CONNMARK restore
49 11461 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 reply
144 10493 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 connmark match 0x1/0x1
165 11026 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK set 0xfde80001
105 7261 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 --msrc 192.168.1.0/24 MARK set 0x321002
105 7261 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 --msrc 192.168.1.0/24 MARK set 0x1e1001
105 7261 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 --msrc 192.168.1.0/24 MARK or 0xa000000000000
0 0 MARK icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmp type 13 MARK set 0x0
165 11026 CONNMARK all -- * * 0.0.0.0/0 0.0.0.0/0 CONNMARK save mask 0xffffffffffffffff
30 → 20, 50 → 30 룰 수정 후 traffic test (이 경우 우선순위가 낮은 30번 룰이 카운팅 되는 문제였다.)
iptables -t mangle -L -vn
Chain PREROUTING (policy ACCEPT 15377 packets, 1077K bytes)
pkts bytes target prot opt in out source destination
228 16008 CONNMARK all -- * * 0.0.0.0/0 0.0.0.0/0 connmark match !0x0/0xffffffffffffffff CONNMARK restore
16 1261 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 reply
212 14747 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 connmark match 0x1/0x1
77 4192 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK set 0xfde80001
5 323 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 --msrc 192.168.1.0/24 MARK set 0x1e1002
5 323 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 --msrc 192.168.1.0/24 MARK set 0x141001
5 323 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 --msrc 192.168.1.0/24 MARK or 0xa000000000000
0 0 MARK icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmp type 13 MARK set 0x0
77 4192 CONNMARK all -- * * 0.0.0.0/0 0.0.0.0/0 CONNMARK save mask 0xffffffffffffffff
룰을 수정하지 않아도 iptables 의 후순위 룰의 패킷수는 카운팅이 되었다.
mangle tables 에서는 access/drop/reject 에 대한 마킹만하고 실제 정책을 확인하여 패킷에 대한 drop 판단을 하는 곳은 filter tables이다. 그렇기 때문에 iptables의 결과는 문제가 없었다.
UTM 장비에서 packet counting 로직은 filter tables 에서 last time 을 업데이트하면 패킷 카운팅 결과를 출력하는 로직으로 구성되어있었다.
1. 룰이 netfilter hook을 타고 이동하면서 정책이 적용되고 카운팅되는 로직 분석
2. 상태를 가진 세션의 경우 룰 변경 시 어떻게 상태를 유지하며 룰이 반영되는지
iptables 의 경우 순차적으로 매칭하여 룰 개수 증가 시 성능이 급격하게 저하되는데 사내 UTM 장비의 경우 iptables-tng 코드를 패치하여 tuple 기반으로 hash table을 활용한 classify 기능을 추가하여 룰 개수가 많아져도 일정한 CPS를 유지하도록 하도록 개발되어있었다.
관련 개발 사항과 룰 적용 시 패킷이 netfilter hook에서 어떻게 처리되는지 분석해보았다.
개발 내역
다음과 같이 ssh 접속시 패스워드 변경 기능을 추가한다.
먼저 SSH의 packet은 다음과 같이 구성되어 있다.
Packet Length | Padding Length | Payload | Random Padding | Message Authentication Code (MAC) |
// ssh packet 형식
uint32 packet_length // payload + padding 길이 (4바이트)
byte padding_length // padding 길이 (1바이트)
byte[n1] payload // 메시지 데이터 (메시지 타입 + 메시지 본문)
byte[n2] random padding // 암호화 블록 크기 맞추기용 랜덤 패딩
[MAC] optional, if MAC is enabled // 메시지 인증 코드 (무결성 확인용, 선택적)
// 키 교환 패킷 예시 (SSH_MSG_KEXINIT)
packet_length: 0x000001fc
padding_length: 0x0c
payload: (message type 20) + kex algorithms, hostkey algorithms, etc.
padding: 12 bytes of random data
MAC: (only if enabled, e.g. hmac-sha2-256)
Wireshark로 캡처한 SSH 패킷이다.
HMAC은 세 가지 필드로 구성되어있다. 패딩, 페이로드, 랜덤 패딩의 내용은 암호화된 패킷 필드에 있다.
(https://www.iana.org/assignments/ssh-parameters/ssh-parameters.xhtml)
메세지 타입 번호 | 메세지 이름 | 설명 |
1 | SSH_MSG_DISCONNECT | 연결 종료 |
2 | SSH_MSG_IGNORE | 무시 가능한 패킷 |
20 | SSH_MSG_KEXINIT | 키 교환 초기화 |
21 | SSH_MSG_NEWKEYS | 새로운 키 적용 |
30~49 | 키 교환 관련 메시지들 | ECDH 등 포함 |
50 | SSH_MSG_USERAUTH_REQUEST | 사용자 인증 요청 |
52 | SSH_MSG_USERAUTH_SUCCESS | 인증 성공 |
90 | SSH_MSG_CHANNEL_OPEN | 채널 열기 |
94 | SSH_MSG_CHANNEL_DATA | 터미널 데이터 송신 |
SSH 는 다음과 같은 세가지 레이어로 구성되어있다.
1. SSH 연결이 설정되면, 클라이언트와 서버는 키 교환을 통해 안전한 통신 채널을 설정한다.
2. 클라이언트는 서버에 사용자 인증을 수행한다.
3. 인증이 성공하면 클라이언트는 SSH_MSG_CHANNEL_OPEN 메시지를 보내 새로운 채널을 열 수 있다.
byte SSH_MSG_CHANNEL_OPEN
string "session"
uint32 sender channel
uint32 initial window size
uint32 maximum packet size
4. 채널이 생성된 후 클라이언트가 SSH_MSG_CHANNEL_OPEN 메시지를 보내고, 서버가 이 요청을 수락하면, 클라이언트는 이제 해당 채널을 통해 데이터를 주고받을 수 있다.
5. 클라이언트는 SSH_MSG_CHANNEL_REQUEST 메시지를 통해 특정 작업을 요청할 수 있다. (pty-req, shell)
Dropbear는 SSH 프로토콜의 경량 구현이다. SSH 서버는 Dropbear 기반으로 MatrixPKI(matrixssl에 포함된 버전으로) 또는 OpenSSL 호환 API로 구현하는 호환 레이어(cryptowrapper*)로 구성되어있다.
dropbear 커스텀 코드 분석
비밀 번호 인증 절차는 다음과 같은 절차로 이루어진다.
1. 클라이언트 -> 서버 (SSH_MSG_USERAUTH_REQUEST)
2. 서버 -> 클라이언트 - SSH_MSG_USERAUTH_INFO_REQUEST
3. 클라이언트 -> 서버 - SSH_MSG_USERAUTH_INFO_RESPONSE - password
byte SSH_MSG_USERAUTH_REQUEST
string user name
string service name
string "password"
boolean FALSE
string plaintext password in ISO-10646 UTF-8 encoding [RFC3629]
4. 서버 -> 클라이언트 - SSH_MSG_USERAUTH_SUCCESS
비밀 번호 인증 로직을 기반으로 비밀번호 만료 로직을 다음과 같이 설계했다.
1. 클라이언트 -> 서버 - SSH_MSG_USERAUTH_REQUEST
2. 서버 -> 클라이언트 비밀 번호 요청 - SSH_MSG_USERAUTH_INFO_REQUEST
3. 클라이언트 -> 서버 - SSH_MSG_USERAUTH_INFO_RESPONSE
4. 서버 -> 클라이언트 비밀번호 만료, 새 비밀번호 요청 - SSH_MSG_USERAUTH_INFO_REQUEST
byte SSH_MSG_USERAUTH_PASSWD_CHANGEREQ
string prompt in ISO-10646 UTF-8 encoding [RFC3629]
string language tag [RFC3066]
byte SSH_MSG_USERAUTH_REQUEST
string user name
string service name
string "password"
boolean TRUE
string plaintext old password in ISO-10646 UTF-8 encoding [RFC3629]
string plaintext new password in ISO-10646 UTF-8 encoding [RFC3629]
5. 클라이언트 -> 서버 새 비밀번호를 두 번 입력 - SSH_MSG_USERAUTH_INFO_RESPONSE
6. 서버 -> 클라이언트 비밀번호 변경 성공 알림 - SSH_MSG_USERAUTH_INFO_REQUEST -> SSH_MSG_USERAUTH_SUCCESS
0. _dropbear_log, _dropbear_exit 등 callback 함수 치환
#define _dropbear_log(...) zlog_info(__VA_ARGS__)
#define _dropbear_exit(...) custom_exit_handler(__VA_ARGS__)
1. svr_session 구조를 통째로 쓰지 않고 필요한 부분만 직접 실행
2. chaninitialise() 할 때 우리가 만든 핸들러로 교체
svr_chantypes[CHAN_SESSION].inithandler = my_vty_chansess_init;
3. ses.packettypes에 custom handler 등록
static const packettype svr_packettypes[] = {
{SSH_MSG_USERAUTH_REQUEST, recv_msg_userauth_request}, /* server */
4. service_loop() 직접 호출
5. session 수동 생성
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
6. 인증 처리
7. channel 과 세션 연결
vrrp 구성 (Active-Standby or Active-Active) 시 UTM 장비 failover 시 sync 패킷 전송 중 panic 발생하는 문제가 있었다. 외부 고객사에서 발생한 문제라 내부 재현은 불가능하여 코드 분석 레벨에서 se...