The Poor Man's (or Woman's) Intrusion Detection System
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

400 lines
11 KiB

  1. //////////////////////////////////////////////////////////////////////
  2. // IP traffic analyzer
  3. // Written by Jonathan A. Foster <ChipMaster@YeOlPiShack.net>
  4. // Started April 23rd, 2021
  5. //
  6. // The idea is to analyze iptables LOG entries in combination with
  7. // DNSmasq's query log entries and combine them to list the hosts
  8. // that were accessed. The main reasons for not just inspecting HTTP
  9. // packets through a netfilter socket is due to HTTPS hiding the
  10. // "host" field. So I'm deducing based on DNS query timing.
  11. //
  12. // NOTE: its assumed that the log being processed is in chronological
  13. // order. This is the usual way things get logged. ;-)
  14. //
  15. // 2021-05-14 <ChipMaster@YeOlPiShack.net>
  16. // Dumbed down for C++ < 11.
  17. //////////////////////////////////////////////////////////////////////
  18. //////////////////////////////////////////////////////////////////////
  19. // Additional Router setup:
  20. //
  21. // ipset -N evilhosts iphash
  22. // ipset -N evilnets nethash
  23. //////////////////////////////////////////////////////////////////////
  24. //////////////////////////////////////////////////////////////////////
  25. // Obvious ignores:
  26. //
  27. // 10.10.10.1 -> 134.215.160.1 ICMP[8]
  28. //
  29. //////////////////////////////////////////////////////////////////////
  30. // TODO: map names according to time and host. time is probably automatic
  31. #include <string.h>
  32. #include <stdlib.h>
  33. #include <string>
  34. #include <iostream>
  35. #include <fstream>
  36. #include <stdexcept>
  37. #include <vector>
  38. #include <map>
  39. using namespace std;
  40. //////////////////////////////////////////////////////////////////////
  41. // Splits: a util class to devide a line into space sep pieces
  42. //////////////////////////////////////////////////////////////////////
  43. // TODO: implement begin() + end() to make "for( : )" work
  44. // TODO: implement field enclosing & escaping chars
  45. struct Splits {
  46. /// CONFIG ///
  47. enum { FieldMax=256, LineMax=1024 };
  48. /// properties ///
  49. char line[LineMax]; // Line buffer
  50. int len; // Length of line (after split())
  51. char sep; // Separator character.
  52. bool combine; // Treat multiple consecutive seps as one (combine)
  53. char *fields[FieldMax]; // pointers to fields in line
  54. int count; // How many fields there were
  55. // construct
  56. Splits(): count(0), len(0), sep(' '), combine(true) { line[LineMax-1] = 0; }
  57. // Convert field[] to string
  58. inline string operator[](int i) const { string s(fields[i]); return s; }
  59. // split line. Returns count.
  60. int split() {
  61. len = count = 0;
  62. if(!*line) return count;
  63. fields[0] = line;
  64. while(len<LineMax && line[len]) {
  65. if(line[len]==sep) {
  66. line[len++]=0;
  67. if(combine) while(len<LineMax && line[len]==sep) len++;
  68. if(++count<FieldMax) {
  69. // this shouldn't happen
  70. if(len>=LineMax) throw
  71. runtime_error("Splits::split: end of buffer null missing!");
  72. fields[count] = line+len;
  73. } else
  74. throw runtime_error("Splits::split: Too many fields in the line");
  75. } else
  76. len++;
  77. }
  78. return count++;
  79. }
  80. };
  81. // istream >> operator: getline() + .split()
  82. istream &operator>>(istream &in, Splits &sp) {
  83. if(in.getline(sp.line, sp.LineMax-1)) sp.split();
  84. return in;
  85. }
  86. //////////////////////////////////////////////////////////////////////
  87. // TSV version of Splits
  88. //////////////////////////////////////////////////////////////////////
  89. struct TSV: public Splits {
  90. TSV() { sep='\t'; combine=false; }
  91. };
  92. //////////////////////////////////////////////////////////////////////
  93. // Function to match a list of prefixes against a string
  94. //
  95. // Since C++ < 11 doesn't support constant vector initialization we'll
  96. // do this the old fashioned way with a null terminated char*[].
  97. //////////////////////////////////////////////////////////////////////
  98. bool pre_match(char **list, const string &s) {
  99. const char *p = s.c_str();
  100. for(; *list; list++)
  101. if(!strncmp(*list, p, strlen(*list))) return true;
  102. return false;
  103. }
  104. /* And because I think this may be useful in the future I'll hang on to it.
  105. bool pre_match(const vector<string> &list, const string &s) {
  106. for(
  107. vector<string>::const_iterator p=list.begin();
  108. p!=list.end();
  109. p++
  110. )
  111. if(s.substr(0, p->size())==*p) return true;
  112. return false;
  113. }*/
  114. //////////////////////////////////////////////////////////////////////
  115. // Connection between "us" and "them"
  116. //////////////////////////////////////////////////////////////////////
  117. typedef unsigned short word;
  118. struct Conn {
  119. string us; // address on our side
  120. word us_port; // the port on our side
  121. string them; // address on their side
  122. word them_port; // the port on their side
  123. string name; // name of the address
  124. string protocol; // protocol used to communicate
  125. bool in; // whether this was an inward bound connection.
  126. Conn(): in(false) {}
  127. Conn &clear() { us = them = name = protocol = ""; in=false; us_port = them_port = 0; }
  128. // swap polarity of record
  129. Conn &swap() {
  130. string s;
  131. int x;
  132. s = us;
  133. us = them;
  134. them =s;
  135. x = us_port;
  136. us_port = them_port;
  137. them_port = x;
  138. in=!in;
  139. return *this;
  140. }
  141. // scan & copy data from log record in
  142. Conn &operator=(const Splits &sp) {
  143. int x;
  144. clear();
  145. for(x=0; x<sp.count; x++) {
  146. if(!strncmp(sp.fields[x], "SRC=", 4)) {
  147. us = sp.fields[x]+4;
  148. continue;
  149. }
  150. if(!strncmp(sp.fields[x], "DST=", 4)) {
  151. them = sp.fields[x]+4;
  152. continue;
  153. }
  154. if(!strncmp(sp.fields[x], "SPT=", 4)) {
  155. us_port = atoi(sp.fields[x]+4);
  156. continue;
  157. }
  158. if(!strncmp(sp.fields[x], "DPT=", 4)) {
  159. them_port = atoi(sp.fields[x]+4);
  160. continue;
  161. }
  162. if(!strncmp(sp.fields[x], "TYPE=", 5) && protocol=="ICMP") {
  163. us_port = them_port = atoi(sp.fields[x]+5);
  164. continue;
  165. }
  166. if(!strncmp(sp.fields[x], "PROTO=", 6))
  167. protocol = sp.fields[x]+6;
  168. }
  169. }
  170. // TODO: does < > have any actual meaning in this context?
  171. int cmp(const Conn &gtr) const {
  172. if(us!="*" && gtr.us!="*") {
  173. if(us<gtr.us) return -1;
  174. if(us>gtr.us) return 1;
  175. }
  176. // TODO: auto-wildcard port based on in?
  177. if(us_port && gtr.us_port) { // 0 = no comparison wildcard
  178. if(us_port<gtr.us_port) return -1;
  179. if(us_port>gtr.us_port) return 1;
  180. }
  181. if(them!="*" && gtr.them!="*") {
  182. if(them<gtr.them) return -1;
  183. if(them>gtr.them) return 1;
  184. }
  185. if(them_port && gtr.them_port) { // 0 = no comparison wildcard
  186. if(them_port<gtr.them_port) return -1;
  187. if(them_port>gtr.them_port) return 1;
  188. }
  189. // TODO: do we want to consider the name?
  190. if(name!="*" && gtr.name!="*") {
  191. if(name<gtr.name) return -1;
  192. if(name>gtr.name) return 1;
  193. }
  194. if(protocol<gtr.protocol) return -1;
  195. if(protocol>gtr.protocol) return 1;
  196. if(in<gtr.in) return -1;
  197. if(in>gtr.in) return 1;
  198. return 0;
  199. }
  200. inline bool operator<(const Conn &gtr) const { return cmp(gtr) <0; }
  201. inline bool operator<=(const Conn &gtr) const { return cmp(gtr)<=0; }
  202. inline bool operator>(const Conn &gtr) const { return cmp(gtr) >0; }
  203. inline bool operator>=(const Conn &gtr) const { return cmp(gtr)>=0; }
  204. inline bool operator==(const Conn &gtr) const { return cmp(gtr)==0; }
  205. inline bool operator!=(const Conn &gtr) const { return cmp(gtr)!=0; }
  206. };
  207. // A text output of this record
  208. ostream &operator<<(ostream &out, const Conn &c) {
  209. out << c.us
  210. << ( c.in ? " <- " : " -> " )
  211. << c.them
  212. << " " << c.protocol
  213. << "[" << ( c.in ? c.us_port : c.them_port ) << "] "
  214. << c.name;
  215. return out;
  216. }
  217. // Copy data from TSV in
  218. const TSV &operator>>(const TSV &tsv, Conn &conn) {
  219. if(tsv.count<7) throw runtime_error("Conn=TSV: too few columns");
  220. conn.clear();
  221. conn.us = tsv[0];
  222. conn.us_port = atoi(tsv.fields[1]);
  223. conn.them = tsv[2];
  224. conn.them_port = atoi(tsv.fields[3]);
  225. conn.name = tsv[4];
  226. conn.protocol = tsv[5];
  227. conn.in = tsv[6]=="1";
  228. return tsv;
  229. }
  230. //////////////////////////////////////////////////////////////////////
  231. // List of connections
  232. //////////////////////////////////////////////////////////////////////
  233. struct ConnList: public vector<Conn> {
  234. int find(Conn &needle) {
  235. int r;
  236. for(r=0; r<size(); r++) if((*this)[r]==needle) return r;
  237. return -1;
  238. }
  239. };
  240. //////////////////////////////////////////////////////////////////////
  241. // Busy indicator aka. "Live Bug"
  242. //////////////////////////////////////////////////////////////////////
  243. struct LiveBug {
  244. string seq;
  245. char pre;
  246. int p;
  247. LiveBug(): seq("-\\|/"), pre('\r'), p(0) {}
  248. inline char next() { if(p>=seq.size()) p=0; return seq[p++]; }
  249. };
  250. ostream &operator<<(ostream &o, LiveBug &bug) {
  251. return o << bug.pre << bug.next();
  252. }
  253. //////////////////////////////////////////////////////////////////////
  254. // Roll through file
  255. //////////////////////////////////////////////////////////////////////
  256. //#define DEBUG
  257. typedef map<string,string> NameVal;
  258. // TODO: define us[] in conf file
  259. char *us[] = { (char *)"10.10.10.", (char *)"192.168.255.", (char *)"2001:470:a:169:", 0 };
  260. char *dns_ignore[] = { (char *)"v=spf1", (char *)"https:", 0 };
  261. char *dns_del[] = { (char *)"NODATA-", (char *)"NXDOMAIN-", 0 };
  262. #define PATH "/srv/backups/iptraffic"
  263. ifstream log(PATH "/test.log");
  264. ofstream out(PATH "/processed.log");
  265. Splits ln;
  266. int lnno = 0, ict = 0;
  267. LiveBug bug;
  268. NameVal rdns;
  269. NameVal::iterator nvp;
  270. string name, address, s;
  271. Conn conn;
  272. bool match;
  273. ConnList ignores;
  274. void dlog(const string msg) {
  275. #ifdef DEBUG
  276. cerr << "\r" << lnno << ": " << msg << endl;
  277. #endif
  278. }
  279. int main(int argc, char **argv) {
  280. /// Load lists ///
  281. /// Read in ignore list ///
  282. {
  283. TSV tsv;
  284. ifstream in(PATH "/ignores.lst");
  285. while(in >> tsv) {
  286. if(tsv.count>6) {
  287. tsv >> conn;
  288. ignores.push_back(conn);
  289. }
  290. }
  291. }
  292. /// parse log file ///
  293. while((log >> ln)) {
  294. lnno++;
  295. cout << bug << " " << lnno << flush;
  296. /// DNS query result ///
  297. // TODO: need to get more specific on tying us + them + time to DNS
  298. if(ln.count>8 && strncmp(ln.fields[4], "dnsmasq[", 8)==0) {
  299. if(ln[5]=="reply") {
  300. name = ln[6];
  301. address = ln[8];
  302. // Hmm... is this reply an address?
  303. if(pre_match(dns_ignore, address)) continue; // nope
  304. if(pre_match(dns_del, address)) continue; // does not exist reply
  305. if((nvp=rdns.find(address))!=rdns.end()) {
  306. if(nvp->second==name) continue;
  307. dlog("WARN: DNS address overlap "+address+": "+nvp->second+" : "+name);
  308. }
  309. rdns[address] = name;
  310. dlog("Added "+address+" = "+name);
  311. #ifdef DEBUG
  312. cout << '\r' << lnno << ": " << name << endl;
  313. #endif
  314. continue;
  315. }
  316. }
  317. /// process connections ///
  318. if(ln.count>5
  319. && ln[4]=="kernel:"
  320. && ln[5]=="ACCEPT"
  321. ) {
  322. conn = ln;
  323. if(!pre_match(us, conn.us)) conn.swap();
  324. if((nvp=rdns.find(conn.them))!=rdns.end())
  325. conn.name = nvp->second;
  326. if(ignores.find(conn)<0)
  327. out << ln[0] << " " << ln[1] << " " << ln[2] << " " << conn << "\n";
  328. else
  329. ict++;
  330. }
  331. }
  332. cout << "\nIgnored: " << ict << endl;
  333. cout << "Total rDNS: " << rdns.size() << "\n";
  334. return 0;
  335. }