processor.go 11 KB

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