summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordaniel <daniel@planethacker.net>2025-05-06 16:57:32 -0700
committerdaniel <daniel@planethacker.net>2025-05-06 16:57:32 -0700
commit2278df1493e064c197913e49b5d1935942d83448 (patch)
tree42f06ab2f76e2ddf228bafbb03f79621975a4534
initial import
-rw-r--r--LICENSE.md22
-rw-r--r--Makefile56
-rw-r--r--README.md116
-rw-r--r--include/agent_context.h23
-rw-r--r--include/aho-corasick.h30
-rw-r--r--include/av_rules.h86
-rw-r--r--include/base64.h4
-rw-r--r--include/djb2.h5
-rw-r--r--include/dns.h15
-rw-r--r--include/entropy.h12
-rw-r--r--include/error.h8
-rw-r--r--include/fanotify.h6
-rw-r--r--include/hash_ledger.h59
-rw-r--r--include/json.h54
-rw-r--r--include/md5.h6
-rw-r--r--include/net.h9
-rw-r--r--include/output.h4
-rw-r--r--include/proc.h49
-rw-r--r--include/proc_connector.h24
-rw-r--r--include/proc_ledger.h61
-rw-r--r--include/sha256.h40
-rw-r--r--include/sniffer.h44
-rw-r--r--include/string_common.h6
-rw-r--r--include/time_common.h4
-rw-r--r--misc/comm.c18
-rw-r--r--misc/evil.c6
-rw-r--r--misc/server_example.py51
-rw-r--r--noawareness-pid-check.sh28
-rw-r--r--rules10
-rw-r--r--src/aho-corasick.c317
-rw-r--r--src/av_rules.c380
-rw-r--r--src/base64.c124
-rw-r--r--src/djb2.c14
-rw-r--r--src/dns.c113
-rw-r--r--src/entropy.c65
-rw-r--r--src/error.c57
-rw-r--r--src/fanotify.c210
-rw-r--r--src/hash_ledger.c184
-rw-r--r--src/json.c321
-rw-r--r--src/md5.c57
-rw-r--r--src/md5/Makefile10
-rw-r--r--src/md5/global.h41
-rw-r--r--src/md5/md5.h46
-rw-r--r--src/md5/md5c.c380
-rw-r--r--src/net.c60
-rw-r--r--src/noawareness.c512
-rw-r--r--src/output.c53
-rw-r--r--src/proc.c268
-rw-r--r--src/proc_connector.c758
-rw-r--r--src/proc_ledger.c344
-rw-r--r--src/sha256.c198
-rw-r--r--src/sniffer.c582
-rw-r--r--src/string.c56
-rw-r--r--src/time.c35
-rw-r--r--test/test-endswith.c17
-rw-r--r--test/test-parse_dns_udp-multi.c47
-rw-r--r--test/test-parse_dns_udp.c42
-rw-r--r--test/test-sha256.c75
58 files changed, 6222 insertions, 0 deletions
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..2d2b923
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) 2019-2025 Daniel Roberson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9c41967
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,56 @@
+CC ?= gcc
+CFLAGS += -g -Wall -Iinclude
+LFLAGS += -static
+#LFLAGS +=
+LIBS =
+
+SRC_DIR = src
+BUILD_DIR = build
+MD5_DIR = src/md5
+
+SRC_FILES = $(wildcard $(SRC_DIR)/*.c)
+OFILES = $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRC_FILES))
+MD5_OBJ = $(MD5_DIR)/md5c.o
+
+TEST_DIR = test
+TEST_SRC_FILES = $(wildcard $(TEST_DIR)/*.c)
+TEST_OFILES = $(patsubst $(TEST_DIR)/%.c, $(BUILD_DIR)/%.o, $(TEST_SRC_FILES))
+TEST_BINARIES = $(patsubst $(TEST_DIR)/%.c, $(BUILD_DIR)/%, $(TEST_SRC_FILES))
+
+all: noawareness test
+
+$(BUILD_DIR):
+ mkdir -p $(BUILD_DIR)
+
+$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c include/*.h | $(BUILD_DIR)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(MD5_OBJ): $(MD5_DIR)/md5c.c $(MD5_DIR)/md5.h
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+noawareness: $(OFILES) $(MD5_OBJ)
+ $(CC) $(CFLAGS) $(LFLAGS) $(OFILES) $(MD5_OBJ) -o $@ $(LIBS)
+
+
+
+$(BUILD_DIR)/test-endswith: $(TEST_DIR)/test-endswith.c $(BUILD_DIR)/string.o
+ $(CC) $(CFLAGS) $^ -o $@
+
+$(BUILD_DIR)/test-sha256: $(TEST_DIR)/test-sha256.c $(BUILD_DIR)/sha256.o
+ $(CC) $(CFLAGS) $^ -o $@
+
+$(BUILD_DIR)/test-parse_dns_udp: $(TEST_DIR)/test-parse_dns_udp.c $(BUILD_DIR)/dns.o
+ $(CC) $(CFLAGS) $^ -o $@
+
+$(BUILD_DIR)/test-parse_dns_udp-multi: $(TEST_DIR)/test-parse_dns_udp-multi.c $(BUILD_DIR)/dns.o
+ $(CC) $(CFLAGS) $^ -o $@
+
+test: $(TEST_BINARIES)
+ @echo "Running tests..."
+ @for test in $(TEST_BINARIES); do \
+ echo "Running $$test..."; \
+ $$test; \
+ done
+
+clean:
+ rm -rf $(BUILD_DIR)/* $(MD5_OBJ) noawareness *~
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e435970
--- /dev/null
+++ b/README.md
@@ -0,0 +1,116 @@
+# noawareness
+
+`noawareness` is a lightweight process and network activity logger for
+Linux designed for incident response, threat hunting, and postmortem
+analysis.
+
+It monitors and logs:
+
+- Process creation and destruction.
+- UID/GID changes
+- `ptrace` activity
+- DNS queries
+- TCP session activity
+
+It also has the capability of blocking executions with user-defined
+signatures via fanotify's FAN_OPEN_EXEC_PERM feature (see "rules" for
+a basic examples of rule definitions).
+
+All output is formatted with JSON making it easy to ingest into tools
+like Splunk, Elasticsearch, or your own log processors.
+
+Warning: This project is under active development and was designed for
+usage at CTFs--not production use. It's crude, incomplete, and
+buggy. You've been warned.
+
+## Building
+
+To build, run:
+````
+make
+```
+
+This produces a binary named `noawareness` that can be dropped onto most
+Linux systems and run as-is.
+
+## Usage
+
+Run `./noawareness` with apropriate flags. Example:
+
+```sh
+sudo ./noawareness -xi eth0
+```
+
+For a list of command line flags, use the `-h` option:
+```
+./noawareness -h
+```
+
+```
+usage: ./noawareness [-h?]
+
+ -h/-? - Print this menu and exit.
+ -d - Daemonize. Default: no
+ -i <iface> - Interface to sniff
+ -x - Toggle promiscuous mode. Default: false
+ -m <bytes> - Max size of file to hash. Default: 104857600
+ -o <file> - Outfile for JSON output, Default: /var/log/noawareness.json.log
+ -O - Toggle local JSON logging. Default: false
+ -P <path> - Path to PID file. Default: /var/run/noawareness.pid
+ -r - Toggle remote logging. Default: true
+ -R <path> - Path to AV rule definitions. Default: rules
+ -s <IP> - Remote log server. Default: 127.0.0.1
+ -S - Toggle syslog. Default: true
+ -p <port> - Port of remote server. Default: 55555
+ -q - Toggle quiet mode. Default: false
+```
+
+
+## Logging
+
+noawareness can send logs over UDP to a remote server for further
+processing and ingestion into a log search engine such as Splunk or
+Elasticsearch. The choice of UDP was made for simplicity. More robust
+and reliable logging over TCP is in the roadmap of upcoming features.
+
+## musl
+
+You can statically compile `noawareness` with `musl` to run it on a wide
+range of systems, including older or stripped-down Linux installs:
+
+- Symlink Linux headers if your system uses multiarch paths (some
+ setups require this).
+
+ ```
+ sudo ln -s /usr/include/linux /usr/include/x86_64-linux-musl/
+ sudo ln -s /usr/include/asm-generic /usr/include/x86_64-linux-musl/
+ sudo ln -s /usr/include/x86_64-linux-gnu/asm /usr/include/x86_64-linux-musl/
+ ```
+
+ Optionally, sanitized kernel headers such as the ones provided by
+ Sabotage Linux may be used if desired (this is untested):
+ https://github.com/sabotage-linux/kernel-headers
+
+ See here for more detail:
+ https://groups.google.com/g/linux.debian.bugs.dist/c/Q2KVBy8QXYM
+
+- Set CC to musl-gcc or whichever cross-compiler you are using:
+
+ ```
+ CC=musl-gcc make
+ ```
+
+## Why?
+
+I needed a lightweight, easy to deploy, and portable tool to track
+activity on Linux boxes at CTFs. Many similar tools require excessive
+dependencies, configuration, or were paywalled.
+
+## Credits
+
+- base64 implementation by Jouni Malinen
+
+- The SHA256 implementation by Brad Conte: https://github.com/B-Con/crypto-algorithms
+
+- The MD5 implementation used in this project was originally written
+ by RSA Data Security, Inc.
diff --git a/include/agent_context.h b/include/agent_context.h
new file mode 100644
index 0000000..01bd3d0
--- /dev/null
+++ b/include/agent_context.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <limits.h>
+
+//#include <linux/limits.h>
+
+#include "proc_ledger.h"
+#include "hash_ledger.h"
+#include "aho-corasick.h"
+#include "av_rules.h"
+
+struct agent_context {
+ struct proc_ledger *proc_ledger;
+ struct hash_ledger *hash_ledger;
+ rule_set_t rules;
+ ac_context_t *ac;
+ char hostname[HOST_NAME_MAX];
+ long ticks;
+ unsigned long boot_time;
+ size_t maxsize;
+};
+
+typedef struct agent_context agent_context_t;
diff --git a/include/aho-corasick.h b/include/aho-corasick.h
new file mode 100644
index 0000000..a468256
--- /dev/null
+++ b/include/aho-corasick.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+
+#define AC_BUF_SIZE 65536
+#define AC_ALPHABET_SIZE 256
+
+// forward declaration to allow users to define context
+struct ac_context;
+
+typedef struct ac_match {
+ const char *id;
+ size_t offset;
+ size_t len;
+} ac_match_t;
+
+typedef struct ac_context ac_context_t;
+typedef struct pattern_node ac_node_t;
+typedef struct pattern_entry ac_pattern_t;
+
+typedef void (*ac_callback)(const ac_match_t *match, void *user_data);
+
+ac_context_t *ac_new(void);
+void ac_free(ac_context_t *ctx);
+int ac_add_pattern(ac_context_t *ctx, const char *id, const uint8_t *pattern, size_t len);
+int ac_build(ac_context_t *ctx);
+int ac_match(ac_context_t *ctx, const uint8_t *data, size_t len, ac_callback cb, void *user_data);
+int ac_match_fd(ac_context_t *ctx, int fd, ac_callback cb, void *user_data);
+int ac_match_path(ac_context_t *ctx, const char *path, ac_callback cb, void *user_data);
diff --git a/include/av_rules.h b/include/av_rules.h
new file mode 100644
index 0000000..8e55a54
--- /dev/null
+++ b/include/av_rules.h
@@ -0,0 +1,86 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#define MAX_RULE_CONDITIONS 16
+#define MAX_RULES 128
+#define MAX_PATTERNS 512
+#define MAX_MATCH_OFFSETS 64
+#define PATTERN_TABLE_SIZE 1024
+
+typedef struct {
+ const char *id;
+ uint8_t *bytes;
+ size_t len;
+
+ size_t match_offsets[MAX_MATCH_OFFSETS];
+ size_t match_count;
+} pattern_t;
+
+/* typedef struct { */
+/* const char *id; */
+/* pattern_t *entry; */
+/* } pattern_bucket_t; */
+
+typedef struct pattern_bucket {
+ const char *id;
+ pattern_t *entry;
+ struct pattern_bucket *next;
+} pattern_bucket_t;
+
+//pattern_bucket_t *buckets[PATTERN_TABLE_SIZE];
+
+typedef struct {
+ pattern_t patterns[MAX_PATTERNS];
+ size_t count;
+
+ pattern_bucket_t *buckets[PATTERN_TABLE_SIZE];
+} pattern_table_t;
+
+
+typedef enum {
+ RULE_BLOCK,
+ RULE_ALLOW,
+ RULE_QUARANTINE,
+ RULE_INFORMATIONAL
+} rule_action_t;
+
+typedef enum {
+ COND_TYPE_REQUIRED, // and
+ COND_TYPE_OPTIONAL, // or
+ COND_TYPE_NEGATED // not
+} condition_type_t;
+
+typedef struct {
+ const char *pattern_id;
+ size_t offset;
+ bool has_offset;
+ condition_type_t type;
+} rule_condition_t;
+
+typedef struct {
+ const char *id;
+ rule_action_t action;
+ rule_condition_t conditions[MAX_RULE_CONDITIONS];
+ size_t condition_count;
+} rule_t;
+
+typedef struct {
+ rule_t rules[MAX_RULES];
+ size_t rule_count;
+ pattern_table_t patterns;
+} rule_set_t;
+
+
+pattern_t *pattern_table_find(pattern_table_t *table, const char *id);
+int pattern_table_add(pattern_table_t *table, const char *id, const uint8_t *bytes, size_t len);
+void pattern_table_clear_matches(pattern_table_t *table);
+void pattern_table_free(pattern_table_t *table);
+
+int load_rules(const char *path, rule_set_t *out_ruleset);
+rule_action_t parse_action(const char *s);
+bool evaluate_rule(const rule_t *rule, pattern_table_t *patterns);
+void dump_rule(const rule_t *r);
+void free_rules(rule_set_t *rules);
diff --git a/include/base64.h b/include/base64.h
new file mode 100644
index 0000000..7938980
--- /dev/null
+++ b/include/base64.h
@@ -0,0 +1,4 @@
+#pragma once
+
+unsigned char *base64_encode(const unsigned char *, size_t, size_t *);
+unsigned char *base64_decode(const unsigned char *, size_t, size_t *);
diff --git a/include/djb2.h b/include/djb2.h
new file mode 100644
index 0000000..fda01f9
--- /dev/null
+++ b/include/djb2.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include <stdint.h>
+
+uint32_t djb2(const char *str);
diff --git a/include/dns.h b/include/dns.h
new file mode 100644
index 0000000..eb8f125
--- /dev/null
+++ b/include/dns.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+#define MAX_DNS_NAME_LEN 256
+#define MAX_DNS_QUESTIONS 8
+
+struct dns_question {
+ char name[MAX_DNS_NAME_LEN];
+ uint16_t qtype;
+};
+
+const char *dns_type_to_string(uint16_t type);
+size_t parse_dns_udp(const uint8_t *payload, size_t len, struct dns_question *out, size_t max_qs);
diff --git a/include/entropy.h b/include/entropy.h
new file mode 100644
index 0000000..e246682
--- /dev/null
+++ b/include/entropy.h
@@ -0,0 +1,12 @@
+#include <stddef.h>
+
+#pragma once
+
+typedef struct entropy_ctx {
+ size_t freq[256];
+ size_t total_bytes;
+} entropy_ctx;
+
+void entropy_init(entropy_ctx *ctx);
+void entropy_update(entropy_ctx *ctx, const unsigned char *buf, size_t len);
+double entropy_final(entropy_ctx *ctx);
diff --git a/include/error.h b/include/error.h
new file mode 100644
index 0000000..5ae9b0d
--- /dev/null
+++ b/include/error.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include <string.h>
+#include <errno.h>
+
+
+void error(const char *, ...);
+void error_fatal(const char *, ...);
diff --git a/include/fanotify.h b/include/fanotify.h
new file mode 100644
index 0000000..d58109f
--- /dev/null
+++ b/include/fanotify.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include "agent_context.h"
+
+int setup_fanotify(void);
+void select_fanotify(int fan_fd, agent_context_t *ctx);
diff --git a/include/hash_ledger.h b/include/hash_ledger.h
new file mode 100644
index 0000000..24e2ac3
--- /dev/null
+++ b/include/hash_ledger.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <stddef.h>
+#include <stdbool.h>
+#include <limits.h>
+
+#define MAX_MATCHED_PATTERNS 32
+#define MAX_MATCHED_RULES 16
+
+typedef enum {
+ VERDICT_UNKNOWN = 0,
+ VERDICT_ALLOW,
+ VERDICT_BLOCK,
+ VERDICT_QUARANTINE,
+ VERDICT_INFORMATIONAL
+} scan_verdict_t;
+
+struct hash_entry {
+ char path[PATH_MAX];
+ char md5[33];
+ char sha256[65];
+ double entropy;
+ struct stat sb;
+ time_t last_scanned;
+ size_t scan_count;
+
+ scan_verdict_t verdict;
+ const char *matched_patterns[MAX_MATCHED_PATTERNS];
+ size_t matched_pattern_count;
+ const char *matched_rules[MAX_MATCHED_RULES];
+ size_t matched_rule_count;
+
+ struct hash_entry *next; // collision handling
+};
+
+struct hash_ledger {
+ size_t num_buckets;
+ struct hash_entry **buckets;
+};
+
+// structure to hold output for multihash()
+struct multihash {
+ char md5[33];
+ char sha256[65];
+ double entropy;
+};
+
+// TODO hash_ledger_t
+struct hash_ledger *hash_ledger_init(size_t num_buckets);
+void hash_ledger_destroy(struct hash_ledger *ledger);
+size_t hash_ledger_bucket(struct hash_ledger *ledger, const char *path);
+struct hash_entry *hash_ledger_find(struct hash_ledger *ledger, const char *path);
+struct hash_entry *hash_ledger_add_or_update(struct hash_ledger *ledger,
+ const char *path,
+ struct stat *sb);
+bool multihash_file(const char *path, struct multihash *out);
diff --git a/include/json.h b/include/json.h
new file mode 100644
index 0000000..db508df
--- /dev/null
+++ b/include/json.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#define JSON_MAX_DEPTH 8
+
+typedef enum {
+ JSON_CONTEXT_OBJECT,
+ JSON_CONTEXT_ARRAY
+} json_context_t;
+
+typedef struct {
+ char *data;
+ size_t length;
+ size_t capacity;
+ json_context_t context_stack[JSON_MAX_DEPTH];
+ int context_depth;
+ bool needs_comma[JSON_MAX_DEPTH];
+} json_t;
+
+void json_init(json_t *buf);
+void json_free(json_t *buf);
+const char *json_get(json_t *buf);
+
+void json_start_object(json_t *buf);
+void json_end_object(json_t *buf);
+void json_set_needs_comma(json_t *buf);
+bool json_needs_comma(json_t *buf);
+
+void json_append(json_t *buf, const char *fmt, ...);
+void json_escape_string(json_t *buf, const char *input);
+
+void json_add_string(json_t *buf, const char *key, const char *value);
+void json_add_string_or_null(json_t *buf, const char *key, const char *value);
+void json_add_int(json_t *buf, const char *key, int value);
+void json_add_int64(json_t *buf, const char *key, int64_t value);
+void json_add_uint64(json_t *buf, const char *key, uint64_t value);
+void json_add_double(json_t *buf, const char *key, double value);
+void json_add_bool(json_t *buf, const char *key, bool value);
+void json_add_null(json_t *buf, const char *key);
+
+void json_start_array(json_t *buf);
+void json_end_array(json_t *buf);
+void json_add_array_start(json_t *buf, const char *key);
+
+void json_array_add_string(json_t *buf, const char *value);
+void json_array_add_int(json_t *buf, int value);
+void json_array_add_int64(json_t *buf, int64_t value);
+void json_array_add_uint64(json_t *buf, uint64_t value);
+void json_array_add_double(json_t *buf, double value);
+void json_array_add_bool(json_t *buf, bool value);
+void json_array_add_null(json_t *buf);
diff --git a/include/md5.h b/include/md5.h
new file mode 100644
index 0000000..dedb0d1
--- /dev/null
+++ b/include/md5.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#define MD5_DIGEST_LENGTH 16
+#define MD5_TOO_LARGE "TOOLARGETOHASH"
+
+char *md5_digest_file(const char *);
diff --git a/include/net.h b/include/net.h
new file mode 100644
index 0000000..83e1c47
--- /dev/null
+++ b/include/net.h
@@ -0,0 +1,9 @@
+#pragma once
+
+typedef int sock_t;
+typedef unsigned short port_t;
+
+int sockprintf(int, const char *, ...);
+bool validate_ip(const char *);
+bool validate_ipv4(const char *);
+char *get_default_iface(void);
diff --git a/include/output.h b/include/output.h
new file mode 100644
index 0000000..cb59956
--- /dev/null
+++ b/include/output.h
@@ -0,0 +1,4 @@
+#pragma once
+
+void output(const char *);
+void msg(const char *, ...);
diff --git a/include/proc.h b/include/proc.h
new file mode 100644
index 0000000..be737a6
--- /dev/null
+++ b/include/proc.h
@@ -0,0 +1,49 @@
+#pragma once
+
+// /proc/PID/status
+struct proc_status {
+ char name[17];
+ char state;
+ pid_t pid;
+ pid_t tgid;
+ pid_t ppid;
+ pid_t tracer_pid;
+ uid_t uid;
+ uid_t euid;
+ uid_t ssuid;
+ uid_t fsuid;
+ gid_t gid;
+ gid_t egid;
+ gid_t ssgid;
+ gid_t fsgid;
+ unsigned int fdsize;
+ unsigned int threads;
+ unsigned long cap_inh;
+ unsigned long cap_prm;
+ unsigned long cap_eff;
+ unsigned long cap_bnd;
+ unsigned long cap_amb;
+ int no_new_privs;
+ int seccomp;
+};
+
+// /proc/PID/stat
+struct proc_stat {
+ char state;
+ unsigned long starttime;
+ unsigned long utime; // user time
+ unsigned long stime; // kernel time
+ long priority;
+ long nice;
+ unsigned long vsize;
+ long rss;
+ int tty_nr;
+};
+
+char *proc_cwd(pid_t);
+char *proc_environ(pid_t);
+char *proc_exe_path(pid_t);
+char *proc_get_exe_path(pid_t);
+char *proc_get_cmdline(pid_t);
+struct proc_stat proc_parse_stat(pid_t);
+struct proc_status proc_get_status(pid_t);
diff --git a/include/proc_connector.h b/include/proc_connector.h
new file mode 100644
index 0000000..10a20ff
--- /dev/null
+++ b/include/proc_connector.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <linux/connector.h>
+#include <linux/cn_proc.h>
+
+#include "agent_context.h"
+#include "proc_ledger.h"
+#include "json.h"
+#include "net.h"
+
+sock_t setup_proc_connector(void);
+void select_proc_connector(sock_t, agent_context_t *);
+
+json_t handle_PROC_EVENT_SID(struct proc_event *, agent_context_t *);
+json_t handle_PROC_EVENT_COMM(struct proc_event *, agent_context_t *, const char *);
+json_t handle_PROC_EVENT_COREDUMP(struct proc_event *, agent_context_t *);
+json_t handle_PROC_EVENT_FORK(struct proc_event *, agent_context_t *);
+json_t handle_PROC_EVENT_EXEC(struct proc_event *, agent_context_t *);
+json_t handle_PROC_EVENT_EXEC_environment(struct proc_event *, agent_context_t *);
+json_t handle_PROC_EVENT_EXIT(struct proc_event *, agent_context_t *);
+json_t handle_PROC_EVENT_UID(struct proc_event *, agent_context_t *, int, int);
+json_t handle_PROC_EVENT_GID(struct proc_event *, agent_context_t *, int, int);
+json_t handle_PROC_EVENT_PTRACE(struct proc_event *, agent_context_t *);
+json_t handle_PROC_EVENT_UNKNOWN(struct proc_event *, agent_context_t *);
diff --git a/include/proc_ledger.h b/include/proc_ledger.h
new file mode 100644
index 0000000..d715384
--- /dev/null
+++ b/include/proc_ledger.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <sys/types.h>
+#include <time.h>
+#include <limits.h>
+//#include <pthread.h> // for the future..maybe...
+
+#include "json.h"
+
+struct agent_context;
+typedef struct agent_context agent_context_t;
+
+struct proc_ledger_entry {
+ pid_t pid;
+ pid_t tgid;
+ pid_t ppid;
+ char exe[PATH_MAX];
+ char comm[17];
+ char cmdline[4096];
+ char cwd[PATH_MAX];
+ uid_t uid;
+ uid_t euid;
+ gid_t gid;
+ gid_t egid;
+ time_t start_time;
+ unsigned long cpu_user_ticks;
+ unsigned long cpu_kernel_ticks;
+ long rss;
+ unsigned long vsize;
+ bool daemonized;
+ bool is_traced;
+ pid_t tracer_pid;
+ char state;
+ int seccomp;
+ unsigned long cap_eff;
+ unsigned int threads;
+ bool has_tty;
+ struct proc_ledger_entry *next; // for hash collisions
+};
+
+struct proc_ledger {
+ size_t num_buckets;
+ struct proc_ledger_entry **buckets;
+ //pthread_mutex_t lock; // for the future..maybe...
+};
+
+struct proc_ledger *proc_ledger_init(size_t num_buckets);
+void proc_ledger_destroy(struct proc_ledger *ledger);
+
+struct proc_ledger_entry *proc_ledger_find(struct proc_ledger *ledger, pid_t pid);
+struct proc_ledger_entry *proc_ledger_entry_create(pid_t pid, agent_context_t *ctx);
+bool proc_ledger_add(struct proc_ledger *ledger, struct proc_ledger_entry *entry);
+bool proc_ledger_remove(struct proc_ledger *ledger, pid_t pid);
+bool proc_ledger_replace(struct proc_ledger *ledger, struct proc_ledger_entry *new_entry);
+json_t proc_ledger_entry_to_json(struct proc_ledger_entry *entry,
+ const char *event_type,
+ struct agent_context *ctx);
+void proc_ledger_hydrate(agent_context_t *ctx);
+size_t proc_ledger_bucket(struct proc_ledger *ledger, pid_t pid);
diff --git a/include/sha256.h b/include/sha256.h
new file mode 100644
index 0000000..72cf9c3
--- /dev/null
+++ b/include/sha256.h
@@ -0,0 +1,40 @@
+/*********************************************************************
+* Filename: sha256.h
+* Author: Brad Conte (brad AT bradconte.com)
+* Copyright:
+* Disclaimer: This code is presented "as is" without any guarantees.
+* Details: Defines the API for the corresponding SHA256 implementation.
+*********************************************************************/
+
+#ifndef SHA256_H
+#define SHA256_H
+
+/*************************** HEADER FILES ***************************/
+#include <stddef.h>
+#include <stdint.h>
+
+/****************************** MACROS ******************************/
+#define SHA256_BLOCK_SIZE 32 // SHA256 outputs a 32 byte digest
+#define SHA256_DIGEST_LENGTH SHA256_BLOCK_SIZE
+#define SHA256_TOO_LARGE "TOOLARGETOHASH"
+
+#undef SHA256_SHOW_ERRORS // for debugging
+
+/**************************** DATA TYPES ****************************/
+typedef unsigned char BYTE; // 8-bit byte
+typedef uint32_t WORD; // 32-bit word
+
+typedef struct {
+ BYTE data[64];
+ WORD datalen;
+ uint64_t bitlen;
+ //unsigned long long bitlen;
+ WORD state[8];
+} SHA256_CTX;
+
+/*********************** FUNCTION DECLARATIONS **********************/
+void sha256_init(SHA256_CTX *);
+void sha256_update(SHA256_CTX *, const BYTE [], size_t);
+void sha256_final(SHA256_CTX *, BYTE []);
+char *sha256_digest_file(const char *);
+#endif // SHA256_H
diff --git a/include/sniffer.h b/include/sniffer.h
new file mode 100644
index 0000000..8b85100
--- /dev/null
+++ b/include/sniffer.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "agent_context.h"
+#include "net.h"
+
+#ifndef TH_FIN
+#define TH_FIN 0x01
+#endif
+
+#ifndef TH_SYN
+#define TH_SYN 0x02
+#endif
+
+#ifndef TH_RST
+#define TH_RST 0x04
+#endif
+
+#ifndef TH_PUSH
+#define TH_PUSH 0x08
+#endif
+
+#ifndef TH_ACK
+#define TH_ACK 0x10
+#endif
+
+#ifndef TH_URG
+#define TH_URG 0x20
+#endif
+
+#ifndef TH_ECE
+#define TH_ECE 0x40
+#endif
+
+#ifndef TH_CWR
+#define TH_CWR 0x80
+#endif
+
+sock_t sniffer_init_interface(const char *interface, bool promisc);
+void sniffer_handle_packet(sock_t sniffer, agent_context_t *ctx);
+
+pid_t match_udp_inode(uint32_t ip_be, uint16_t port_be);
diff --git a/include/string_common.h b/include/string_common.h
new file mode 100644
index 0000000..bca8985
--- /dev/null
+++ b/include/string_common.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include <stdbool.h>
+
+bool startswith(const char *, const char *);
+bool endswith(const char *, const char *);
diff --git a/include/time_common.h b/include/time_common.h
new file mode 100644
index 0000000..9f39eef
--- /dev/null
+++ b/include/time_common.h
@@ -0,0 +1,4 @@
+#pragma once
+
+double timestamp(void);
+unsigned long get_boot_time(void);
diff --git a/misc/comm.c b/misc/comm.c
new file mode 100644
index 0000000..989325e
--- /dev/null
+++ b/misc/comm.c
@@ -0,0 +1,18 @@
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <unistd.h>
+
+int main(void) {
+ char new_name[] = "notcomm";
+
+ if (prctl(PR_SET_NAME, (unsigned long)new_name, 0, 0, 0) != 0) {
+ perror("prctl");
+ return 1;
+ }
+
+ printf("Changed comm to: %s\n", new_name);
+ sleep(1);
+ return 0;
+}
diff --git a/misc/evil.c b/misc/evil.c
new file mode 100644
index 0000000..c3c4524
--- /dev/null
+++ b/misc/evil.c
@@ -0,0 +1,6 @@
+#include <stdio.h>
+
+int main() {
+ puts("this is a very evil program");
+}
+
diff --git a/misc/server_example.py b/misc/server_example.py
new file mode 100644
index 0000000..4b53315
--- /dev/null
+++ b/misc/server_example.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+
+#
+# basic example showing how to use noawareness data
+#
+# - set up webhook as you see fit
+# - configure your logging search engine to ingest noawareness.log
+# - point noawareness agents at this host to collect logs centrally
+#
+
+import socket
+import json
+import logging
+import requests
+
+# Configuration
+UDP_PORT = 55555
+LOG_FILE = "noawareness.log"
+DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/your/webhook/url"
+TRIGGER_MD5 = "foo"
+
+# Setup logging to just log raw messages
+logging.basicConfig(filename=LOG_FILE, level=logging.INFO, format="%(message)s")
+
+# Create UDP socket
+sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+sock.bind(("0.0.0.0", UDP_PORT))
+print(f"Listening on UDP port {UDP_PORT}...")
+
+def send_discord_alert(message):
+ payload = {"content": message}
+ try:
+ requests.post(DISCORD_WEBHOOK_URL, json=payload)
+ except requests.RequestException:
+ pass
+
+while True:
+ try:
+ data, _ = sock.recvfrom(65535)
+ message = data.decode("utf-8", errors="ignore").strip()
+ logging.info(message)
+
+ try:
+ parsed = json.loads(message)
+ if parsed.get("md5") == TRIGGER_MD5:
+ send_discord_alert(f"Trigger match: md5 == {TRIGGER_MD5}")
+ except json.JSONDecodeError:
+ pass
+
+ except KeyboardInterrupt:
+ break
diff --git a/noawareness-pid-check.sh b/noawareness-pid-check.sh
new file mode 100644
index 0000000..0d82fec
--- /dev/null
+++ b/noawareness-pid-check.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+# noawareness-pid-check.sh -- by Daniel Roberson
+# -- simple script to respawn noawareness if it dies.
+# -- meant to be placed in your crontab!
+# --
+# -- * * * * * /path/to/noawareness-pid-check.sh
+
+# Season to taste:
+PIDFILE="/var/run/noawareness.pid"
+BINPATH="/root/noawareness/noawareness -d -p $PIDFILE"
+
+if [ ! -f $PIDFILE ]; then
+ # PIDFILE doesnt exist!
+ echo "noawareness not running. Attempting to start.."
+ $BINPATH
+ exit
+else
+ # PID file exists. check if its running!
+ kill -0 "$(head -n 1 $PIDFILE)" 2>/dev/null
+ if [ $? -eq 0 ]; then
+ exit 0
+ else
+ echo "noawareness not running. Attempting to start.."
+ $BINPATH
+ fi
+fi
+
diff --git a/rules b/rules
new file mode 100644
index 0000000..5fb2389
--- /dev/null
+++ b/rules
@@ -0,0 +1,10 @@
+pattern elf_header 7f454c46
+string ls1 invalid suffix in %s%s argument '%s'
+string ls2 General help using GNU software: <%s>
+string ls3 "List information about the FILEs (the current directory by default).\n"
+string foo DSKJFDSALKFJDSAFJDSALKF
+string beep alksjfakdsfjsadfjdsalkfj
+string evil1 this is a very evil program
+
+rule elf allow elf_header:0
+rule evil block elf_header:0 evil1
diff --git a/src/aho-corasick.c b/src/aho-corasick.c
new file mode 100644
index 0000000..c69fafd
--- /dev/null
+++ b/src/aho-corasick.c
@@ -0,0 +1,317 @@
+// TODO ac_match_range() -- match within a range of a buffer, file, ...
+// TODO streaming support -- match over a socket
+// TODO ac_strerror() -- report errors better
+
+// Aho-Corasick searching algorithm implementation
+// https://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_algorithm
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include "aho-corasick.h"
+
+struct pattern_node {
+ ac_node_t *children[AC_ALPHABET_SIZE];
+ ac_node_t *fail;
+ ac_pattern_t *matches;
+};
+
+struct pattern_entry {
+ const char *id;
+ uint8_t *pattern;
+ size_t len;
+ ac_pattern_t *next;
+};
+
+struct ac_context {
+ ac_node_t *root;
+};
+
+static ac_node_t *node_new(void) {
+ return calloc(1, sizeof(ac_node_t));
+}
+
+ac_context_t *ac_new(void) {
+ ac_context_t *ctx = calloc(1, sizeof(ac_context_t));
+
+ ctx->root = node_new();
+
+ return ctx;
+}
+
+static void free_node(ac_node_t *node) {
+ if (!node) {
+ return;
+ }
+
+ for (int i = 0; i < AC_ALPHABET_SIZE; i++) {
+ free_node(node->children[i]);
+ }
+
+ ac_pattern_t *p = node->matches;
+ while(p) {
+ ac_pattern_t *next = p->next;
+ free(p->pattern);
+ free(p);
+ p = next;
+ }
+
+ free(node);
+}
+
+void ac_free(ac_context_t *ctx) {
+ if (!ctx) {
+ return;
+ }
+
+ free_node(ctx->root);
+ free(ctx);
+}
+
+int ac_add_pattern(ac_context_t *ctx, const char *id, const uint8_t *pattern, size_t len) {
+ if (!ctx || !id || !pattern || len == 0) {
+ return -1;
+ }
+
+ ac_node_t *node = ctx->root;
+
+ for (size_t i = 0; i < len; i++) {
+ uint8_t c = pattern[i];
+
+ if (!node->children[c]) {
+ node->children[c] = node_new();
+ }
+
+ node = node->children[c];
+ }
+
+ for (ac_pattern_t *p = node->matches; p; p = p->next) {
+ if (p->len == len &&
+ memcmp(p->pattern, pattern, len) == 0 &&
+ strcmp(p->id, id) == 0) {
+ return 0; // duplicate pattern. don't add.
+ }
+ }
+
+ ac_pattern_t *entry = malloc(sizeof(*entry));
+ entry->id = id;
+ entry->len = len;
+ entry->pattern = malloc(len);
+ if (!entry->pattern) {
+ return -1;
+ } else {
+ memcpy(entry->pattern, pattern, len);
+ }
+ entry->next = node->matches;
+ node->matches = entry;
+
+ return 0;
+}
+
+struct node_queue {
+ ac_node_t **data;
+ size_t head;
+ size_t tail;
+ size_t cap;
+};
+
+static struct node_queue *queue_new(size_t cap) {
+ struct node_queue *q = calloc(1, sizeof(*q));
+
+ if (cap == 0) {
+ cap = 64;
+ }
+
+ q->data = malloc(sizeof(void *) * cap);
+ q->cap = cap;
+
+ return q;
+}
+
+static void queue_free(struct node_queue *q) {
+ free(q->data);
+ free(q);
+}
+
+static void queue_push(struct node_queue *q, ac_node_t *node) {
+ if (q->tail >= q->cap) {
+ size_t new_cap = q->cap ? q->cap * 2 : 64;
+ ac_node_t **new_data = realloc(q->data, sizeof(void *) * new_cap);
+
+ if (!new_data) {
+ fprintf(stderr, "queue_push: realloc failure\n");
+ exit(EXIT_FAILURE); // TODO error flag instead of exit
+ }
+
+ q->data = new_data;
+ q->cap = new_cap;
+ }
+
+ q->data[q->tail++] = node;
+}
+
+static ac_node_t *queue_pop(struct node_queue *q) {
+ return q->head < q->tail ? q->data[q->head++] : NULL;
+}
+
+static bool queue_empty(struct node_queue *q) {
+ return q->head == q->tail;
+}
+
+int ac_build(ac_context_t *ctx) {
+ if (!ctx || !ctx->root) {
+ return -1;
+ }
+
+ struct node_queue *q = queue_new(0);
+ if (!q) {
+ return -1;
+ }
+
+ ac_node_t *root = ctx->root;
+
+ for (int i = 0; i < AC_ALPHABET_SIZE; i++) {
+ if (root->children[i]) {
+ root->children[i]->fail = root;
+ queue_push(q, root->children[i]);
+ }
+ }
+
+ while (!queue_empty(q)) {
+ ac_node_t *node = queue_pop(q);
+
+ for (int i = 0; i < AC_ALPHABET_SIZE; i++) {
+ ac_node_t *child = node->children[i];
+ if (!child) {
+ continue;
+ }
+
+ ac_node_t *fail = node->fail;
+ while (fail && !fail->children[i]) {
+ fail = fail->fail;
+ }
+
+ if (fail) {
+ child->fail = fail->children[i];
+ } else {
+ child->fail = root;
+ }
+
+ queue_push(q, child);
+ }
+ }
+
+ queue_free(q);
+
+ return 0;
+}
+
+int ac_match(ac_context_t *ctx, const uint8_t *data, size_t len, ac_callback callback, void *user_data) {
+ if (!ctx || !data || !callback) {
+ // TODO error context
+ return -1;
+ }
+
+ ac_node_t *node = ctx->root;
+
+ for (size_t i = 0; i < len; i++) {
+ uint8_t c = data[i];
+
+ while (node && !node->children[c]) {
+ node = node->fail;
+ }
+
+ if (!node) {
+ node = ctx->root;
+ } else {
+ node = node->children[c];
+ }
+
+ ac_node_t *temp = node;
+ while (temp) {
+ for (ac_pattern_t *p = temp->matches; p; p = p->next) {
+ ac_match_t match = {
+ .id = p->id,
+ .offset = i + 1 - p->len,
+ .len = p->len
+ };
+
+ callback(&match, user_data);
+ }
+
+ temp = temp->fail;
+ }
+ }
+
+ return 0;
+}
+
+int ac_match_fd(ac_context_t *ctx, int fd, ac_callback callback, void *user_data) {
+ if (!ctx || fd < 0 || !callback) {
+ // TODO error context
+ return -1;
+ }
+
+ char buffer[AC_BUF_SIZE];
+ size_t offset = 0;
+ ssize_t n;
+
+ ac_node_t *node = ctx->root;
+
+ while ((n = read(fd, buffer, sizeof(buffer))) > 0) {
+ for (ssize_t i = 0; i < n; i++) {
+ uint8_t c = buffer[i];
+
+ while (node && !node->children[c]) {
+ node = node->fail;
+ }
+
+ if (!node) {
+ node = ctx->root;
+ } else {
+ node = node->children[c];
+ }
+
+ ac_node_t *temp = node;
+ while (temp) {
+ for (ac_pattern_t *p = temp->matches; p; p = p->next) {
+ ac_match_t match = {
+ .id = p->id,
+ .offset = (offset + i + 1) - p->len,
+ .len = p->len
+ };
+
+ callback(&match, user_data);
+ }
+
+ temp = temp->fail;
+ }
+ }
+
+ offset += n;
+ }
+
+ return (n < 0) ? -1 : 0;
+}
+
+int ac_match_path(ac_context_t *ctx, const char *path, ac_callback callback, void *user_data) {
+ if (!ctx || !path || !callback) {
+ return -1;
+ }
+
+ int fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ return -1;
+ }
+
+ int ret = ac_match_fd(ctx, fd, callback, user_data);
+
+ close(fd);
+
+ return ret;
+}
diff --git a/src/av_rules.c b/src/av_rules.c
new file mode 100644
index 0000000..bf405cc
--- /dev/null
+++ b/src/av_rules.c
@@ -0,0 +1,380 @@
+// TODO rules validator cli tool
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "djb2.h"
+#include "av_rules.h"
+
+// TODO move to string.c
+static char *trim(char *s) {
+ while (isspace(*s)) {
+ s++;
+ }
+
+ if (*s == '\0') {
+ return s;
+ }
+
+ char *end = s + strlen(s) - 1;
+ while (end > s && isspace(*end)) {
+ *end-- = '\0';
+ }
+
+ return s;
+}
+
+rule_action_t parse_action(const char *s) {
+ if (strcmp(s, "block") == 0) return RULE_BLOCK;
+ if (strcmp(s, "allow") == 0) return RULE_ALLOW;
+ if (strcmp(s, "quarantine") == 0) return RULE_QUARANTINE;
+ if (strcmp(s, "info") == 0) return RULE_INFORMATIONAL;
+
+ return RULE_ALLOW; // default fallback
+}
+
+static condition_type_t parse_condition_type(const char *tok, char **out_id, size_t *out_offset, bool *has_offset) {
+ condition_type_t type = COND_TYPE_REQUIRED;
+ const char *p = tok;
+
+ if (*p == '!') {
+ type = COND_TYPE_NEGATED;
+ p++;
+ } else if (*p == '?') {
+ type = COND_TYPE_OPTIONAL;
+ p++;
+ }
+
+ char *colon = strchr(p, ':');
+ if (colon) {
+ *colon = '\0';
+ *out_id = strdup(p);
+ *out_offset = (size_t)atoi(colon + 1);
+ *has_offset = true;
+ } else {
+ *out_id = strdup(p);
+ *out_offset = 0;
+ *has_offset = false;
+ }
+
+ return type;
+}
+
+char *strip_quotes(char *s) {
+ size_t len = strlen(s);
+
+ if (len >= 2 && s[0] == '"' && s[len - 1] == '"') {
+ s[len - 1] = '\0'; // ending quote
+ return s + 1; // leading quote
+ }
+
+ return s;
+}
+
+static int hex_char_to_nibble(char c) {
+ if ('0' <= c && c <= '9') return c - '0';
+ if ('a' <= c && c <= 'f') return c - 'a' + 10;
+ if ('A' <= c && c <= 'F') return c - 'A' + 10;
+
+ return -1;
+}
+
+size_t hex_to_bytes(const char *hex, uint8_t *out, size_t maxlen) {
+ size_t len = 0;
+
+ while (*hex && *(hex + 1) && len < maxlen) {
+ while (*hex && isspace((unsigned char)*hex)) {
+ hex++;
+ }
+
+ int hi = hex_char_to_nibble(*hex++);
+ int lo = hex_char_to_nibble(*hex++);
+ if (hi < 0 || lo < 0) {
+ break;
+ }
+
+ out[len++] = (hi << 4) | lo;
+ }
+
+ return len;
+}
+
+void unescape_string(char *s) {
+ char *src = s;
+ char *dst = s;
+
+ while (*src) {
+ if (*src == '\\') {
+ src++;
+ switch(*src) {
+ case 'n': *dst++ = '\n'; break;
+ case 'r': *dst++ = '\r'; break;
+ case 't': *dst++ = '\t'; break;
+ case '0': *dst++ = '\0'; break;
+ case '\\': *dst++ = '\\'; break;
+ case '"': *dst++ = '\"'; break;
+ case 'x':
+ int hi = hex_char_to_nibble(*(++src));
+ int lo = hex_char_to_nibble(*(++src));
+ if (hi >= 0 && lo >= 0) {
+ *dst++ = (hi << 4) | lo;
+ }
+ break;
+ default:
+ *dst++ = *src;
+ break;
+ }
+
+ src++;
+ } else {
+ *dst++ = *src++;
+ }
+ }
+
+ *dst = '\0';
+}
+
+int load_rules(const char *path, rule_set_t *out_ruleset) {
+ FILE *f = fopen(path, "r");
+ if (!f) {
+ return -1;
+ }
+
+ char line[1024];
+ rule_t *current_rule = NULL;
+
+ while (fgets(line, sizeof(line), f)) {
+ char *comment = strchr(line, '#');
+ if (comment) {
+ *comment = '\0';
+ }
+
+ char *s = trim(line);
+ if (*s == '\0') {
+ continue;
+ }
+
+ char *tok = strtok(s, " \t\n");
+ if (!tok) {
+ continue;
+ }
+
+ if (strcmp(tok, "rule") == 0) {
+ if (out_ruleset->rule_count >= MAX_RULES) {
+ break;
+ }
+
+ current_rule = &out_ruleset->rules[out_ruleset->rule_count++];
+ memset(current_rule, 0, sizeof(rule_t));
+
+ char *rule_id = strtok(NULL, " \t\n");
+ char *action_str = strtok(NULL, " \t\n");
+ if (!rule_id || !action_str) {
+ continue;
+ }
+
+ current_rule->id = strdup(rule_id);
+ current_rule->action = parse_action(action_str);
+
+ while ((tok = strtok(NULL, " \t\n")) &&
+ current_rule->condition_count < MAX_RULE_CONDITIONS) {
+ char *id = NULL;
+ size_t offset = 0;
+ bool has_offset = false;
+
+ condition_type_t ctype = parse_condition_type(tok, &id, &offset, &has_offset);
+
+ rule_condition_t *cond = &current_rule->conditions[current_rule->condition_count++];
+ cond->pattern_id = id;
+ cond->offset = offset;
+ cond->has_offset = has_offset;
+ cond->type = ctype;
+ }
+ } else if (strcmp(tok, "string") == 0) {
+ char *id = strtok(NULL, " \t\n");
+ char *value = strtok(NULL, "");
+
+ if (!id || !value) {
+ // malformed string declaration
+ continue;
+ }
+
+ if (*value == '"') {
+ // if value is quoted, remove quotes and unescape
+ value = strip_quotes(value);
+ unescape_string(value);
+ }
+
+ // TODO this silently fails
+ pattern_table_add(&out_ruleset->patterns, id, (uint8_t *)value, strlen(value));
+ } else if (strcmp(tok, "pattern") == 0) {
+ char *id = strtok(NULL, " \t\n");
+ char *hexstr = strtok(NULL, "\t\n");
+
+ if (!id || !hexstr) {
+ // malformed hex pattern
+ continue;
+ }
+
+ uint8_t bytes[512]; // max pattern size
+ size_t byte_len = hex_to_bytes(hexstr, bytes, sizeof(bytes));
+ if (byte_len == 0) {
+ continue;
+ }
+
+ // TODO this silently fails
+ pattern_table_add(&out_ruleset->patterns, id, bytes, byte_len);
+ }
+ }
+
+ fclose(f);
+
+ return 0;
+}
+
+bool evaluate_rule(const rule_t *rule, pattern_table_t *patterns) {
+ for (size_t i = 0; i < rule->condition_count; i++) {
+ const rule_condition_t *cond = &rule->conditions[i];
+ pattern_t *p = pattern_table_find(patterns, cond->pattern_id);
+
+ bool matched = false;
+
+ if (p) {
+ if (cond->has_offset) {
+ for (size_t j = 0; j < p->match_count; j++) {
+ if (p->match_offsets[j] == cond->offset) {
+ matched = true;
+ break;
+ }
+ }
+ } else {
+ matched = (p->match_count > 0);
+ }
+ }
+
+ switch (cond->type) {
+ case COND_TYPE_REQUIRED:
+ if (!matched) {
+ return false;
+ }
+ break;
+ case COND_TYPE_NEGATED:
+ if (matched) {
+ return false;
+ }
+ break;
+ case COND_TYPE_OPTIONAL:
+ break;
+ }
+ }
+
+ return true;
+}
+
+void dump_rule(const rule_t *r) {
+ printf("rule %s %s\n", r->id,
+ r->action == RULE_BLOCK ? "block" :
+ r->action == RULE_ALLOW ? "allow" :
+ r->action == RULE_QUARANTINE ? "quarantine" :
+ "info");
+
+ for (size_t i = 0; i < r->condition_count; i++) {
+ const rule_condition_t *c = &r->conditions[i];
+
+ char offset_buf[16];
+ if (c->has_offset) sprintf(offset_buf, "%zu", c->offset);
+ printf(" %s%s%s%s\n",
+ c->type == COND_TYPE_NEGATED ? "!" :
+ c->type == COND_TYPE_OPTIONAL ? "?" : "",
+ c->pattern_id,
+ c->has_offset ? ":" : "",
+ c->has_offset ? offset_buf : "");
+ }
+}
+
+void free_rules(rule_set_t *rules) {
+ for (size_t i = 0; i < rules->rule_count; i++) {
+ rule_t *r = &rules->rules[i];
+ free((void *)r->id);
+
+ for (size_t j = 0; j < r->condition_count; j++) {
+ free((void *)r->conditions[j].pattern_id);
+ }
+ }
+}
+
+pattern_t *pattern_table_find(pattern_table_t *table, const char *id) {
+ uint32_t h = djb2(id) % PATTERN_TABLE_SIZE;
+
+ for (pattern_bucket_t *b = table->buckets[h]; b; b = b->next) {
+ if (strcmp(b->id, id) == 0) {
+ return b->entry;
+ }
+ }
+
+ return NULL;
+}
+
+int pattern_table_add(pattern_table_t *table, const char *id, const uint8_t *bytes, size_t len) {
+ if (table->count >= MAX_PATTERNS) {
+ return -1;
+ }
+
+ if (pattern_table_find(table, id)) {
+ // id already exists. skip adding it.
+ return -1;
+ }
+
+ pattern_t *p = &table->patterns[table->count++];
+ p->id = strdup(id);
+ p->bytes = malloc(len);
+ if (!p->bytes) {
+ return -1;
+ }
+
+ memcpy(p->bytes, bytes, len);
+ p->len = len;
+ p->match_count = 0;
+
+ uint32_t h = djb2(id) % PATTERN_TABLE_SIZE;
+
+ pattern_bucket_t *b = malloc(sizeof(*b));
+ if (!b) {
+ return -1;
+ }
+
+ b->id = p->id;
+ b->entry = p;
+ b->next = table->buckets[h];
+ table->buckets[h] = b;
+
+ return 0;
+}
+
+void pattern_table_clear_matches(pattern_table_t *table) {
+ for (size_t i = 0; i < table->count; i++) {
+ table->patterns[i].match_count = 0;
+ memset(table->patterns[i].match_offsets, 0, sizeof(table->patterns[i].match_offsets));
+ }
+}
+
+void pattern_table_free(pattern_table_t *table) {
+ for (size_t i = 0; i < PATTERN_TABLE_SIZE; i++) {
+ pattern_bucket_t *b = table->buckets[i];
+
+ while (b) {
+ pattern_bucket_t *next = b->next;
+ free((void *)b->id);
+ free(b);
+ b = next;
+ }
+ }
+
+ for (size_t i = 0; i < table->count; i++) {
+ free(table->patterns[i].bytes);
+ }
+
+ memset(table, 0, sizeof(*table));
+}
diff --git a/src/base64.c b/src/base64.c
new file mode 100644
index 0000000..0fc1caa
--- /dev/null
+++ b/src/base64.c
@@ -0,0 +1,124 @@
+#include <stdlib.h>
+#include <string.h>
+#include <stddef.h>
+
+// Stole from Jouni Malinen <j@w1.fi>
+unsigned char *base64_encode(const unsigned char *src, size_t len, size_t *out_len) {
+ const unsigned char base64_table[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ unsigned char *out, *pos;
+ const unsigned char *end, *in;
+ size_t olen;
+
+ olen = len * 4 / 3 + 4; /* 3-byte blocks to 4-byte */
+ olen += olen / 72; /* line feeds */
+ olen++; /* nul termination */
+ if (olen < len) {
+ return NULL; /* integer overflow */
+ }
+ out = malloc(olen);
+ if (out == NULL) {
+ return NULL;
+ }
+
+ end = src + len;
+ in = src;
+ pos = out;
+
+ while (end - in >= 3) {
+ *pos++ = base64_table[in[0] >> 2];
+ *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
+ *pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)];
+ *pos++ = base64_table[in[2] & 0x3f];
+ in += 3;
+ }
+
+ if (end - in) {
+ *pos++ = base64_table[in[0] >> 2];
+ if (end - in == 1) {
+ *pos++ = base64_table[(in[0] & 0x03) << 4];
+ *pos++ = '=';
+ } else {
+ *pos++ = base64_table[((in[0] & 0x03) << 4) |
+ (in[1] >> 4)];
+ *pos++ = base64_table[(in[1] & 0x0f) << 2];
+ }
+ *pos++ = '=';
+ }
+
+ *pos = '\0';
+ if (out_len) {
+ *out_len = pos - out;
+ }
+
+ return out;
+}
+
+// Stole from Jouni Malinen <j@w1.fi>
+unsigned char *base64_decode(const unsigned char *src, size_t len, size_t *out_len) {
+ const unsigned char base64_table[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ unsigned char dtable[256], *out, *pos, block[4], tmp;
+ size_t i, count, olen;
+ int pad = 0;
+
+ memset(dtable, 0x80, 256);
+ for (i = 0; i < sizeof(base64_table) - 1; i++) {
+ dtable[base64_table[i]] = (unsigned char) i;
+ }
+ dtable['='] = 0;
+
+ count = 0;
+ for (i = 0; i < len; i++) {
+ if (dtable[src[i]] != 0x80) {
+ count++;
+ }
+ }
+
+ if (count == 0 || count % 4) {
+ return NULL;
+ }
+
+ olen = count / 4 * 3;
+ pos = out = malloc(olen);
+ if (out == NULL) {
+ return NULL;
+ }
+
+ count = 0;
+ for (i = 0; i < len; i++) {
+ tmp = dtable[src[i]];
+ if (tmp == 0x80) {
+ continue;
+ }
+
+ if (src[i] == '=') {
+ pad++;
+ }
+ block[count] = tmp;
+ count++;
+ if (count == 4) {
+ *pos++ = (block[0] << 2) | (block[1] >> 4);
+ *pos++ = (block[1] << 4) | (block[2] >> 2);
+ *pos++ = (block[2] << 6) | block[3];
+ count = 0;
+ if (pad) {
+ if (pad == 1) {
+ pos--;
+ } else if (pad == 2) {
+ pos -= 2;
+ } else {
+ /* Invalid padding */
+ free(out);
+ return NULL;
+ }
+ break;
+ }
+ }
+ }
+
+ *out_len = pos - out;
+ return out;
+}
diff --git a/src/djb2.c b/src/djb2.c
new file mode 100644
index 0000000..43c25b6
--- /dev/null
+++ b/src/djb2.c
@@ -0,0 +1,14 @@
+
+#include <stdint.h>
+
+// dbj2 hash
+uint32_t djb2(const char *str) {
+ uint32_t hash = 5381;
+ int c;
+
+ while ((c = *str++)) {
+ hash = ((hash << 5) + hash) + c;
+ }
+
+ return hash;
+}
diff --git a/src/dns.c b/src/dns.c
new file mode 100644
index 0000000..f16f568
--- /dev/null
+++ b/src/dns.c
@@ -0,0 +1,113 @@
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+
+#include <arpa/inet.h>
+
+#include "dns.h"
+
+/* DNS packet:
+ +--------+-------+-----------------------------------------------+
+ | Offset | Bytes | Field |
+ +--------+--------+----------------------------------------------+
+ | 0 | 2 | Transaction ID |
+ | 2 | 2 | Flags |
+ | 4 | 2 | QDCOUNT (Number of questions) |
+ | 6 | 2 | ANCOUNT (Number of answers) |
+ | 8 | 2 | NSCOUNT (Number of authority RRs) |
+ | 10 | 2 | ARCOUNT (Number of additional RRs) |
+ +--------+--------+----------------------------------------------+
+ | 12 | var | Questions (name + qtype + qclass) |
+ +--------+--------+----------------------------------------------+
+ | ?? | var | Answers (name+type+class+TTL+RDLENGTH+RDATA) |
+ +--------+--------+----------------------------------------------+
+ | ?? | var | Authority RRs (same format) |
+ +--------+--------+----------------------------------------------+
+ | ?? | var | Additional RRs (same format) |
+ +--------+--------+----------------------------------------------+
+*/
+size_t parse_dns_udp(const uint8_t *payload,
+ size_t len,
+ struct dns_question *out, size_t max_qs) {
+ if (len < 12) {
+ // not long enough
+ return 0;
+ }
+
+ uint16_t qdcount = ntohs(*(uint16_t *)(payload + 4));
+ if (qdcount == 0) {
+ return 0;
+ }
+ if (qdcount > max_qs) {
+ qdcount = max_qs; // truncate safely
+ }
+
+ const uint8_t *ptr = payload + 12;
+ const uint8_t *end = payload + len;
+
+ size_t parsed = 0;
+ for (int q = 0; q < qdcount && ptr < end; q++) {
+ char name[MAX_DNS_NAME_LEN];
+ int name_len = 0;
+
+ while (ptr < end && *ptr != 0 && name_len < MAX_DNS_NAME_LEN - 1) {
+ uint8_t label_len = *ptr++;
+
+ if (label_len == 0 || ptr + label_len > end) {
+ return 0;
+ }
+
+ if (name_len > 0) {
+ name[name_len++] = '.';
+ }
+
+ memcpy(name + name_len, ptr, label_len);
+ name_len += label_len;
+ ptr += label_len;
+ }
+
+ name[name_len] = '\0';
+ ptr++; // skip null terminator
+
+ if (ptr + 4 > end) {
+ return 0;
+ }
+
+ uint16_t qtype = ntohs(*(uint16_t *)(ptr));
+ ptr += 2;
+ ptr += 2; // qclass
+
+ strncpy(out[parsed].name, name, MAX_DNS_NAME_LEN);
+ out[parsed].name[MAX_DNS_NAME_LEN - 1] = '\0';
+ out[parsed].qtype = qtype;
+
+ parsed++;
+ }
+
+ return parsed;
+}
+
+// resolve DNS query type to human-readable string.
+// TODO static buffer may yield incorrect resultsif multiple unknown
+// DNS types are encountered in a single message.
+const char *dns_type_to_string(uint16_t type) {
+ switch(type) {
+ case 1: return "A";
+ case 2: return "NS";
+ case 5: return "CNAME";
+ case 6: return "SOA";
+ case 12: return "PTR";
+ case 15: return "MX";
+ case 16: return "TXT";
+ case 28: return "AAAA";
+ case 33: return "SRV";
+ case 64: return "SVCB"; // RFC 9460 https://www.rfc-editor.org/rfc/rfc9460
+ case 65: return "HTTPS"; // RFC 9460 https://www.rfc-editor.org/rfc/rfc9460
+ case 255: return "ANY";
+ default: // return TYPEXXXX if type isn't known
+ static char buf[16];
+ snprintf(buf, sizeof(buf), "TYPE%d", type);
+ return buf;
+ }
+}
+
diff --git a/src/entropy.c b/src/entropy.c
new file mode 100644
index 0000000..a70789a
--- /dev/null
+++ b/src/entropy.c
@@ -0,0 +1,65 @@
+
+#include <stdint.h>
+#include <stddef.h>
+#include <string.h>
+
+#include "entropy.h"
+
+// shitty log2 function to avoid linking to -lm
+static inline double entropy_log2(double x) {
+ if (x <= 0.0) {
+ return -1e9;
+ }
+
+ union { double d; uint64_t i; } vx = { x };
+ int exp = (int)((vx.i >> 52) & 0x7FF) - 1023;
+ vx.i &= ((1ULL << 52) - 1);
+ vx.i |= 0x3FF0000000000000ULL;
+
+ double m = vx.d;
+ double log2_m = (m - 1.0) - (m - 1.0) * (m - 1.0) * 0.5;
+ return (double)exp + log2_m;
+}
+
+// Initialize entropy context
+void entropy_init(struct entropy_ctx *ctx) {
+ memset(ctx, 0, sizeof(*ctx));
+}
+
+// Update entropy context
+void entropy_update(struct entropy_ctx *ctx, const unsigned char *buf, size_t len) {
+
+ size_t i;
+ for (i = 0; i + 4 < len; i += 4) {
+ ctx->freq[buf[i]]++;;
+ ctx->freq[buf[i + 1]]++;
+ ctx->freq[buf[i + 2]]++;
+ ctx->freq[buf[i + 2]]++;
+ }
+
+ // process the remaining bytes
+ for(; i < len; i++) {
+ ctx->freq[buf[i]]++;
+ }
+
+ ctx->total_bytes += len;
+}
+
+// Finalize entropy context
+double entropy_final(struct entropy_ctx *ctx) {
+ if (ctx->total_bytes == 0) {
+ return 0.0;
+ }
+
+ double entropy = 0.0;
+ for (int i = 0; i < 256; i++) {
+ if (ctx->freq[i] == 0) {
+ continue;
+ }
+ double p = (double)ctx->freq[i] / (double)ctx->total_bytes;
+ entropy -= p * entropy_log2(p);
+ //entropy -= p * log2(p);
+ }
+
+ return entropy;
+}
diff --git a/src/error.c b/src/error.c
new file mode 100644
index 0000000..c5d718a
--- /dev/null
+++ b/src/error.c
@@ -0,0 +1,57 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <syslog.h>
+
+extern bool use_syslog;
+
+/* error() - Print messages to stderr.
+ *
+ * Args:
+ * fmt - Message with format strings.
+ * ... - Optional arguments to fill format strings.
+ *
+ * Returns:
+ * Nothing.
+ */
+void error(const char *fmt, ...) {
+ char msg[8192] = {0};
+ va_list vl;
+
+ va_start(vl, fmt);
+ vsnprintf(msg, sizeof(msg), fmt, vl);
+ va_end(vl);
+
+ fprintf(stderr, "%s\n", msg);
+
+ if (use_syslog) {
+ syslog(LOG_INFO | LOG_USER, "%s", msg);
+ }
+}
+
+/* error_fatal() - Print message to stderr and exit with EXIT_FAILURE
+ *
+ * Args:
+ * fmt - Message with format strings.
+ * ... - Optional arguments to fill format strings.
+ *
+ * Returns
+ * Nothing, but exits the program with EXIT_FAILURE
+ */
+void error_fatal(const char *fmt, ...) {
+ char msg[8192] = {0};
+ va_list vl;
+
+ va_start(vl, fmt);
+ vsnprintf(msg, sizeof(msg), fmt, vl);
+ va_end(vl);
+
+ fprintf(stderr, "%s\n", msg);
+
+ if (use_syslog) {
+ syslog(LOG_INFO | LOG_USER, "%s", msg);
+ }
+
+ exit(EXIT_FAILURE);
+}
diff --git a/src/fanotify.c b/src/fanotify.c
new file mode 100644
index 0000000..869cc49
--- /dev/null
+++ b/src/fanotify.c
@@ -0,0 +1,210 @@
+
+#define _GNU_SOURCE
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <sys/fanotify.h>
+
+#include "json.h"
+#include "error.h"
+#include "output.h"
+#include "fanotify.h"
+#include "hash_ledger.h"
+#include "time_common.h"
+
+// set up fanotify descriptor. returns initialized descriptor on
+// success, -1 on error
+int setup_fanotify(void) {
+ int fan_fd = fanotify_init(FAN_CLASS_PRE_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK,
+ O_RDONLY | O_CLOEXEC | O_LARGEFILE);
+
+ if (fan_fd < 0) {
+ error("fanotify_init failed: %s", strerror(errno));
+ return -1;
+ }
+
+ // TODO mark all mount points instead of /
+ if (fanotify_mark(fan_fd,
+ FAN_MARK_ADD | FAN_MARK_MOUNT,
+ FAN_OPEN_EXEC | FAN_OPEN_EXEC_PERM,
+ AT_FDCWD,
+ "/") < 0) {
+ error("fanotify_mark failed: %s", strerror(errno));
+ close(fan_fd);
+ return -1;
+ }
+
+ // set non-blocking
+ int flags = fcntl(fan_fd, F_GETFL, 0);
+ fcntl(fan_fd, F_SETFL, flags | O_NONBLOCK);
+
+ return fan_fd;
+}
+
+typedef struct {
+ struct hash_entry *entry;
+ pattern_table_t *patterns;
+ size_t match_count;
+} match_context_t;
+
+static void match_callback(const struct ac_match *m, void *user_data) {
+ match_context_t *ctx = user_data;
+
+ //printf("fanotify matched: %s at offset %zu\n", m->id, m->offset);
+
+ if (ctx->entry->matched_pattern_count < MAX_MATCHED_PATTERNS) {
+ ctx->entry->matched_patterns[ctx->entry->matched_pattern_count++] = m->id;
+ }
+
+ pattern_t *p = pattern_table_find(ctx->patterns, m->id);
+ if (p && p->match_count < MAX_MATCHED_PATTERNS) {
+ p->match_offsets[p->match_count++] = m->offset;
+ }
+}
+
+// TODO handle old style FID-based events as well as old style FD
+// TODO hash-based allow and blocklists
+void select_fanotify(int fan_fd, agent_context_t *ctx) {
+ struct fanotify_event_metadata buf[4096];
+ ssize_t len = read(fan_fd, buf, sizeof(buf));
+ if (len < 0) {
+ if (errno != EAGAIN) {
+ error("fanotify read: %s", strerror(errno));
+ }
+
+ return;
+ }
+
+ struct fanotify_event_metadata *metadata;
+ for (metadata = buf; FAN_EVENT_OK(metadata, len); metadata = FAN_EVENT_NEXT(metadata, len)) {
+ if (metadata->mask & FAN_OPEN_EXEC_PERM) {
+ struct fanotify_response response;
+ response.fd = metadata->fd;
+ response.response = FAN_ALLOW;
+
+ char path[PATH_MAX];
+ snprintf(path, sizeof(path), "/proc/self/fd/%d", metadata->fd);
+
+ char target[PATH_MAX];
+ ssize_t len = readlink(path, target, sizeof(target) - 1);
+ if (len >= 0) {
+ target[len] = '\0';
+ } else {
+ error("readlink: %s", strerror(errno));
+ }
+
+ // hash_ledger_find(ctx->hash_ledger, target)
+ // - if not in ledger, create and scan.
+ // - if in ledger
+ // - scan if too much time passed, scan again
+ // - if verdict is block, send FAN_DENY and emit log w/ rule matches
+ // - if verdict is quarantine, block and quarantine
+ // = if verdict is allow, allow it.
+
+ struct hash_entry *he = hash_ledger_find(ctx->hash_ledger, target);
+ if (!he) {
+ struct stat sb;
+ stat(target, &sb);
+ he = hash_ledger_add_or_update(ctx->hash_ledger, target, &sb);
+ }
+
+ scan_verdict_t verdict = VERDICT_INFORMATIONAL;
+ if (he->last_scanned == 0 || time(NULL) - he->last_scanned > 600) {
+ match_context_t mctx = {
+ .entry = he,
+ .patterns = &ctx->rules.patterns,
+ .match_count = 0
+ };
+ for (size_t i = 0; i < ctx->rules.patterns.count; i++) {
+ ctx->rules.patterns.patterns[i].match_count = 0;
+ }
+ pattern_table_clear_matches(&ctx->rules.patterns);
+
+ ac_match_path(ctx->ac, target, match_callback, &mctx);
+ //ac_match_fd(ctx->ac, metadata->fd, match_callback, &mctx);
+
+ he->matched_rule_count = 0;
+ he->matched_pattern_count = 0;
+
+ for (size_t i = 0; i < ctx->rules.rule_count; i++) {
+ const rule_t *r = &ctx->rules.rules[i];
+
+ if (evaluate_rule(r, &ctx->rules.patterns)) {
+ //printf("fanotify match: %s\n", r->id);
+ if (he->matched_rule_count < MAX_MATCHED_RULES) {
+ he->matched_rules[he->matched_rule_count++] = r->id;
+ }
+
+ if (r->action == RULE_BLOCK) {
+ verdict = VERDICT_BLOCK;
+ break;
+ } else if (r->action == RULE_QUARANTINE && verdict < VERDICT_QUARANTINE) {
+ verdict = VERDICT_QUARANTINE;
+ } else if (r->action == RULE_ALLOW && verdict < VERDICT_ALLOW) {
+ verdict = VERDICT_ALLOW;
+ }
+ }
+ }
+
+ he->verdict = verdict;
+ he->last_scanned = time(NULL);
+ he->scan_count++;
+ }
+
+ switch (he->verdict) {
+ case VERDICT_BLOCK:
+ case VERDICT_QUARANTINE:
+ response.response = FAN_DENY; // deny blocked/quarantined files.
+ break;
+ default:
+ response.response = FAN_ALLOW; // fail open
+ break;
+ }
+
+ // TODO emit log for rule matches w/ allow action, not only block/quarantine
+ if (response.response == FAN_DENY) {
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "fanotify_block");
+ json_add_string(&buf, "verdict",
+ he->verdict == VERDICT_BLOCK ? "block" :
+ he->verdict == VERDICT_QUARANTINE ? "quarantine" :
+ he->verdict == VERDICT_ALLOW ? "allow" : "informational");
+
+ // TODO filesize
+ json_add_string(&buf, "md5", he->md5);
+ json_add_string(&buf, "sha256", he->sha256);
+ json_add_double(&buf, "entropy", he->entropy);
+
+ json_add_array_start(&buf, "matched_rules");
+ for (size_t i = 0; i < he->matched_rule_count; i++) {
+ json_array_add_string(&buf, he->matched_rules[i]);
+ }
+ json_end_array(&buf);
+
+ json_end_object(&buf);
+ output(json_get(&buf));
+ json_free(&buf);
+ }
+
+ //printf("\n\n\nfanotify: fd: %d, pid: %d, mask: %lld, target: %s (%s) verdict: %d\n\n\n", metadata->fd, metadata->pid, metadata->mask, target, he->md5, he->verdict);
+
+ if (write(fan_fd, &response, sizeof(response)) < 0) {
+ error("fanotify write response: %s", strerror(errno));
+ }
+ } else if (metadata->mask & FAN_OPEN_EXEC) {
+ // TODO separate branch for FAN_OPEN_EXEC if PERM isn't
+ // available--prefer this over proc_connector EXEC events
+ }
+
+ if (metadata->fd >= 0) {
+ close(metadata->fd);
+ }
+ }
+}
diff --git a/src/hash_ledger.c b/src/hash_ledger.c
new file mode 100644
index 0000000..5ae1939
--- /dev/null
+++ b/src/hash_ledger.c
@@ -0,0 +1,184 @@
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <limits.h>
+
+#include "djb2.h"
+#include "entropy.h"
+#include "md5.h"
+#include "md5/global.h"
+#include "md5/md5.h"
+#include "sha256.h"
+#include "hash_ledger.h"
+
+
+static void hexify(const unsigned char *in, size_t len, char *out) {
+ for (size_t i = 0; i < len; i++) {
+ sprintf(out + (i * 2), "%02x", in[i]);
+ }
+
+ out[len * 2] = '\0';
+}
+
+size_t hash_ledger_bucket(struct hash_ledger *ledger, const char *path) {
+ return djb2(path) % ledger->num_buckets;
+}
+
+struct hash_ledger *hash_ledger_init(size_t num_buckets) {
+ struct hash_ledger *ledger = calloc(1, sizeof(struct hash_ledger));
+ if (!ledger) {
+ return NULL;
+ }
+
+ ledger->num_buckets = num_buckets;
+ ledger->buckets = calloc(num_buckets, sizeof(struct hash_entry *));
+ if (!ledger->buckets) {
+ free(ledger);
+ return NULL;
+ }
+
+ return ledger;
+}
+
+void hash_ledger_destroy(struct hash_ledger *ledger) {
+ if (!ledger) {
+ return;
+ }
+
+ for (size_t i = 0; i < ledger->num_buckets; i++) {
+ struct hash_entry *entry = ledger->buckets[i];
+ while (entry) {
+ struct hash_entry *next = entry->next;
+ free(entry);
+ entry = next;
+ }
+ }
+
+ free(ledger->buckets);
+ free(ledger);
+}
+
+struct hash_entry *hash_ledger_find(struct hash_ledger *ledger, const char *path) {
+ if (!ledger || !path) {
+ return NULL;
+ }
+
+ size_t idx = hash_ledger_bucket(ledger, path);
+ struct hash_entry *entry = ledger->buckets[idx];
+
+ while (entry) {
+ if (strcmp(entry->path, path) == 0) {
+ return entry;
+ }
+
+ entry = entry->next;
+ }
+
+ return NULL;
+}
+
+struct hash_entry *hash_ledger_add_or_update(struct hash_ledger *ledger,
+ const char *path,
+ struct stat *sb) {
+ if (!ledger || !path || !sb) {
+ return NULL;
+ }
+
+ struct hash_entry *entry = hash_ledger_find(ledger, path);
+
+ if (entry) {
+ if (entry->sb.st_ino != sb->st_ino ||
+ entry->sb.st_mtime != sb->st_mtime ||
+ entry->sb.st_size != sb->st_size) {
+ // file changed. must rehash
+ struct multihash hashes;
+ if (multihash_file(path, &hashes)) {
+ entry->entropy = hashes.entropy;
+ memcpy(entry->md5, hashes.md5, sizeof(entry->md5));
+ memcpy(entry->sha256, hashes.sha256, sizeof(entry->sha256));
+ }
+ memcpy(&entry->sb, sb, sizeof(struct stat));
+ entry->last_scanned = 0; // force rescan
+ entry->scan_count = 0;
+ entry->verdict = VERDICT_UNKNOWN;
+ //entry->last_scanned = time(NULL);
+ }
+
+ // hash found. increment scan count
+ //entry->scan_count++;
+ return entry;
+ }
+
+ // not found. create new entry
+ entry = calloc(1, sizeof(struct hash_entry));
+ if (!entry) {
+ return NULL;
+ }
+
+ strncpy(entry->path, path, sizeof(entry->path) - 1);
+ memcpy(&entry->sb, sb, sizeof(struct stat));
+ //entry->last_scanned = time(NULL);
+ //entry->scan_count = 1;
+ entry->last_scanned = 0;
+ entry->scan_count = 0;
+ entry->verdict = VERDICT_UNKNOWN;
+ memset(entry->matched_rules, 0, sizeof(entry->matched_rules));
+
+ struct multihash hashes;
+ if (multihash_file(path, &hashes)) {
+ entry->entropy = hashes.entropy;
+ memcpy(entry->md5, hashes.md5, sizeof(entry->md5));
+ memcpy(entry->sha256, hashes.sha256, sizeof(entry->sha256));
+ }
+
+ size_t idx = hash_ledger_bucket(ledger, path);
+ entry->next = ledger->buckets[idx];
+ ledger->buckets[idx] = entry;
+
+ return entry;
+}
+
+bool multihash_file(const char *path, struct multihash *out) {
+ if (!path || !out) {
+ return false;
+ }
+
+ FILE *fp = fopen(path, "rb");
+ if (!fp) {
+ return false;
+ }
+
+ unsigned char buf[8192];
+ size_t n;
+ MD5_CTX md5_ctx;
+ SHA256_CTX sha256_ctx;
+ entropy_ctx ent_ctx;
+
+ MD5Init(&md5_ctx);
+ sha256_init(&sha256_ctx);
+ entropy_init(&ent_ctx);
+
+ while((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
+ MD5Update(&md5_ctx, buf, n);
+ sha256_update(&sha256_ctx, buf, n);
+ entropy_update(&ent_ctx, buf, n);
+ }
+
+ fclose(fp);
+
+ out->entropy = entropy_final(&ent_ctx);
+
+ unsigned char md5_result[MD5_DIGEST_LENGTH];
+ MD5Final(md5_result, &md5_ctx);
+ hexify(md5_result, sizeof(md5_result), out->md5);
+
+ unsigned char sha256_result[SHA256_DIGEST_LENGTH];
+ sha256_final(&sha256_ctx, sha256_result);
+ hexify(sha256_result, sizeof(sha256_result), out->sha256);
+
+ return true;
+}
diff --git a/src/json.c b/src/json.c
new file mode 100644
index 0000000..a09556a
--- /dev/null
+++ b/src/json.c
@@ -0,0 +1,321 @@
+// TODO pretty print json
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "json.h"
+#include "error.h"
+
+#define JSON_BUF_INITIAL_CAPACITY 1024
+
+static void json_push_context(json_t *buf, json_context_t ctx) {
+ if (buf->context_depth >= JSON_MAX_DEPTH) {
+ // TODO handle errors better than murdering the program
+ error_fatal("Exceeded JSON max nesting depth");
+ }
+ buf->context_stack[buf->context_depth] = ctx;
+ buf->needs_comma[buf->context_depth] = false;
+ buf->context_depth++;
+}
+
+static void json_pop_context(json_t *buf) {
+ if (buf->context_depth <= 0) {
+ // TODO better error handling
+ error_fatal("JSON context pop");
+ }
+ buf->context_depth--;
+}
+
+bool json_needs_comma(json_t *buf) {
+ return buf->needs_comma[buf->context_depth - 1];
+}
+
+void json_set_needs_comma(json_t *buf) {
+ buf->needs_comma[buf->context_depth - 1] = true;
+}
+
+static void json_ensure_capacity(json_t *buf, size_t needed) {
+ if (buf->length + needed >= buf->capacity) {
+ size_t new_capacity = buf->capacity += JSON_BUF_INITIAL_CAPACITY;
+ while (buf->length + needed >= new_capacity) {
+ new_capacity += JSON_BUF_INITIAL_CAPACITY;
+ }
+
+ char *new_data = realloc(buf->data, new_capacity);
+ if (!new_data) {
+ error_fatal("realloc: %s", strerror(errno));
+ }
+
+ buf->data = new_data;
+ buf->capacity = new_capacity;
+ }
+}
+
+void json_append(json_t *buf, const char *fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+ int needed = vsnprintf(NULL, 0, fmt, args);
+ va_end(args);
+
+ if (needed < 0) {
+ error_fatal("vsnprintf: %s", strerror(errno));
+ }
+
+ json_ensure_capacity(buf, (size_t)needed + 1);
+
+ va_start(args, fmt);
+ vsprintf(buf->data + buf->length, fmt, args);
+ va_end(args);
+
+ buf->length += (size_t)needed;
+}
+
+void json_init(json_t *buf) {
+ buf->capacity = JSON_BUF_INITIAL_CAPACITY;
+ buf->length = 0;
+ buf->data = malloc(buf->capacity);
+ if (!buf->data) {
+ error_fatal("malloc: %s", strerror(errno));
+ }
+ buf->data[0] = '\0';
+ buf->context_depth = 0;
+ memset(buf->needs_comma, 0, sizeof(buf->needs_comma));
+}
+
+void json_free(json_t *buf) {
+ if (buf->data) {
+ free(buf->data);
+ }
+ buf->data = NULL;
+ buf->length = 0;
+ buf->capacity = 0;
+ memset(buf->needs_comma, 0, sizeof(buf->needs_comma));
+ buf->context_depth = 0;
+}
+
+const char *json_get(json_t *buf) {
+ return buf && buf->data ? buf->data : "{}";
+}
+
+void json_escape_string(json_t *buf, const char *input) {
+ json_append(buf, "\"");
+ for (size_t i = 0; input[i] != '\0'; i++) {
+ char c = input[i];
+ switch(c) {
+ case '\\':
+ json_append(buf, "\\\\");
+ break;
+ case '"':
+ json_append(buf, "\\\"");
+ break;
+ case '\b':
+ json_append(buf, "\\b");
+ break;
+ case '\f':
+ json_append(buf, "\\f");
+ break;
+ case '\n':
+ json_append(buf, "\\n");
+ break;
+ case '\r':
+ json_append(buf, "\\r");
+ break;
+ case '\t':
+ json_append(buf, "\\t");
+ break;
+ default:
+ if ((unsigned char)c < 0x20) {
+ json_append(buf, "\\u%04x", c);
+ } else {
+ json_append(buf, "%c", c);
+ }
+ }
+ }
+
+ json_append(buf, "\"");
+}
+
+void json_start_object(json_t *buf) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+
+ json_append(buf, "{");
+ json_push_context(buf, JSON_CONTEXT_OBJECT);
+}
+
+void json_end_object(json_t *buf) {
+ json_append(buf, "}");
+ json_pop_context(buf);
+}
+
+void json_add_string(json_t *buf, const char *key, const char *value) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_escape_string(buf, key);
+ json_append(buf, ":");
+ json_escape_string(buf, value);
+ //buf->needs_comma = true;
+}
+
+void json_add_string_or_null(json_t *buf, const char *key, const char *value) {
+ if (value && value[0] != '\0') {
+ json_add_string(buf, key, value);
+ } else {
+ json_add_null(buf, key);
+ }
+}
+
+void json_add_int(json_t *buf, const char *key, int value) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_escape_string(buf, key);
+ json_append(buf, ":%d", value);
+}
+
+void json_add_int64(json_t *buf, const char *key, int64_t value) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_escape_string(buf, key);
+ json_append(buf, ":%lld", value);
+}
+
+void json_add_uint64(json_t *buf, const char *key, uint64_t value) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_escape_string(buf, key);
+ json_append(buf, ":%llu", value);
+}
+
+void json_add_double(json_t *buf, const char *key, double value) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_escape_string(buf, key);
+ json_append(buf, ":%f", value);
+}
+
+void json_add_bool(json_t *buf, const char *key, bool value) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_escape_string(buf, key);
+ json_append(buf, ":%s", value ? "true" : "false");
+}
+
+void json_add_null(json_t *buf, const char *key) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_escape_string(buf, key);
+ json_append(buf, ":null");
+}
+
+void json_start_array(json_t *buf) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+
+ json_append(buf, "[");
+ json_push_context(buf, JSON_CONTEXT_ARRAY);
+}
+
+void json_end_array(json_t *buf) {
+ json_append(buf, "]");
+ json_pop_context(buf);
+}
+
+void json_add_array_start(json_t *buf, const char *key) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+
+ json_escape_string(buf, key);
+ json_append(buf, ":");
+ json_push_context(buf, JSON_CONTEXT_ARRAY);
+ json_append(buf, "[");
+ buf->needs_comma[buf->context_depth - 1] = false;
+}
+
+void json_array_add_string(json_t *buf, const char *value) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_escape_string(buf, value);
+ json_set_needs_comma(buf);
+}
+
+void json_array_add_int(json_t *buf, int value) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_append(buf, "%d", value);
+}
+
+void json_array_add_int64(json_t *buf, int64_t value) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_append(buf, "%lld", value);
+}
+
+void json_array_add_uint64(json_t *buf, uint64_t value) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_append(buf, "%llu", value);
+}
+
+void json_array_add_double(json_t *buf, double value) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_append(buf, "%f", value);
+}
+
+void json_array_add_bool(json_t *buf, bool value) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_append(buf, "%s", value ? "true" : "false");
+}
+
+void json_array_add_null(json_t *buf) {
+ if (json_needs_comma(buf)) {
+ json_append(buf, ",");
+ }
+ json_set_needs_comma(buf);
+
+ json_append(buf, "null");
+}
diff --git a/src/md5.c b/src/md5.c
new file mode 100644
index 0000000..daa3f6b
--- /dev/null
+++ b/src/md5.c
@@ -0,0 +1,57 @@
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "md5/global.h"
+#include "md5/md5.h"
+
+#include "base64.h"
+#include "md5.h"
+#include "error.h"
+
+#undef MD5_SHOW_ERRORS
+
+
+/* md5_digest_file - Calculate MD5 digest of a file.
+ *
+ * Args:
+ * path - Path of file to hash.
+ *
+ * Returns:
+ * string containing the digest on success, "" on failure
+ */
+char *md5_digest_file(const char *path) {
+ unsigned char c[MD5_DIGEST_LENGTH];
+ FILE *fp;
+ int bytes;
+ unsigned char data[1024 * 64];
+ static char digest[MD5_DIGEST_LENGTH * 2 + 1];
+ MD5_CTX context;
+
+ fp = fopen(path, "rb");
+ if (fp == NULL) {
+#ifdef MD5_SHOW_ERRORS
+ error("md5_digest_file: unable to open %s for reading: %s",
+ path,
+ strerror(errno));
+#endif /* MD5_SHOW_ERRORS */
+ return NULL;
+ }
+
+ MD5Init(&context);
+ while ((bytes = fread(data, 1, sizeof(data), fp)) != 0)
+ MD5Update(&context, data, bytes);
+ MD5Final(c, &context);
+
+ fclose(fp);
+
+ snprintf(digest, sizeof(digest),
+ "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
+ c[0], c[1], c[2], c[3], c[4],
+ c[5], c[6], c[7], c[8], c[9],
+ c[10], c[11], c[12], c[13], c[14],
+ c[15]);
+
+ return digest;
+}
diff --git a/src/md5/Makefile b/src/md5/Makefile
new file mode 100644
index 0000000..e8801a5
--- /dev/null
+++ b/src/md5/Makefile
@@ -0,0 +1,10 @@
+# Makefile for MD5 from rfc1321 code
+
+CCF = -O -DMD=5
+
+md5c.o: md5.h global.h
+ gcc $(CCF) -c md5c.c
+
+clean:
+ rm -f *.o core
+
diff --git a/src/md5/global.h b/src/md5/global.h
new file mode 100644
index 0000000..983377d
--- /dev/null
+++ b/src/md5/global.h
@@ -0,0 +1,41 @@
+/* GLOBAL.H - RSAREF types and constants
+ */
+
+#include <stdint.h>
+
+/* PROTOTYPES should be set to one if and only if the compiler supports
+ function argument prototyping.
+The following makes PROTOTYPES default to 0 if it has not already
+
+
+
+Rivest [Page 7]
+
+RFC 1321 MD5 Message-Digest Algorithm April 1992
+
+
+ been defined with C compiler flags.
+ */
+#ifndef PROTOTYPES
+#define PROTOTYPES 0
+#endif
+
+/* POINTER defines a generic pointer type */
+typedef unsigned char *POINTER;
+
+/* UINT2 defines a two byte word */
+typedef uint16_t UINT2;
+
+/* UINT4 defines a four byte word */
+//typedef unsigned long int UINT4;
+typedef uint32_t UINT4;
+
+/* PROTO_LIST is defined depending on how PROTOTYPES is defined above.
+If using PROTOTYPES, then PROTO_LIST returns the list, otherwise it
+ returns an empty list.
+ */
+#if PROTOTYPES
+#define PROTO_LIST(list) list
+#else
+#define PROTO_LIST(list) ()
+#endif
diff --git a/src/md5/md5.h b/src/md5/md5.h
new file mode 100644
index 0000000..047811a
--- /dev/null
+++ b/src/md5/md5.h
@@ -0,0 +1,46 @@
+/* MD5.H - header file for MD5C.C
+ */
+
+/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
+rights reserved.
+
+License to copy and use this software is granted provided that it
+is identified as the "RSA Data Security, Inc. MD5 Message-Digest
+Algorithm" in all material mentioning or referencing this software
+or this function.
+
+License is also granted to make and use derivative works provided
+that such works are identified as "derived from the RSA Data
+Security, Inc. MD5 Message-Digest Algorithm" in all material
+mentioning or referencing the derived work.
+
+RSA Data Security, Inc. makes no representations concerning either
+the merchantability of this software or the suitability of this
+software for any particular purpose. It is provided "as is"
+without express or implied warranty of any kind.
+
+
+
+
+Rivest [Page 8]
+
+RFC 1321 MD5 Message-Digest Algorithm April 1992
+
+
+These notices must be retained in any copies of any part of this
+documentation and/or software.
+ */
+
+#define MD5_HASHSIZE 16
+
+/* MD5 context. */
+typedef struct {
+ UINT4 state[4]; /* state (ABCD) */
+ UINT4 count[2]; /* number of bits, modulo 2^64 (lsb first) */
+ unsigned char buffer[64]; /* input buffer */
+} MD5_CTX;
+
+void MD5Init PROTO_LIST ((MD5_CTX *));
+void MD5Update PROTO_LIST
+ ((MD5_CTX *, unsigned char *, unsigned int));
+void MD5Final PROTO_LIST ((unsigned char [16], MD5_CTX *));
diff --git a/src/md5/md5c.c b/src/md5/md5c.c
new file mode 100644
index 0000000..5a656c4
--- /dev/null
+++ b/src/md5/md5c.c
@@ -0,0 +1,380 @@
+/* MD5C.C - RSA Data Security, Inc., MD5 message-digest algorithm
+ */
+
+/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
+rights reserved.
+
+License to copy and use this software is granted provided that it
+is identified as the "RSA Data Security, Inc. MD5 Message-Digest
+Algorithm" in all material mentioning or referencing this software
+or this function.
+
+License is also granted to make and use derivative works provided
+that such works are identified as "derived from the RSA Data
+Security, Inc. MD5 Message-Digest Algorithm" in all material
+mentioning or referencing the derived work.
+
+RSA Data Security, Inc. makes no representations concerning either
+the merchantability of this software or the suitability of this
+software for any particular purpose. It is provided "as is"
+without express or implied warranty of any kind.
+
+These notices must be retained in any copies of any part of this
+documentation and/or software.
+ */
+
+#include "global.h"
+#include "md5.h"
+
+/* Constants for MD5Transform routine.
+ */
+
+
+/*
+Rivest [Page 9]
+
+RFC 1321 MD5 Message-Digest Algorithm April 1992
+*/
+
+#define S11 7
+#define S12 12
+#define S13 17
+#define S14 22
+#define S21 5
+#define S22 9
+#define S23 14
+#define S24 20
+#define S31 4
+#define S32 11
+#define S33 16
+#define S34 23
+#define S41 6
+#define S42 10
+#define S43 15
+#define S44 21
+
+static void MD5Transform PROTO_LIST ((UINT4 [4], unsigned char [64]));
+static void Encode PROTO_LIST
+ ((unsigned char *, UINT4 *, unsigned int));
+static void Decode PROTO_LIST
+ ((UINT4 *, unsigned char *, unsigned int));
+static void MD5_memcpy PROTO_LIST ((POINTER, POINTER, unsigned int));
+static void MD5_memset PROTO_LIST ((POINTER, int, unsigned int));
+
+static unsigned char PADDING[64] = {
+ 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+};
+
+/* F, G, H and I are basic MD5 functions.
+ */
+#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
+#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
+#define H(x, y, z) ((x) ^ (y) ^ (z))
+#define I(x, y, z) ((y) ^ ((x) | (~z)))
+
+/* ROTATE_LEFT rotates x left n bits.
+ */
+#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32-(n))))
+
+/* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4.
+Rotation is separate from addition to prevent recomputation.
+ */
+#define FF(a, b, c, d, x, s, ac) { \
+ (a) += F ((b), (c), (d)) + (x) + (UINT4)(ac); \
+ (a) = ROTATE_LEFT ((a), (s)); \
+ (a) += (b); \
+ }
+#define GG(a, b, c, d, x, s, ac) { \
+ (a) += G ((b), (c), (d)) + (x) + (UINT4)(ac); \
+ (a) = ROTATE_LEFT ((a), (s)); \
+ (a) += (b); \
+ }
+#define HH(a, b, c, d, x, s, ac) { \
+ (a) += H ((b), (c), (d)) + (x) + (UINT4)(ac); \
+ (a) = ROTATE_LEFT ((a), (s)); \
+ (a) += (b); \
+ }
+#define II(a, b, c, d, x, s, ac) { \
+ (a) += I ((b), (c), (d)) + (x) + (UINT4)(ac); \
+ (a) = ROTATE_LEFT ((a), (s)); \
+ (a) += (b); \
+ }
+
+/* MD5 initialization. Begins an MD5 operation, writing a new context.
+ */
+void MD5Init (context)
+MD5_CTX *context; /* context */
+{
+ context->count[0] = context->count[1] = 0;
+ /* Load magic initialization constants.
+*/
+ context->state[0] = 0x67452301;
+ context->state[1] = 0xefcdab89;
+ context->state[2] = 0x98badcfe;
+ context->state[3] = 0x10325476;
+}
+
+/* MD5 block update operation. Continues an MD5 message-digest
+ operation, processing another message block, and updating the
+ context.
+ */
+void MD5Update (context, input, inputLen)
+MD5_CTX *context; /* context */
+unsigned char *input; /* input block */
+unsigned int inputLen; /* length of input block */
+{
+ unsigned int i, index, partLen;
+
+ /* Compute number of bytes mod 64 */
+ index = (unsigned int)((context->count[0] >> 3) & 0x3F);
+
+ /* Update number of bits */
+ if ((context->count[0] += ((UINT4)inputLen << 3))
+
+
+/*
+Rivest [Page 11]
+
+RFC 1321 MD5 Message-Digest Algorithm April 1992
+*/
+
+ < ((UINT4)inputLen << 3))
+ context->count[1]++;
+ context->count[1] += ((UINT4)inputLen >> 29);
+
+ partLen = 64 - index;
+
+ /* Transform as many times as possible.
+*/
+ if (inputLen >= partLen) {
+ MD5_memcpy
+ ((POINTER)&context->buffer[index], (POINTER)input, partLen);
+ MD5Transform (context->state, context->buffer);
+
+ for (i = partLen; i + 63 < inputLen; i += 64)
+ MD5Transform (context->state, &input[i]);
+
+ index = 0;
+ }
+ else
+ i = 0;
+
+ /* Buffer remaining input */
+ MD5_memcpy
+ ((POINTER)&context->buffer[index], (POINTER)&input[i],
+ inputLen-i);
+}
+
+/* MD5 finalization. Ends an MD5 message-digest operation, writing the
+ the message digest and zeroizing the context.
+ */
+void MD5Final (digest, context)
+unsigned char digest[16]; /* message digest */
+MD5_CTX *context; /* context */
+{
+ unsigned char bits[8];
+ unsigned int index, padLen;
+
+ /* Save number of bits */
+ Encode (bits, context->count, 8);
+
+ /* Pad out to 56 mod 64.
+*/
+ index = (unsigned int)((context->count[0] >> 3) & 0x3f);
+ padLen = (index < 56) ? (56 - index) : (120 - index);
+ MD5Update (context, PADDING, padLen);
+
+ /* Append length (before padding) */
+ MD5Update (context, bits, 8);
+
+
+/*
+Rivest [Page 12]
+
+RFC 1321 MD5 Message-Digest Algorithm April 1992
+*/
+
+ /* Store state in digest */
+ Encode (digest, context->state, 16);
+
+ /* Zeroize sensitive information.
+*/
+ MD5_memset ((POINTER)context, 0, sizeof (*context));
+}
+
+/* MD5 basic transformation. Transforms state based on block.
+ */
+static void MD5Transform (state, block)
+UINT4 state[4];
+unsigned char block[64];
+{
+ UINT4 a = state[0], b = state[1], c = state[2], d = state[3], x[16];
+
+ Decode (x, block, 64);
+
+ /* Round 1 */
+ FF (a, b, c, d, x[ 0], S11, 0xd76aa478); /* 1 */
+ FF (d, a, b, c, x[ 1], S12, 0xe8c7b756); /* 2 */
+ FF (c, d, a, b, x[ 2], S13, 0x242070db); /* 3 */
+ FF (b, c, d, a, x[ 3], S14, 0xc1bdceee); /* 4 */
+ FF (a, b, c, d, x[ 4], S11, 0xf57c0faf); /* 5 */
+ FF (d, a, b, c, x[ 5], S12, 0x4787c62a); /* 6 */
+ FF (c, d, a, b, x[ 6], S13, 0xa8304613); /* 7 */
+ FF (b, c, d, a, x[ 7], S14, 0xfd469501); /* 8 */
+ FF (a, b, c, d, x[ 8], S11, 0x698098d8); /* 9 */
+ FF (d, a, b, c, x[ 9], S12, 0x8b44f7af); /* 10 */
+ FF (c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */
+ FF (b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */
+ FF (a, b, c, d, x[12], S11, 0x6b901122); /* 13 */
+ FF (d, a, b, c, x[13], S12, 0xfd987193); /* 14 */
+ FF (c, d, a, b, x[14], S13, 0xa679438e); /* 15 */
+ FF (b, c, d, a, x[15], S14, 0x49b40821); /* 16 */
+
+ /* Round 2 */
+ GG (a, b, c, d, x[ 1], S21, 0xf61e2562); /* 17 */
+ GG (d, a, b, c, x[ 6], S22, 0xc040b340); /* 18 */
+ GG (c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */
+ GG (b, c, d, a, x[ 0], S24, 0xe9b6c7aa); /* 20 */
+ GG (a, b, c, d, x[ 5], S21, 0xd62f105d); /* 21 */
+ GG (d, a, b, c, x[10], S22, 0x2441453); /* 22 */
+ GG (c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */
+ GG (b, c, d, a, x[ 4], S24, 0xe7d3fbc8); /* 24 */
+ GG (a, b, c, d, x[ 9], S21, 0x21e1cde6); /* 25 */
+ GG (d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */
+ GG (c, d, a, b, x[ 3], S23, 0xf4d50d87); /* 27 */
+
+
+/*
+Rivest [Page 13]
+
+RFC 1321 MD5 Message-Digest Algorithm April 1992
+*/
+
+ GG (b, c, d, a, x[ 8], S24, 0x455a14ed); /* 28 */
+ GG (a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */
+ GG (d, a, b, c, x[ 2], S22, 0xfcefa3f8); /* 30 */
+ GG (c, d, a, b, x[ 7], S23, 0x676f02d9); /* 31 */
+ GG (b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */
+
+ /* Round 3 */
+ HH (a, b, c, d, x[ 5], S31, 0xfffa3942); /* 33 */
+ HH (d, a, b, c, x[ 8], S32, 0x8771f681); /* 34 */
+ HH (c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */
+ HH (b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */
+ HH (a, b, c, d, x[ 1], S31, 0xa4beea44); /* 37 */
+ HH (d, a, b, c, x[ 4], S32, 0x4bdecfa9); /* 38 */
+ HH (c, d, a, b, x[ 7], S33, 0xf6bb4b60); /* 39 */
+ HH (b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */
+ HH (a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */
+ HH (d, a, b, c, x[ 0], S32, 0xeaa127fa); /* 42 */
+ HH (c, d, a, b, x[ 3], S33, 0xd4ef3085); /* 43 */
+ HH (b, c, d, a, x[ 6], S34, 0x4881d05); /* 44 */
+ HH (a, b, c, d, x[ 9], S31, 0xd9d4d039); /* 45 */
+ HH (d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */
+ HH (c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */
+ HH (b, c, d, a, x[ 2], S34, 0xc4ac5665); /* 48 */
+
+ /* Round 4 */
+ II (a, b, c, d, x[ 0], S41, 0xf4292244); /* 49 */
+ II (d, a, b, c, x[ 7], S42, 0x432aff97); /* 50 */
+ II (c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */
+ II (b, c, d, a, x[ 5], S44, 0xfc93a039); /* 52 */
+ II (a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */
+ II (d, a, b, c, x[ 3], S42, 0x8f0ccc92); /* 54 */
+ II (c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */
+ II (b, c, d, a, x[ 1], S44, 0x85845dd1); /* 56 */
+ II (a, b, c, d, x[ 8], S41, 0x6fa87e4f); /* 57 */
+ II (d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */
+ II (c, d, a, b, x[ 6], S43, 0xa3014314); /* 59 */
+ II (b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */
+ II (a, b, c, d, x[ 4], S41, 0xf7537e82); /* 61 */
+ II (d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */
+ II (c, d, a, b, x[ 2], S43, 0x2ad7d2bb); /* 63 */
+ II (b, c, d, a, x[ 9], S44, 0xeb86d391); /* 64 */
+
+ state[0] += a;
+ state[1] += b;
+ state[2] += c;
+ state[3] += d;
+
+ /* Zeroize sensitive information.
+
+
+Rivest [Page 14]
+
+RFC 1321 MD5 Message-Digest Algorithm April 1992
+
+*/
+ MD5_memset ((POINTER)x, 0, sizeof (x));
+}
+
+/* Encodes input (UINT4) into output (unsigned char). Assumes len is
+ a multiple of 4.
+ */
+static void Encode (output, input, len)
+unsigned char *output;
+UINT4 *input;
+unsigned int len;
+{
+ unsigned int i, j;
+
+ for (i = 0, j = 0; j < len; i++, j += 4) {
+ output[j] = (unsigned char)(input[i] & 0xff);
+ output[j+1] = (unsigned char)((input[i] >> 8) & 0xff);
+ output[j+2] = (unsigned char)((input[i] >> 16) & 0xff);
+ output[j+3] = (unsigned char)((input[i] >> 24) & 0xff);
+ }
+}
+
+/* Decodes input (unsigned char) into output (UINT4). Assumes len is
+ a multiple of 4.
+ */
+static void Decode (output, input, len)
+UINT4 *output;
+unsigned char *input;
+unsigned int len;
+{
+ unsigned int i, j;
+
+ for (i = 0, j = 0; j < len; i++, j += 4)
+ output[i] = ((UINT4)input[j]) | (((UINT4)input[j+1]) << 8) |
+ (((UINT4)input[j+2]) << 16) | (((UINT4)input[j+3]) << 24);
+}
+
+/* Note: Replace "for loop" with standard memcpy if possible.
+ */
+
+static void MD5_memcpy (output, input, len)
+POINTER output;
+POINTER input;
+unsigned int len;
+{
+ unsigned int i;
+
+ for (i = 0; i < len; i++)
+
+
+/*
+Rivest [Page 15]
+
+RFC 1321 MD5 Message-Digest Algorithm April 1992
+*/
+
+ output[i] = input[i];
+}
+
+/* Note: Replace "for loop" with standard memset if possible.
+ */
+static void MD5_memset (output, value, len)
+POINTER output;
+int value;
+unsigned int len;
+{
+ unsigned int i;
+
+ for (i = 0; i < len; i++)
+ ((char *)output)[i] = (char)value;
+}
+
diff --git a/src/net.c b/src/net.c
new file mode 100644
index 0000000..02c3d6e
--- /dev/null
+++ b/src/net.c
@@ -0,0 +1,60 @@
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include <net/if.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+
+
+int sockprintf(int s, const char *fmt, ...) {
+ int n;
+ char buf[8192] = {0};
+ va_list vl;
+
+ va_start(vl, fmt);
+ n = vsnprintf(buf, sizeof(buf), fmt, vl);
+ va_end(vl);
+
+ return send(s, buf, n, 0);
+}
+
+bool validate_ipv4(const char *ip) {
+ return inet_aton(ip, NULL) ? true : false;
+}
+
+bool validate_ip(const char *ip) {
+ struct in_addr addr4;
+ struct in6_addr addr6;
+
+ return inet_pton(AF_INET, ip, &addr4) == 1 || inet_pton(AF_INET6, ip, &addr6) == 1;
+}
+
+// get the interface assigned to the default route. this is a sensible
+// default for most deployments, but not perfect.
+char *get_default_iface(void) {
+ static char iface[IFNAMSIZ] = {0};
+ FILE *fp = fopen("/proc/net/route", "r");
+ if (!fp) {
+ return NULL;
+ }
+
+ char line[256];
+ fgets(line, sizeof(line), fp); // skip header line
+ while (fgets(line, sizeof(line), fp)) {
+ char iface_name[IFNAMSIZ];
+ unsigned long dest;
+ if (sscanf(line, "%s %lx", iface_name, &dest) == 2) {
+ if (dest == 0) { // default route
+ strncpy(iface, iface_name, IFNAMSIZ - 1);
+ fclose(fp);
+ return iface;
+ }
+ }
+ }
+
+ fclose(fp);
+ return NULL;
+}
diff --git a/src/noawareness.c b/src/noawareness.c
new file mode 100644
index 0000000..dbabe44
--- /dev/null
+++ b/src/noawareness.c
@@ -0,0 +1,512 @@
+// https://www.kernel.org/doc/Documentation/connector/connector.txt
+// TODO CLI options to toggle sniffer, proc_connector, fanotify, ...
+// TODO map uids and gids to real names
+ // implement getgrgid and getpwuid myself because cant link these static
+// TODO permissions, attributes, owner, groupships of files
+// TODO limits? /proc/X/limits
+// TODO atexit() handler to log when this dies
+// TODO emit a json log startup event
+// TODO emit heartbeat json logs
+// TODO multiple interfaces for sniffing
+
+#include <stdio.h>
+#include <errno.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <limits.h>
+#include <syslog.h>
+#include <signal.h>
+#include <netdb.h>
+#include <fcntl.h>
+
+#include <arpa/inet.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/select.h>
+#include <sys/stat.h>
+
+#include "net.h"
+#include "error.h"
+#include "output.h"
+#include "sniffer.h"
+#include "fanotify.h"
+#include "proc_ledger.h"
+#include "hash_ledger.h"
+#include "proc_connector.h"
+#include "time_common.h"
+#include "agent_context.h"
+#include "av_rules.h"
+#include "aho-corasick.h"
+
+/* dirty hack to make this work on ancient systems */
+#ifndef PROC_EVENT_PTRACE
+#define PROC_EVENT_PTRACE 0x00000100
+#endif
+
+#ifndef PROC_EVENT_COMM
+#define PROC_EVENT_COMM 0x00000200
+#endif
+
+#ifndef PROC_EVENT_COREDUMP
+#define PROC_EVENT_COREDUMP 0x40000000
+#endif
+
+/*
+ * Globals
+ */
+sock_t sock;
+bool quiet = false;
+bool use_syslog = true;
+bool log_to_file = false;
+bool remote_logging = true;
+char *log_server = "127.0.0.1";
+port_t log_server_port = 55555;
+char *outfile = "/var/log/noawareness.json.log";
+FILE *outfilep;
+
+bool promisc = false;
+
+unsigned long maxsize = 1024 * 1024 * 100; // 100Mb
+unsigned long boot_time;
+long ticks;
+
+bool daemonize = false;
+char *pidfile = "/var/run/noawareness.pid";
+static volatile sig_atomic_t reload_config = false;
+
+char *rules_file = "rules";
+
+// Write pid file to disk
+void write_pid_file(const char *path, pid_t pid) {
+ FILE *pidfile;
+
+ pidfile = fopen(path, "w");
+ if (pidfile == NULL) {
+ error_fatal("Unable to open PID file %s: %s", path, strerror(errno));
+ }
+
+ fprintf(pidfile, "%d", pid);
+ fclose(pidfile);
+}
+
+FILE *open_log_file(const char *outfile) {
+ FILE *fp;
+
+ fp = fopen(outfile, "a+");
+ if (fp == NULL) {
+ error_fatal("Unable to open log file: %s", strerror(errno));
+ }
+
+ return fp;
+}
+
+// Sets up a UDP socket to a remote host for sending log events
+// TODO strategies to do TCP, encryption, etc. the remote logging
+// setup is pretty dirty
+static void setup_remote_logging() {
+ struct addrinfo hints, *res, *rp;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_DGRAM;
+
+ char pstr[6];
+ snprintf(pstr, sizeof(pstr), "%d", log_server_port);
+
+ if (getaddrinfo(log_server, pstr, &hints, &res) != 0) {
+ error_fatal("getaddrinfo(): %s", strerror(errno));
+ }
+
+ for (rp = res; rp != NULL; rp = rp->ai_next) {
+ sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+ if (sock == -1) {
+ continue;
+ }
+
+ if (connect(sock, rp->ai_addr, rp->ai_addrlen) != -1) {
+ break;
+ }
+
+ close(sock);
+ }
+
+ if (rp == NULL) {
+ error_fatal("Unable to connect to %s: %s\n", log_server, strerror(errno));
+ }
+
+ char addr_s[INET6_ADDRSTRLEN];
+ void *addr_ptr = NULL;
+
+ if (rp->ai_family == AF_INET) {
+ struct sockaddr_in *ipv4 = (struct sockaddr_in *)rp->ai_addr;
+ addr_ptr = &(ipv4->sin_addr);
+ } else if (rp->ai_family == AF_INET6) {
+ struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)rp->ai_addr;
+ addr_ptr = &(ipv6->sin6_addr);
+ }
+
+ if (addr_ptr &&
+ inet_ntop(rp->ai_family, addr_ptr, addr_s, sizeof(addr_s))) {
+ msg("Connected to logserver: %s:%d", addr_s, log_server_port);
+ }
+
+ freeaddrinfo(res);
+}
+
+// SIGHUP handler
+static void handle_sighup(int sig, siginfo_t *siginfo, void *context) {
+ reload_config = true;
+}
+
+// install SIGHUP handler
+static void install_sighup_handler() {
+ struct sigaction act = {0};
+
+ act.sa_sigaction = handle_sighup;
+ act.sa_flags = 0;
+
+ if (sigaction(SIGHUP, &act, NULL) < 0) {
+ error_fatal("sigaction(): %s", strerror(errno));
+ }
+}
+
+// Variadic function that returns the maximum value input
+static int max_fd(int count, ...) {
+ va_list args;
+
+ va_start(args, count);
+
+ int max = va_arg(args, int);
+
+ for (int i = 1; i < count; i++) {
+ int val = va_arg(args, int);
+ if (val > max) {
+ max = val;
+ }
+ }
+
+ va_end(args);
+
+ return max;
+}
+
+ac_context_t *build_matcher_from_rules(rule_set_t *rules) {
+ ac_context_t *ctx = ac_new();
+ if (!ctx) {
+ return NULL;
+ }
+
+ for (size_t i = 0; i < rules->patterns.count; i++) {
+ pattern_t *p = &rules->patterns.patterns[i];
+ if (ac_add_pattern(ctx, p->id, p->bytes, p->len) != 0) {
+ error("Failed to add pattern %s to Aho-Corasick", p->id);
+ ac_free(ctx);
+ return NULL;
+ }
+ }
+
+ if (ac_build(ctx) != 0) {
+ error("Failed to build Aho-Corasick automaton");
+ ac_free(ctx);
+ return NULL;
+ }
+
+ return ctx;
+}
+
+// Display help/usage menu
+static void usage(const char *progname) {
+ fprintf(stderr, "usage: %s [-h?]\n\n", progname);
+ fprintf(stderr, " -h/-? - Print this menu and exit.\n");
+ fprintf(stderr, " -d - Daemonize. Default: %s\n",
+ daemonize ? "yes" : "no");
+ fprintf(stderr, " -i <iface> - Interface to sniff\n");
+ fprintf(stderr, " -x - Toggle promiscuous mode. Default: %s\n",
+ promisc ? "true" : "false");
+ fprintf(stderr, " -m <bytes> - Max size of file to hash. Default: %ld\n",
+ maxsize);
+ fprintf(stderr, " -o <file> - Outfile for JSON output, Default: %s\n",
+ outfile);
+ fprintf(stderr, " -O - Toggle local JSON logging. Default: %s\n",
+ log_to_file ? "true" : "false");
+ fprintf(stderr, " -P <path> - Path to PID file. Default: %s\n", pidfile);
+ fprintf(stderr, " -r - Toggle remote logging. Default: %s\n",
+ remote_logging ? "true" : "false");
+ fprintf(stderr, " -R <path> - Path to AV rule definitions. Default: %s\n", rules_file);
+ fprintf(stderr, " -s <IP> - Remote log server. Default: %s\n",
+ log_server);
+ fprintf(stderr, " -S - Toggle syslog. Default: %s\n",
+ use_syslog ? "true" : "false");
+ fprintf(stderr, " -p <port> - Port of remote server. Default: %d\n",
+ log_server_port);
+ fprintf(stderr, " -q - Toggle quiet mode. Default: %s\n",
+ quiet ? "true" : "false");
+
+ exit(EXIT_FAILURE);
+}
+
+int main(int argc, char *argv[]) {
+ int opt;
+ pid_t pid;
+ sock_t proc_connector;
+ sock_t sniffer; // socket for sniffing to create network events
+ char *interface = NULL;
+ int fan_fd; // fanotify fd
+ fd_set fdset;
+ agent_context_t *agent_ctx;
+
+ /* Parse CLI options */
+ // TODO flag to enable/disable fanotify blocking
+ while((opt = getopt(argc, argv, "qrRdm:s:So:Op:rP:xi:h?")) != -1) {
+ switch (opt) {
+ case 'd': /* Daemonize */
+ daemonize = daemonize ? false : true;
+ break;
+
+ case 'i': /* Interface for SOCK_RAW sniffing */
+ // TODO if optarg == "list", list interfaces
+ interface = optarg;
+ break;
+
+ case 'm': /* Maximum filesize to hash */
+ maxsize = atol(optarg);
+ break;
+
+ case 'o': /* Path to outfile */
+ // TODO check if writeable
+ outfile = optarg;
+ log_to_file = true;
+ break;
+
+ case 'O': /* Toggle logging to a file */
+ log_to_file = log_to_file ? false : true;
+ break;
+
+ case 'p': /* Remote server port */
+ log_server_port = atoi(optarg);
+ break;
+
+ case 'P': /* PID file location */
+ pidfile = optarg;
+ break;
+
+ case 'q': /* Toggle quiet mode */
+ quiet = quiet ? false : true;
+ break;
+
+ case 'r': /* Toggle remote logging */
+ remote_logging = remote_logging ? false : true;
+ break;
+
+ case 'R': /* AV rules file */
+ rules_file = optarg;
+ break;
+
+ case 's': /* Remote server */
+ log_server = optarg;
+ if (!validate_ip(log_server)) {
+ error_fatal("Invalid IP address: %s", log_server);
+ }
+ break;
+
+ case 'S': /* Toggle syslog */
+ use_syslog = use_syslog ? false : true;
+ break;
+
+ case 'x': /* Toggle promiscuous mode */
+ promisc = promisc ? false : true;
+ break;
+
+ /* All of these effectively call usage(), so roll them over */
+ case 'h':
+ case '?':
+ default:
+ usage(argv[0]);
+ }
+ }
+
+ /* Set up syslog() */
+ if (use_syslog) {
+ openlog("noawareness", LOG_PID, LOG_USER);
+ }
+
+ /* SIGHUP handler. */
+ install_sighup_handler();
+
+ /* Open log file. */
+ if (log_to_file) {
+ outfilep = open_log_file(outfile);
+ }
+
+ /* Allocate agent_ctx */
+ agent_ctx = calloc(1, sizeof(*agent_ctx));
+ if (!agent_ctx) {
+ error_fatal("calloc: %s", strerror(errno));
+ }
+ /* Get our hostname for reporting purposes. */
+ if (gethostname(agent_ctx->hostname, sizeof(agent_ctx->hostname)) == -1) {
+ error_fatal("gethostname(): %s", strerror(errno));
+ }
+
+ /* Get system boot time */
+ ticks = sysconf(_SC_CLK_TCK);
+ boot_time = get_boot_time();
+
+ /* Set up remote logging */
+ if (remote_logging) {
+ setup_remote_logging();
+ }
+
+ /* Create hash ledger */
+ agent_ctx->hash_ledger = hash_ledger_init(4096);
+ if (!agent_ctx->hash_ledger) {
+ fprintf(stderr, "FATAL: Error creating hash ledger: %s\n", strerror(errno));
+ return EXIT_FAILURE;
+ }
+
+ /* Parse AV rules and create Aho-Corasick context */
+ //agent_ctx->rules = {0};
+ if (load_rules(rules_file, &agent_ctx->rules) != 0) {
+ error_fatal("FATAL: Failed to load AV signatures from file %s: %s", rules_file, strerror(errno));
+ }
+
+ agent_ctx->ac = build_matcher_from_rules(&agent_ctx->rules);
+ if (!agent_ctx->ac) {
+ error_fatal("FATAL: Failed to create Aho-Corasick context");
+ }
+
+
+ /* Create process ledger--this needs to be done before the
+ fanotify handler, otherwise processes will be blocked until the
+ process ledger is hydrated */
+ agent_ctx->proc_ledger = proc_ledger_init(4096);
+ proc_ledger_hydrate(agent_ctx);
+
+ /* Create proc_connector socket. */
+ proc_connector = setup_proc_connector();
+ if (proc_connector == -1) {
+ error_fatal("error setting up proc_connector: %s", strerror(errno));
+ }
+
+ /* Create fanotify descriptor */
+ fan_fd = setup_fanotify();
+ if (fan_fd == -1) {
+ error("Error setting up fanotify: %s", strerror(errno));
+ }
+
+ /* Create sniffer raw socket */
+ // TODO flag to toggle sniffer
+ if (!interface) {
+ interface = get_default_iface();
+ if (interface == NULL) {
+ error_fatal("FATAL: Unable to determine network interface. Please specify with -i");
+ }
+ }
+
+ sniffer = sniffer_init_interface(interface, promisc);
+ if (sniffer == -1) {
+ error("Error setting up SOCK_RAW socket: %s", strerror(errno));
+ }
+
+ /* Daemonize the process if desired. */
+ if (daemonize) {
+ pid = fork();
+ if (pid < 0) {
+ error_fatal("FATAL: fork(): %s", strerror(errno));
+ }
+
+ else if (pid > 0) {
+ write_pid_file(pidfile, pid);
+ exit(EXIT_SUCCESS);
+ }
+ }
+
+ /* Set up select loop. */
+ int setsize = max_fd(3, proc_connector, fan_fd, sniffer) + 1;
+
+ for(;;) {
+ FD_ZERO(&fdset);
+ if (proc_connector != -1) FD_SET(proc_connector, &fdset);
+ if (fan_fd != -1) FD_SET(fan_fd, &fdset);
+ if (sniffer != -1) FD_SET(sniffer, &fdset);
+
+ struct timeval select_timeout;
+ select_timeout.tv_sec = 1;
+ select_timeout.tv_usec = 0;
+
+ int activity = select(setsize, &fdset, NULL, NULL, &select_timeout);
+ if (activity < 0) {
+ if (errno != EINTR) {
+ error_fatal("select(): %s", strerror(errno));
+ }
+ }
+
+ if (activity == 0) {
+ //printf("no activity...\n");
+ continue;
+ }
+
+ if (FD_ISSET(proc_connector, &fdset)) {
+ select_proc_connector(proc_connector, agent_ctx);
+ }
+
+ if (FD_ISSET(fan_fd, &fdset)) {
+ select_fanotify(fan_fd, agent_ctx);
+ }
+
+ if (FD_ISSET(sniffer, &fdset)) {
+ sniffer_handle_packet(sniffer, agent_ctx);
+ }
+
+ if (reload_config) { // TODO break this into a reload function
+ reload_config = false;
+ msg("Caught SIGHUP.");
+
+ if (log_to_file) {
+ msg("Reloading JSON log file");
+ fclose(outfilep);
+ outfilep = open_log_file(outfile);
+ }
+
+ msg("Flushing hash ledger");
+ hash_ledger_destroy(agent_ctx->hash_ledger);
+ agent_ctx->hash_ledger = hash_ledger_init(4096);
+ if (!agent_ctx->hash_ledger) {
+ error_fatal("FATAL: Error creating hash ledger: %s\n", strerror(errno));
+ }
+
+ msg("Reloading AV signatures");
+ pattern_table_free(&agent_ctx->rules.patterns);
+ free_rules(&agent_ctx->rules);
+ ac_free(agent_ctx->ac);
+ if (load_rules(rules_file, &agent_ctx->rules) != 0) {
+ error_fatal("FATAL: Failed to load AV signatures from file %s: %s", rules_file, strerror(errno));
+ }
+ agent_ctx->ac = build_matcher_from_rules(&agent_ctx->rules);
+ if (!agent_ctx->ac) {
+ error_fatal("FATAL: Failed to create Aho-Corasick context");
+ }
+
+ msg("Rehydrating process list");
+ proc_ledger_destroy(agent_ctx->proc_ledger);
+ agent_ctx->proc_ledger = proc_ledger_init(4096);
+ proc_ledger_hydrate(agent_ctx);
+ }
+ }
+
+ /* Shouldn't ever get here */
+ proc_ledger_destroy(agent_ctx->proc_ledger);
+ hash_ledger_destroy(agent_ctx->hash_ledger);
+ ac_free(agent_ctx->ac);
+ pattern_table_free(&agent_ctx->rules.patterns);
+ free_rules(&agent_ctx->rules);
+ close(proc_connector);
+ close(sniffer);
+ close(sock);
+ closelog();
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/output.c b/src/output.c
new file mode 100644
index 0000000..daa25f2
--- /dev/null
+++ b/src/output.c
@@ -0,0 +1,53 @@
+// TODO general logging overhaul. make sure messages make sense, are consistent, etc.
+// TODO make this not rely on so many globals.
+// TODO colorize stdout for easier reading
+
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <syslog.h>
+
+#include "net.h"
+
+extern bool quiet;
+extern bool daemonize;
+extern bool remote_logging;
+extern bool use_syslog;
+extern bool log_to_file;
+extern FILE *outfilep;
+extern sock_t sock;
+
+// output logs according to runtime configuration
+void output(const char *msg) {
+ if (!daemonize) {
+ if (!quiet) {
+ printf("%s\n", msg);
+ }
+ }
+
+ if (remote_logging) {
+ sockprintf(sock, "%s\r\n", msg);
+ }
+
+ if (log_to_file) {
+ fprintf(outfilep, "%s\n", msg);
+ }
+}
+
+// Display/log runtime messages
+void msg(const char *fmt, ...) {
+ char buf[8192] = {0};
+ va_list vl;
+
+ va_start(vl, fmt);
+ vsnprintf(buf, sizeof(buf), fmt, vl);
+ va_end(vl);
+
+ if (use_syslog) {
+ syslog(LOG_INFO | LOG_USER, "%s", buf);
+ }
+
+ if (!daemonize) {
+ printf("%s\n", buf);
+ }
+}
diff --git a/src/proc.c b/src/proc.c
new file mode 100644
index 0000000..8381ac7
--- /dev/null
+++ b/src/proc.c
@@ -0,0 +1,268 @@
+/* proc.c - various functions to deal with stuff in the /proc directory */
+
+#include <stdio.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/types.h>
+
+#include <linux/limits.h>
+
+#include "error.h"
+#include "proc.h"
+#include "base64.h"
+#include "string_common.h"
+
+#undef SHOW_READLINK_ERRORS
+#undef SHOW_GET_PROC_STATUS_ERRORS
+
+// helper to build proc paths
+static void build_proc_path(char *buf, size_t size, pid_t pid, const char *suffix) {
+ snprintf(buf, size, "/proc/%d/%s", pid, suffix);
+}
+
+// Get the CWD of a PID from /proc/PID/cwd
+char *proc_cwd(pid_t pid) {
+ char cwd_path[PATH_MAX];
+ static char cwd[PATH_MAX];
+
+ memset(cwd, '\0', sizeof(cwd));
+ build_proc_path(cwd_path, sizeof(cwd_path), pid, "cwd");
+
+ if (readlink(cwd_path, cwd, sizeof(cwd)) == -1) {
+#ifdef SHOW_READLINK_ERRORS
+ error("readlink %s: %s", cwd_path, strerror(errno));
+#endif /* SHOW_READLINK_ERRORS */
+ return NULL;
+ }
+
+ return cwd;
+}
+
+// Get the environment of a PID from /proc/PID/environ
+char *proc_environ(pid_t pid) {
+ int fd;
+ int bytes;
+ char environ_path[PATH_MAX];
+ static char environ[ARG_MAX];
+
+ build_proc_path(environ_path, sizeof(environ_path), pid, "environ");
+
+ fd = open(environ_path, O_RDONLY);
+ if (fd == -1) {
+ return NULL;
+ }
+
+ bytes = read(fd, environ, sizeof(environ));
+ close(fd);
+
+ return (char *)base64_encode((const unsigned char *)environ, bytes, NULL);
+}
+
+// Get the path of a PID's executable file from /proc/PID/exe
+char *proc_get_exe_path(pid_t pid) {
+ char exe_path[PATH_MAX];
+ static char real_path[PATH_MAX];
+
+ memset(real_path, '\0', sizeof(real_path));
+ build_proc_path(exe_path, sizeof(exe_path), pid, "exe");
+
+ if (readlink(exe_path, real_path, sizeof(real_path)) == -1) {
+#ifdef SHOW_READLINK_ERRORS
+ error("readlink %s: %s", exe_path, strerror(errno));
+#endif /* SHOW_READLINK_ERRORS */
+ return NULL;
+ }
+
+ return real_path;
+}
+
+// Get the command line of a PID from /proc/PID/cmdline
+char *proc_get_cmdline(pid_t pid) {
+ int fd;
+ char cmdline_path[PATH_MAX] = {0};
+ static char buf[ARG_MAX];
+ int bytes;
+
+ build_proc_path(cmdline_path, sizeof(cmdline_path), pid, "cmdline");
+
+ /* read *argv[] from cmdline_path */
+ fd = open(cmdline_path, O_RDONLY);
+ if (fd == -1) {
+ return NULL;
+ }
+
+ bytes = read(fd, buf, sizeof(buf));
+ close(fd);
+
+ /* *argv[] is null delimited, replace nulls with spaces */
+ for (int i = 0; i < bytes - 1; i++) {
+ if (buf[i] == '\0') {
+ buf[i] = ' ';
+ }
+ }
+
+ return buf;
+}
+
+// Extract useful information from /proc/PID/status
+struct proc_status proc_get_status(pid_t pid) {
+ FILE *fp;
+ struct proc_status result = {0};
+ char proc_status[PATH_MAX];
+ char buf[1024];
+
+ build_proc_path(proc_status, sizeof(proc_status), pid, "status");
+
+ fp = fopen(proc_status, "r");
+ if (fp == NULL) {
+#ifdef SHOW_GET_PROC_STATUS_ERRORS
+ error("error opening %s: %s", proc_status, strerror(errno));
+#endif
+ result.pid = -1;
+ return result;
+ }
+
+ result.pid = pid;
+
+ while(fgets(buf, sizeof(buf), fp) != NULL) {
+ /*
+ * use a switch on the first letter so we aren't parsing every
+ * line like a villager. This is ugly af but fast.
+ */
+ switch (buf[0]) {
+ case 'C':
+ if (startswith(buf, "CapInh:")) {
+ sscanf(buf, "CapInh:\t%lx", &result.cap_inh);
+ } else if (startswith(buf, "CapPrm:")) {
+ sscanf(buf, "CapPrm:\t%lx", &result.cap_prm);
+ } else if (startswith(buf, "CapEff:")) {
+ sscanf(buf, "CapEff:\t%lx", &result.cap_eff);
+ } else if (startswith(buf, "CapBnd:")) {
+ sscanf(buf, "CapBnd:\t%lx", &result.cap_bnd);
+ } else if (startswith(buf, "CapAmb:")) {
+ sscanf(buf, "CapAmb:\t%lx", &result.cap_amb);
+ }
+ break;
+
+ case 'F':
+ if (startswith(buf, "FDSize:")) {
+ sscanf(buf, "FDSize:\t%u", &result.fdsize);
+ }
+ break;
+
+ case 'G':
+ if (startswith(buf, "Gid:")) {
+ sscanf(buf, "Gid:\t%d\t%d\t%d\t%d\n",
+ &result.gid, &result.egid,
+ &result.ssgid, &result.fsgid);
+ }
+ break;
+
+ case 'N':
+ if (startswith(buf, "Name:")) {
+ sscanf(buf, "Name:\t%16s\n", result.name);
+ } else if (startswith(buf, "NoNewPrivs:")) {
+ sscanf(buf, "NoNewPrivs:\t%d", &result.no_new_privs);
+ }
+ break;
+
+ case 'P':
+ if (startswith(buf, "PPid:")) {
+ sscanf(buf, "PPid:\t%d", &result.ppid);
+ }
+ break;
+
+ case 'S':
+ if (startswith(buf, "State:")) {
+ sscanf(buf, "State:\t%c", &result.state);
+ } else if (startswith(buf, "Seccomp:")) {
+ sscanf(buf, "Seccomp:\t%d", &result.seccomp);
+ }
+ break;
+
+ case 'T':
+ if (startswith(buf, "Tgid:")) {
+ sscanf(buf, "Tgid:\t%d", &result.tgid);
+ } else if (startswith(buf, "Threads:")) {
+ sscanf(buf, "Threads:\t%u", &result.threads);
+ } else if (startswith(buf, "TracerPid:")) {
+ sscanf(buf, "TracerPid:\t%d", &result.tracer_pid);
+ }
+ break;
+
+ case 'U':
+ if (startswith(buf, "Uid:")) {
+ sscanf(buf, "Uid:\t%d\t%d\t%d\t%d\n",
+ &result.uid, &result.euid,
+ &result.ssuid, &result.fsuid);
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ fclose(fp);
+ return result;
+}
+
+// Get useful information from /proc/PID/stat
+struct proc_stat proc_parse_stat(pid_t pid) {
+ struct proc_stat ps = {0};
+ char path[PATH_MAX];
+ FILE *fp;
+ char buf[4096];
+
+ build_proc_path(path, sizeof(path), pid, "stat");
+
+ fp = fopen(path, "r");
+ if (!fp) {
+ return ps;
+ }
+
+ /*
+ * pid (comm) state ppid pgrp session tty_nr tpgid flags minflt
+ * cminflt majflt cmajflt utime stime cutime cstime priority nice
+ * num_threads itrealvalue starttime vsize rss ...
+ *
+ * comm can have spaces. skip to ')' then read values into
+ * corresponding members of proc_stat structure.
+ */
+ if (fgets(buf, sizeof(buf), fp)) {
+ char *ptr = strchr(buf, ')');
+ if (ptr) {
+ ptr += 2; // skip ") "
+
+ // read state separately to shut up gcc
+ sscanf(ptr, "%c", &ps.state);
+ ptr++;
+ while (*ptr == ' ') ptr++;
+
+ // TODO clean this up.
+ sscanf(ptr,
+ "%*d %*d %*d %d " // skip ppid, pgrp, session. capture tty_nr
+ "%*d %*u %*u %*u %*u %*u %*u %*u %*u "
+ "%lu %lu " // utime stime
+ "%*ld %*ld "
+ "%ld %ld " // priority nice
+ "%*d %*ld "
+ "%lu " // starttime
+ "%lu " // vsize
+ "%ld", // rss
+ &ps.tty_nr,
+ &ps.utime,
+ &ps.stime,
+ &ps.priority,
+ &ps.nice,
+ &ps.starttime,
+ &ps.vsize,
+ &ps.rss);
+ }
+ }
+
+ fclose(fp);
+ return ps;
+}
diff --git a/src/proc_connector.c b/src/proc_connector.c
new file mode 100644
index 0000000..f771ecf
--- /dev/null
+++ b/src/proc_connector.c
@@ -0,0 +1,758 @@
+
+#include <string.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <limits.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#include <linux/cn_proc.h>
+#include <linux/netlink.h>
+#include <linux/connector.h>
+
+#include "net.h"
+#include "json.h"
+#include "error.h"
+#include "output.h"
+#include "md5.h"
+#include "sha256.h"
+#include "base64.h"
+#include "proc.h"
+#include "agent_context.h"
+#include "proc_ledger.h"
+#include "time_common.h"
+#include "string_common.h"
+#include "proc_connector.h"
+
+extern unsigned long maxsize;
+
+
+static void handle_proc_connector_message(struct cn_msg *cn_message, agent_context_t *ctx) {
+ struct proc_event *event;
+ json_t json = {0};
+ pid_t process;
+ struct proc_ledger_entry *entry;
+
+ event = (struct proc_event *)cn_message->data;
+
+ switch (event->what) {
+ case PROC_EVENT_NONE:
+ break;
+
+ case PROC_EVENT_FORK:
+ pid_t child = event->event_data.fork.child_pid;
+
+ entry = proc_ledger_entry_create(child, ctx);
+ if (entry) {
+ proc_ledger_add(ctx->proc_ledger, entry);
+ }
+ json = handle_PROC_EVENT_FORK(event, ctx);
+ break;
+
+ case PROC_EVENT_EXEC:
+ process = event->event_data.exec.process_pid;
+ entry = proc_ledger_entry_create(process, ctx);
+
+ if (entry) {
+ proc_ledger_replace(ctx->proc_ledger, entry);
+
+ json = handle_PROC_EVENT_EXEC(event, ctx);
+
+ json_t environment = handle_PROC_EVENT_EXEC_environment(event, ctx);
+ if (environment.data) {
+ output(json_get(&environment));
+ json_free(&environment);
+ }
+ }
+ break;
+
+ case PROC_EVENT_EXIT:
+ process = event->event_data.exit.process_pid;
+ json = handle_PROC_EVENT_EXIT(event, ctx);
+ proc_ledger_remove(ctx->proc_ledger, process);
+ break;
+
+ case PROC_EVENT_UID:
+ // update ledger with new UIDs
+ entry = proc_ledger_find(ctx->proc_ledger, event->event_data.id.process_pid);
+ if (entry) {
+ int old_ruid = entry->uid;
+ int old_euid = entry->euid;
+ entry->uid = event->event_data.id.r.ruid;
+ entry->euid = event->event_data.id.e.euid;
+ json = handle_PROC_EVENT_UID(event, ctx, old_ruid, old_euid);
+ } else {
+ json = handle_PROC_EVENT_UID(event, ctx, -1, -1);
+ }
+ break;
+
+ case PROC_EVENT_GID:
+ // update ledger with new GIDs
+ entry = proc_ledger_find(ctx->proc_ledger, event->event_data.id.process_pid);
+ if (entry) {
+ int old_rgid = entry->gid;
+ int old_egid = entry->egid;
+ entry->gid = event->event_data.id.r.rgid;
+ entry->egid = event->event_data.id.e.egid;
+ json = handle_PROC_EVENT_GID(event, ctx, old_rgid, old_egid);
+ } else {
+ json = handle_PROC_EVENT_GID(event, ctx, -1, -1);
+ }
+ break;
+
+ case PROC_EVENT_PTRACE:
+ json = handle_PROC_EVENT_PTRACE(event, ctx);
+ break;
+
+ case PROC_EVENT_SID:
+ json = handle_PROC_EVENT_SID(event, ctx);
+ break;
+
+ case PROC_EVENT_COMM:
+ // update ledger with new comm
+ char *old_comm = NULL;
+ entry = proc_ledger_find(ctx->proc_ledger, event->event_data.comm.process_pid);
+ if (entry) {
+ old_comm = strdup(entry->comm);
+ strncpy(entry->comm, event->event_data.comm.comm, sizeof(entry->comm) - 1);
+ entry->comm[sizeof(entry->comm) - 1] = '\0';
+ }
+
+ json = handle_PROC_EVENT_COMM(event, ctx, old_comm);
+ if (old_comm) {
+ free(old_comm);
+ }
+
+ break;
+
+ case PROC_EVENT_COREDUMP:
+ json = handle_PROC_EVENT_COREDUMP(event, ctx);
+ break;
+
+ default:
+ json = handle_PROC_EVENT_UNKNOWN(event, ctx);
+ break;
+ }
+
+ /* If we have data to output, deal with it. */
+ if (json.data) {
+ output(json_get(&json));
+ json_free(&json);
+ }
+}
+
+sock_t setup_proc_connector(void) {
+ int err;
+ sock_t proc_connector;
+ struct sockaddr_nl nl_userland;
+ enum proc_cn_mcast_op *mcop_msg;
+ struct cn_msg *cn_message;
+ char buf[1024] = {0};
+ struct nlmsghdr *nl_header;
+
+ proc_connector = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR);
+ if (proc_connector == -1) {
+ error("error creating proc_connector socket: %s", strerror(errno));
+ return -1;
+ }
+
+ nl_userland.nl_family = AF_NETLINK;
+ nl_userland.nl_groups = CN_IDX_PROC;
+ nl_userland.nl_pid = getpid();
+
+ err = bind(proc_connector, (struct sockaddr *)&nl_userland, sizeof(nl_userland));
+ if (err == -1) {
+ error("error binding proc_connector socket: %s", strerror(errno));
+ return -1;
+ }
+
+ nl_header = (struct nlmsghdr *)buf;
+ cn_message = (struct cn_msg *)NLMSG_DATA(nl_header);
+ mcop_msg = (enum proc_cn_mcast_op *)&cn_message->data[0];
+ *mcop_msg = PROC_CN_MCAST_LISTEN;
+
+ cn_message->id.idx = CN_IDX_PROC;
+ cn_message->id.val = CN_VAL_PROC;
+ cn_message->seq = 0;
+ cn_message->ack = 0;
+ cn_message->len = sizeof(enum proc_cn_mcast_op);
+
+ nl_header->nlmsg_len = NLMSG_LENGTH(sizeof(struct cn_msg) + sizeof(enum proc_cn_mcast_op));
+ nl_header->nlmsg_type = NLMSG_DONE;
+ nl_header->nlmsg_flags = 0;
+ nl_header->nlmsg_seq = 0;
+ nl_header->nlmsg_pid = getpid();
+
+ err = send(proc_connector, nl_header, nl_header->nlmsg_len, 0);
+ if (err != nl_header->nlmsg_len) {
+ error("send: %s", strerror(errno));
+ close(proc_connector);
+ return -1;
+ }
+
+ return proc_connector;
+}
+
+void select_proc_connector(sock_t proc_connector, agent_context_t *ctx) {
+ int recv_length;
+ socklen_t nl_kernel_len;
+ struct nlmsghdr *nlh;
+ struct cn_msg *cn_message;
+ struct sockaddr_nl nl_kernel;
+ char buf[1024] = {0};
+
+ nl_kernel_len = sizeof(nl_kernel);
+
+ recv_length = recvfrom(proc_connector,
+ buf,
+ sizeof(buf),
+ 0,
+ (struct sockaddr *)&nl_kernel,
+ &nl_kernel_len);
+ nlh = (struct nlmsghdr *)buf;
+
+ if ((recv_length < 1) || (nl_kernel.nl_pid != 0)) {
+ return;
+ }
+
+ while (NLMSG_OK(nlh, recv_length)) {
+ cn_message = NLMSG_DATA(nlh);
+
+ if ((nlh->nlmsg_type == NLMSG_NOOP) ||
+ (nlh->nlmsg_type == NLMSG_ERROR)) {
+ continue;
+ }
+
+ if (nlh->nlmsg_type == NLMSG_OVERRUN) {
+ break;
+ }
+
+ handle_proc_connector_message(cn_message, ctx);
+
+ if (nlh->nlmsg_type == NLMSG_DONE) {
+ break;
+ } else {
+ nlh = NLMSG_NEXT(nlh, recv_length);
+ }
+ }
+}
+
+static inline const char *safe_exe_path(struct proc_ledger_entry *entry) {
+ if (entry && entry->exe[0]) {
+ return entry->exe;
+ }
+ return "UNKNOWN";
+}
+
+/* handle_PROC_EVENT_FORK() - Handle PROC_EVENT_FORK events.
+ *
+ * The following are available for this event:
+ * - pid_t parent_pid
+ * - pid_t parent_tgid
+ * - pid_t child_pid
+ * - pid_t child_tgid
+ *
+ * Args:
+ * event - proc_event structure (linux/cn_proc.h)
+ * ctx - agent context containing process/hash hedgers
+ *
+ * Returns:
+ * json_t containing serialized JSON object describing this event.
+ *
+ * TODO: Deal with parent_pid and child_pid being the same somehow (see below)
+ */
+json_t handle_PROC_EVENT_FORK(struct proc_event *event, agent_context_t *ctx) {
+ pid_t parent_pid = event->event_data.fork.parent_pid;
+ pid_t parent_tgid = event->event_data.fork.parent_tgid;
+ pid_t child_pid = event->event_data.fork.child_pid;
+ pid_t child_tgid = event->event_data.fork.child_tgid;
+
+ struct proc_ledger_entry *parent_entry = proc_ledger_find(ctx->proc_ledger, parent_pid);
+ if (!parent_entry) {
+ json_t empty = {0};
+ return empty;
+ }
+
+ struct proc_ledger_entry *child_entry = proc_ledger_entry_create(child_pid, ctx);
+ if (child_entry && parent_entry) {
+ memcpy(child_entry, parent_entry, sizeof(struct proc_ledger_entry));
+ child_entry->pid = child_pid;
+ child_entry->tgid = child_tgid;
+ snprintf(child_entry->comm, sizeof(child_entry->comm), "%s", parent_entry->comm);
+ proc_ledger_add(ctx->proc_ledger, child_entry);
+ }
+
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "fork");
+ json_add_int(&buf, "parent_pid", parent_pid);
+ json_add_int(&buf, "parent_tgid", parent_tgid);
+ json_add_int(&buf, "child_pid", child_pid);
+ json_add_int(&buf, "child_tgid", child_tgid);
+ json_add_string(&buf, "parent_exepath", parent_entry->exe);
+ json_add_string(&buf, "parent_comm", parent_entry->comm);
+ json_add_string(&buf, "cmdline", parent_entry->cmdline);
+ bool deleted = endswith(parent_entry->exe, " (deleted)");
+ json_add_bool(&buf, "deleted", deleted);
+ json_add_int(&buf, "uid", parent_entry->uid);
+ json_add_int(&buf, "euid", parent_entry->euid);
+ json_add_int(&buf, "gid", parent_entry->gid);
+ json_add_int(&buf, "egid", parent_entry->egid);
+
+ // TODO is parent_entry correct, or child_entry?
+ struct hash_entry *hentry = hash_ledger_find(ctx->hash_ledger, parent_entry->exe);
+ if (hentry) {
+ if (hentry->md5[0]) {
+ json_add_string(&buf, "md5", hentry->md5);
+ }
+ if (hentry->sha256[0]) {
+ json_add_string(&buf, "sha256", hentry->sha256);
+ }
+
+ json_add_double(&buf, "entropy", hentry->entropy);
+ }
+
+ json_end_object(&buf);
+
+ return buf;
+}
+
+
+/* handle_PROC_EVENT_EXEC() - Handle PROC_EVENT_EXEC events.
+ *
+ * The following are available for this event:
+ * - pid_t process_pid
+ * - pid_t process_tgid
+ *
+ * Args:
+ * event - proc_event structure (linux/cn_proc.h)
+ * ctx - agent context containing process/hash hedgers
+ *
+ * Returns:
+ * json_t containing serialized JSON object describing this event.
+ */
+json_t handle_PROC_EVENT_EXEC(struct proc_event *event, agent_context_t *ctx) {
+ struct proc_ledger_entry *entry = proc_ledger_find(ctx->proc_ledger,
+ event->event_data.exec.process_pid);
+ if (!entry) {
+ json_t empty = {0};
+ return empty;
+ }
+
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "exec");
+ json_add_int(&buf, "pid", entry->pid);
+ json_add_int(&buf, "tgid", entry->tgid);
+ json_add_int(&buf, "uid", entry->uid);
+ json_add_int(&buf, "euid", entry->euid);
+ json_add_int(&buf, "gid", entry->gid);
+ json_add_int(&buf, "egid", entry->egid);
+ json_add_string(&buf, "exepath", entry->exe);
+ json_add_string(&buf, "cmdline", entry->cmdline);
+ json_add_string_or_null(&buf, "cwd", entry->cwd);
+
+ struct hash_entry *hentry = hash_ledger_find(ctx->hash_ledger, entry->exe);
+ if (hentry) {
+ json_add_string_or_null(&buf, "md5", hentry->md5);
+ json_add_string_or_null(&buf, "sha256", hentry->sha256);
+ json_add_double(&buf, "entropy", hentry->entropy);
+ }
+
+ json_end_object(&buf);
+
+ return buf;
+}
+
+/* handle_PROC_EVENT_EXEC_environment() - Grab the environment from
+ * PROC_EVENT_EXEC events.
+ *
+ * This is called after handle_PROC_EVENT_EXEC() to attempt to grab
+ * the environment from /proc/X/environ for a newly-executed
+ * process. This information may be useful from a forensics
+ * standpoint, and possibly to detect malicious activity.
+ *
+ * Since an environment can contain anything, it is base64 encoded so
+ * it doesn't break our JSON formatting.
+ *
+ * Args:
+ * event - proc_event structure (linux/cn_proc.h)
+ * ctx - agent context containing process/hash hedgers
+ *
+ * Returns:
+ * json_t containing serialized JSON object describing this event.
+ */
+json_t handle_PROC_EVENT_EXEC_environment(struct proc_event *event, agent_context_t *ctx) {
+ struct proc_ledger_entry *entry = proc_ledger_find(ctx->proc_ledger,
+ event->event_data.exec.process_pid);
+ if (!entry) {
+ json_t empty = {0};
+ return empty;
+ }
+
+ pid_t pid = event->event_data.exec.process_pid;
+
+ char *environment = proc_environ(pid);
+ if (!environment) {
+ json_t empty = {0};
+ return empty;
+ }
+
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "environment");
+ json_add_int(&buf, "pid", entry->pid);
+ json_add_int(&buf, "uid", entry->uid);
+ json_add_int(&buf, "euid", entry->euid);
+ json_add_int(&buf, "gid", entry->gid);
+ json_add_int(&buf, "egid", entry->egid);
+ json_add_string(&buf, "exepath", entry->exe);
+ json_add_string(&buf, "environment", environment);
+
+ json_end_object(&buf);
+
+ free(environment);
+ return buf;
+}
+
+/* handle_PROC_EVENT_EXIT() - Handle PROC_EVENT_EXIT events.
+ *
+ * The following are available for this event:
+ * - pid_t process_pid
+ * - pid_t process_tgid
+ * - u32 exit_code
+ * - u32 exit_signal
+ *
+ * Args:
+ * event - proc_event structure (linux/cn_proc.h)
+ * ctx - agent context containing process/hash hedgers
+ *
+ * Returns:
+ * json_t containing serialized JSON object describing this event.
+ */
+json_t handle_PROC_EVENT_EXIT(struct proc_event *event, agent_context_t *ctx) {
+ struct proc_ledger_entry *entry = proc_ledger_find(ctx->proc_ledger,
+ event->event_data.exit.process_pid);
+ if (!entry) {
+ json_t empty = {0};
+ return empty;
+ }
+
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "exit");
+ json_add_int(&buf, "pid", event->event_data.exit.process_pid);
+ json_add_int(&buf, "tgid", event->event_data.exit.process_tgid);
+ json_add_int(&buf, "exitcode", event->event_data.exit.exit_code);
+ json_add_int(&buf, "signal", event->event_data.exit.exit_signal);
+ json_add_string(&buf, "exepath", safe_exe_path(entry));
+
+ json_end_object(&buf);
+
+ return buf;
+}
+
+/* handle_PROC_EVENT_UID() - Handle PROC_EVENT_UID events.
+ * handle_PROC_EVENT_GID() - Handle PROC_EVENT_GID events.
+ *
+ * The following are availabie for this event:
+ * - pid_t process_ppid
+ * - pid_t process_tgid
+ * - union { u32 ruid; u32 rgid } r
+ * - union { u32 euid; u32 egid } e
+ *
+ * Args:
+ * event - proc_event structure (linux/cn_proc.h)
+ * ctx - agent context containing process/hash hedgers
+ * old_ruid - old real uid
+ * old_euid - old effective uid
+ *
+ * Returns:
+ * json_t containing serialized JSON object describing this event
+ *
+ * Note:
+ * The handle_PROC_EVENT_UID and handle_PROC_EVENT_GID functions
+ * are nearly identical. If no record of the old ruid/euid of the
+ * process exists, -1 will be displayed
+ */
+
+json_t handle_PROC_EVENT_UID(struct proc_event *event, agent_context_t *ctx, int old_ruid, int old_euid) {
+ struct proc_ledger_entry *entry = proc_ledger_find(ctx->proc_ledger,
+ event->event_data.id.process_pid);
+ if (!entry) {
+ json_t empty = {0};
+ return empty;
+ }
+
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "uid");
+ json_add_int(&buf, "pid", entry->pid);
+ json_add_int(&buf, "tgid", entry->tgid);
+ json_add_int(&buf, "old_ruid", old_ruid);
+ json_add_int(&buf, "new_ruid", event->event_data.id.r.ruid);
+ json_add_int(&buf, "old_euid", old_euid);
+ json_add_int(&buf, "new_euid", event->event_data.id.e.euid);
+ json_add_string(&buf, "exepath", entry->exe);
+
+ json_end_object(&buf);
+
+ return buf;
+}
+
+json_t handle_PROC_EVENT_GID(struct proc_event *event, agent_context_t *ctx, int old_rgid, int old_egid) {
+ struct proc_ledger_entry *entry = proc_ledger_find(ctx->proc_ledger,
+ event->event_data.id.process_pid);
+ if (!entry) {
+ json_t empty = {0};
+ return empty;
+ }
+
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "gid");
+ json_add_int(&buf, "pid", entry->pid);
+ json_add_int(&buf, "tgid", entry->tgid);
+ json_add_int(&buf, "old_rgid", old_rgid);
+ json_add_int(&buf, "new_rgid", event->event_data.id.r.rgid);
+ json_add_int(&buf, "old_egid", old_egid);
+ json_add_int(&buf, "new_egid", event->event_data.id.e.egid);
+ json_add_string(&buf, "exepath", entry->exe);
+
+ json_end_object(&buf);
+
+ return buf;
+}
+
+/* handle_PROC_EVENT_PTRACE() - Handle PROC_EVENT_PTRACE events.
+ *
+ * The following are availabie for this event:
+ * - pid_t process_ppid
+ * - pid_t process_tgid
+ * - pid_t tracer_pid
+ * - pid_t tracer_tgid
+ *
+ * Args:
+ * event - proc_event structure (linux/cn_proc.h)
+ * ctx - agent context containing process/hash hedgers
+ *
+ * Returns:
+ * json_t containing serialized JSON object describing this event
+ *
+ * Note:
+ * This appears to be called AFTER ptrace() calls, so obtaining the
+ * path, hash, etc of the tracer process may fail sometimes.
+ */
+json_t handle_PROC_EVENT_PTRACE(struct proc_event *event, agent_context_t *ctx) {
+ pid_t target_pid = event->event_data.ptrace.process_pid;
+ pid_t tracer_pid = event->event_data.ptrace.tracer_pid;
+
+ struct proc_ledger_entry *target_entry = proc_ledger_find(ctx->proc_ledger, target_pid);
+ struct proc_ledger_entry *tracer_entry = proc_ledger_find(ctx->proc_ledger, tracer_pid);
+
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "ptrace");
+ json_add_int(&buf, "pid", target_pid);
+ json_add_int(&buf, "tgid", event->event_data.ptrace.process_tgid);
+ json_add_int(&buf, "tracer_pid", tracer_pid);
+ json_add_int(&buf, "tracer_tgid", event->event_data.ptrace.tracer_tgid);
+
+ if (tracer_entry && tracer_entry->exe[0]) {
+ json_add_string(&buf, "tracer_exepath", tracer_entry->exe);
+ }
+
+ if (target_entry && target_entry->exe[0]) {
+ json_add_string(&buf, "target_exepath", target_entry->exe);
+ }
+
+ json_end_object(&buf);
+
+ return buf;
+}
+
+/* handle_PROC_EVENT_SID() - Handle PROC_EVENT_SID events.
+ *
+ * The following are availabie for this event:
+ * - pid_t process_ppid
+ * - pid_t process_tgid
+ *
+ * Args:
+ * event - proc_event structure (linux/cn_proc.h)
+ * ctx - agent context containing process/hash hedgers
+ *
+ * Returns:
+ * json_t containing serialized JSON object describing this event
+ *
+ * Note:
+ * This event occurs when the process calls setsid(). This can be
+ * used to detect when processes daemonize/fork/detach from a terminal.
+ *
+ * TODO: add contextual process ancestry to the output
+ */
+json_t handle_PROC_EVENT_SID(struct proc_event *event, agent_context_t *ctx) {
+ struct proc_ledger_entry *entry = proc_ledger_find(ctx->proc_ledger,
+ event->event_data.sid.process_pid);
+
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "sid");
+ json_add_int(&buf, "pid", event->event_data.sid.process_pid);
+ json_add_int(&buf, "tgid", event->event_data.sid.process_tgid);
+
+ if (entry && entry->exe[0]) {
+ json_add_string(&buf, "exepath", entry->exe);
+ }
+
+ json_end_object(&buf);
+
+ return buf;
+}
+
+/* handle_PROC_EVENT_COMM() - Handle PROC_EVENT_COMM events.
+ *
+ * The following are availabie for this event:
+ * - pid_t process_ppid
+ * - pid_t process_tgid
+ * - char comm[16]
+ *
+ * Args:
+ * event - proc_event structure (linux/cn_proc.h)
+ * ctx - agent context containing process/hash hedgers
+ * old_comm - string containing old value of comm
+ *
+ * Returns:
+ * json_t containing serialized JSON object describing this event
+ *
+ * Note:
+ * This may be triggered for prctl(PR_SET_NAME, ...) and be
+ * suitable for finding processes attempting to obfuscate
+ * themselves (ex: malware changing its name to 'sshd')
+ *
+ * Apparently this can be triggered by threads setting their names
+ * as well.
+ *
+ * TODO: show old comm if possible
+ * TODO: alert for "sketchy" names like sshd, cron, http, [kthread], ...
+ */
+json_t handle_PROC_EVENT_COMM(struct proc_event *event, agent_context_t *ctx, const char *old_comm) {
+ struct proc_ledger_entry *entry = proc_ledger_find(ctx->proc_ledger,
+ event->event_data.comm.process_pid);
+
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "comm");
+ json_add_int(&buf, "pid", event->event_data.comm.process_pid);
+ json_add_int(&buf, "tgid", event->event_data.comm.process_tgid);
+
+ json_add_string_or_null(&buf, "exepath", entry->exe);
+
+ json_add_string_or_null(&buf, "old_comm", old_comm);
+ json_add_string(&buf, "new_comm", event->event_data.comm.comm);
+
+ json_end_object(&buf);
+
+ return buf;
+}
+
+/* handle_PROC_EVENT_COREDUMP() - Handle PROC_EVENT_COREDUMP events.
+ *
+ * The following are availabie for this event:
+ * - pid_t process_ppid
+ * - pid_t process_tgid
+ * - pid_t parent_pid
+ * - pid_t parent_tgid
+ *
+ * Args:
+ * event - proc_event structure (linux/cn_proc.h)
+ * ctx - agent context containing process/hash hedgers
+ *
+ * Returns:
+ * json_t containing serialized JSON object describing this event
+ */
+json_t handle_PROC_EVENT_COREDUMP(struct proc_event *event, agent_context_t *ctx) {
+ struct proc_ledger_entry *entry = proc_ledger_find(ctx->proc_ledger,
+ event->event_data.exit.process_pid);
+
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "coredump");
+ json_add_int(&buf, "pid", event->event_data.coredump.process_pid);
+ json_add_int(&buf, "tgid", event->event_data.coredump.process_tgid);
+ json_add_int(&buf, "parent_pid", event->event_data.coredump.parent_pid);
+ json_add_int(&buf, "parent_tgid", event->event_data.coredump.parent_tgid);
+ json_add_string(&buf, "exefile", safe_exe_path(entry));
+
+ json_end_object(&buf);
+
+ return buf;
+}
+
+/* handle_PROC_EVENT_UNKNOWN() - Handle unknown events.
+ *
+ * This is a generic placeholder for unknown/unhandled events.
+ *
+ * Args:
+ * event - proc_event structure (linux/cn_proc.h)
+ * ctx - agent context
+ *
+ * Returns:
+ * char * containing serialized JSON object describing the event
+ */
+json_t handle_PROC_EVENT_UNKNOWN(struct proc_event *event, agent_context_t *ctx) {
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "unknown");
+ json_add_int(&buf, "event_code", event->what);
+
+ json_end_object(&buf);
+
+ return buf;
+}
diff --git a/src/proc_ledger.c b/src/proc_ledger.c
new file mode 100644
index 0000000..9faf2f6
--- /dev/null
+++ b/src/proc_ledger.c
@@ -0,0 +1,344 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <dirent.h>
+//#include <pthread.h>
+
+#include "agent_context.h"
+#include "proc_ledger.h"
+#include "hash_ledger.h"
+#include "output.h"
+#include "error.h"
+#include "json.h"
+#include "proc.h"
+#include "md5.h"
+#include "sha256.h"
+#include "time_common.h"
+
+// TODO these should probably be moved into agent context
+extern unsigned long boot_time;
+extern unsigned long maxsize;
+extern long ticks;
+
+
+bool proc_has_tty(pid_t pid) {
+ char path[PATH_MAX];
+
+ snprintf(path, sizeof(path), "/proc/%d/fd/0", pid);
+
+ struct stat sb;
+ if (stat(path, &sb) == -1) {
+ return false;
+ }
+
+ return S_ISCHR(sb.st_mode);
+}
+
+struct proc_ledger *proc_ledger_init(size_t num_buckets) {
+ struct proc_ledger *ledger = calloc(1, sizeof(struct proc_ledger));
+ if (!ledger) {
+ error_fatal("ledger calloc: %s", strerror(errno));
+ }
+
+ ledger->buckets = calloc(num_buckets, sizeof(struct proc_ledger_entry *));
+ if (!ledger->buckets) {
+ free(ledger);
+ error_fatal("proc ledger buckets calloc: %s", strerror(errno));
+ }
+
+ ledger->num_buckets = num_buckets;
+
+ //if (pthread_mutex_init(&ledger->lock, NULL) != 0) {
+ // error("pthread_mutex_init: %s", strerror(errno));
+ // free(ledger->buckets);
+ // free(ledger);
+ // exit(EXIT_FAILURE);
+ //}
+
+ return ledger;
+}
+
+void proc_ledger_destroy(struct proc_ledger *ledger) {
+ if (!ledger) {
+ return;
+ }
+
+ for (size_t i = 0; i < ledger->num_buckets; i++) {
+ struct proc_ledger_entry *entry = ledger->buckets[i];
+ while(entry) {
+ struct proc_ledger_entry *next = entry->next;
+ free(entry);
+ entry = next;
+ }
+ }
+
+ free(ledger->buckets);
+ //pthread_mutex_destroy(&ledger->lock);
+ free(ledger);
+}
+
+size_t proc_ledger_bucket(struct proc_ledger *ledger, pid_t pid) {
+ return ((size_t)pid) % ledger->num_buckets;
+}
+
+bool proc_ledger_add(struct proc_ledger *ledger, struct proc_ledger_entry *new_entry) {
+ if (!ledger || !new_entry) {
+ return false;
+ }
+
+ //pthread_mutex_lock(&ledger->lock);
+
+ size_t idx = proc_ledger_bucket(ledger, new_entry->pid);
+ new_entry->next = ledger->buckets[idx];
+ ledger->buckets[idx] = new_entry;
+
+ //pthread_mutex_unlock(&ledger->lock);
+
+ return true;
+}
+
+bool proc_ledger_remove(struct proc_ledger *ledger, pid_t pid) {
+ if (!ledger) {
+ return false;
+ }
+
+ //pthread_mutex_lock(&ledger->lock);
+
+ size_t idx = proc_ledger_bucket(ledger, pid);
+ struct proc_ledger_entry *prev = NULL;
+ struct proc_ledger_entry *entry = ledger->buckets[idx];
+
+ while (entry) {
+ if (entry->pid == pid) {
+ if (prev) {
+ prev->next = entry->next;
+ } else {
+ ledger->buckets[idx] = entry->next;
+ }
+
+ free(entry);
+ //pthread_mutex_unlock(&ledger->lock);
+ return true;
+ }
+
+ prev = entry;
+ entry = entry->next;
+ }
+
+ //pthread_mutex_unlock(&ledger->lock);
+ return false; // not found
+}
+
+bool proc_ledger_replace(struct proc_ledger *ledger, struct proc_ledger_entry *new_entry) {
+ if (!ledger || !new_entry) {
+ return false;
+ }
+
+ size_t idx = proc_ledger_bucket(ledger, new_entry->pid);
+ struct proc_ledger_entry **cur = &ledger->buckets[idx];
+
+ while (*cur) {
+ if ((*cur)->pid == new_entry->pid) {
+ new_entry->next = (*cur)->next;
+ free(*cur);
+ *cur = new_entry;
+ return true;
+ }
+
+ cur = &(*cur)->next;
+ }
+
+ // replace if found, otherwise add new entry to bucket
+ new_entry->next = ledger->buckets[idx];
+ ledger->buckets[idx] = new_entry;
+
+ return true;
+}
+
+struct proc_ledger_entry *proc_ledger_find(struct proc_ledger *ledger, pid_t pid) {
+ size_t idx = proc_ledger_bucket(ledger, pid);
+ struct proc_ledger_entry *entry = ledger->buckets[idx];
+
+ if (!entry) {
+ return NULL;
+ }
+
+ if (entry->pid == pid) {
+ return entry;
+ }
+
+ // search list if necessary
+ for (entry = entry->next; entry; entry = entry->next) {
+ if (entry->pid == pid) {
+ return entry;
+ }
+ }
+
+ return NULL; // not found
+}
+
+struct proc_ledger_entry *proc_ledger_entry_create(pid_t pid, agent_context_t *ctx) {
+ struct proc_status ps = proc_get_status(pid);
+ if (ps.pid == -1) { // couldn't fetch status.
+ return NULL;
+ }
+
+ struct proc_stat pstat = proc_parse_stat(pid); // TODO rename this?
+
+ struct proc_ledger_entry *entry = calloc(1, sizeof(struct proc_ledger_entry));
+ if (!entry) {
+ return NULL;
+ }
+
+ entry->pid = ps.pid;
+ entry->tgid = ps.tgid;
+ entry->ppid = ps.ppid;
+ entry->uid = ps.uid;
+ entry->euid = ps.euid;
+ entry->gid = ps.gid;
+ entry->egid = ps.egid;
+ entry->start_time = boot_time + (pstat.starttime / ticks);
+
+ char *exe_path = proc_get_exe_path(pid);
+ if (exe_path) {
+ strncpy(entry->exe, exe_path, sizeof(entry->exe) - 1);
+ entry->exe[sizeof(entry->exe) - 1] = '\0';
+
+ struct stat sb;
+ if (stat(exe_path, &sb) == 0 && sb.st_size < maxsize) {
+ hash_ledger_add_or_update(ctx->hash_ledger, exe_path, &sb);
+ }
+ }
+
+ char *cmdline = proc_get_cmdline(pid);
+ if (cmdline) {
+ strncpy(entry->cmdline, cmdline, sizeof(entry->cmdline) - 1);
+ entry->cmdline[sizeof(entry->cmdline) - 1] = '\0';
+ }
+
+ char *cwd = proc_cwd(pid);
+ if (cwd) {
+ strncpy(entry->cwd, cwd, sizeof(entry->cwd) - 1);
+ entry->cwd[sizeof(entry->cwd) - 1] = '\0';
+ }
+
+ strncpy(entry->comm, ps.name, sizeof(entry->comm) - 1);
+ entry->comm[sizeof(entry->comm) - 1] = '\0';
+
+ entry->daemonized = (ps.ppid == 1); // child of init
+ entry->is_traced = (ps.tracer_pid != 0); // being ptraced
+ entry->tracer_pid = ps.tracer_pid;
+ entry->state = ps.state;
+ entry->seccomp = ps.seccomp;
+ entry->cap_eff = ps.cap_eff;
+ entry->threads = ps.threads;
+ entry->has_tty = proc_has_tty(pid);
+
+ return entry;
+}
+
+void proc_ledger_hydrate(agent_context_t *ctx) {
+ DIR *proc = opendir("/proc");
+ struct dirent *de;
+
+ if (!proc) {
+ error("opendir /proc: %s", strerror(errno));
+ return;
+ }
+
+ while ((de = readdir(proc)) != NULL) {
+ pid_t pid = atoi(de->d_name);
+ if (pid <= 0) {
+ continue;
+ }
+
+ struct proc_ledger_entry *entry = proc_ledger_entry_create(pid, ctx);
+ if (!entry) { // TODO should this have a visible error?
+ continue;
+ }
+
+ proc_ledger_add(ctx->proc_ledger, entry);
+
+ struct stat sb;
+ if (entry->exe[0] && stat(entry->exe, &sb) == 0 && sb.st_size < maxsize) {
+ hash_ledger_add_or_update(ctx->hash_ledger, entry->exe, &sb);
+ }
+
+ json_t json = proc_ledger_entry_to_json(entry, "hydrate", ctx);
+ output(json_get(&json));
+ json_free(&json);
+ }
+
+ closedir(proc);
+}
+
+json_t proc_ledger_entry_to_json(struct proc_ledger_entry *entry,
+ const char *event_type,
+ struct agent_context *ctx) {
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", event_type);
+ json_add_int(&buf, "pid", entry->pid);
+ json_add_int(&buf, "ppid", entry->ppid);
+ json_add_int(&buf, "tgid", entry->tgid);
+
+ if (entry->exe[0]) {
+ json_add_string(&buf, "exepath", entry->exe);
+
+ struct hash_entry *he = hash_ledger_find(ctx->hash_ledger, entry->exe);
+ if (he) {
+ json_add_string_or_null(&buf, "md5", he->md5);
+ json_add_string_or_null(&buf, "sha256", he->sha256);
+ json_add_double(&buf, "entropy", he->entropy);
+ }
+ } else {
+ json_add_null(&buf, "exepath");
+ json_add_null(&buf, "md5");
+ json_add_null(&buf, "sha256");
+ json_add_null(&buf, "entropy");
+ }
+
+ json_add_string_or_null(&buf, "comm", entry->comm);
+ json_add_string_or_null(&buf, "cmdline", entry->cmdline);
+ json_add_string_or_null(&buf, "cwd", entry->cwd);
+
+ json_add_int(&buf, "uid", entry->uid);
+ json_add_int(&buf, "euid", entry->euid);
+ json_add_int(&buf, "gid", entry->gid);
+ json_add_int(&buf, "egid", entry->egid);
+
+ json_add_int64(&buf, "start_time", entry->start_time);
+ json_add_int64(&buf, "cpu_user_ticks", entry->cpu_user_ticks);
+ json_add_int64(&buf, "cpu_kernel_ticks", entry->cpu_kernel_ticks);
+ json_add_int64(&buf, "rss_bytes", entry->rss);
+ json_add_int64(&buf, "vsize_bytes", entry->vsize);
+
+ if (entry->daemonized) {
+ json_add_bool(&buf, "daemonized", entry->daemonized);
+ }
+ if (entry->is_traced) {
+ json_add_bool(&buf, "is_traced", entry->is_traced);
+ }
+
+ json_add_int(&buf, "tracer_pid", entry->tracer_pid);
+
+ char state[2] = { entry->state, '\0' };
+ json_add_string(&buf, "state", state);
+
+ json_add_int(&buf, "seccomp", entry->seccomp);
+ json_add_uint64(&buf, "cap_eff", entry->cap_eff);
+ json_add_int(&buf, "threads", entry->threads);
+ json_add_bool(&buf, "has_tty", entry->has_tty);
+
+ json_end_object(&buf);
+
+ return buf;
+}
diff --git a/src/sha256.c b/src/sha256.c
new file mode 100644
index 0000000..7ba502c
--- /dev/null
+++ b/src/sha256.c
@@ -0,0 +1,198 @@
+
+/*********************************************************************
+* Filename: sha256.c
+* Author: Brad Conte (brad AT bradconte.com)
+* Copyright:
+* Disclaimer: This code is presented "as is" without any guarantees.
+* Details: Implementation of the SHA-256 hashing algorithm.
+ SHA-256 is one of the three algorithms in the SHA2
+ specification. The others, SHA-384 and SHA-512, are not
+ offered in this implementation.
+ Algorithm specification can be found here:
+ * http://csrc.nist.gov/publications/fips/fips180-2/fips180-2withchangenotice.pdf
+ This implementation uses little endian byte order.
+*********************************************************************/
+
+/*************************** HEADER FILES ***************************/
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "sha256.h"
+
+/****************************** MACROS ******************************/
+#define ROTLEFT(a,b) (((a) << (b)) | ((a) >> (32-(b))))
+#define ROTRIGHT(a,b) (((a) >> (b)) | ((a) << (32-(b))))
+
+#define CH(x,y,z) (((x) & (y)) ^ (~(x) & (z)))
+#define MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
+#define EP0(x) (ROTRIGHT(x,2) ^ ROTRIGHT(x,13) ^ ROTRIGHT(x,22))
+#define EP1(x) (ROTRIGHT(x,6) ^ ROTRIGHT(x,11) ^ ROTRIGHT(x,25))
+#define SIG0(x) (ROTRIGHT(x,7) ^ ROTRIGHT(x,18) ^ ((x) >> 3))
+#define SIG1(x) (ROTRIGHT(x,17) ^ ROTRIGHT(x,19) ^ ((x) >> 10))
+
+/**************************** VARIABLES *****************************/
+static const WORD k[64] = {
+ 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
+ 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
+ 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
+ 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
+ 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
+ 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
+ 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
+ 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
+};
+
+/*********************** FUNCTION DEFINITIONS ***********************/
+void sha256_transform(SHA256_CTX *ctx, const BYTE data[])
+{
+ WORD a, b, c, d, e, f, g, h, i, j, t1, t2, m[64];
+
+ for (i = 0, j = 0; i < 16; ++i, j += 4) {
+ m[i] = (data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | (data[j + 3]);
+ }
+
+ for ( ; i < 64; ++i) {
+ m[i] = SIG1(m[i - 2]) + m[i - 7] + SIG0(m[i - 15]) + m[i - 16];
+ }
+
+ a = ctx->state[0];
+ b = ctx->state[1];
+ c = ctx->state[2];
+ d = ctx->state[3];
+ e = ctx->state[4];
+ f = ctx->state[5];
+ g = ctx->state[6];
+ h = ctx->state[7];
+
+ for (i = 0; i < 64; ++i) {
+ t1 = h + EP1(e) + CH(e,f,g) + k[i] + m[i];
+ t2 = EP0(a) + MAJ(a,b,c);
+ h = g;
+ g = f;
+ f = e;
+ e = d + t1;
+ d = c;
+ c = b;
+ b = a;
+ a = t1 + t2;
+ }
+
+ ctx->state[0] += a;
+ ctx->state[1] += b;
+ ctx->state[2] += c;
+ ctx->state[3] += d;
+ ctx->state[4] += e;
+ ctx->state[5] += f;
+ ctx->state[6] += g;
+ ctx->state[7] += h;
+}
+
+void sha256_init(SHA256_CTX *ctx)
+{
+ ctx->datalen = 0;
+ ctx->bitlen = 0;
+ ctx->state[0] = 0x6a09e667;
+ ctx->state[1] = 0xbb67ae85;
+ ctx->state[2] = 0x3c6ef372;
+ ctx->state[3] = 0xa54ff53a;
+ ctx->state[4] = 0x510e527f;
+ ctx->state[5] = 0x9b05688c;
+ ctx->state[6] = 0x1f83d9ab;
+ ctx->state[7] = 0x5be0cd19;
+}
+
+void sha256_update(SHA256_CTX *ctx, const BYTE data[], size_t len)
+{
+ WORD i;
+
+ for (i = 0; i < len; ++i) {
+ ctx->data[ctx->datalen] = data[i];
+ ctx->datalen++;
+ if (ctx->datalen == 64) {
+ sha256_transform(ctx, ctx->data);
+ ctx->bitlen += 512;
+ ctx->datalen = 0;
+ }
+ }
+}
+
+void sha256_final(SHA256_CTX *ctx, BYTE hash[])
+{
+ WORD i;
+
+ i = ctx->datalen;
+
+ // Pad whatever data is left in the buffer.
+ if (ctx->datalen < 56) {
+ ctx->data[i++] = 0x80;
+ while (i < 56)
+ ctx->data[i++] = 0x00;
+ }
+ else {
+ ctx->data[i++] = 0x80;
+ while (i < 64)
+ ctx->data[i++] = 0x00;
+ sha256_transform(ctx, ctx->data);
+ memset(ctx->data, 0, 56);
+ }
+
+ // Append to the padding the total message's length in bits and transform.
+ ctx->bitlen += ctx->datalen * 8;
+ ctx->data[63] = ctx->bitlen;
+ ctx->data[62] = ctx->bitlen >> 8;
+ ctx->data[61] = ctx->bitlen >> 16;
+ ctx->data[60] = ctx->bitlen >> 24;
+ ctx->data[59] = ctx->bitlen >> 32;
+ ctx->data[58] = ctx->bitlen >> 40;
+ ctx->data[57] = ctx->bitlen >> 48;
+ ctx->data[56] = ctx->bitlen >> 56;
+ sha256_transform(ctx, ctx->data);
+
+ // Since this implementation uses little endian byte ordering and SHA uses big endian,
+ // reverse all the bytes when copying the final state to the output hash.
+ for (i = 0; i < 4; ++i) {
+ hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0x000000ff;
+ }
+}
+
+// not thread safe due to static buffer usage in digest[]
+char *sha256_digest_file(const char *path) {
+ unsigned char c[SHA256_DIGEST_LENGTH];
+ FILE *fp;
+ int bytes;
+ unsigned char data[1024 * 256];
+ static char digest[SHA256_DIGEST_LENGTH * 2 + 1];
+ SHA256_CTX context;
+
+ fp = fopen(path, "rb");
+ if (fp == NULL) {
+#ifdef SHA256_SHOW_ERRORS
+ error("sha256_digest_file(): Unable to open %s for reading: %s\n",
+ path,
+ strerror(errno));
+#endif /* SHA256_SHOW_ERRORS */
+ return NULL;
+ }
+
+ sha256_init(&context);
+ while ((bytes = fread(data, 1, sizeof(data), fp)) != 0) {
+ sha256_update(&context, data, bytes);
+ }
+ sha256_final(&context, c);
+
+ fclose(fp);
+
+ for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
+ sprintf(digest + (i * 2), "%02x", c[i]);
+ }
+
+ return digest;
+}
diff --git a/src/sniffer.c b/src/sniffer.c
new file mode 100644
index 0000000..8b71570
--- /dev/null
+++ b/src/sniffer.c
@@ -0,0 +1,582 @@
+#define _GNU_SOURCE
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <limits.h>
+#include <errno.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/tcp.h>
+#include <netinet/udp.h>
+#include <netinet/ip_icmp.h>
+
+#include <arpa/inet.h>
+#include <net/if.h>
+
+#include <linux/if_packet.h>
+#include <linux/if_ether.h>
+#include <linux/filter.h>
+
+// for mapping DNS query -> uid
+/* #define BUF_SIZE 8192 */
+/* #include <errno.h> */
+/* #include <linux/inet_diag.h> */
+/* #include <linux/netlink.h> */
+/* #include <linux/rtnetlink.h> */
+/* #include <linux/sock_diag.h> */
+/* static void dump_udp_sockets(); */
+
+
+#include "agent_context.h"
+#include "output.h"
+#include "sniffer.h"
+#include "error.h"
+#include "time_common.h"
+#include "net.h"
+#include "dns.h"
+
+// apply a BPF filter to the socket
+// TODO user-specified patterns
+void sniffer_apply_filter(sock_t s) {
+ // tcpdump -dd '(ip or ip6) and (udp port 53 or (tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0))'
+ struct sock_filter bpf[] = {
+ { 0x28, 0, 0, 0x0000000c },
+ { 0x15, 0, 15, 0x00000800 },
+ { 0x30, 0, 0, 0x00000017 },
+ { 0x15, 0, 7, 0x00000011 },
+ { 0x28, 0, 0, 0x00000014 },
+ { 0x45, 19, 0, 0x00001fff },
+ { 0xb1, 0, 0, 0x0000000e },
+ { 0x48, 0, 0, 0x0000000e },
+ { 0x15, 15, 0, 0x00000035 },
+ { 0x48, 0, 0, 0x00000010 },
+ { 0x15, 13, 14, 0x00000035 },
+ { 0x15, 0, 13, 0x00000006 },
+ { 0x28, 0, 0, 0x00000014 },
+ { 0x45, 11, 0, 0x00001fff },
+ { 0xb1, 0, 0, 0x0000000e },
+ { 0x50, 0, 0, 0x0000001b },
+ { 0x45, 7, 8, 0x00000007 },
+ { 0x15, 0, 7, 0x000086dd },
+ { 0x30, 0, 0, 0x00000014 },
+ { 0x15, 0, 5, 0x00000011 },
+ { 0x28, 0, 0, 0x00000036 },
+ { 0x15, 2, 0, 0x00000035 },
+ { 0x28, 0, 0, 0x00000038 },
+ { 0x15, 0, 1, 0x00000035 },
+ { 0x6, 0, 0, 0x00040000 },
+ { 0x6, 0, 0, 0x00000000 },
+ };
+
+ struct sock_fprog program = {
+ .len = sizeof(bpf) / sizeof(bpf[0]),
+ .filter = bpf,
+ };
+
+ if (setsockopt(s, SOL_SOCKET, SO_ATTACH_FILTER, &program, sizeof(program)) == -1) {
+ error_fatal("FATAL: setsockopt SO_ATTACH_FILTER: %s", strerror(errno));
+ }
+}
+
+sock_t sniffer_init_interface(const char *interface, bool promisc) {
+ if (!interface) {
+ error("setup_sniffer: NULL interface");
+ return -1;
+ }
+
+ sock_t sniffer = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
+ if (sniffer < 0) {
+ error("socket: %s", strerror(errno));
+ return -1;
+ }
+
+ // set non-blocking
+ int flags = fcntl(sniffer, F_GETFL, 0);
+ if (flags == -1) {
+ error("fcntl F_GETFL: %s", strerror(errno));
+ close(sniffer);
+ return -1;
+ }
+
+ if (fcntl(sniffer, F_SETFL, flags | O_NONBLOCK) == -1) {
+ error("fcntl F_SETFL: %s", strerror(errno));
+ close(sniffer);
+ return -1;
+ }
+
+ struct sockaddr_ll sll;
+ struct ifreq ifr;
+
+ memset(&ifr, 0, sizeof(ifr));
+ strncpy(ifr.ifr_name, interface, IFNAMSIZ - 1);
+ if (ioctl(sniffer, SIOCGIFINDEX, &ifr) == -1) {
+ error("ioctl SIOCGIFINDEX: %s", strerror(errno));
+ close(sniffer);
+ return -1;
+ }
+
+ memset(&sll, 0, sizeof(sll));
+ sll.sll_family = AF_PACKET;
+ sll.sll_ifindex = ifr.ifr_ifindex;
+ sll.sll_protocol = htons(ETH_P_ALL);
+ if (bind(sniffer, (struct sockaddr *)&sll, sizeof(sll)) == -1) {
+ error("bind: %s", strerror(errno));
+ close(sniffer);
+ return -1;
+ }
+
+ // TODO Enable promiscuous mode if desired
+ if (promisc) {
+ struct packet_mreq mreq;
+ memset(&mreq, 0, sizeof(mreq));
+ mreq.mr_ifindex = ifr.ifr_ifindex;
+ mreq.mr_type = PACKET_MR_PROMISC;
+ if (setsockopt(sniffer, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) == -1) {
+ error("setsockopt PACKET_ADD_MEMBERSHIP: %s", strerror(errno));
+ close(sniffer);
+ return -1;
+ }
+ }
+
+ sniffer_apply_filter(sniffer);
+
+ return sniffer;
+}
+
+static void json_add_tcp_flag_array(json_t *buf, uint8_t flags) {
+ json_add_int(buf, "flags_int", flags);
+ json_add_array_start(buf, "flags");
+
+ if (flags & TH_SYN) json_array_add_string(buf, "SYN");
+ if (flags & TH_ACK) json_array_add_string(buf, "ACK");
+ if (flags & TH_FIN) json_array_add_string(buf, "FIN");
+ if (flags & TH_RST) json_array_add_string(buf, "RST");
+ if (flags & TH_PUSH) json_array_add_string(buf, "PSH");
+ if (flags & TH_URG) json_array_add_string(buf, "URG");
+ if (flags & TH_ECE) json_array_add_string(buf, "ECE");
+ if (flags & TH_CWR) json_array_add_string(buf, "CWR");
+
+ json_end_array(buf);
+}
+
+static void sniffer_log_tcp_event(agent_context_t *ctx,
+ const char *ifname,
+ const char *proto,
+ const char *src_ip, uint16_t src_port,
+ const char *dst_ip, uint16_t dst_port,
+ int ttl_or_hop,
+ uint8_t flags) {
+ json_t buf;
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "tcp");
+ json_add_string(&buf, "interface", ifname);
+ json_add_string(&buf, "protocol", proto);
+ json_add_string(&buf, "source_ip", src_ip);
+ json_add_int(&buf, "source_port", src_port);
+ json_add_string(&buf, "dest_ip", dst_ip);
+ json_add_int(&buf, "dest_port", dst_port);
+ json_add_int(&buf, (strcmp(proto, "ipv6") == 0) ? "hop_limit" : "ttl", ttl_or_hop);
+ json_add_tcp_flag_array(&buf, flags);
+
+ json_end_object(&buf);
+ output(json_get(&buf));
+ json_free(&buf);
+}
+
+static void sniffer_log_dns_event(agent_context_t *ctx, const char *ifname, const struct dns_question *q) {
+ json_t buf;
+ json_init(&buf);
+ json_start_object(&buf);
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "dns_query");
+ json_add_string(&buf, "interface", ifname);
+ json_add_string(&buf, "question", q->name);
+ json_add_int(&buf, "qtype", q->qtype);
+ json_add_string(&buf, "qtype_str", dns_type_to_string(q->qtype));
+ json_end_object(&buf);
+ output(json_get(&buf));
+ json_free(&buf);
+}
+
+static void sniffer_parse_ipv4(agent_context_t *ctx, const char *ifname, uint8_t *data, size_t len) {
+ if (len < sizeof(struct iphdr)) {
+ return;
+ }
+
+ struct iphdr *ip_header = (struct iphdr *)data;
+
+ // TODO handle fragmentation better.
+ if (ntohs(ip_header->frag_off) & 0x1FFF) {
+ //printf("Skipping fragmented IPv4 packet\n");
+ return;
+ }
+
+ struct sockaddr_in source, dest;
+ memset(&source, 0, sizeof(source));
+ source.sin_addr.s_addr = ip_header->saddr;
+ memset(&dest, 0, sizeof(dest));
+ dest.sin_addr.s_addr = ip_header->daddr;
+
+ char src_ip[INET_ADDRSTRLEN];
+ char dst_ip[INET_ADDRSTRLEN];
+ inet_ntop(AF_INET, &source.sin_addr, src_ip, sizeof(src_ip));
+ inet_ntop(AF_INET, &dest.sin_addr, dst_ip, sizeof(dst_ip));
+
+ uint8_t *transport = data + ip_header->ihl * 4;
+
+ switch (ip_header->protocol) {
+ case IPPROTO_TCP:
+ if (len < (transport - data) + sizeof(struct tcphdr)) {
+ // not big enough for TCP header
+ return;
+ }
+
+ struct tcphdr *tcp = (struct tcphdr *)transport;
+
+ size_t tcp_header_len = tcp->doff * 4;
+ if (len < (transport - data) + tcp_header_len) {
+ // incomplete TCP header
+ return;
+ }
+
+ uint16_t tcp_sport = ntohs(tcp->source);
+ uint16_t tcp_dport = ntohs(tcp->dest);
+
+ // TODO map socket to PID
+ sniffer_log_tcp_event(ctx, ifname, "ipv4", src_ip, tcp_sport, dst_ip, tcp_dport, ip_header->ttl, tcp->th_flags);
+ break;
+
+ case IPPROTO_UDP:
+ if (len < (transport - data) + sizeof(struct udphdr)) {
+ // not big enough for UDP header
+ return;
+ }
+
+ struct udphdr *udp = (struct udphdr *)transport;
+ uint16_t udp_sport = ntohs(udp->source);
+ uint16_t udp_dport = ntohs(udp->dest);
+
+ if (udp_sport == 53 || udp_dport == 53) { // DNS
+ size_t dns_len = len - (transport - data) - sizeof(struct udphdr);
+ const uint8_t *dns_payload = (const uint8_t *)(udp + 1);
+
+ //pid_t pid = match_udp_inode(ip_header->saddr, sport);
+ struct dns_question qs[MAX_DNS_QUESTIONS];
+ size_t count = parse_dns_udp(dns_payload, dns_len, qs, MAX_DNS_QUESTIONS);
+ for (int i = 0; i < count; i++) {
+ sniffer_log_dns_event(ctx, ifname, &qs[i]);
+ }
+ }
+ break;
+
+ case IPPROTO_ICMP:
+ //printf("ICMP: %s -> %s\n", src_ip, dst_ip);
+ break;
+
+ default:
+ // shouldn't get here due to bpf filter
+ //printf("Unknown IPv4 protocol: %d\n", ip_header->protocol);
+ break;
+ }
+}
+
+static void sniffer_parse_ipv6(agent_context_t *ctx, const char *ifname, uint8_t *data, size_t len) {
+ if (len < sizeof(struct ip6_hdr))
+ return;
+
+ struct ip6_hdr *ip6h = (struct ip6_hdr *)data;
+
+ char src_ip[INET6_ADDRSTRLEN];
+ char dst_ip[INET6_ADDRSTRLEN];
+ inet_ntop(AF_INET6, &ip6h->ip6_src, src_ip, sizeof(src_ip));
+ inet_ntop(AF_INET6, &ip6h->ip6_dst, dst_ip, sizeof(dst_ip));
+
+ uint8_t *transport = data + sizeof(struct ip6_hdr);
+ size_t transport_len = len - sizeof(struct ip6_hdr);
+
+ switch (ip6h->ip6_nxt) {
+ case IPPROTO_TCP:
+ if (transport_len < sizeof(struct tcphdr)) {
+ return;
+ }
+ struct tcphdr *tcp = (struct tcphdr *)transport;
+ uint16_t tcp_sport = ntohs(tcp->source);
+ uint16_t tcp_dport = ntohs(tcp->dest);
+
+ // TODO map socket to PID
+ sniffer_log_tcp_event(ctx, ifname, "ipv6", src_ip, tcp_sport, dst_ip, tcp_dport, ip6h->ip6_hlim, tcp->th_flags);
+ break;
+
+ case IPPROTO_UDP:
+ if (transport_len < sizeof(struct udphdr)) {
+ return;
+ }
+ struct udphdr *udp = (struct udphdr *)transport;
+ uint16_t udp_sport = ntohs(udp->source);
+ uint16_t udp_dport = ntohs(udp->dest);
+
+ if (udp_sport == 53 || udp_dport == 53) { // DNS
+ size_t dns_len = len - (transport - data) - sizeof(struct udphdr);
+ const uint8_t *dns_payload = (const uint8_t *)(udp + 1);
+
+ //pid_t pid = match_udp_inode(ip_header->saddr, sport);
+ struct dns_question qs[MAX_DNS_QUESTIONS];
+ int count = parse_dns_udp(dns_payload, dns_len, qs, MAX_DNS_QUESTIONS);
+ for (int i = 0; i < count; i++) {
+ sniffer_log_dns_event(ctx, ifname, &qs[i]);
+ }
+ }
+ break;
+
+ case IPPROTO_ICMPV6:
+ //printf("ICMPv6: %s -> %s\n", src_ip, dst_ip);
+ break;
+
+ default:
+ // shouldn't get here due to bpf filter
+ //printf("Unknown IPv6 NextHeader: %d\n", ip6h->nexthdr);
+ break;
+ }
+}
+
+static void sniffer_parse_ethernet(agent_context_t *ctx, const char *ifname, uint8_t *buffer, size_t len) {
+ if (len < sizeof(struct ethhdr)) {
+ return;
+ }
+
+ struct ethhdr *eth = (struct ethhdr *)buffer;
+ uint16_t ethertype = ntohs(eth->h_proto);
+ size_t offset = sizeof(struct ethhdr);
+
+ // TODO log vlan tags
+ while (ethertype == ETH_P_8021Q) {
+ if (len < offset + 4)
+ return;
+ ethertype = ntohs(*(uint16_t *)(buffer + offset + 2));
+ offset += 4;
+ }
+
+ switch(ethertype) {
+ case ETH_P_IP:
+ sniffer_parse_ipv4(ctx, ifname, buffer + offset, len - offset);
+ break;
+ case ETH_P_IPV6:
+ sniffer_parse_ipv6(ctx, ifname, buffer + offset, len - offset);
+ break;
+ default:
+ // unknown/unsupported Ethertype
+ break;
+ }
+}
+
+void sniffer_handle_packet(sock_t sniffer, agent_context_t *ctx) {
+ uint8_t buffer[2048];
+ struct sockaddr_ll sll;
+
+ struct iovec iov = {
+ .iov_base = buffer,
+ .iov_len = sizeof(buffer)
+ };
+
+ struct msghdr msg = {
+ .msg_name = &sll,
+ .msg_namelen = sizeof(sll),
+ .msg_iov = &iov,
+ .msg_iovlen = 1
+ };
+
+ ssize_t data_size = recvmsg(sniffer, &msg, 0);
+ if (data_size < 0) {
+ error("recvmsg: %s", strerror(errno));
+ return;
+ }
+
+ char ifname[IFNAMSIZ] = "unknown";
+ if_indextoname(sll.sll_ifindex, ifname);
+
+ sniffer_parse_ethernet(ctx, ifname, buffer, data_size);
+}
+
+// This attempts to parse /proc/net/udp to find the inode of the
+// socket, then walk /proc/PID/fd/* to correlate it to a process. this
+// is too slow to give meaningful results most of the time
+/* pid_t match_udp_inode(uint32_t ip_be, uint16_t port_be) { */
+/* FILE *fp = fopen("/proc/net/udp", "r"); */
+/* if (!fp) { */
+/* return 0; */
+/* } */
+
+/* char line[512]; */
+/* fgets(line, sizeof(line), fp); // skip header */
+
+/* char local_addr[64]; */
+/* uint32_t ip_hex; */
+/* uint32_t port_hex; */
+/* unsigned long inode = 0; */
+
+/* while (fgets(line, sizeof(line), fp)) { */
+/* sscanf(line, "%*d: %64[0-9A-Fa-f]:%x %*s %*s %*s %*s %*s %lu", */
+/* local_addr, */
+/* &port_hex, */
+/* &inode); */
+
+/* sscanf(local_addr, "%x", &ip_hex); */
+
+/* if (ip_hex == ntohl(ip_be) && port_hex == ntohs(port_be)) { */
+/* break; */
+/* } else { */
+/* inode = 0; */
+/* } */
+/* } */
+
+/* fclose(fp); */
+/* if (!inode) { */
+/* return 0; */
+/* } */
+
+/* DIR *proc = opendir("/proc"); */
+/* if (!proc) { */
+/* return 0; */
+/* } */
+
+/* struct dirent *entry; */
+/* while ((entry = readdir(proc))) { */
+/* if (!isdigit(entry->d_name[0])) { */
+/* continue; */
+/* } */
+
+/* char fd_dir[PATH_MAX]; */
+/* snprintf(fd_dir, sizeof(fd_dir), "/proc/%s/fd", entry->d_name); */
+
+/* DIR *fd = opendir(fd_dir); */
+/* if (!fd) { */
+/* continue; */
+/* } */
+
+/* struct dirent *fd_entry; */
+/* while((fd_entry = readdir(fd))) { */
+/* char link_path[PATH_MAX + 1024]; */
+/* char target[PATH_MAX]; */
+
+/* snprintf(link_path, sizeof(link_path), "%s/%s", fd_dir, fd_entry->d_name); */
+/* ssize_t len = readlink(link_path, target, sizeof(target) - 1); */
+/* if (len == -1) { */
+/* continue; */
+/* } */
+/* target[len] = '\0'; */
+
+/* char socket_str[64] = {0}; */
+/* snprintf(socket_str, sizeof(socket_str), "socket:[%lu]", inode); */
+/* if (strcmp(target, socket_str) == 0) { */
+/* closedir(fd); */
+/* closedir(proc); */
+/* return atoi(entry->d_name); */
+/* } */
+/* } */
+
+/* closedir(fd); */
+/* } */
+
+/* closedir(proc); */
+
+/* return 0; */
+/* } */
+
+
+
+// TODO investigate this further. Would be nice to correlate DNS
+// requests to a process id, but this search method is too slow.
+
+// This is able to get the UID initiating the DNS query and inode, but
+// traversing /proc to match inode->PID is too slow.
+/* static void dump_udp_sockets() { */
+/* int sock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_INET_DIAG); */
+/* if (sock < 0) { */
+/* perror("socket"); */
+/* exit(1); */
+/* } */
+
+/* char req_buf[BUF_SIZE]; */
+/* memset(req_buf, 0, sizeof(req_buf)); */
+
+/* struct nlmsghdr *nlh = (struct nlmsghdr *)req_buf; */
+/* struct inet_diag_req_v2 *req = (struct inet_diag_req_v2 *)(nlh + 1); */
+
+/* nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*req)); */
+/* nlh->nlmsg_type = SOCK_DIAG_BY_FAMILY; */
+/* nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; */
+/* nlh->nlmsg_seq = 123456; */
+
+/* req->sdiag_family = AF_INET; */
+/* req->sdiag_protocol = IPPROTO_UDP; */
+/* req->idiag_ext = 0; */
+/* req->idiag_states = -1; */
+
+/* struct sockaddr_nl sa = {0}; */
+/* sa.nl_family = AF_NETLINK; */
+
+/* if (sendto(sock, req_buf, nlh->nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa)) < 0) { */
+/* perror("sendto"); */
+/* close(sock); */
+/* exit(1); */
+/* } */
+
+/* char buffer[BUF_SIZE]; */
+/* while (1) { */
+/* int len = recv(sock, buffer, sizeof(buffer), 0); */
+/* if (len < 0) { */
+/* perror("recv"); */
+/* break; */
+/* } */
+
+/* struct nlmsghdr *nlh = (struct nlmsghdr *)buffer; */
+/* for (; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) { */
+/* if (nlh->nlmsg_type == NLMSG_DONE) */
+/* return; */
+/* if (nlh->nlmsg_type == NLMSG_ERROR) { */
+/* fprintf(stderr, "netlink error\n"); */
+/* return; */
+/* } */
+
+/* struct inet_diag_msg *diag = (struct inet_diag_msg *)NLMSG_DATA(nlh); */
+/* struct sockaddr_in src = { */
+/* .sin_family = AF_INET, */
+/* .sin_port = diag->id.idiag_sport, */
+/* .sin_addr.s_addr = diag->id.idiag_src[0], */
+/* }; */
+/* struct sockaddr_in dst = { */
+/* .sin_family = AF_INET, */
+/* .sin_port = diag->id.idiag_dport, */
+/* .sin_addr.s_addr = diag->id.idiag_dst[0], */
+/* }; */
+
+/* char src_ip[INET_ADDRSTRLEN]; */
+/* char dst_ip[INET_ADDRSTRLEN]; */
+/* inet_ntop(AF_INET, &src.sin_addr, src_ip, sizeof(src_ip)); */
+/* inet_ntop(AF_INET, &dst.sin_addr, dst_ip, sizeof(dst_ip)); */
+
+/* printf("UDP %s:%d -> %s:%d inode=%u uid=%u\n", */
+/* src_ip, ntohs(src.sin_port), */
+/* dst_ip, ntohs(dst.sin_port), */
+/* diag->idiag_inode, */
+/* diag->idiag_uid); */
+/* } */
+/* } */
+
+/* close(sock); */
+/* } */
diff --git a/src/string.c b/src/string.c
new file mode 100644
index 0000000..805d711
--- /dev/null
+++ b/src/string.c
@@ -0,0 +1,56 @@
+/* string.c - various string-related functions that don't come with string.h */
+
+#include <string.h>
+#include <stdbool.h>
+
+/* startswith() - Check if string starts with prefix.
+ *
+ * Args:
+ * string - String to check (haystack).
+ * prefix - Prefix to check (needle).
+ *
+ * Returns:
+ * true if 'string' starts with 'prefix', otherwise false.
+ */
+bool startswith(const char *string, const char *prefix) {
+ if ((string == NULL) || (prefix == NULL)) {
+ return false;
+ }
+
+ while (*prefix) {
+ if (*prefix++ != *string++) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/* endswith() - Check if string ends with suffix.
+ *
+ * Args:
+ * string - String to check (haystack).
+ * suffix - Suffix to check (needle).
+ *
+ * Returns:
+ * true if 'string' ends with 'suffix', otherwise false.
+ */
+bool endswith(const char *string, const char *suffix) {
+ size_t string_length;
+ size_t suffix_length;
+
+ if ((string == NULL) || (suffix == NULL)) {
+ return false;
+ }
+
+ string_length = strlen(string);
+ suffix_length = strlen(suffix);
+
+ if (suffix_length > string_length) {
+ return false;
+ }
+
+ return (strncmp(string + string_length - suffix_length,
+ suffix,
+ suffix_length) == 0) ? true : false;
+}
diff --git a/src/time.c b/src/time.c
new file mode 100644
index 0000000..5338cb9
--- /dev/null
+++ b/src/time.c
@@ -0,0 +1,35 @@
+#include <stdio.h>
+#include <stddef.h>
+#include <sys/time.h>
+
+#include "error.h"
+
+double timestamp(void) {
+ struct timeval tv;
+
+ if (gettimeofday(&tv, NULL) == -1) {
+ return -1;
+ }
+
+ return tv.tv_sec + (tv.tv_usec * 1e-6);
+}
+
+unsigned long get_boot_time(void) {
+ FILE *fp = fopen("/proc/stat", "r");
+ if (!fp) {
+ error("fopen /proc/stat: %s", strerror(errno));
+ return 0;
+ }
+
+ char buf[256];
+ unsigned long btime = 0;
+
+ while (fgets(buf, sizeof(buf), fp)) {
+ if (sscanf(buf, "btime %lu", &btime) == 1) {
+ break;
+ }
+ }
+
+ fclose(fp);
+ return btime;
+}
diff --git a/test/test-endswith.c b/test/test-endswith.c
new file mode 100644
index 0000000..7ad61a7
--- /dev/null
+++ b/test/test-endswith.c
@@ -0,0 +1,17 @@
+#include <stdio.h>
+#include "string_common.h"
+
+int main() {
+ int r, result1, result2;
+ char *foo = "this is a string";
+ char *bar = "string";
+
+ result1 = endswith(foo, bar);
+ result2 = endswith("string", "asdf");
+
+ r = (result1 == 1) && (result2 == 0);
+
+ printf("%s: endswith\n", r ? "PASS" : "FAIL");
+
+ return r;
+}
diff --git a/test/test-parse_dns_udp-multi.c b/test/test-parse_dns_udp-multi.c
new file mode 100644
index 0000000..854b61a
--- /dev/null
+++ b/test/test-parse_dns_udp-multi.c
@@ -0,0 +1,47 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdint.h>
+#include "dns.h"
+
+int main() {
+ uint8_t pkt[512] = {0};
+
+ // DNS header
+ pkt[4] = 0x00;
+ pkt[5] = 0x02; // QDCOUNT = 2
+
+ uint8_t *ptr = pkt + 12;
+
+ // Question 1: foo.com A
+ *ptr++ = 3; memcpy(ptr, "foo", 3); ptr += 3;
+ *ptr++ = 3; memcpy(ptr, "com", 3); ptr += 3;
+ *ptr++ = 0; // null terminator
+ *ptr++ = 0x00; *ptr++ = 0x01; // QTYPE A
+ *ptr++ = 0x00; *ptr++ = 0x01; // QCLASS IN
+
+ // Question 2: bar.org AAAA
+ *ptr++ = 3; memcpy(ptr, "bar", 3); ptr += 3;
+ *ptr++ = 3; memcpy(ptr, "org", 3); ptr += 3;
+ *ptr++ = 0;
+ *ptr++ = 0x00; *ptr++ = 0x1c; // QTYPE AAAA
+ *ptr++ = 0x00; *ptr++ = 0x01;
+
+ size_t pkt_len = ptr - pkt;
+
+ struct dns_question out[4];
+ size_t count = parse_dns_udp(pkt, pkt_len, out, 4);
+
+ //printf("Got %zu questions:\n", count);
+ //for (size_t i = 0; i < count; i++) {
+ // printf(" %s (%s)\n", out[i].name, dns_type_to_string(out[i].qtype));
+ //}
+
+ int res = (count == 2 &&
+ strcmp(out[0].name, "foo.com") == 0 &&
+ out[0].qtype == 1 &&
+ strcmp(out[1].name, "bar.org") == 0 &&
+ out[1].qtype == 28);
+
+ printf("%s: parse_dns_udp-multi\n", res ? "PASS" : "FAIL");
+ return res;
+}
diff --git a/test/test-parse_dns_udp.c b/test/test-parse_dns_udp.c
new file mode 100644
index 0000000..7fec577
--- /dev/null
+++ b/test/test-parse_dns_udp.c
@@ -0,0 +1,42 @@
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include "../include/dns.h"
+
+#define ASSERT(x) do { \
+ if (!(x)) { \
+ fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #x); \
+ return EXIT_FAILURE; \
+ } \
+} while (0)
+
+int main(void) {
+ // Simple DNS query for "example.com" A record
+ // Manually constructed:
+ // - Header: 12 bytes
+ // - Question: example.com (as labels) + QTYPE A + QCLASS IN
+ uint8_t pkt[] = {
+ 0x12, 0x34, // Transaction ID
+ 0x01, 0x00, // Flags: standard query
+ 0x00, 0x01, // QDCOUNT: 1
+ 0x00, 0x00, // ANCOUNT
+ 0x00, 0x00, // NSCOUNT
+ 0x00, 0x00, // ARCOUNT
+ 0x07, 'e','x','a','m','p','l','e',
+ 0x03, 'c','o','m',
+ 0x00, // null terminator
+ 0x00, 0x01, // QTYPE A
+ 0x00, 0x01 // QCLASS IN
+ };
+
+ struct dns_question qs[MAX_DNS_QUESTIONS];
+ size_t count = parse_dns_udp(pkt, sizeof(pkt), qs, MAX_DNS_QUESTIONS);
+
+ ASSERT(count == 1);
+ ASSERT(strcmp(qs[0].name, "example.com") == 0);
+ ASSERT(qs[0].qtype == 1);
+
+ printf("PASS: parse_dns_udp\n");
+ return EXIT_SUCCESS;
+}
diff --git a/test/test-sha256.c b/test/test-sha256.c
new file mode 100644
index 0000000..a00d22c
--- /dev/null
+++ b/test/test-sha256.c
@@ -0,0 +1,75 @@
+/*********************************************************************
+* Filename: sha256.c
+* Author: Brad Conte (brad AT bradconte.com)
+* Copyright:
+* Disclaimer: This code is presented "as is" without any guarantees.
+* Details: Performs known-answer tests on the corresponding SHA1
+ implementation. These tests do not encompass the full
+ range of available test vectors, however, if the tests
+ pass it is very, very likely that the code is correct
+ and was compiled properly. This code also serves as
+ example usage of the functions.
+*********************************************************************/
+
+/*************************** HEADER FILES ***************************/
+#include <stdio.h>
+#include <memory.h>
+#include <string.h>
+#include "sha256.h"
+
+/*********************** FUNCTION DEFINITIONS ***********************/
+int sha256_test()
+{
+ BYTE text1[] = {"abc"};
+ BYTE text2[] = {"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"};
+ BYTE text3[] = {"aaaaaaaaaa"};
+ BYTE hash1[SHA256_BLOCK_SIZE] = {
+ 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea,
+ 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
+ 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c,
+ 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad
+ };
+ BYTE hash2[SHA256_BLOCK_SIZE] = {
+ 0x24, 0x8d, 0x6a, 0x61, 0xd2, 0x06, 0x38, 0xb8,
+ 0xe5, 0xc0, 0x26, 0x93, 0x0c, 0x3e, 0x60, 0x39,
+ 0xa3, 0x3c, 0xe4, 0x59, 0x64, 0xff, 0x21, 0x67,
+ 0xf6, 0xec, 0xed, 0xd4, 0x19, 0xdb, 0x06, 0xc1
+ };
+ BYTE hash3[SHA256_BLOCK_SIZE] = {
+ 0xcd, 0xc7, 0x6e, 0x5c, 0x99, 0x14, 0xfb, 0x92,
+ 0x81, 0xa1, 0xc7, 0xe2, 0x84, 0xd7, 0x3e, 0x67,
+ 0xf1, 0x80, 0x9a, 0x48, 0xa4, 0x97, 0x20, 0x0e,
+ 0x04, 0x6d, 0x39, 0xcc, 0xc7, 0x11, 0x2c, 0xd0
+ };
+ BYTE buf[SHA256_BLOCK_SIZE];
+ SHA256_CTX ctx;
+ int idx;
+ int pass = 1;
+
+ sha256_init(&ctx);
+ sha256_update(&ctx, text1, strlen((const char *)text1));
+ sha256_final(&ctx, buf);
+ pass = pass && !memcmp(hash1, buf, SHA256_BLOCK_SIZE);
+
+ sha256_init(&ctx);
+ sha256_update(&ctx, text2, strlen((const char *)text2));
+ sha256_final(&ctx, buf);
+ pass = pass && !memcmp(hash2, buf, SHA256_BLOCK_SIZE);
+
+ sha256_init(&ctx);
+ for (idx = 0; idx < 100000; ++idx) {
+ sha256_update(&ctx, text3, strlen((const char *)text3));
+ }
+ sha256_final(&ctx, buf);
+ pass = pass && !memcmp(hash3, buf, SHA256_BLOCK_SIZE);
+
+ return pass;
+}
+
+int main()
+{
+ int result = sha256_test();
+ printf("%s: SHA-256\n", result ? "PASS" : "FAIL");
+
+ return result ? 0 : 1;
+}