|
|
|
@@ -1,13 +1,10 @@
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
# dns_bpf_correlate.sh
|
|
|
|
# dns_bpf_correlate_fixed.sh
|
|
|
|
# Usage: sudo ./dns_bpf_correlate.sh
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# Requires: bpftrace, tcpdump, date (GNU)
|
|
|
|
|
|
|
|
# Purpose: correlate DNS packets (tcpdump) with the process that issued them using bpftrace syscall tracing.
|
|
|
|
|
|
|
|
set -u
|
|
|
|
set -u
|
|
|
|
|
|
|
|
|
|
|
|
DOMAIN=$1
|
|
|
|
DOMAIN="tracker1.myporn.club"
|
|
|
|
WINDOW_MS=2000 # how many ms back to search for matching bpftrace events
|
|
|
|
WINDOW_MS=2000
|
|
|
|
|
|
|
|
HALF_WINDOW_MS=$((WINDOW_MS/2))
|
|
|
|
BASE_TS="$(date +%Y%m%d_%H%M%S)"
|
|
|
|
BASE_TS="$(date +%Y%m%d_%H%M%S)"
|
|
|
|
DNSLOG="${DOMAIN}_${BASE_TS}_dns.log"
|
|
|
|
DNSLOG="${DOMAIN}_${BASE_TS}_dns.log"
|
|
|
|
BPFLOG="bpftrace_${BASE_TS}.log"
|
|
|
|
BPFLOG="bpftrace_${BASE_TS}.log"
|
|
|
|
@@ -19,12 +16,12 @@ RED="\e[31m"; GREEN="\e[32m"; YELLOW="\e[33m"; BLUE="\e[34m"; RESET="\e[0m"
|
|
|
|
TCPDUMP_PID=""
|
|
|
|
TCPDUMP_PID=""
|
|
|
|
BPF_PID=""
|
|
|
|
BPF_PID=""
|
|
|
|
|
|
|
|
|
|
|
|
now_ms() { date +%s%3N; } # epoch milliseconds (GNU date)
|
|
|
|
now_ms() { date +%s%3N; }
|
|
|
|
|
|
|
|
|
|
|
|
cleanup() {
|
|
|
|
cleanup() {
|
|
|
|
echo -e "${BLUE}[*] Stopping background processes...${RESET}"
|
|
|
|
echo -e "${BLUE}[*] Stopping background processes...${RESET}"
|
|
|
|
[[ -n "$TCPDUMP_PID" ]] && sudo kill "$TCPDUMP_PID" 2>/dev/null || true
|
|
|
|
[[ -n "$TCPDUMP_PID" ]] && kill "$TCPDUMP_PID" 2>/dev/null || true
|
|
|
|
[[ -n "$BPF_PID" ]] && sudo kill "$BPF_PID" 2>/dev/null || true
|
|
|
|
[[ -n "$BPF_PID" ]] && kill "$BPF_PID" 2>/dev/null || true
|
|
|
|
sleep 0.1
|
|
|
|
sleep 0.1
|
|
|
|
echo -e "${BLUE}[*] Done.${RESET}"
|
|
|
|
echo -e "${BLUE}[*] Done.${RESET}"
|
|
|
|
exit 0
|
|
|
|
exit 0
|
|
|
|
@@ -35,64 +32,50 @@ echo -e "${BLUE}[*] Domain: ${YELLOW}$DOMAIN${RESET}"
|
|
|
|
echo -e "${BLUE}[*] DNS log: ${YELLOW}$DNSLOG${RESET}"
|
|
|
|
echo -e "${BLUE}[*] DNS log: ${YELLOW}$DNSLOG${RESET}"
|
|
|
|
echo -e "${BLUE}[*] bpftrace log: ${YELLOW}$BPFLOG${RESET}"
|
|
|
|
echo -e "${BLUE}[*] bpftrace log: ${YELLOW}$BPFLOG${RESET}"
|
|
|
|
echo -e "${BLUE}[*] Incidents directory: ${YELLOW}$INCIDENT_DIR${RESET}"
|
|
|
|
echo -e "${BLUE}[*] Incidents directory: ${YELLOW}$INCIDENT_DIR${RESET}"
|
|
|
|
echo -e "${BLUE}[*] Window for correlation: ${YELLOW}${WINDOW_MS} ms${RESET}"
|
|
|
|
echo -e "${BLUE}[*] Window for correlation: ${YELLOW}${WINDOW_MS} ms (±${HALF_WINDOW_MS} ms)${RESET}"
|
|
|
|
echo ""
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
|
|
# Start tcpdump -> DNSLOG (writes raw tcpdump lines)
|
|
|
|
# Start tcpdump
|
|
|
|
sudo tcpdump -n -i any port 53 -s0 -ttt -l 2>/dev/null >> "$DNSLOG" &
|
|
|
|
tcpdump -n -i any port 53 -s0 -ttt -l 2>/dev/null >> "$DNSLOG" &
|
|
|
|
TCPDUMP_PID=$!
|
|
|
|
TCPDUMP_PID=$!
|
|
|
|
echo -e "${GREEN}[*] tcpdump started (PID $TCPDUMP_PID)${RESET}"
|
|
|
|
echo -e "${GREEN}[*] tcpdump started (PID $TCPDUMP_PID)${RESET}"
|
|
|
|
|
|
|
|
|
|
|
|
# Start bpftrace inline printing epoch seconds + ms and PID/COMM/FD
|
|
|
|
# Start bpftrace inline (correct syntax)
|
|
|
|
# We print: <epoch_s>.<ms> PID=<pid> COMM=<comm> FD=<fd>
|
|
|
|
bpftrace -e '
|
|
|
|
# Use time (seconds since epoch) and nsecs to produce ms fraction.
|
|
|
|
|
|
|
|
sudo bpftrace -e '
|
|
|
|
|
|
|
|
tracepoint:syscalls:sys_enter_sendto
|
|
|
|
tracepoint:syscalls:sys_enter_sendto
|
|
|
|
{
|
|
|
|
{
|
|
|
|
$s = time; // seconds since epoch
|
|
|
|
$s = time; $ms = nsecs/1000000 % 1000;
|
|
|
|
$ms = (nsecs/1000000) % 1000;
|
|
|
|
|
|
|
|
printf("%d.%03d PID=%d COMM=%s FD=%d\n", $s, $ms, pid, comm, args->fd);
|
|
|
|
printf("%d.%03d PID=%d COMM=%s FD=%d\n", $s, $ms, pid, comm, args->fd);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
' > "$BPFLOG" 2>/dev/null &
|
|
|
|
' > "$BPFLOG" 2>/dev/null &
|
|
|
|
BPF_PID=$!
|
|
|
|
BPF_PID=$!
|
|
|
|
sleep 0.2
|
|
|
|
sleep 0.2
|
|
|
|
|
|
|
|
|
|
|
|
# Quick liveness check for bpftrace log (it may stay empty until events occur, but we ensure process exists)
|
|
|
|
|
|
|
|
if ! kill -0 "$BPF_PID" 2>/dev/null; then
|
|
|
|
if ! kill -0 "$BPF_PID" 2>/dev/null; then
|
|
|
|
echo -e "${YELLOW}[!] bpftrace failed to start (process exited). Check bpftrace availability and permissions.${RESET}"
|
|
|
|
echo -e "${RED}[!] bpftrace failed to start!${RESET}"
|
|
|
|
echo -e "${YELLOW} Try: sudo bpftrace -e 'tracepoint:syscalls:sys_enter_sendto { printf(\"hi\\n\"); }'${RESET}"
|
|
|
|
|
|
|
|
cleanup
|
|
|
|
cleanup
|
|
|
|
fi
|
|
|
|
fi
|
|
|
|
|
|
|
|
echo -e "${GREEN}[*] bpftrace started (PID $BPF_PID)${RESET}"
|
|
|
|
# Wait a short moment and check file is being appended to (if no events yet file may be empty - warn instead of failing)
|
|
|
|
|
|
|
|
sleep 1
|
|
|
|
sleep 1
|
|
|
|
if [[ ! -s "$BPFLOG" ]]; then
|
|
|
|
|
|
|
|
echo -e "${YELLOW}[!] Note: bpftrace log is currently empty (no sendto events observed yet). This is normal until a process performs sendto().${RESET}"
|
|
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
echo -e "${GREEN}[*] bpftrace started (PID $BPF_PID) -- logging to $BPFLOG${RESET}"
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
|
|
# Helper: gather proc info for PID
|
|
|
|
# Helper function: /proc info for PID
|
|
|
|
proc_info_for_pid() {
|
|
|
|
proc_info_for_pid() {
|
|
|
|
local pid="$1"
|
|
|
|
local pid="$1"
|
|
|
|
if [[ -r "/proc/$pid/cmdline" ]]; then
|
|
|
|
if [[ -r "/proc/$pid/cmdline" ]]; then
|
|
|
|
local exe
|
|
|
|
local exe=$(readlink -f "/proc/$pid/exe" 2>/dev/null || true)
|
|
|
|
exe=$(sudo readlink -f "/proc/$pid/exe" 2>/dev/null || true)
|
|
|
|
local cmd=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)
|
|
|
|
local cmd
|
|
|
|
|
|
|
|
cmd=$(sudo tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)
|
|
|
|
|
|
|
|
echo "pid=$pid exe=${exe:-N/A} cmdline='${cmd:-N/A}'"
|
|
|
|
echo "pid=$pid exe=${exe:-N/A} cmdline='${cmd:-N/A}'"
|
|
|
|
else
|
|
|
|
else
|
|
|
|
echo "pid=$pid (no /proc info)"
|
|
|
|
echo "pid=$pid (no /proc info)"
|
|
|
|
fi
|
|
|
|
fi
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Tail dns log and correlate with bpftrace by timestamp
|
|
|
|
# Tail tcpdump and correlate symmetrically
|
|
|
|
tail -Fn0 "$DNSLOG" | while IFS= read -r dnsline; do
|
|
|
|
tail -Fn0 "$DNSLOG" | while IFS= read -r dnsline; do
|
|
|
|
# case-insensitive match for domain
|
|
|
|
|
|
|
|
if echo "$dnsline" | grep -qi -- "$DOMAIN"; then
|
|
|
|
if echo "$dnsline" | grep -qi -- "$DOMAIN"; then
|
|
|
|
detect_ms=$(now_ms)
|
|
|
|
detect_ms=$(now_ms)
|
|
|
|
echo -e "${RED}[!] Suspicious DNS request detected @ ${YELLOW}${detect_ms}${RESET}"
|
|
|
|
echo -e "${RED}[!] Suspicious DNS request detected @${YELLOW}$detect_ms${RESET}"
|
|
|
|
echo -e "${YELLOW}$dnsline${RESET}"
|
|
|
|
echo -e "${YELLOW}$dnsline${RESET}"
|
|
|
|
echo ""
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
|
|
@@ -104,66 +87,51 @@ tail -Fn0 "$DNSLOG" | while IFS= read -r dnsline; do
|
|
|
|
echo "detected_at_ms: $detect_ms"
|
|
|
|
echo "detected_at_ms: $detect_ms"
|
|
|
|
} > "$INCIDENT_FILE"
|
|
|
|
} > "$INCIDENT_FILE"
|
|
|
|
|
|
|
|
|
|
|
|
# extract src token (token after IP token)
|
|
|
|
# extract source IP/port
|
|
|
|
src_tok=$(echo "$dnsline" | awk '{for(i=1;i<=NF;i++) if($i=="IP" || $i=="IP6"){print $(i+1); exit}}' || true)
|
|
|
|
src_tok=$(echo "$dnsline" | awk '{for(i=1;i<=NF;i++) if($i=="IP" || $i=="IP6"){print $(i+1); exit}}' || true)
|
|
|
|
if [[ -n "$src_tok" ]]; then
|
|
|
|
if [[ -n "$src_tok" ]]; then
|
|
|
|
srcPort="${src_tok##*.}"
|
|
|
|
srcPort="${src_tok##*.}"
|
|
|
|
srcIP="${src_tok%.*}"
|
|
|
|
srcIP="${src_tok%.*}"
|
|
|
|
echo -e "${GREEN}[+] Extracted source: IP=${YELLOW}${srcIP}${RESET} PORT=${YELLOW}${srcPort}${RESET}"
|
|
|
|
echo -e "${GREEN}[+] Extracted source: IP=${YELLOW}$srcIP${RESET} PORT=${YELLOW}$srcPort${RESET}"
|
|
|
|
echo "extracted_src_ip: $srcIP" >> "$INCIDENT_FILE"
|
|
|
|
echo "extracted_src_ip: $srcIP" >> "$INCIDENT_FILE"
|
|
|
|
echo "extracted_src_port: $srcPort" >> "$INCIDENT_FILE"
|
|
|
|
echo "extracted_src_port: $srcPort" >> "$INCIDENT_FILE"
|
|
|
|
else
|
|
|
|
|
|
|
|
echo -e "${YELLOW}[!] Could not extract source IP/port from tcpdump line${RESET}"
|
|
|
|
|
|
|
|
echo "extracted_src_ip: N/A" >> "$INCIDENT_FILE"
|
|
|
|
|
|
|
|
echo "extracted_src_port: N/A" >> "$INCIDENT_FILE"
|
|
|
|
|
|
|
|
fi
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Search BPFLOG for events within WINDOW_MS before detection
|
|
|
|
# Symmetric window: half before, half after
|
|
|
|
# BPF lines start like: 1600000000.123 PID=123 COMM=foo FD=...
|
|
|
|
low_ms=$((detect_ms - HALF_WINDOW_MS))
|
|
|
|
low_ms=$((detect_ms - WINDOW_MS))
|
|
|
|
high_ms=$((detect_ms + HALF_WINDOW_MS))
|
|
|
|
high_ms=$((detect_ms))
|
|
|
|
|
|
|
|
awk -v low="$low_ms" -v high="$high_ms" '
|
|
|
|
awk -v low="$low_ms" -v high="$high_ms" '
|
|
|
|
BEGIN{FS=" "; OFS=" "}
|
|
|
|
BEGIN{FS=" "; OFS=" "}
|
|
|
|
{
|
|
|
|
{
|
|
|
|
# parse first token as seconds.ms
|
|
|
|
|
|
|
|
if ($1 ~ /^[0-9]+\.[0-9]{3}$/) {
|
|
|
|
if ($1 ~ /^[0-9]+\.[0-9]{3}$/) {
|
|
|
|
split($1, a, ".")
|
|
|
|
split($1, a, ".")
|
|
|
|
t_s = a[1]; t_ms = a[2]
|
|
|
|
t_s=a[1]; t_ms=a[2]; t=t_s*1000+t_ms
|
|
|
|
t = t_s * 1000 + t_ms
|
|
|
|
|
|
|
|
if (t >= low && t <= high) print $0
|
|
|
|
if (t >= low && t <= high) print $0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}' "$BPFLOG" | tee -a "$INCIDENT_FILE" > /tmp/_bpf_matches.$$ || true
|
|
|
|
}' "$BPFLOG" | tee -a "$INCIDENT_FILE" > /tmp/_bpf_matches.$$ || true
|
|
|
|
|
|
|
|
|
|
|
|
if [[ -s /tmp/_bpf_matches.$$ ]]; then
|
|
|
|
if [[ -s /tmp/_bpf_matches.$$ ]]; then
|
|
|
|
echo -e "${GREEN}[+] bpftrace events found within ${WINDOW_MS} ms:${RESET}"
|
|
|
|
echo -e "${GREEN}[+] bpftrace events found in ±${HALF_WINDOW_MS}ms window:${RESET}"
|
|
|
|
sed 's/^/ /' /tmp/_bpf_matches.$$
|
|
|
|
sed 's/^/ /' /tmp/_bpf_matches.$$
|
|
|
|
echo "" >> "$INCIDENT_FILE"
|
|
|
|
|
|
|
|
echo "bpftrace_matches:" >> "$INCIDENT_FILE"
|
|
|
|
echo "bpftrace_matches:" >> "$INCIDENT_FILE"
|
|
|
|
cat /tmp/_bpf_matches.$$ >> "$INCIDENT_FILE"
|
|
|
|
cat /tmp/_bpf_matches.$$ >> "$INCIDENT_FILE"
|
|
|
|
|
|
|
|
|
|
|
|
# extract PIDs
|
|
|
|
|
|
|
|
pids=$(sed -n 's/.*PID=\([0-9]\+\).*/\1/p' /tmp/_bpf_matches.$$ | sort -u)
|
|
|
|
pids=$(sed -n 's/.*PID=\([0-9]\+\).*/\1/p' /tmp/_bpf_matches.$$ | sort -u)
|
|
|
|
if [[ -n "$pids" ]]; then
|
|
|
|
for pid in $pids; do
|
|
|
|
echo -e "${GREEN}[+] Candidate PIDs: ${YELLOW}$pids${RESET}"
|
|
|
|
info=$(proc_info_for_pid "$pid")
|
|
|
|
echo "" >> "$INCIDENT_FILE"
|
|
|
|
echo " [*] $info"
|
|
|
|
echo "candidate_pids:" >> "$INCIDENT_FILE"
|
|
|
|
echo " - $info" >> "$INCIDENT_FILE"
|
|
|
|
for pid in $pids; do
|
|
|
|
done
|
|
|
|
info=$(proc_info_for_pid "$pid")
|
|
|
|
|
|
|
|
echo " - $info"
|
|
|
|
|
|
|
|
echo " - $info" >> "$INCIDENT_FILE"
|
|
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
else
|
|
|
|
else
|
|
|
|
echo -e "${YELLOW}[!] No bpftrace events found in window (this can happen if syscall happened >${WINDOW_MS}ms before tcpdump line or bpftrace didn't capture).${RESET}"
|
|
|
|
echo -e "${YELLOW}[!] No bpftrace events in window${RESET}"
|
|
|
|
echo "bpftrace_matches: NONE in ${WINDOW_MS}ms window" >> "$INCIDENT_FILE"
|
|
|
|
echo "bpftrace_matches: NONE in ±${HALF_WINDOW_MS}ms" >> "$INCIDENT_FILE"
|
|
|
|
fi
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# append an ss snapshot for context
|
|
|
|
|
|
|
|
echo "" >> "$INCIDENT_FILE"
|
|
|
|
echo "" >> "$INCIDENT_FILE"
|
|
|
|
echo "ss_snapshot:" >> "$INCIDENT_FILE"
|
|
|
|
echo "ss_snapshot:" >> "$INCIDENT_FILE"
|
|
|
|
sudo ss -tulpn 2>/dev/null >> "$INCIDENT_FILE" || true
|
|
|
|
ss -tulpn 2>/dev/null >> "$INCIDENT_FILE" || true
|
|
|
|
|
|
|
|
|
|
|
|
echo -e "${GREEN}[+] Incident saved to ${YELLOW}${INCIDENT_FILE}${RESET}"
|
|
|
|
echo -e "${GREEN}[+] Incident saved to ${YELLOW}$INCIDENT_FILE${RESET}"
|
|
|
|
echo -e "${BLUE}------------------------------------------------------${RESET}"
|
|
|
|
echo -e "${BLUE}------------------------------------------------------${RESET}"
|
|
|
|
echo ""
|
|
|
|
echo ""
|
|
|
|
rm -f /tmp/_bpf_matches.$$
|
|
|
|
rm -f /tmp/_bpf_matches.$$
|
|
|
|
|