/** @file unix_socket_log.h @brief UNIX socket log capturing @details Copyright (c) 2025 Acronis International GmbH @author Denis Kopyrin ([email protected]) @since $Id: $ */ #include "unix_socket_log.h" #include "memory.h" #include "net_events.h" #include "network/net_compat.h" #include "string_view.h" #include "transport.h" #include <linux/string.h> #include <linux/types.h> #include <linux/un.h> static const string_view_t k_dev_logs[] = { STRING_VIEW_CONST("/dev/log"), STRING_VIEW_CONST("/run/systemd/journal/dev-log") }; static const string_view_t k_keywords_failed[] = { STRING_VIEW_CONST("Connection ") // "reset" or ".... timed out" or " from ...: refusing" , STRING_VIEW_CONST("Failed ") , STRING_VIEW_CONST("Disconnecting ") // 'Disconnecting' covers all the cases, 'Disconnected' is usually a logout message // , STRING_VIEW_CONST("Disconnected ") , STRING_VIEW_CONST("Unable to negotiate with ") }; static const string_view_t k_keywords_success[] = { STRING_VIEW_CONST("Accepted ") }; static bool is_dev_log(const string_view_t *sv) { int i; for (i = 0; i < (int) ARRAY_SIZE(k_dev_logs); i++) { if (string_view_equal(&k_dev_logs[i], sv)) return true; } return false; } static bool check_if_log_line_starts_with_keywords(const string_view_t* sv, const string_view_t* keywords, int keywords_amount) { int i; for (i = 0; i < keywords_amount; i++) { if (string_view_starts_with(sv, &keywords[i])) return true; } return false; } static bool check_if_log_line_failed(const string_view_t* sv) { return check_if_log_line_starts_with_keywords(sv, k_keywords_failed, sizeof(k_keywords_failed) / sizeof(*k_keywords_failed)); } static bool check_if_log_line_success(const string_view_t* sv) { return check_if_log_line_starts_with_keywords(sv, k_keywords_success, sizeof(k_keywords_success) / sizeof(*k_keywords_success)); } static bool check_if_auth_log(string_view_t* sv) { if (sv->str[0] != '<') return false; if (sv->str[1] == '8') { // Levels from 80 to 86 are non-debug authpriv logs if ('0' > sv->str[2] || sv->str[2] > '6') { return false; } } else if (sv->str[1] == '3') { // Levels from 32 to 38 are non-debug auth logs if ('2' > sv->str[2] || sv->str[2] > '8') { return false; } } else { return false; } if (sv->str[3] != '>') return false; string_view_advance(sv, 4); return true; } // musl memmem static const unsigned char *threebyte_memmem(const unsigned char *h, size_t k, const unsigned char *n) { uint32_t nw = (uint32_t)n[0]<<24 | n[1]<<16 | n[2]<<8; uint32_t hw = (uint32_t)h[0]<<24 | h[1]<<16 | h[2]<<8; for (h+=3, k-=3; k; k--, hw = (hw|*h++)<<8) if (hw == nw) return h-3; return hw == nw ? h-3 : NULL; } typedef struct { // check if 'cut_log.str' to check if message is useful string_view_t cut_log; bool is_success; } log_analysis_result_t; #define LOG_ANALYSIS_RESULT_OK(res) ((res).cut_log.str) // Lines that arrive here look like "<36> Jan 20 19:09:58 sshd[44171]: Accepted password for user from 192.168.223.1 port 59064 ssh2" // Our target is to extract line that looks like "Accepted password for user from 192.168.223.1 port 59064 ssh2" static log_analysis_result_t analyze_log_msg(string_view_t log_msg) { log_analysis_result_t result; char* log_start; bool any_known = false; string_view_init_empty(&result.cut_log); result.is_success = false; if (!check_if_auth_log(&log_msg)) return result; string_view_advance(&log_msg, sizeof(" Jan 20 19:09:58 ") - 1); // Find the "]: " pattern when actual event starts. We are guaranteed that such event pattern will exist: // LogTag is expected to be equal to argv0 or __progname - not NULL. LOG_PID flag is enabled for sshd. /* if (LogStat & LOG_PID) fprintf (f, "[%d]", (int) __getpid ()); if (LogTag != NULL) { __putc_unlocked (':', f); __putc_unlocked (' ', f); } */ if (log_msg.len < 3) return result; log_start = (char*) threebyte_memmem((unsigned char*) log_msg.str, log_msg.len, (const unsigned char*) "]: "); if (!log_start) return result; string_view_advance(&log_msg, log_start - log_msg.str + 3); // Check if prefix matches well-known line prefixes if (!any_known && check_if_log_line_failed(&log_msg)) { any_known = true; result.is_success = false; } if (!any_known && check_if_log_line_success(&log_msg)) { any_known = true; result.is_success = true; } if (any_known) result.cut_log = log_msg; return result; } #define MIN_LEN ((int) sizeof("<86>Jan 20 14:53:50 []: Failed ")) #define SNIFF_LEN 512 static void handle_log_msg(task_info_t *task_info, string_view_t log_msg) { transport_ids_t transport_ids; int event; uint64_t generatedEventsMask; log_analysis_result_t result = analyze_log_msg(log_msg); if (!LOG_ANALYSIS_RESULT_OK(result)) return; event = result.is_success ? FP_SI_OT_NOTIFY_SOCKET_AUTH_LOG_SUCCESS : FP_SI_OT_NOTIFY_SOCKET_AUTH_LOG_FAILED; generatedEventsMask = MSG_TYPE_TO_EVENT_MASK(event); if (!(generatedEventsMask & transport_global_get_combined_mask())) return; transport_global_get_ids(&transport_ids, generatedEventsMask); if (task_info_can_skip(task_info, &transport_ids, generatedEventsMask)) return; net_event_auth_log(task_info, result.cut_log, result.is_success); } void unix_socket_log_sendmsg(task_info_t *task_info, struct socket *sock, struct msghdr *msg, int size) { struct sockaddr_storage storage; int sniff_amount; size_t copied_size; string_view_t target_path; char* log_msg; if (size < MIN_LEN) return; string_view_init_empty(&target_path); if (msg->msg_name) { if (msg->msg_namelen > (int) (sizeof(sa_family_t) + 1)) { struct sockaddr_un* sun = (struct sockaddr_un*) msg->msg_name; string_view_init(&target_path, sun->sun_path, msg->msg_namelen - sizeof(sa_family_t)); } } else { int len = sock_to_addr(sock, &storage, 2 /*peer*/); if (len > (int) (sizeof(sa_family_t) + 1)) { struct sockaddr_un* sun = (struct sockaddr_un*) &storage; string_view_init(&target_path, sun->sun_path, len - sizeof(sa_family_t)); } } if (!target_path.str) return; target_path.len = strnlen(target_path.str, target_path.len); if (!target_path.len) return; if (!is_dev_log(&target_path)) return; sniff_amount = size; if (sniff_amount > SNIFF_LEN) sniff_amount = SNIFF_LEN; log_msg = (char*) mem_alloc(sniff_amount); if (!log_msg) return; copied_size = msg_data_peek(msg, log_msg, sniff_amount); if (copied_size > MIN_LEN) { string_view_t log_msg_sv; string_view_init(&log_msg_sv, log_msg, copied_size); handle_log_msg(task_info, log_msg_sv); } mem_free(log_msg); }