Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Binaries and object files
bin/
*.o
*.elf

Expand Down
Binary file modified bin/ssh_monitor.o
Binary file not shown.
21 changes: 21 additions & 0 deletions cmd/ssh_fail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package cmd

import (
"fmt"
"netbarrier/core"

"github.com/spf13/cobra"
)

var sshFailCmd = &cobra.Command{
Use: "ssh-fail-monitor",
Short: "Run SSH failed login monitor using eBPF",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Running SSH Fail Monitor")
core.RunSSHFailMonitor()
},
}

func init() {
rootCmd.AddCommand(sshFailCmd)
}
155 changes: 155 additions & 0 deletions core/ssh_fail_monitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package core

import (
"bytes"
"encoding/binary"
"fmt"
"log"
"os"
"os/signal"
"os/user"
"strings"
"sync"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
)

type sshFailEvent struct {
Pid uint32
Uid uint32
TimestampNs uint64
Comm [16]byte
FailReason uint8
_ [7]byte // padding
}

var attemptCount = struct {
sync.Mutex
data map[uint32]int // map[PID] -> count
}{
data: make(map[uint32]int),
}

func RunSSHFailMonitor() {
spec, err := ebpf.LoadCollectionSpec("bin/ssh_fail_monitor.o")
if err != nil {
log.Fatalf("Failed to load eBPF spec: %v", err)
}

objs := struct {
SshFailMonitor *ebpf.Program `ebpf:"ssh_fail_monitor"`
SshFailRingbuf *ebpf.Map `ebpf:"SSH_FAIL_RINGBUF"`
}{}

if err := spec.LoadAndAssign(&objs, nil); err != nil {
log.Fatalf("Failed to load eBPF objects: %v", err)
}
defer func() {
_ = objs.SshFailMonitor.Close()
_ = objs.SshFailRingbuf.Close()
}()

libpamPath := findLibPam()
if libpamPath == "" {
log.Fatal("libpam.so not found")
}
log.Printf("Using libpam at: %s", libpamPath)

up, err := link.OpenExecutable(libpamPath)
if err != nil {
log.Fatalf("OpenExecutable failed: %v", err)
}

attached, err := up.Uretprobe("pam_authenticate", objs.SshFailMonitor, nil)
if err != nil {
log.Fatalf("Failed to attach uretprobe: %v", err)
}
defer attached.Close()

log.Println("Successfully attached uretprobe to pam_authenticate")

rd, err := ringbuf.NewReader(objs.SshFailRingbuf)
if err != nil {
log.Fatalf("Failed to open ringbuf reader: %v", err)
}
defer rd.Close()

log.Println("SSH fail monitor started. Press Ctrl+C to stop.")
sig := make(chan os.Signal, 1)
done := make(chan struct{})
signal.Notify(sig, os.Interrupt)

go func() {
for {
select {
case <-done:
return
default:
record, err := rd.Read()
if err != nil {
if !strings.Contains(err.Error(), "closed") {
log.Printf("ringbuf read failed: %v", err)
}
continue
}

var event sshFailEvent
if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
log.Printf("binary read failed: %v", err)
continue
}

username := lookupUsername(event.Uid)
processName := strings.TrimRight(string(event.Comm[:]), "\x00")
reason := "unknown"
switch event.FailReason {
case 1:
reason = "invalid password"
case 2:
reason = "invalid user"
}

attemptCount.Lock()
attemptCount.data[event.Pid]++
count := attemptCount.data[event.Pid]
attemptCount.Unlock()

fmt.Printf(
"%s (PID %d, UID %d, user: %s) → Failed SSH login: %s [Attempt #%d]\n",
processName, event.Pid, event.Uid, username, reason, count,
)
}
}
}()

<-sig
fmt.Println("\nExiting SSH monitor.")
close(done)
}

func lookupUsername(uid uint32) string {
userObj, err := user.LookupId(fmt.Sprintf("%d", uid))
if err != nil {
return "unknown"
}
return userObj.Username
}

