diff options
| author | daniel <daniel@planethacker.net> | 2025-05-06 16:57:32 -0700 |
|---|---|---|
| committer | daniel <daniel@planethacker.net> | 2025-05-06 16:57:32 -0700 |
| commit | 2278df1493e064c197913e49b5d1935942d83448 (patch) | |
| tree | 42f06ab2f76e2ddf228bafbb03f79621975a4534 | |
initial import
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 + @@ -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 = ¤t_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; +} |
