processor.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. package mitmengine
  2. import (
  3. "bufio"
  4. "bytes"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "io/ioutil"
  9. "log"
  10. "os"
  11. "strings"
  12. "github.com/cloudflare/mitmengine/db"
  13. fp "github.com/cloudflare/mitmengine/fputil"
  14. )
  15. var (
  16. // ErrorUnknownUserAgent indicates that the user agent is not supported.
  17. ErrorUnknownUserAgent = errors.New("unknown_user_agent")
  18. )
  19. // A Processor generates heuristic-based man-in-the-middle (MiTM) detection
  20. // reports for a TLS client hello and corresponding HTTP user agent.
  21. type Processor struct {
  22. FileNameMap map[string]string
  23. BrowserDatabase db.Database
  24. MitmDatabase db.Database
  25. BadHeaderSet fp.StringSet
  26. }
  27. // A Config contains information for initializing the processor such as the
  28. // file names to read records from.
  29. type Config struct {
  30. BrowserFileName string
  31. MitmFileName string
  32. BadHeaderFileName string
  33. }
  34. // NewProcessor returns a new Processor initialized from the config.
  35. func NewProcessor(config Config) (Processor, error) {
  36. var a Processor
  37. err := a.Load(config)
  38. return a, err
  39. }
  40. // Load (or reload) the processor state from the provided configuration.
  41. func (a *Processor) Load(config Config) error {
  42. var file io.ReadCloser
  43. var err error
  44. if file, err = os.Open(config.BrowserFileName); err != nil {
  45. log.Printf("browser file: %v", err)
  46. file = ioutil.NopCloser(bytes.NewReader(nil))
  47. }
  48. if a.BrowserDatabase, err = db.NewDatabase(file); err != nil {
  49. return err
  50. }
  51. file.Close()
  52. if file, err = os.Open(config.MitmFileName); err != nil {
  53. log.Printf("mitm file: %v", err)
  54. file = ioutil.NopCloser(bytes.NewReader(nil))
  55. }
  56. if a.MitmDatabase, err = db.NewDatabase(file); err != nil {
  57. return err
  58. }
  59. file.Close()
  60. if file, err = os.Open(config.BadHeaderFileName); err != nil {
  61. log.Printf("badheader file: %v", err)
  62. file = ioutil.NopCloser(bytes.NewReader(nil))
  63. }
  64. scanner := bufio.NewScanner(file)
  65. var badHeaderList fp.StringList
  66. for scanner.Scan() {
  67. badHeaderList = append(badHeaderList, scanner.Text())
  68. }
  69. a.BadHeaderSet = badHeaderList.Set()
  70. file.Close()
  71. return nil
  72. }
  73. // Check if the supplied client hello fields match the expected client hello
  74. // fields for the the brower specified by the supplied user agent, and return a
  75. // report including the mitm detection result, security details, and client
  76. // hello fingerprints.
  77. func (a *Processor) Check(uaFingerprint fp.UAFingerprint, rawUa string,
  78. actualReqFin fp.RequestFingerprint) Report {
  79. // Add user agent fingerprint quirks.
  80. if strings.Contains(rawUa, "Dragon/") {
  81. uaFingerprint.Quirk = append(uaFingerprint.Quirk, "dragon")
  82. }
  83. if strings.Contains(rawUa, "GSA/") {
  84. uaFingerprint.Quirk = append(uaFingerprint.Quirk, "gsa")
  85. }
  86. if strings.Contains(rawUa, "Silk-Accelerated=true") {
  87. uaFingerprint.Quirk = append(uaFingerprint.Quirk, "silk_accelerated")
  88. }
  89. if strings.Contains(rawUa, "PlayStation Vita") {
  90. uaFingerprint.Quirk = append(uaFingerprint.Quirk, "playstation")
  91. }
  92. // Remove grease ciphers, extensions, and curves from request fingerprint and add as quirk instead.
  93. hasGrease := false
  94. idx := 0
  95. for _, elem := range actualReqFin.Cipher {
  96. if (elem & 0x0f0f) == 0x0a0a {
  97. hasGrease = true
  98. } else {
  99. actualReqFin.Cipher[idx] = elem
  100. idx++
  101. }
  102. }
  103. actualReqFin.Cipher = actualReqFin.Cipher[:idx]
  104. idx = 0
  105. for _, elem := range actualReqFin.Extension {
  106. if (elem & 0x0f0f) == 0x0a0a {
  107. hasGrease = true
  108. } else {
  109. actualReqFin.Extension[idx] = elem
  110. idx++
  111. }
  112. }
  113. actualReqFin.Extension = actualReqFin.Extension[:idx]
  114. idx = 0
  115. for _, elem := range actualReqFin.Curve {
  116. if (elem & 0x0f0f) == 0x0a0a {
  117. hasGrease = true
  118. } else {
  119. actualReqFin.Curve[idx] = elem
  120. idx++
  121. }
  122. }
  123. actualReqFin.Curve = actualReqFin.Curve[:idx]
  124. if hasGrease {
  125. actualReqFin.Quirk = append(actualReqFin.Quirk, "grease")
  126. }
  127. // Check for 'bad' headers that browsers never send and add as quirk.
  128. hasBadHeader := false
  129. for _, elem := range actualReqFin.Header {
  130. if a.BadHeaderSet[elem] {
  131. hasBadHeader = true
  132. }
  133. }
  134. if hasBadHeader {
  135. actualReqFin.Quirk = append(actualReqFin.Quirk, "badhdr")
  136. }
  137. // Create mitm detection report
  138. var r Report
  139. // Find the browser record matching the user agent fingerprint
  140. browserRecordIds := a.BrowserDatabase.GetByUAFingerprint(uaFingerprint)
  141. if len(browserRecordIds) == 0 {
  142. return Report{Error: ErrorUnknownUserAgent}
  143. }
  144. var browserRecord db.Record
  145. match := false
  146. for _, id := range browserRecordIds {
  147. browserRecord = a.BrowserDatabase.RecordMap[id]
  148. if browserRecord.RequestSignature.Match(actualReqFin) == fp.MatchPossible {
  149. match = true
  150. break
  151. }
  152. }
  153. // use the first matched browser record, or otherwise the last browser record in the list
  154. browserReqSig := browserRecord.RequestSignature
  155. r.MatchedUASignature = browserRecord.UASignature.String()
  156. r.BrowserSignature = browserRecord.RequestSignature.String()
  157. r.BrowserGrade = browserReqSig.Grade()
  158. r.ActualGrade = actualReqFin.Version.Grade().Merge(fp.GlobalCipherCheck.Grade(actualReqFin.Cipher))
  159. // No need to add to the report if we have match.
  160. if match {
  161. r.BrowserSignatureMatch = fp.MatchPossible
  162. return r
  163. }
  164. // Find the heuristics that flagged the connection as invalid
  165. matchMap := browserReqSig.MatchMap(actualReqFin)
  166. var reason []string
  167. var reasonDetails []string
  168. switch {
  169. case matchMap["version"] == fp.MatchImpossible:
  170. r.BrowserSignatureMatch = fp.MatchImpossible
  171. reason = append(reason, "invalid_version")
  172. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.Version, actualReqFin.Version))
  173. case matchMap["cipher"] == fp.MatchImpossible:
  174. r.BrowserSignatureMatch = fp.MatchImpossible
  175. reason = append(reason, "invalid_cipher")
  176. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.Cipher, actualReqFin.Cipher))
  177. case matchMap["extension"] == fp.MatchImpossible:
  178. r.BrowserSignatureMatch = fp.MatchImpossible
  179. reason = append(reason, "invalid_extension")
  180. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.Extension, actualReqFin.Extension))
  181. case matchMap["curve"] == fp.MatchImpossible:
  182. r.BrowserSignatureMatch = fp.MatchImpossible
  183. reason = append(reason, "invalid_curve")
  184. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.Curve, actualReqFin.Curve))
  185. case matchMap["ecpointfmt"] == fp.MatchImpossible:
  186. r.BrowserSignatureMatch = fp.MatchImpossible
  187. reason = append(reason, "invalid_ecpointfmt")
  188. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.EcPointFmt, actualReqFin.EcPointFmt))
  189. case matchMap["header"] == fp.MatchImpossible:
  190. r.BrowserSignatureMatch = fp.MatchImpossible
  191. reason = append(reason, "invalid_header")
  192. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.Header, actualReqFin.Header))
  193. case matchMap["quirk"] == fp.MatchImpossible:
  194. r.BrowserSignatureMatch = fp.MatchImpossible
  195. reason = append(reason, "invalid_quirk")
  196. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.Quirk, actualReqFin.Quirk))
  197. // put 'unlikely' reasons after 'impossible' reasons
  198. case matchMap["version"] == fp.MatchUnlikely:
  199. r.BrowserSignatureMatch = fp.MatchUnlikely
  200. reason = append(reason, "unlikely_version")
  201. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.Version, actualReqFin.Version))
  202. case matchMap["cipher"] == fp.MatchUnlikely:
  203. r.BrowserSignatureMatch = fp.MatchUnlikely
  204. reason = append(reason, "unlikely_cipher")
  205. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.Cipher, actualReqFin.Cipher))
  206. case matchMap["extension"] == fp.MatchUnlikely:
  207. r.BrowserSignatureMatch = fp.MatchUnlikely
  208. reason = append(reason, "unlikely_extension")
  209. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.Extension, actualReqFin.Extension))
  210. case matchMap["curve"] == fp.MatchUnlikely:
  211. r.BrowserSignatureMatch = fp.MatchUnlikely
  212. reason = append(reason, "unlikely_curve")
  213. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.Curve, actualReqFin.Curve))
  214. case matchMap["ecpointfmt"] == fp.MatchUnlikely:
  215. r.BrowserSignatureMatch = fp.MatchUnlikely
  216. reason = append(reason, "unlikely_ecpointfmt")
  217. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.EcPointFmt, actualReqFin.EcPointFmt))
  218. case matchMap["header"] == fp.MatchUnlikely:
  219. r.BrowserSignatureMatch = fp.MatchUnlikely
  220. reason = append(reason, "unlikely_header")
  221. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.Header, actualReqFin.Header))
  222. case matchMap["quirk"] == fp.MatchUnlikely:
  223. r.BrowserSignatureMatch = fp.MatchUnlikely
  224. reason = append(reason, "unlikely_quirk")
  225. reasonDetails = append(reasonDetails, fmt.Sprintf("%s vs %s", browserReqSig.Quirk, actualReqFin.Quirk))
  226. default:
  227. r.BrowserSignatureMatch = fp.MatchPossible
  228. }
  229. r.Reason = strings.Join(reason, ",")
  230. r.ReasonDetails = strings.Join(reasonDetails, ",")
  231. // Check if MITM affects the connection security level
  232. switch r.BrowserSignatureMatch {
  233. case fp.MatchImpossible, fp.MatchUnlikely:
  234. if browserReqSig.IsPfs() && fp.GlobalCipherCheck.IsFirstPfs(actualReqFin.Cipher) {
  235. r.LosesPfs = true
  236. }
  237. mitmRecordIds := a.MitmDatabase.GetByRequestFingerprint(actualReqFin)
  238. if len(mitmRecordIds) == 0 {
  239. break
  240. }
  241. mitmRecord := a.MitmDatabase.RecordMap[mitmRecordIds[0]]
  242. r.ActualGrade = r.ActualGrade.Merge(mitmRecord.MitmInfo.Grade)
  243. r.MatchedMitmName = mitmRecord.MitmInfo.NameList.String()
  244. r.MatchedMitmType = mitmRecord.MitmInfo.Type
  245. r.MatchedMitmSignature = mitmRecord.RequestSignature.String()
  246. }
  247. return r
  248. }