func findLibPam() string {
possiblePaths := []string{
"/lib/x86_64-linux-gnu/libpam.so.0",
"/lib64/libpam.so.0",
"/usr/lib/libpam.so.0",
"/usr/lib64/libpam.so.0",
"/lib/libpam.so.0",
}

for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
return path
}
}
return ""
}
18 changes: 18 additions & 0 deletions ebpf-programs/ssh_fail_monitor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "ssh_fail_monitor"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["staticlib"]
name = "ssh_fail_monitor"

[dependencies]
aya-ebpf = { version = "0.1.1", default-features = false }

[profile.release]
opt-level = "z"
lto = true
panic = "abort"
codegen-units = 1
strip = "debuginfo"
4 changes: 4 additions & 0 deletions ebpf-programs/ssh_fail_monitor/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[toolchain]
channel = "nightly"
components = ["rust-src"]
targets = ["bpfel-unknown-none"]
96 changes: 96 additions & 0 deletions ebpf-programs/ssh_fail_monitor/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#![no_std]
#![no_main]

use aya_ebpf::{
macros::map,
maps::{HashMap, RingBuf},
programs::ProbeContext,
helpers::{
bpf_get_current_pid_tgid,
bpf_get_current_uid_gid,
bpf_get_current_comm,
bpf_ktime_get_ns,
},
EbpfContext,
cty::c_int,
};
use aya_ebpf::bpf_printk;

#[repr(C)]
#[derive(Clone, Copy)]
pub struct SshFailEvent {
pub pid: u32,
pub uid: u32,
pub timestamp_ns: u64,
pub comm: [u8; 16],
pub ret_code: i32,
_padding: [u8; 3],
}

#[map(name = "SSH_FAIL_RINGBUF")]
static mut SSH_FAIL_RINGBUF: RingBuf = RingBuf::with_byte_size(4096, 0);

#[map(name = "SSH_FAIL_COUNTS")]
static mut SSH_FAIL_COUNTS: HashMap<u32, u32> =
HashMap::<u32, u32>::with_max_entries(1024, 0);

#[no_mangle]
#[link_section = "uretprobe/pam_authenticate"]
pub fn ssh_fail_monitor(ctx: ProbeContext) -> u32 {
let _ = try_monitor(ctx);
0
}

fn try_monitor(ctx: ProbeContext) -> Result<(), ()> {
// SAFELY extract return value from `ctx`

let ret_val = unsafe { *(ctx.as_ptr() as *const c_int) };
unsafe {
bpf_printk!(b"ssh_fail_monitor: ret_val seen\n");
}
if ret_val == 0 {
return Ok(());
}

let pid = (bpf_get_current_pid_tgid() >> 32) as u32;
let uid = (bpf_get_current_uid_gid() >> 32) as u32;


unsafe {
bpf_printk!(b"[ssh_fail] processing event\n");
}

let mut comm = [0u8; 16];
if let Ok(val) = bpf_get_current_comm() {
comm.copy_from_slice(&val);
}

let ts = unsafe { bpf_ktime_get_ns() };

let event = SshFailEvent {
pid,
uid,
timestamp_ns: ts,
comm,
ret_code: ret_val,
_padding: [0; 3],
};

unsafe {
let count = SSH_FAIL_COUNTS.get(&pid).copied().unwrap_or(0);
SSH_FAIL_COUNTS.insert(&pid, &(count + 1), 0).ok();
SSH_FAIL_RINGBUF.output(&event, 0);
bpf_printk!(b"[ssh_fail] event emitted\n");
}

Ok(())
}

#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}

#[no_mangle]
#[link_section = "license"]
pub static LICENSE: [u8; 4] = *b"GPL\0";
4 changes: 2 additions & 2 deletions ebpf-programs/ssh_monitor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[package]
name = "ssh_monitor_ebpf"
name = "ssh_monitor"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["staticlib"]
name = "ssh_monitor_ebpf"
name = "ssh_monitor"

[dependencies]
aya-ebpf = { version = "0.1.1", default-features = false }
Expand Down
Loading