개발 내역
다음과 같이 ssh 접속시 패스워드 변경 기능을 추가한다.
SSH Packet
먼저 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 Protocol Stack
SSH 는 다음과 같은 세가지 레이어로 구성되어있다.
- transport layer: 알고리즘 교섭(negotiation), 키 교환을 담당한다. (RFC 4253)
- authentication layer: 사용자 인증을 담당한다. password(질문-답변), keyboard-interactive, public-key(공개키 기반) 등의 방법이 있다. (RFC 4252)
- connection layer: 실제로 터미널을 열거나 하는 과정을 담당 (RFC 4254)
SSH connection layer
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 메시지를 보내고, 서버가 이 요청을 수락하면, 클라이언트는 이제 해당 채널을 통해 데이터를 주고받을 수 있다.
- SSH_MSG_CHANNEL_OPEN_CONFIRMATION 91
- SSH_MSG_CHANNEL_OPEN_FAILURE 92
5. 클라이언트는 SSH_MSG_CHANNEL_REQUEST 메시지를 통해 특정 작업을 요청할 수 있다. (pty-req, shell)
Dropbear 를 이용한 SSH 서버 구성
Dropbear는 SSH 프로토콜의 경량 구현이다. SSH 서버는 Dropbear 기반으로 MatrixPKI(matrixssl에 포함된 버전으로) 또는 OpenSSL 호환 API로 구현하는 호환 레이어(cryptowrapper*)로 구성되어있다.
dropbear 커스텀 코드 분석
- svr-main.c:
- 연결을 받는 connection pool을 만드는 과정 → thread (event loop)로 대체
- 실제로는 ipv4 / ipv6 socket 생성만 하고 끝나는 코드라 삭제 가능
- 대신 bind(), listen() 등 옵션 세팅만 유지하는 형태로 개발
- svr-chansession.c
- shell 실행을 위한 channel session을 관리
- svr-chantypes 는 session type 별 핸들러를 지정하는 구조. 여기서 &svrchansess 를 바꾸면 handler 를 교체하여 새로운 처리 방식 가능
- 핵심은 channel request-handler 에서 connection 에 적절한 readfd /writefd를 설정해주고 실제 데이터 read/write 는 common-channel.c 에서 처리됨
- 실제 channel operation 은 common-channel.c 에서 이루어짐
- 비밀번호 수정 로직과 연결할 때 pipe 를 만들어 연결하면 dropbear 는 그것을 통해 사용자 입출력을 주고 받을 수 있다.
- authentication
- dropbear 는 기본적으로 public key 기반 인증을 지원하지만 기존 password 인증로직에 연결해야함
비밀번호 인증 절차
비밀 번호 인증 절차는 다음과 같은 절차로 이루어진다.
1. 클라이언트 -> 서버 (SSH_MSG_USERAUTH_REQUEST)
- username, service, method(password, keyboard-interactive)을 서버에 요청
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
비밀번호 만료 로직 설계
비밀 번호 인증 로직을 기반으로 비밀번호 만료 로직을 다음과 같이 설계했다.
.png)
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
Dropbear SSH 서버 기반으로 비밀번호 만료 로직 개발
0. _dropbear_log, _dropbear_exit 등 callback 함수 치환
- Dropbear 내부 로깅 함수와 종료 함수를 **시스템 로그 방식(zlog)**이나 커스터마이징된 종료 방식으로 대체
#define _dropbear_log(...) zlog_info(__VA_ARGS__)
#define _dropbear_exit(...) custom_exit_handler(__VA_ARGS__)
1. svr_session 구조를 통째로 쓰지 않고 필요한 부분만 직접 실행
- svr_session은 Dropbear에서 세션 설정을 종합적으로 다루는 구조체지만,
- 비밀번호 만료로직과 연동하는 방식이므로 필요한 부분만 직접 초기화하고 사용.
- 예: ses.channel, ses.authstate, ses.sock_in, ses.remotehost 등만 수동 설정.
2. chaninitialise() 할 때 우리가 만든 핸들러로 교체
- Dropbear는 chaninitialise()에서 여러 채널 타입 (session, x11, direct-tcpip)을 등록.
- 여기서 svrchansess 핸들러를 우리가 만든 vty 기반 핸들러로 바꿔 등록.
svr_chantypes[CHAN_SESSION].inithandler = my_vty_chansess_init;
3. ses.packettypes에 custom handler 등록
- SSH_MSG_USERAUTH_REQUEST → 사용자가 로그인 시 보내는 메시지
- 해당 메시지에 대해 custom handler 등록
static const packettype svr_packettypes[] = {
{SSH_MSG_USERAUTH_REQUEST, recv_msg_userauth_request}, /* server */
4. service_loop() 직접 호출
- dropbear 는 내부적으로 event loop(service loop) 를 통해 패킷 처리 및 세션 유지
- 기존 svr_main_loop() 에서 이 함수를 호출하는데 여기서는 직접 호출하여 흐름 제어
5. session 수동 생성
- pipe 대신 socketpair(AF_UNIX, SOCK_STREAM) 사용
- 이유: pipe 는 반이중, vty->sock 은 전이중(full-duplex)을 기대 → socketpaire 로 해결
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
- 사용자 세션 정보를 갖는 터미널의 한쪽에 dropbear 의 channel에 연결
6. 인증 처리
- SSH 접속 → USERAUTH_REQUEST → 비밀번호 변경로직
7. channel 과 세션 연결
- 세션 생성 시 만든 socketpair의 다른 쪽을 channel의 read/write fd로 설정
- 이후의 event loop는 thread로 대체
- Dropbear는 select() 기반 polling 사용 → 이를 thread_add_read() 등으로 변경
댓글 없음:
댓글 쓰기