The Packets Are Inside The Computer - Building 802.11 Challenges in Congested 802.11 Environments
About The Project
This past weekend was the final Shmoocon and the final Hack Fortress….for now. This blog post is a writeup on developing an 802.11 challenge for Hack Fortress while considering the congested 802.11 environment that is the Shmoocon conference. By leveraging the Linux kernel’s hwsim and hostapd, competitors were able to capture beacon frames, parse SSIDs, and ultimately solve a 802.11 challenge that never actually broadcasted a beacon frame over RF!
Upon solving this and other challenges, CTF Team “MVM” took home the limited edition Shmoocon 2025 Hack Fortress game case as a trophy!
Me, You and 802.11
Hotels are concrete castles that make 802.11 puzzles difficult to solve. If your competitors are not in close proximity to the devices emitting RF, they cannot see your challenge. If conference attendees are partaking in shenanigans with their own RF emitting devices their broadcasting could unintentionally interfere with your challenge, making troubleshooting a nightmare.
Being located near the RF Village, as Hack Fortress often is, only adds to the congested RF space you must contend with.
Enter, the Linux kernel driver mac80211_hwsim, aka hwsim. Per the official documentation.
mac80211_hwsim is a Linux kernel module that can be used to simulate arbitrary number of IEEE 802.11 radios for mac80211. It can be used to test most of the mac80211 functionality and user space tools (e.g., hostapd and wpa_supplicant) in a way that matches very closely with the normal case of using real WLAN hardware. From the mac80211 view point, mac80211_hwsim is yet another hardware driver, i.e., no changes to mac80211 are needed to use this testing too - https://wireless.docs.kernel.org/en/latest/en/users/drivers/mac80211_hwsim.html
By compiling a Linux kernel with the hwsim module, it’s possible to use hostapd or another 802.11 userland utility to mock 802.11 challenges. This is great! It makes sense that some kind of test suite should exist to be able to emulate 802.11 functionality without always buying the latest and greatest WLAN gear and hwsim provides that capability keeping the packets in the computer, and not actually broadcasting them out. First of three fun facts in this blog, the name of this challenge is derived from the 2001 Box Office smash hit, Zoolander.
The Custom OpenWRT Image
Referring to my earlier blog post on OpenWRT, I have a Gl-iNet AX1800. While this model is not currently officially supported by OpenWRT, a pull request on Github has positive feedback by others who have tested the firmware, and a hacker conference is the perfect opportunity to deploy bleeding edge firmware. I was able to easily clone, merge in the pull request and build OpenWRT for the Gl-iNet AX1800.
Examining at the git diff
, a significant amount of changes are relevant to the device tree (dts) which makes sense because its hardware not currently supported by OpenWRT, specifically the Qualcomm processor, and the Mediatek flash chip used by the Gl-iNet router.
modified: include/image-commands.mk
modified: package/boot/uboot-envtools/files/qualcommax_ipq60xx
modified: package/firmware/ipq-wifi/Makefile
modified: target/linux/mediatek/image/filogic.mk
new file: target/linux/qualcommax/files/arch/arm64/boot/dts/qcom/ipq6000-gl-ax1800.dts
new file: target/linux/qualcommax/files/arch/arm64/boot/dts/qcom/ipq6000-gl-axt1800.dts
new file: target/linux/qualcommax/files/arch/arm64/boot/dts/qcom/ipq6000-glinet.dtsi
modified: target/linux/qualcommax/files/arch/arm64/boot/dts/qcom/ipq6010-mango-dvk.dts
modified: target/linux/qualcommax/files/arch/arm64/boot/dts/qcom/ipq6010-xe3-4.dts
modified: target/linux/qualcommax/files/arch/arm64/boot/dts/qcom/ipq6018-cp-cpu.dtsi
deleted: target/linux/qualcommax/files/arch/arm64/boot/dts/qcom/ipq6018-fixed-smps.dtsi
modified: target/linux/qualcommax/files/arch/arm64/boot/dts/qcom/ipq8074-ac-cpu.dtsi
modified: target/linux/qualcommax/files/arch/arm64/boot/dts/qcom/ipq8074-hk-cpu.dtsi
modified: target/linux/qualcommax/image/ipq60xx.mk
modified: target/linux/qualcommax/ipq60xx/base-files/etc/hotplug.d/firmware/11-ath11k-caldata
modified: target/linux/qualcommax/ipq60xx/config-default
new file: target/linux/qualcommax/patches-6.6/0023-v6.7-arm64-dts-qcom-ipq5018-add-watchdog.patch
new file: target/linux/qualcommax/patches-6.6/0051-v6.8-arm64-dts-qcom-ipq5018-enable-the-CPUFreq-support.patch
new file: target/linux/qualcommax/patches-6.6/0064-v6.11-clk-qcom-gcc-ipq6018-update-sdcc-max-clock-frequency.patch
new file: target/linux/qualcommax/patches-6.6/0065-v6.11-arm64-dts-qcom-ipq6018-add-sdhci-node.patch
new file: target/linux/qualcommax/patches-6.6/0066-v6.13-arm64-dts-qcom-ipq-change-labels-to-lower-case.patch
new file: target/linux/qualcommax/patches-6.6/0104-pwm-driver-for-qualcomm-ipq6018-pwm-block.patch
new file: target/linux/qualcommax/patches-6.6/0105-arm64-dts-qcom-ipq6018-add-pwm-node.patch
modified: target/linux/qualcommax/patches-6.6/0122-arm64-dts-ipq8074-add-CPU-clock.patch
modified: target/linux/qualcommax/patches-6.6/0123-arm64-dts-ipq8074-add-cooling-cells-to-CPU-nodes.patch
modified: target/linux/qualcommax/patches-6.6/0130-arm64-dts-qcom-ipq8074-add-CPU-OPP-table.patch
deleted: target/linux/qualcommax/patches-6.6/0137-arm64-dts-qcom-ipq6018-add-SDHCI-node.patch
deleted: target/linux/qualcommax/patches-6.6/0139-arm64-dts-qcom-ipq6018-add-LDOA2-regulator.patch
modified: target/linux/qualcommax/patches-6.6/0140-arm64-dts-qcom-ipq6018-add-NSS-reserved-memory.patch
new file: target/linux/qualcommax/patches-6.6/0151-arm64-dts-qcom-ipq6018-add-1.2GHz-CPU-Frequency.patch
new file: target/linux/qualcommax/patches-6.6/0152-arm64-dts-qcom-ipq6018-add-1.5GHz-CPU-Frequency.patch
new file: target/linux/qualcommax/patches-6.6/0153-arm64-dts-qcom-ipq6018-move-mp5496-regulator-outside.patch
new file: target/linux/qualcommax/patches-6.6/0154-arm64-dts-qcom-ipq6018-mp5496-add-LDOA2-regulator.patch
modified: target/linux/qualcommax/patches-6.6/0906-arm64-dts-qcom-ipq6018-add-wifi-node.patch
modified: target/linux/qualcommax/patches-6.6/0907-soc-qcom-fix-smp2p-ack-on-ipq6018.patch
modified: target/linux/qualcommax/patches-6.6/0909-arm64-dts-qcom-ipq6018-assign-QDSS_AT-clock-to-wifi-.patch
deleted: target/linux/qualcommax/patches-6.6/0910-arm64-dts-qcom-ipq6018-change-voltage-to-perf-levels.patch
With the pull request merged locally into the OpenWRT code base, I leveraged make menuconfig
and browse with ncurses
to enable the hwsim module and kicked off the build with make -j$(nproc)
.
A fun fact about the Gl-iNet AX1800 contains a modified U-Boot bootloader that has an embedded web server. This is to enable easy end user recovery should a device become bricked. The official Gl-iNet documentation takes you through how to boot a device into this mode.
This was also the reason I wasn’t concerned about flashing not officially supported firmware and potentially bricking the device (which I did). Experienced users can forgo the embedded webserver, and leverage the U-Boot
shell during boot up to modify bootloader environment variables, and specify a TFTP server to pull a OpenWRT image from. This will load and run the custom OpenWRT image. The example command below has specific parameters specifying where the AX1800 is expecting the OpenWRT image to be loaded in memory.
$> tftpb 0x44000000 openwrt-qualcommax-ipq60xx-glinet_gl-ax1800-initramfs-uImage-2.itb
I booted with TFTP multiple times for testing as I flashed the wrong image initially (.bin vs .ubi) resulting in a semi-bricked device that I was able to recover from. During testing, I would also find more packages that I would want installed for the competitors on the target device. An example of said package is doom! Another fun fact is that you can compile in gzdoom and have your router run Doom!
➜ openwrt git:(main) ✗ find . -iname \*doom\*
./feeds/video.tmp/info/.packageinfo-games_gzdoom
./feeds/video.tmp/info/.packageinfo-games_sdl2-doom
./feeds/video/games/gzdoom
./feeds/video/games/sdl2-doom
With the custom image built and flashed the final reboot of the device resulted with the a successful working OpenWRT device on not officially supported hardware just in time for Hack Fortress!
Sending and Capturing 802.11 Frames
Hostapd is a critical piece of OpenWrt’s Wi-Fi stack that allows end users to specify the 802.11 networks they want broadcasted along with appropriate settings for their wireless networks. Settings are provided via a key=value
configuration file that include keys such as include SSID
, vendor data
, interface
to broadcast on, etc… The hwsim kernel module will create N-number of radios depending on user provided arguments during the loading of said module. For example, by executing modprobe mac80211_hwsim radios=10
, you would then be able to bring up 10 interfaces on your device. For the challenge, I only required three interfaces. One for monitor mode to capture all WLAN traffic and two other for broadcasting custom SSIDs. The setup of these interfaces is achieved via the commands below.
iw phy phy0 interface add mon0 type monitor
ip link set mon0 up
iw phy phy1 interface add wlan1 type managed
iw phy phy1 interface add wlan2 type managed
$> ip -br a
mon0 UNKNOWN
wlan1 UP 169.254.11.166/16 fe80::9537:62e0:f2c3:bba1/64
wlan2 UP 169.254.11.166/16 fe80::2763:b0fd:b565:61e5/64
With the interfaces up, official examples from the hostapd
were used to make the challenge which broadcasted two separate SSIDs. The configuration files looked as follows:
# hostapd_0
interface=wlan1
driver=nl80211
ssid="0 786f72206b6579206973203432"
hw_mode=g
channel=1
beacon_int=100
# hostapd_1
interface=wlan2
driver=nl80211
ssid="1 242e23252b31203029272c322936"
hw_mode=g
channel=1
beacon_int=100
The objective for the competitor was to:
- Log into the router (credentials provided).
- Sniff traffic on the
mon0
interface. - Decode the SSIDs to discover one decodes to “the xor key is 42” and leverage that key to “decrypt” the other SSID which was the flag.
The snippet below shows the output the competitors would see.
For those following along at home , pop those SSIDs into CyberChef with a from hex
filter and you’re half way done with the puzzle.
root@OpenWrt:~# tcpdump -i mon0
[ 112.908862] mac80211_hwsim hwsim0 mon0: entered promiscuous mode
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on mon0, link-type IEEE802_11_RADIO (802.11 plus radiotap header), snapshot length 262144 bytes
20:13:52.102481 1736540032102424us tsft 1.0 Mb/s 2412 MHz 11b -30dBm signal antenna 0 Beacon ("0 786f72206b6579206973203432") [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1
20:13:52.102493 1736540032102446us tsft 1.0 Mb/s 2412 MHz 11b -30dBm signal antenna 0 Beacon ("1 242e23252b31203029272c322936") [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1
20:13:52.204846 1736540032204814us tsft 1.0 Mb/s 2412 MHz 11b -30dBm signal antenna 0 Beacon ("0 786f72206b6579206973203432") [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1
20:13:52.204854 1736540032204826us tsft 1.0 Mb/s 2412 MHz 11b -30dBm signal antenna 0 Beacon ("1 242e23252b31203029272c322936") [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1
20:13:52.307247 1736540032307214us tsft 1.0 Mb/s 2412 MHz 11b -30dBm signal antenna 0 Beacon ("0 786f72206b6579206973203432") [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1
20:13:52.307255 1736540032307227us tsft 1.0 Mb/s 2412 MHz 11b -30dBm signal antenna 0 Beacon ("1 242e23252b31203029272c322936") [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1
This challenge, was relatively straight forward, and a test of the competitor’s tcpdump
skills more than anything.
However, the journey of developing the challenge was a lot of fun for me and this will serve as a reference for me to visit in the future.
Conclusion
Special thanks to the RF Village at DEF CON, Shmoocon, and BSides that taught me about hwsim! I’ve often found I learn just as much by building CTFs and CTF Challenges that I do competing in them. For all that have played Hack Fortress at DEF CON or Shmoocon, thank you for joining us for unforgettable memories over the years. With Hack Fortress officially on a hiatus, maybe I’ll find some time to play more CTFs. Looking forward to crossing paths with you dear reader!