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.
 
 
 
 

291 lines
7.6 KiB

  1. //////////////////////////////////////////////////////////////////////
  2. // IP traffic analyzer - data objects
  3. // Written by Jonathan A. Foster <ChipMaster@YeOlPiShack.net>
  4. // Started April 23rd, 2021
  5. // Copyright JF Possibilities, Inc. All rights reserved.
  6. //////////////////////////////////////////////////////////////////////
  7. #include <arpa/inet.h>
  8. #include <string.h>
  9. #include <stdlib.h>
  10. #include <stdexcept>
  11. #include <iostream>
  12. #include "data.h"
  13. //////////////////////////////////////////////////////////////////////
  14. // Utils
  15. //////////////////////////////////////////////////////////////////////
  16. std::string ipv6opt(const std::string &addr) {
  17. in6_addr buf;
  18. char s[256];
  19. if(inet_pton(AF_INET6, addr.c_str(), &buf)<1) throw
  20. std::runtime_error("ipv6opt: inet_pton() says '"+addr+"' is not a valid IPv6 address");
  21. if(!inet_ntop(AF_INET6, &buf, s, 255)) throw // should never happen
  22. std::runtime_error("ipv6opt: inet_ntop() refused to convert the address back");
  23. return std::string(s);
  24. }
  25. int addr_wild_comp(const std::string &str1, const std::string &str2) {
  26. int spre1=0, spre2=0;
  27. if(str1=="*" || str2=="*") return 0;
  28. if(str1!="" && (str1.end()[-1]=='.' || str1.end()[-1]==':')) spre1=str1.size();
  29. if(str2!="" && (str2.end()[-1]=='.' || str2.end()[-1]==':')) spre2=str2.size();
  30. if(spre2>spre1) spre1=spre2;
  31. if(spre1) return strncmp(str1.c_str(), str2.c_str(), spre1);
  32. else if(str1<str2) return -1;
  33. else if(str1>str2) return 1;
  34. return 0;
  35. }
  36. //////////////////////////////////////////////////////////////////////
  37. // Conn
  38. //////////////////////////////////////////////////////////////////////
  39. void Conn::clear() {
  40. us = them = name = protocol = "";
  41. in=false;
  42. us_port = them_port = 0;
  43. }
  44. void Conn::compact() {
  45. if(us.find(':')!=us.npos) us=ipv6opt(us);
  46. if(them.find(':')!=us.npos) them=ipv6opt(them);
  47. }
  48. void Conn::swap() {
  49. std::string s;
  50. int x;
  51. s = us;
  52. us = them;
  53. them =s;
  54. x = us_port;
  55. us_port = them_port;
  56. them_port = x;
  57. in=!in;
  58. }
  59. Conn &Conn::operator=(const Splits &sp) {
  60. int x;
  61. clear();
  62. for(x=0; x<sp.count; x++) {
  63. if(!strncmp(sp.fields[x], "SRC=", 4)) {
  64. us = sp.fields[x]+4;
  65. continue;
  66. }
  67. if(!strncmp(sp.fields[x], "DST=", 4)) {
  68. them = sp.fields[x]+4;
  69. continue;
  70. }
  71. if(!strncmp(sp.fields[x], "SPT=", 4)) {
  72. us_port = atoi(sp.fields[x]+4);
  73. continue;
  74. }
  75. if(!strncmp(sp.fields[x], "DPT=", 4)) {
  76. them_port = atoi(sp.fields[x]+4);
  77. continue;
  78. }
  79. if(!strncmp(sp.fields[x], "TYPE=", 5) && protocol=="ICMP") {
  80. us_port = them_port = atoi(sp.fields[x]+5);
  81. continue;
  82. }
  83. if(!strncmp(sp.fields[x], "PROTO=", 6))
  84. protocol = sp.fields[x]+6;
  85. }
  86. }
  87. // TODO: does < > have any actual meaning in this context?
  88. int Conn::cmp(const Conn &gtr) const {
  89. int r;
  90. if(r = addr_wild_comp(us, gtr.us)) return r;
  91. // TODO: auto-wildcard port based on in?
  92. if(us_port && gtr.us_port) { // 0 = no comparison wildcard
  93. if(us_port<gtr.us_port) return -1;
  94. if(us_port>gtr.us_port) return 1;
  95. }
  96. if(r = addr_wild_comp(them, gtr.them)) return r;
  97. if(them_port && gtr.them_port) { // 0 = no comparison wildcard
  98. if(them_port<gtr.them_port) return -1;
  99. if(them_port>gtr.them_port) return 1;
  100. }
  101. // TODO: do we want to consider the name?
  102. if(name!="*" && gtr.name!="*") {
  103. if(name<gtr.name) return -1;
  104. if(name>gtr.name) return 1;
  105. }
  106. if(protocol<gtr.protocol) return -1;
  107. if(protocol>gtr.protocol) return 1;
  108. if(in<gtr.in) return -1;
  109. if(in>gtr.in) return 1;
  110. return 0;
  111. }
  112. std::ostream &operator<<(std::ostream &out, const Conn &c) {
  113. out << c.us
  114. << ( c.in ? " <- " : " -> " )
  115. << c.them
  116. << " " << c.protocol
  117. << "[" << ( c.in ? c.us_port : c.them_port ) << "] "
  118. << c.name;
  119. return out;
  120. }
  121. const Splits &operator>>(const Splits &tsv, Conn &conn) {
  122. if(tsv.count<7) throw std::runtime_error("Conn=TSV: too few columns");
  123. conn.clear();
  124. conn.us = tsv[0];
  125. conn.us_port = atoi(tsv.fields[1]);
  126. conn.them = tsv[2];
  127. conn.them_port = atoi(tsv.fields[3]);
  128. conn.name = tsv[4];
  129. conn.protocol = tsv[5];
  130. conn.in = tsv[6]=="1";
  131. return tsv;
  132. }
  133. //////////////////////////////////////////////////////////////////////
  134. // ConnList
  135. //////////////////////////////////////////////////////////////////////
  136. int ConnList::find(Conn &needle) {
  137. int r;
  138. for(r=0; r<size(); r++) if((*this)[r]==needle) return r;
  139. return -1;
  140. }
  141. //////////////////////////////////////////////////////////////////////
  142. // LogAnalyzer
  143. //////////////////////////////////////////////////////////////////////
  144. LogAnalyzer::LogAnalyzer():
  145. us(0)
  146. { // I'd rather this initialization be static...
  147. dns_ignore.push_back("v=spf1");
  148. dns_ignore.push_back("https:");
  149. dns_del.push_back("NODATA-");
  150. dns_del.push_back("NXDOMAIN-");
  151. }
  152. bool LogAnalyzer::line(const std::string &in) {
  153. int ict=0;
  154. NameVal::iterator nvp;
  155. std::string name, address, s;
  156. /// setup ///
  157. if(!us)
  158. throw std::runtime_error("LogAnalyzer::line: us list is not assigned");
  159. ln=in;
  160. /// DNS query result ///
  161. // TODO: need to get more specific on tying us + them + time to DNS
  162. if(ln.count>8 && strncmp(ln.fields[4], "dnsmasq[", 8)==0) {
  163. if(ln[5]=="reply" || ln[5]=="cached") {
  164. name = ln[6];
  165. address = ln[8];
  166. /* NOTE: CNAME resolution seems to follow this order in logs:
  167. 1. A result line (reply/cached) with an address of <CNAME>
  168. 2. One or more consecutive result lines for the canonical name
  169. Looking over the logs it doesn't appear that dnsmasq will log
  170. anything between the original and CNAME resolutions. The exception
  171. is if a CNAME record is cached and it has to resolve what it
  172. points to. In this case there would be a "cached" and then a
  173. "forwarded" record eventually followed by "reply ... <CNAME>".
  174. In that case we want to operate on the reply.
  175. I just saw that CNAME log entries can be chained. It looks like
  176. they are an "is <CNAME>" entry followed by another. We want to
  177. keep the original name (alias).
  178. */
  179. // we're handling a CNAME entry
  180. if(address=="<CNAME>") {
  181. // If we don't have a cname yet then this is a CNAME to a CNAME.
  182. if(alias=="" || cname!="") {
  183. alias = name;
  184. cname = "";
  185. }
  186. return 0;
  187. }
  188. // If in cname _mode_:
  189. if(alias!="") {
  190. if(cname=="") {
  191. cname = name; // This is our target name
  192. name = alias; // substitute the alias
  193. } else if(cname==name) {
  194. name = alias; // substitute the alias
  195. } else {
  196. cname = ""; // These are different records reset
  197. name = "";
  198. }
  199. }
  200. // Hmm... is this reply an address?
  201. if(pre_match(dns_ignore, address)) return 0; // nope
  202. if(pre_match(dns_del, address)) return 0; // does not exist reply
  203. if((nvp=rdns.find(address))!=rdns.end()) {
  204. if(nvp->second==name) return 0;
  205. //dlog("WARN: DNS address overlap "+address+": "+nvp->second+" : "+name);
  206. }
  207. rdns[address] = name;
  208. //dlog("Added "+address+" = "+name);
  209. return 0;
  210. } else if(alias!="") {
  211. // we've fallen out of CNAME resolution.
  212. alias = "";
  213. cname = "";
  214. }
  215. }
  216. /// process connections ///
  217. if((ln.count>5 // old Linux style
  218. && ln[4]=="kernel:"
  219. && ln[5]=="ACCEPT"
  220. ) || (ln.count>6 // new Linux style
  221. && ln[4]=="vmunix:"
  222. && ln[6]=="ACCEPT")
  223. ) {
  224. conn = ln;
  225. conn.compact();
  226. if(!pre_match(*us, conn.us)) conn.swap();
  227. if((nvp=rdns.find(conn.them))!=rdns.end()) conn.name = nvp->second;
  228. return 1;
  229. }
  230. return 0;
  231. }