diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d418c9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +nfqueue_tamper +*.o +*.d +*.log +.vscode \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e351c79 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2021 Peter Farley + +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. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..8dd0893 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +MAINDIR = . +SRC = $(MAINDIR)/src +INC = $(MAINDIR)/inc + +CXXSRC = $(wildcard $(SRC)/*.cpp) $(wildcard $(SRC)/*/*.cpp) +OBJ = $(patsubst %.cpp,%.o,$(CXXSRC)) +DEPS = $(patsubst %.cpp,%.d,$(CXXSRC)) +EXEC = nfqueue_tamper + +CXXFLAGS = -I$(INC) -Wall -Wextra -Werror -std=c++17 -O2 -g +LDFLAGS = -lboost_program_options -lnetfilter_queue + +ifeq ($(CXX), "clang++") + CXXFLAGS += -Weverything \ + -Wno-c++98-compat +endif + +.PHONY: all, clean +all: $(EXEC) + +$(EXEC): $(OBJ) + @echo -e "\033[33m \033[1mLD\033[21m \033[34m$(EXEC)\033[0m" + @$(CXX) $(OBJ) $(LDFLAGS) -o $(EXEC) + +clean: + @echo -e "\033[33m \033[1mCleaning $(EXEC)\033[0m" + @rm -f $(OBJ) $(EXEC) + +-include $(DEPS) + +%.o: %.cpp + @echo -e "\033[32m \033[1mCXX\033[21m \033[34m$<\033[0m" + @$(CXX) $(CXXFLAGS) -MMD -MP -c -o $@ $< diff --git a/README.md b/README.md new file mode 100644 index 0000000..a48cfce --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +NFQueue Tamper +============== + +A tool to help in testing client/server robustness in the presence of malformed +data. + +Building +-------- + +Requirements: + - `g++`/`clang++` + - `make` + - boost: https://www.boost.org/ + - libnetfilter_queue: https://www.netfilter.org/projects/libnetfilter_queue/ + +Build with `make` + +Usage +----- + + nfqueue_tamer -q -t "method1;opt1;opt2" -t "method2;opt1" + +Queue number defaults to 0. Must be run as root. + +To create a queue: + + iptables -A [filter criteria] -j NFQUEUE --queue-num + +For example, to trap all outbound UDP traffic coming from port 63: + + iptables -A OUTPUT -p udp --sport 63 -J NFQUEUE --queue-num 0 + +Available methods and associated options: + - `rand` - Randomly tamper with data + - `off` - Offset, or offset range, at which to apply randomization + - Defaults to 0:-1 (0 - end) + - `con` - If offset is a range, whether or not modified bytes must be consecutive + - Defaults to 0 (non-consecutive allowed) + - NOT CURRENTLY SUPPORTED + - `sz` - Number of bytes to modify, can be a range + - Defaults to 1 + +Global options: + - `chance` - How likely the tamper method is to be used on a given packet + - Probability value between 0 and 1 + - Defaults to 1 + +Example: + + nfqueue -q 0 -t "rand;chance=.5;off=0:4;sz=1:2" + +This will have a 50% chance on every packet of replacing one or two of the first +five bytes in the payload (application-layer data) with a random value. \ No newline at end of file diff --git a/inc/NFQueue.hpp b/inc/NFQueue.hpp new file mode 100644 index 0000000..366516f --- /dev/null +++ b/inc/NFQueue.hpp @@ -0,0 +1,37 @@ +#ifndef _NFQUEUE_HPP_ +#define _NFQUEUE_HPP_ + +#include + +#include "PacketHandler.hpp" + +#define NFQUEUE_BUFF_SZ 4096 + +class NFQueue { + private: + PacketHandler &pHandler; + + struct nfq_handle *nfq_hand = NULL; + struct nfq_q_handle *nfq_queue = NULL; + int _nfq_fd = -1; + + int queue; + + uint8_t buff[NFQUEUE_BUFF_SZ]; + uint8_t newPayload[NFQUEUE_BUFF_SZ]; + + static int handle_callback(struct nfq_q_handle *_queue, struct nfgenmsg *_nfmsg, struct nfq_data *_data, void *_class); + + public: + NFQueue(int _queue, PacketHandler &_pHandler); + + int callback(struct nfq_q_handle *_queue, struct nfgenmsg *_nfmsg, struct nfq_data *_data); + + void open(void); + + void run(void); + + void close(void); +}; + +#endif /* _NFQUEUE_HPP_ */ diff --git a/inc/PacketHandler.hpp b/inc/PacketHandler.hpp new file mode 100644 index 0000000..c7a71d0 --- /dev/null +++ b/inc/PacketHandler.hpp @@ -0,0 +1,30 @@ +#ifndef _PACKET_HANDLER_HPP_ +#define _PACKET_HANDLER_HPP_ + +#include +#include +#include + +#include "TamperMethod.hpp" + +class PacketHandler { + private: + std::vector meths; + + std::random_device rand_rd; + std::default_random_engine rand_engine; + + int handleTCPPacket(struct iphdr *_ip_head); + int handleUDPPacket(struct iphdr *_ip_head); + + int doTamper(size_t len, uint8_t *data); + + public: + PacketHandler(); + + void addTamperMethod(TamperMethod *_meth); + + int handlePacket(size_t len, uint8_t *data); +}; + +#endif /* _PACKET_HANDLER_HPP_ */ diff --git a/inc/TamperMethod.hpp b/inc/TamperMethod.hpp new file mode 100644 index 0000000..5b86ad3 --- /dev/null +++ b/inc/TamperMethod.hpp @@ -0,0 +1,26 @@ +#ifndef _TAMPER_METHOD_HPP_ +#define _TAMPER_METHOD_HPP_ + +#include +#include + +class TamperMethod { + private: + double probability = 1; /*!< Probability between 0 and 1 that this tamper method will act on a given packet */ + + public: + TamperMethod(std::map &_opts); + + virtual int tamper(size_t len, uint8_t *data) = 0; + + double getProbability(void); + + static TamperMethod *create(std::string &_str); + + static int parseRange(std::string &_str, int &_min, int &_max); + static int parseBool(std::string &_str, bool &_val); + + static int randRange(int _min, int _max, int _sz); +}; + +#endif /* _TAMPER_METHOD_HPP_ */ diff --git a/inc/TamperMethods/TamperMethodRand.hpp b/inc/TamperMethods/TamperMethodRand.hpp new file mode 100644 index 0000000..8670b50 --- /dev/null +++ b/inc/TamperMethods/TamperMethodRand.hpp @@ -0,0 +1,27 @@ +#ifndef _TAMPER_METHOD_RAND_HPP_ +#define _TAMPER_METHOD_RAND_HPP_ + +#include + +#include "TamperMethod.hpp" + +class TamperMethodRand : public TamperMethod { + private: + int offset_min = 0; /*!< Minimum offset */ + int offset_max = -1; /*!< Maximum offset */ + + bool consecutive = false; + + int size_min = 1; + int size_max = 1; + + std::random_device rand_rd; + std::default_random_engine rand_engine; + + public: + TamperMethodRand(std::map &_opts); + + int tamper(size_t len, uint8_t *data); +}; + +#endif /* _TAMPER_METHOD_RAND_HPP_ */ diff --git a/src/NFQueue.cpp b/src/NFQueue.cpp new file mode 100644 index 0000000..e3a40f2 --- /dev/null +++ b/src/NFQueue.cpp @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include + +#include "NFQueue.hpp" + +int NFQueue::handle_callback(struct nfq_q_handle *_queue, struct nfgenmsg *_nfmsg, struct nfq_data *_data, void *_class) { + return reinterpret_cast(_class)->callback(_queue, _nfmsg, _data); +} + +NFQueue::NFQueue(int _queue, PacketHandler &_pHandler) : +pHandler(_pHandler) { + this->queue = _queue; +} + +void NFQueue::open(void) { + this->nfq_hand = nfq_open(); + if(this->nfq_hand == NULL) { + throw std::runtime_error("Could not open NFQ handle!"); + } + + if(nfq_unbind_pf(this->nfq_hand, AF_INET) < 0) { + throw std::runtime_error("nfq_unbind_pf"); + } + + if(nfq_bind_pf(this->nfq_hand, AF_INET) < 0) { + throw std::runtime_error("nfq_bind_pf"); + } + + this->nfq_queue = nfq_create_queue(this->nfq_hand, this->queue, NFQueue::handle_callback, this); + if(this->nfq_queue == NULL) { + throw std::runtime_error("nfq_create_queue"); + } + + if(nfq_set_mode(this->nfq_queue, NFQNL_COPY_PACKET, 0xFFFF) < 0) { + throw std::runtime_error("nfq_set_mode"); + } + + this->_nfq_fd = nfq_fd(this->nfq_hand); +} + +void NFQueue::run(void) { + int len; + while((len = recv(this->_nfq_fd, this->buff, NFQUEUE_BUFF_SZ, 0))) { + nfq_handle_packet(this->nfq_hand, (char *)this->buff, len); + } +} + +void NFQueue::close(void) { + if(nfq_destroy_queue(this->nfq_queue) < 0) { + throw std::runtime_error("nfq_destroy_queue"); + } + + if(nfq_close(this->nfq_hand) < 0) { + throw std::runtime_error("nfq_close"); + } +} + + +int NFQueue::callback(struct nfq_q_handle *_queue, struct nfgenmsg *_nfmsg, struct nfq_data *_data) { + (void)_nfmsg; + + uint32_t id; + struct nfqnl_msg_packet_hdr *head; + uint8_t *payload; + size_t payload_len; + + std::cerr << "---------------------------" << std::endl; + + head = nfq_get_msg_packet_hdr(_data); + id = ntohl(head->packet_id); + + /*std::cerr << " Proto: " << std::hex << ntohs(head->hw_protocol) << std::dec; + std::cerr << ", hook: " << head->hook << ", id: " << head->packet_id << std::endl;*/ + + payload_len = nfq_get_payload(_data, &payload); + + /*std::cerr << " Payload size: " << payload_len << std::endl;*/ + memcpy(this->newPayload, payload, payload_len); + + this->pHandler.handlePacket(payload_len, payload); + /* TODO: Modify payload */ + + /* TODO: Accept dropping packets */ + return nfq_set_verdict(_queue, id, NF_ACCEPT, payload_len, payload); +} \ No newline at end of file diff --git a/src/PacketHandler.cpp b/src/PacketHandler.cpp new file mode 100644 index 0000000..f92bd53 --- /dev/null +++ b/src/PacketHandler.cpp @@ -0,0 +1,73 @@ +#include +#include +#include +#include +#include + +#include "PacketHandler.hpp" + +PacketHandler::PacketHandler() : +rand_engine(rand_rd()) { + +} + +void PacketHandler::addTamperMethod(TamperMethod *_meth) { + this->meths.push_back(_meth); +} + +int PacketHandler::handlePacket(size_t len, uint8_t *data) { + (void)len; + + struct iphdr *ip_head = (struct iphdr *)data; + + std::cerr << " IPv" << ip_head->version; + std::cerr << std::hex <<" Src: " << ip_head->saddr; + std::cerr << " Dest: " << ip_head->daddr << std::endl << std::dec; + std::cerr << " Proto: " << ip_head->protocol << " Size: " << ntohs(ip_head->tot_len) << std::endl; + + switch(ip_head->protocol) { + case IPPROTO_TCP: + std::cerr << " TCP Packet" << std::endl; + this->handleTCPPacket(ip_head); + break; + case IPPROTO_UDP: + std::cerr << " UDP Packet" << std::endl; + this->handleUDPPacket(ip_head); + break; + default: + std::cerr << " Unhandled packet type: " << ip_head->protocol << std::endl; + break; + } + + return 0; +} + +int PacketHandler::handleTCPPacket(struct iphdr *_ip_head) { + (void)_ip_head; /* TODO */ + + return 0; +} + +int PacketHandler::handleUDPPacket(struct iphdr *_ip_head) { + struct udphdr *udp_head = (struct udphdr *)((uint8_t *)_ip_head + (_ip_head->ihl * 4)); + + int ret = this->doTamper(ntohs(udp_head->len), (uint8_t *)udp_head + sizeof(udp_head)); + + /* TODO: For now, we are just disabling the checksum. This will not work on + * IPv6 packets. */ + udp_head->check = 0; + + return ret; +} + +int PacketHandler::doTamper(size_t len, uint8_t *data) { + std::uniform_real_distribution rand_dist(0, 1); + + for(TamperMethod *meth : this->meths) { + if(meth->getProbability() >= rand_dist(this->rand_engine)) { + meth->tamper(len, data); + } + } + + return 0; +} diff --git a/src/TamperMethod.cpp b/src/TamperMethod.cpp new file mode 100644 index 0000000..cfd84f5 --- /dev/null +++ b/src/TamperMethod.cpp @@ -0,0 +1,100 @@ +#include +#include +#include + +#include "TamperMethod.hpp" +#include "TamperMethods/TamperMethodRand.hpp" + +TamperMethod::TamperMethod(std::map &_opts) { + if(_opts.count("chance")) { + this->probability = std::stod(_opts["chance"]); + } +} + +double TamperMethod::getProbability(void) { + return this->probability; +} + +TamperMethod *TamperMethod::create(std::string &_str) { + /* TODO: Handle escaped semicolons. */ + const std::regex sep(";"); + + std::vector vec = std::vector( + std::sregex_token_iterator{begin(_str), end(_str), sep, -1}, + std::sregex_token_iterator{} + ); + + std::string meth = vec[0]; + std::map opts; + + for(size_t i = 1; i < vec.size(); i++) { + size_t split = vec[i].find('='); + if(split == std::string::npos) { + /* TODO: Include more info in error. Also, supporting this might + * be useful for default-false booleans. */ + throw std::runtime_error("Config option without matching value!"); + } + + opts.insert({vec[i].substr(0,split), vec[i].substr(split+1)}); + } + + if(meth == "rand") { + return new TamperMethodRand(opts); + } else if(meth == "dummy") { + /* TODO */ + return NULL; + } else { + throw std::runtime_error("Unhandled tamper method: " + meth); + } + + return NULL; +} + +int TamperMethod::parseRange(std::string &_str, int &_min, int &_max) { + size_t colon = _str.find(':'); + if(colon == 0) { + /* Bad formatting */ + return -1; + } else if(colon != std::string::npos) { + _min = std::stoi(_str.substr(0,colon)); + _max = std::stoi(_str.substr(colon+1)); + + if((_min < 0) || (_max < -1) || + ((_max < _min) && (_max != -1))) { + return -1; + } + } else { + _min = _max = std::stoi(_str); + } + + return 0; +} + +int TamperMethod::parseBool(std::string &_str, bool &_val) { + if(_str == "false" || _str == "0") { + _val = false; + } else if(_str == "true" || _str == "1") { + _val = true; + } else { + return -1; + } + + return 0; +} + +int TamperMethod::randRange(int _min, int _max, int _sz) { + if(_max == -1) { + _max = _sz; + } + + if(_min == _max) { + return _min; + } + + /* TODO: This is likely highly ineffecient */ + std::random_device rand_rd; + std::default_random_engine rand_engine(rand_rd()); + std::uniform_int_distribution rand_dist(_min, _max); + + return rand_dist(rand_engine); +} \ No newline at end of file diff --git a/src/TamperMethods/TamperMethodRand.cpp b/src/TamperMethods/TamperMethodRand.cpp new file mode 100644 index 0000000..e19a52e --- /dev/null +++ b/src/TamperMethods/TamperMethodRand.cpp @@ -0,0 +1,36 @@ +#include + +#include "TamperMethods/TamperMethodRand.hpp" + +TamperMethodRand::TamperMethodRand(std::map &_opts) : +TamperMethod(_opts), +rand_engine(rand_rd()) { + if(_opts.count("off") && + TamperMethod::parseRange(_opts["off"], this->offset_min, this->offset_max)) { + throw std::runtime_error("Badly formatted range for off!"); + } + if(_opts.count("sz") && + TamperMethod::parseRange(_opts["sz"], this->size_min, this->size_max)) { + throw std::runtime_error("Badly formatted range for sz!"); + } + if(_opts.count("con") && + TamperMethod::parseBool(_opts["con"], this->consecutive)) { + throw std::runtime_error("Badly formatted boolean for con!"); + } +} + +int TamperMethodRand::tamper(size_t len, uint8_t *data) { + int off = TamperMethod::randRange(this->offset_min, this->offset_max, len); + int sz = TamperMethod::randRange(this->size_min, this->size_max, len); + + std::cerr << "Off: " << off << ", Sz: " << sz << std::endl; + + std::uniform_int_distribution rand_dist(0, 255); + + /* TODO: non-consecutive */ + for(int i = off; i < (off + sz); i++) { + data[i] = rand_dist(this->rand_engine); + } + + return 0; +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..bfb2d35 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include + +#include "NFQueue.hpp" +#include "TamperMethod.hpp" +#include "PacketHandler.hpp" + +static struct { + std::vector tamperMethods; + int queueNumber; +} _options; + +static int _handleCmdLine(int, char **); + +int main(int argc, char **argv) { + int ret = _handleCmdLine(argc, argv); + if(ret) { + if (ret > 0) { + /* Non-error exit case */ + return 0; + } else { + return 1; + } + } + + try { + PacketHandler pHandler; + + for(std::string &meth : _options.tamperMethods) { + pHandler.addTamperMethod(TamperMethod::create(meth)); + } + + NFQueue nfqueue(_options.queueNumber, pHandler); + nfqueue.open(); + nfqueue.run(); + /* TODO: Run in a different thread, close on keyboard interrupt. */ + nfqueue.close(); + } catch (const std::exception &e) { + std::cerr << "Exception: " << e.what() << std::endl; + } catch(...) { + std::cerr << "Unknown Exception" << std::endl; + } + + return 0; +} + +namespace po = boost::program_options; + +static int _handleCmdLine(int argc, char **argv) { + po::options_description desc("Program options"); + + /* TODO: Clean this up if more options are added. */ + desc.add_options() + ("help,h", "Print this help message") + ("tamper,t", po::value>(&_options.tamperMethods)->multitoken(), "Tamper method") + ("queue,q", po::value(&_options.queueNumber)->default_value(0), "Queue number"); + + po::variables_map vmap; + po::store(po::parse_command_line(argc, argv, desc), vmap); + po::notify(vmap); + + if(vmap.count("help")) { + std::cout << "Usage: " << argv[0] << " [options]" << std::endl; + std::cout << desc << std::endl; + return 1; + } + + return 0; +}